dsh-remote-plugin 0.6.0 → 0.6.2
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 +5 -0
- package/index.mjs +68 -3
- package/package.json +1 -1
- package/public/admin.html +17 -0
- package/public/admin.js +9 -0
- package/public/app.js +343 -11
- package/public/desktop/desktop.css +62 -8
- package/public/desktop/desktop.html +204 -18
- package/public/desktop/desktop.js +529 -1
- package/public/donate.png +0 -0
- package/public/index.html +267 -85
- package/public/update.json +10 -4
- package/public/version.json +1 -1
|
@@ -60,6 +60,7 @@ const state = {
|
|
|
60
60
|
streamMode: 'ws', // 'ws' | 'poll'
|
|
61
61
|
pollSeq: { mux: 0, host: 0 },
|
|
62
62
|
fs: { path: null, initial: null, loaded: false },
|
|
63
|
+
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
63
64
|
view: 'sessions'
|
|
64
65
|
}
|
|
65
66
|
const streams = {}
|
|
@@ -96,6 +97,206 @@ function toast(text, kind = '') {
|
|
|
96
97
|
toast._t = setTimeout(() => el.classList.add('hidden'), 2600)
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
/* ---------------- 预设提示词 ---------------- */
|
|
101
|
+
const PRESETS_KEY = 'dshPromptPresets'
|
|
102
|
+
function readPresets() {
|
|
103
|
+
try {
|
|
104
|
+
const v = JSON.parse(LS.get(PRESETS_KEY, '[]') || '[]')
|
|
105
|
+
return Array.isArray(v) ? v.filter(p => p && typeof p.id === 'string' && typeof p.name === 'string' && typeof p.text === 'string') : []
|
|
106
|
+
} catch { return [] }
|
|
107
|
+
}
|
|
108
|
+
function renderPresetMenuDesktop() {
|
|
109
|
+
const btn = $('btn-preset')
|
|
110
|
+
const menu = $('preset-menu')
|
|
111
|
+
if (!btn || !menu) return
|
|
112
|
+
const list = readPresets()
|
|
113
|
+
btn.style.display = ''
|
|
114
|
+
menu.classList.add('hidden')
|
|
115
|
+
if (!list.length) {
|
|
116
|
+
menu.innerHTML = `<div class="ds-empty">${esc(t('ds.presetsEmpty'))}</div><div class="ds-empty ds-presets-guide">${esc(t('ds.presetsGuide'))}</div>`
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
menu.innerHTML = list.map(p => `<button class="ds-feedback-item" data-ds-preset="${esc(p.id)}">${esc(p.name)}</button>`).join('')
|
|
120
|
+
}
|
|
121
|
+
const PRESET_NAME_MAX = 20
|
|
122
|
+
const PRESET_TEXT_MAX = 2000
|
|
123
|
+
const PRESET_LIMIT = 20
|
|
124
|
+
function renderPresetSummary() {
|
|
125
|
+
const home = $('preset-count-home')
|
|
126
|
+
const modal = $('preset-count')
|
|
127
|
+
const n = readPresets().length
|
|
128
|
+
if (home) home.textContent = `· ${n}/${PRESET_LIMIT}`
|
|
129
|
+
if (modal) modal.textContent = `${n}/${PRESET_LIMIT}`
|
|
130
|
+
}
|
|
131
|
+
function writePresets(list) {
|
|
132
|
+
LS.set(PRESETS_KEY, JSON.stringify(list))
|
|
133
|
+
renderPresetSummary()
|
|
134
|
+
renderPresets()
|
|
135
|
+
renderPresetMenuDesktop()
|
|
136
|
+
}
|
|
137
|
+
function renderPresets() {
|
|
138
|
+
const box = $('preset-list')
|
|
139
|
+
if (!box) return
|
|
140
|
+
const list = readPresets()
|
|
141
|
+
renderPresetSummary()
|
|
142
|
+
if (!list.length) {
|
|
143
|
+
box.innerHTML = `<div class="ds-preset-empty">${esc(t('presets.empty'))}</div>`
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
box.innerHTML = list.map(p => `<div class="ds-preset-row">
|
|
147
|
+
<div class="ds-preset-main"><div class="ds-preset-name">${esc(p.name)}</div><div class="ds-preset-preview">${esc((p.text || '').slice(0, 60))}</div></div>
|
|
148
|
+
<button class="ds-mini-btn" data-preset-edit="${esc(p.id)}">${t('presets.edit')}</button>
|
|
149
|
+
<button class="ds-mini-btn" data-preset-del="${esc(p.id)}">${t('presets.delete')}</button>
|
|
150
|
+
</div>`).join('')
|
|
151
|
+
box.querySelectorAll('[data-preset-edit]').forEach(b => b.addEventListener('click', () => editPreset(b.dataset.presetEdit)))
|
|
152
|
+
box.querySelectorAll('[data-preset-del]').forEach(b => b.addEventListener('click', () => deletePreset(b.dataset.presetDel)))
|
|
153
|
+
}
|
|
154
|
+
function promptPreset(id) {
|
|
155
|
+
const list = readPresets()
|
|
156
|
+
const existing = id ? list.find(p => p.id === id) : null
|
|
157
|
+
const name = prompt(t('presets.namePrompt'), existing?.name || '')
|
|
158
|
+
if (name == null) return
|
|
159
|
+
const text = prompt(t('presets.textPrompt'), existing?.text || '')
|
|
160
|
+
if (text == null) return
|
|
161
|
+
const n = (name || '').trim()
|
|
162
|
+
if (!n) return toast(t('presets.nameEmpty'), 'err')
|
|
163
|
+
if (n.length > PRESET_NAME_MAX) return toast(t('presets.nameTooLong'), 'err')
|
|
164
|
+
if (text.length > PRESET_TEXT_MAX) return toast(t('presets.textTooLong'), 'err')
|
|
165
|
+
if (existing) {
|
|
166
|
+
existing.name = n
|
|
167
|
+
existing.text = text
|
|
168
|
+
} else {
|
|
169
|
+
if (list.length >= PRESET_LIMIT) return toast(t('presets.limit'), 'err')
|
|
170
|
+
list.push({ id: 'p' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), name: n, text })
|
|
171
|
+
}
|
|
172
|
+
writePresets(list)
|
|
173
|
+
toast(existing ? t('presets.saved') : t('presets.added'), 'ok')
|
|
174
|
+
}
|
|
175
|
+
function addPreset() { promptPreset(null) }
|
|
176
|
+
function editPreset(id) { promptPreset(id) }
|
|
177
|
+
function deletePreset(id) {
|
|
178
|
+
const list = readPresets()
|
|
179
|
+
const p = list.find(x => x.id === id)
|
|
180
|
+
if (!p) return
|
|
181
|
+
if (!confirm(t('presets.confirmDelete', { name: p.name }))) return
|
|
182
|
+
writePresets(list.filter(x => x.id !== id))
|
|
183
|
+
toast(t('presets.deleted'), 'ok')
|
|
184
|
+
}
|
|
185
|
+
function openPresetModal() {
|
|
186
|
+
renderPresets()
|
|
187
|
+
const modal = $('modal-presets')
|
|
188
|
+
if (modal) modal.classList.remove('hidden')
|
|
189
|
+
}
|
|
190
|
+
function closePresetModal() {
|
|
191
|
+
const modal = $('modal-presets')
|
|
192
|
+
if (modal) modal.classList.add('hidden')
|
|
193
|
+
}
|
|
194
|
+
function togglePresetMenuDesktop() {
|
|
195
|
+
const menu = $('preset-menu')
|
|
196
|
+
if (!menu) return
|
|
197
|
+
menu.classList.toggle('hidden')
|
|
198
|
+
}
|
|
199
|
+
function toggleCmdMenuDesktop() {
|
|
200
|
+
const menu = $('cmd-menu')
|
|
201
|
+
if (!menu) return
|
|
202
|
+
menu.classList.toggle('hidden')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ---------------- 模型 / 思考深度 ---------------- */
|
|
206
|
+
function toggleModelMenuDesktop() {
|
|
207
|
+
const menu = $('model-menu')
|
|
208
|
+
if (!menu) return
|
|
209
|
+
const willOpen = menu.classList.contains('hidden')
|
|
210
|
+
menu.classList.toggle('hidden', !willOpen)
|
|
211
|
+
if (willOpen && !state.models.loaded && !state.models.loading) loadSessionModels()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function loadSessionModels() {
|
|
215
|
+
if (!state.current || state.models.loading) return
|
|
216
|
+
state.models.loading = true
|
|
217
|
+
renderModelMenu()
|
|
218
|
+
try {
|
|
219
|
+
const v = await rpc('session.models', { sessionId: state.current })
|
|
220
|
+
state.models.groups = v.groups || []
|
|
221
|
+
state.models.current = v.current || null
|
|
222
|
+
state.models.failures = v.failures || []
|
|
223
|
+
state.models.loaded = true
|
|
224
|
+
} catch (e) {
|
|
225
|
+
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); state.models.loading = false; renderModelMenu(); return }
|
|
226
|
+
toast(t('models.loadFailed', { msg: e.message }), 'err')
|
|
227
|
+
}
|
|
228
|
+
state.models.loading = false
|
|
229
|
+
renderModelMenu()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderModelMenu() {
|
|
233
|
+
const box = $('model-menu')
|
|
234
|
+
if (!box) return
|
|
235
|
+
if (state.models.loading) { box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div><span>${t('models.loading')}</span>`; return }
|
|
236
|
+
const groups = state.models.groups || []
|
|
237
|
+
if (!groups.length) {
|
|
238
|
+
box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div><span>${(state.models.failures || []).map(f => f.name + ' ' + t('models.unavailable')).join(';') || t('models.none')}</span>`
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
const cur = state.models.current
|
|
242
|
+
box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div>` + groups.map(g => `
|
|
243
|
+
<div class="ds-model-group">
|
|
244
|
+
<div class="ds-model-provider">${esc(g.name || g.id)}</div>
|
|
245
|
+
<div class="ds-model-chips">${(g.models || []).map(m => {
|
|
246
|
+
const isCur = cur && cur.provider === g.id && cur.model === m.id
|
|
247
|
+
return `<button class="ds-model-chip ${isCur ? 'current' : ''}" data-model="${esc(m.id)}" data-provider="${esc(g.id)}">${esc(m.name || m.id)}</button>`
|
|
248
|
+
}).join('')}</div>
|
|
249
|
+
</div>`).join('') + `<div class="ds-model-effort-group" id="model-effort-group"><div class="ds-model-provider">${t('menu.effortTitle')}</div><div class="ds-model-chips" id="model-efforts"></div></div>`
|
|
250
|
+
box.querySelectorAll('[data-model]').forEach(btn =>
|
|
251
|
+
btn.addEventListener('click', () => selectSessionModel(btn.dataset.provider, btn.dataset.model)))
|
|
252
|
+
renderEffortMenu()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderEffortMenu() {
|
|
256
|
+
const group = $('model-effort-group')
|
|
257
|
+
const box = $('model-efforts')
|
|
258
|
+
if (!group || !box) return
|
|
259
|
+
const cur = state.models.current
|
|
260
|
+
const provider = (state.models.groups || []).find(g => g.id === cur?.provider)
|
|
261
|
+
const model = (provider?.models || []).find(m => m.id === cur?.model)
|
|
262
|
+
const efforts = model?.reasoning?.efforts || []
|
|
263
|
+
group.classList.toggle('hidden', !efforts.length)
|
|
264
|
+
box.innerHTML = efforts.map(e => {
|
|
265
|
+
const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === model.reasoning.defaultEffort)
|
|
266
|
+
return `<button class="ds-model-chip ${isCur ? 'current' : ''}" data-effort="${esc(e.id)}" title="${esc(e.description || '')}">${esc(e.name || e.id)}</button>`
|
|
267
|
+
}).join('')
|
|
268
|
+
box.querySelectorAll('[data-effort]').forEach(btn =>
|
|
269
|
+
btn.addEventListener('click', () => selectSessionEffort(btn.dataset.effort)))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function selectSessionEffort(effortId) {
|
|
273
|
+
const cur = state.models.current
|
|
274
|
+
if (!state.current || !cur) return
|
|
275
|
+
const v = await safeRpc('session.selectModel', {
|
|
276
|
+
sessionId: state.current, provider: cur.provider, model: cur.model, reasoningEffort: effortId
|
|
277
|
+
}, t('models.effortFailed'))
|
|
278
|
+
if (v?.selected) {
|
|
279
|
+
state.models.current = v.selected
|
|
280
|
+
renderEffortMenu()
|
|
281
|
+
toast(t('models.effortSwitched', { effort: effortId }), 'ok')
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function selectSessionModel(provider, modelId) {
|
|
286
|
+
if (!state.current) return
|
|
287
|
+
const group = (state.models.groups || []).find(g => g.id === provider)
|
|
288
|
+
const model = (group?.models || []).find(m => m.id === modelId)
|
|
289
|
+
const payload = { sessionId: state.current, provider, model: modelId }
|
|
290
|
+
const effort = model?.reasoning?.defaultEffort || model?.reasoning?.efforts?.[0]?.id
|
|
291
|
+
if (effort) payload.reasoningEffort = effort
|
|
292
|
+
const v = await safeRpc('session.selectModel', payload, t('models.switchFailed'))
|
|
293
|
+
if (v?.selected) {
|
|
294
|
+
state.models.current = v.selected
|
|
295
|
+
renderModelMenu()
|
|
296
|
+
toast(t('models.switched', { model: v.selected.model }), 'ok')
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
99
300
|
/* ---------------- 反馈 ---------------- */
|
|
100
301
|
const FEEDBACK_LINKS = {
|
|
101
302
|
githubIssues: 'https://github.com/Blank-not-black/dsh-Remote/issues',
|
|
@@ -134,6 +335,120 @@ function openFeedbackModal() {
|
|
|
134
335
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
135
336
|
}
|
|
136
337
|
function closeFeedbackModal() { $('modal-feedback').classList.add('hidden') }
|
|
338
|
+
function openDonateModal() {
|
|
339
|
+
const m = $('modal-donate')
|
|
340
|
+
if (m) m.classList.remove('hidden')
|
|
341
|
+
}
|
|
342
|
+
/* ---------------- 更新内容弹窗 ---------------- */
|
|
343
|
+
const NOTES_KEY = 'seenNotesVersion'
|
|
344
|
+
let notesVersion = ''
|
|
345
|
+
let notesPages = []
|
|
346
|
+
let notesPage = 0
|
|
347
|
+
function parseVersion(v) {
|
|
348
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(v || '').trim())
|
|
349
|
+
if (!m) return { core: [0, 0, 0], pre: null }
|
|
350
|
+
return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] || null }
|
|
351
|
+
}
|
|
352
|
+
function cmpVersion(a, b) {
|
|
353
|
+
const pa = parseVersion(a), pb = parseVersion(b)
|
|
354
|
+
for (let i = 0; i < 3; i++) {
|
|
355
|
+
const d = pa.core[i] - pb.core[i]
|
|
356
|
+
if (d) return d
|
|
357
|
+
}
|
|
358
|
+
if (!pa.pre && !pb.pre) return 0
|
|
359
|
+
if (!pa.pre) return 1
|
|
360
|
+
if (!pb.pre) return -1
|
|
361
|
+
const sa = String(pa.pre).split('.'), sb = String(pb.pre).split('.')
|
|
362
|
+
for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
|
|
363
|
+
const x = sa[i] ?? '', y = sb[i] ?? ''
|
|
364
|
+
if (x === y) continue
|
|
365
|
+
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y)
|
|
366
|
+
if (nx && ny) { const d = Number(x) - Number(y); if (d) return d }
|
|
367
|
+
else if (nx !== ny) return nx ? -1 : 1
|
|
368
|
+
else { const d = x.localeCompare(y); if (d) return d }
|
|
369
|
+
}
|
|
370
|
+
return 0
|
|
371
|
+
}
|
|
372
|
+
function splitNotes(notes) {
|
|
373
|
+
return String(notes || '').split(/[;;]/).map(s => s.trim()).filter(Boolean)
|
|
374
|
+
}
|
|
375
|
+
function renderNotesPages(items) {
|
|
376
|
+
const box = $('notes-pages')
|
|
377
|
+
if (!box) return
|
|
378
|
+
const pages = []
|
|
379
|
+
for (let i = 0; i < items.length; i += 3) pages.push(items.slice(i, i + 3))
|
|
380
|
+
notesPages = pages
|
|
381
|
+
notesPage = 0
|
|
382
|
+
box.innerHTML = pages.map(page => `<div class="notes-page" style="flex:0 0 100%;scroll-snap-align:start;box-sizing:border-box;min-width:0;">${page.map(item => `<div class="notes-item" style="padding:6px 0;line-height:1.5;">${esc(item)}</div>`).join('')}</div>`).join('')
|
|
383
|
+
box.scrollLeft = 0
|
|
384
|
+
updateNotesPage()
|
|
385
|
+
}
|
|
386
|
+
function renderNotesVersionPages(entries) {
|
|
387
|
+
const box = $('notes-pages')
|
|
388
|
+
if (!box) return
|
|
389
|
+
notesPages = entries
|
|
390
|
+
notesPage = 0
|
|
391
|
+
box.innerHTML = entries.map(entry => {
|
|
392
|
+
const items = splitNotes(entry.notes)
|
|
393
|
+
return `<div class="notes-page" style="flex:0 0 100%;scroll-snap-align:start;box-sizing:border-box;min-width:0;">
|
|
394
|
+
<div class="notes-version-title" style="font-weight:700;margin-bottom:6px;opacity:.9;">v${esc(entry.version)}</div>
|
|
395
|
+
${items.length ? items.map(item => `<div class="notes-item" style="padding:6px 0;line-height:1.5;">${esc(item)}</div>`).join('') : `<div class="notes-item" style="padding:6px 0;line-height:1.5;">${esc(entry.notes || '')}</div>`}
|
|
396
|
+
</div>`
|
|
397
|
+
}).join('')
|
|
398
|
+
box.scrollLeft = 0
|
|
399
|
+
updateNotesPage()
|
|
400
|
+
}
|
|
401
|
+
function updateNotesPage() {
|
|
402
|
+
const box = $('notes-pages')
|
|
403
|
+
const pageEl = $('notes-page')
|
|
404
|
+
if (!box || !pageEl) return
|
|
405
|
+
const total = notesPages.length || 1
|
|
406
|
+
const idx = Math.min(Math.max(0, Math.round(box.scrollLeft / Math.max(1, box.clientWidth))), total - 1)
|
|
407
|
+
notesPage = idx
|
|
408
|
+
pageEl.textContent = t('ds.notesPage', { current: idx + 1, total })
|
|
409
|
+
}
|
|
410
|
+
function scrollNotes(dir) {
|
|
411
|
+
const box = $('notes-pages')
|
|
412
|
+
if (box) box.scrollBy({ left: dir * box.clientWidth, behavior: 'smooth' })
|
|
413
|
+
}
|
|
414
|
+
function openNotesModal(info) {
|
|
415
|
+
if (!info?.version) return
|
|
416
|
+
const history = Array.isArray(info.history) ? info.history.filter(h => h && typeof h.version === 'string' && typeof h.notes === 'string' && !String(h.version).includes('-rc')) : []
|
|
417
|
+
const latestStable = history[0]?.version || info.version
|
|
418
|
+
if (history.length) {
|
|
419
|
+
const seen = LS.get(NOTES_KEY, '')
|
|
420
|
+
let entries
|
|
421
|
+
if (seen) entries = history.filter(h => cmpVersion(h.version, seen) > 0)
|
|
422
|
+
else entries = history.slice(0, 3)
|
|
423
|
+
if (!entries.length) return
|
|
424
|
+
notesVersion = latestStable
|
|
425
|
+
const vEl = $('notes-version')
|
|
426
|
+
if (vEl) vEl.textContent = 'v' + latestStable
|
|
427
|
+
renderNotesVersionPages(entries.slice().reverse())
|
|
428
|
+
$('modal-notes').classList.remove('hidden')
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
if (String(info.version).includes('-rc')) return
|
|
432
|
+
if (LS.get(NOTES_KEY) === info.version) return
|
|
433
|
+
const items = splitNotes(info.notes)
|
|
434
|
+
if (!items.length) return
|
|
435
|
+
notesVersion = info.version
|
|
436
|
+
const vEl = $('notes-version')
|
|
437
|
+
if (vEl) vEl.textContent = 'v' + info.version
|
|
438
|
+
renderNotesPages(items)
|
|
439
|
+
$('modal-notes').classList.remove('hidden')
|
|
440
|
+
}
|
|
441
|
+
function closeNotesModal() {
|
|
442
|
+
$('modal-notes').classList.add('hidden')
|
|
443
|
+
if (notesVersion) { LS.set(NOTES_KEY, notesVersion); notesVersion = '' }
|
|
444
|
+
}
|
|
445
|
+
async function checkNotesOnStart() {
|
|
446
|
+
try {
|
|
447
|
+
const res = await fetch('../update.json?t=' + Date.now())
|
|
448
|
+
if (!res.ok) return
|
|
449
|
+
openNotesModal(await res.json())
|
|
450
|
+
} catch {}
|
|
451
|
+
}
|
|
137
452
|
async function submitFeedback() {
|
|
138
453
|
const type = document.querySelector('#fb-chips .ds-fb-chip.current')?.dataset.fbType || 'bug'
|
|
139
454
|
const message = $('fb-msg').value.trim()
|
|
@@ -637,11 +952,23 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
637
952
|
s.projections.values[key] = value
|
|
638
953
|
s.projections.asOfSeq = Math.max(s.projections.asOfSeq || 0, seq || 0)
|
|
639
954
|
}
|
|
640
|
-
if (state.current === sessionId)
|
|
955
|
+
if (state.current === sessionId) {
|
|
956
|
+
renderSessions()
|
|
957
|
+
if (['goal', 'todos'].includes(key)) renderSessionCards()
|
|
958
|
+
}
|
|
641
959
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) refreshSessions()
|
|
642
960
|
}
|
|
643
961
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
644
962
|
function titleOf(s) { return proj(s, 'title') || short(s.sessionId) }
|
|
963
|
+
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
964
|
+
function isGoalTerminal(goal) {
|
|
965
|
+
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
966
|
+
}
|
|
967
|
+
function goalOf(s) {
|
|
968
|
+
const p = proj(s, 'goal')
|
|
969
|
+
if (!p) return null
|
|
970
|
+
return p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
971
|
+
}
|
|
645
972
|
function onSessionEvent(sessionId, event) {
|
|
646
973
|
if (state.current === sessionId && event) {
|
|
647
974
|
const h = state.history
|
|
@@ -652,6 +979,7 @@ function onSessionEvent(sessionId, event) {
|
|
|
652
979
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
653
980
|
renderHistory()
|
|
654
981
|
}
|
|
982
|
+
if (String(event.type || '').startsWith('goal/') || String(event.type || '').startsWith('todo/')) renderSessionCards()
|
|
655
983
|
}
|
|
656
984
|
}
|
|
657
985
|
|
|
@@ -680,15 +1008,19 @@ function renderSessions() {
|
|
|
680
1008
|
async function openSession(id) {
|
|
681
1009
|
state.current = id
|
|
682
1010
|
state.history = { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity }
|
|
1011
|
+
state.models = { loaded: false, loading: false, groups: [], current: null, failures: [] }
|
|
683
1012
|
showView('view-chat')
|
|
684
1013
|
$('ds-title').textContent = titleOf(state.byId.get(id)) || t('ds.sessions')
|
|
685
1014
|
$('history').innerHTML = `<div class="ds-empty">${t('ds.historyLoading')}</div>`
|
|
686
1015
|
renderSessions()
|
|
1016
|
+
renderSessionCards()
|
|
687
1017
|
await loadHistory()
|
|
688
1018
|
}
|
|
689
1019
|
function closeSession() {
|
|
690
1020
|
state.current = null
|
|
691
1021
|
state.history = { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity }
|
|
1022
|
+
const cards = $('session-cards')
|
|
1023
|
+
if (cards) cards.innerHTML = ''
|
|
692
1024
|
showView('view-sessions')
|
|
693
1025
|
}
|
|
694
1026
|
async function loadHistory() {
|
|
@@ -735,6 +1067,13 @@ function blockHtml(b) {
|
|
|
735
1067
|
if (b.type === 'image') return `<div>🖼</div>`
|
|
736
1068
|
return ''
|
|
737
1069
|
}
|
|
1070
|
+
function systemReminderText(blocks) {
|
|
1071
|
+
if (!Array.isArray(blocks)) return ''
|
|
1072
|
+
return blocks
|
|
1073
|
+
.filter(b => b && typeof b === 'object' && b.type === 'text' && String(b.text ?? '').trimStart().startsWith('<system-reminder>'))
|
|
1074
|
+
.map(b => String(b.text ?? ''))
|
|
1075
|
+
.join('\n')
|
|
1076
|
+
}
|
|
738
1077
|
function eventHtml(entry) {
|
|
739
1078
|
const ev = entry.event || {}
|
|
740
1079
|
const data = ev.data || {}
|
|
@@ -744,6 +1083,11 @@ function eventHtml(entry) {
|
|
|
744
1083
|
const msg = data.message || {}
|
|
745
1084
|
const role = data.role || msg.role || (type.startsWith('user') ? 'user' : 'assistant')
|
|
746
1085
|
const blocks = msg.content || data.content || []
|
|
1086
|
+
const sysText = type === 'user/message' ? systemReminderText(blocks) : ''
|
|
1087
|
+
if (sysText) {
|
|
1088
|
+
const shown = sysText.length > 400 ? sysText.slice(0, 400) + '…' : sysText
|
|
1089
|
+
return `<details class="event ds-tool"><summary>${esc(t('ds.eventSystemReminder'))}</summary><pre>${esc(shown)}</pre></details>`
|
|
1090
|
+
}
|
|
747
1091
|
const text = blocks.map(blockHtml).join('')
|
|
748
1092
|
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>`
|
|
749
1093
|
}
|
|
@@ -767,10 +1111,124 @@ function renderHistory() {
|
|
|
767
1111
|
box.innerHTML = items.map(eventHtml).join('') || `<div class="ds-empty">${t('ds.historyEmpty')}</div>`
|
|
768
1112
|
box.scrollTop = box.scrollHeight
|
|
769
1113
|
}
|
|
1114
|
+
|
|
1115
|
+
/* ---------------- 会话信息卡(goal / todo / 子代理) ---------------- */
|
|
1116
|
+
async function renderSessionCards() {
|
|
1117
|
+
const box = $('session-cards')
|
|
1118
|
+
const s = state.byId.get(state.current)
|
|
1119
|
+
if (!box) return
|
|
1120
|
+
if (!s) { box.innerHTML = ''; return }
|
|
1121
|
+
const goal = goalOf(s)
|
|
1122
|
+
const todos = proj(s, 'todos')
|
|
1123
|
+
let html = ''
|
|
1124
|
+
if (goal && !isGoalTerminal(goal)) {
|
|
1125
|
+
html += `<div class="ds-card ds-goal-card"><div class="ds-card-title">${t('goal.title')}</div>
|
|
1126
|
+
<div class="ds-goal-obj">${esc(goal.objective || '')}</div>
|
|
1127
|
+
<div class="ds-goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
|
|
1128
|
+
<div class="ds-goal-actions">
|
|
1129
|
+
${goal.phase === 'active' ? `<button class="ds-mini-btn" data-goal="pause">${t('goal.pause')}</button>` : `<button class="ds-mini-btn" data-goal="resume">${t('goal.resume')}</button>`}
|
|
1130
|
+
<button class="ds-mini-btn" data-goal="complete">${t('goal.complete')}</button>
|
|
1131
|
+
<button class="ds-mini-btn" data-goal="edit">${t('goal.edit')}</button>
|
|
1132
|
+
<button class="ds-mini-btn" data-goal="clear">${t('goal.clear')}</button>
|
|
1133
|
+
</div></div>`
|
|
1134
|
+
}
|
|
1135
|
+
if (todos?.items?.length) {
|
|
1136
|
+
html += `<div class="ds-card"><div class="ds-card-title">${t('todos.title')}</div>${todos.items.map(item =>
|
|
1137
|
+
`<div class="ds-todo-row"><span class="ds-pill ${item.status === 'completed' ? 'done' : item.status === 'in_progress' ? 'active' : ''}">${esc(item.status || 'pending')}</span><span>${esc(item.content || '')}</span></div>`
|
|
1138
|
+
).join('')}</div>`
|
|
1139
|
+
}
|
|
1140
|
+
box.innerHTML = html
|
|
1141
|
+
box.querySelectorAll('[data-goal]').forEach(btn =>
|
|
1142
|
+
btn.addEventListener('click', () => goalAction(btn.dataset.goal)))
|
|
1143
|
+
|
|
1144
|
+
const sub = await safeRpc('subagent.list', { parentSessionId: state.current }, '')
|
|
1145
|
+
if (sub?.entries?.length) {
|
|
1146
|
+
const rows = sub.entries.map(e => {
|
|
1147
|
+
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>`
|
|
1148
|
+
const label = e.label || short(e.id)
|
|
1149
|
+
const running = e.activity === 'running'
|
|
1150
|
+
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>`
|
|
1151
|
+
}).join('')
|
|
1152
|
+
box.insertAdjacentHTML('beforeend', `<div class="ds-card"><div class="ds-card-title">${t('subagent.title')}</div>${rows}</div>`)
|
|
1153
|
+
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
1154
|
+
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function setGoalPhaseLocal(phase) {
|
|
1159
|
+
const s = state.byId.get(state.current)
|
|
1160
|
+
const p = s && proj(s, 'goal')
|
|
1161
|
+
const goal = p && typeof p === 'object' && p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
1162
|
+
if (!goal) return
|
|
1163
|
+
goal.phase = phase
|
|
1164
|
+
renderSessions()
|
|
1165
|
+
renderSessionCards()
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async function goalAction(kind) {
|
|
1169
|
+
const s = state.byId.get(state.current)
|
|
1170
|
+
const goal = goalOf(s)
|
|
1171
|
+
if (!goal) return toast(t('goal.none'), 'err')
|
|
1172
|
+
const ref = { id: goal.id, revision: goal.revision }
|
|
1173
|
+
if (kind === 'edit') {
|
|
1174
|
+
const objective = prompt(t('goal.editPrompt'), goal.objective || '')
|
|
1175
|
+
if (objective === null) return
|
|
1176
|
+
if (!objective.trim()) return toast(t('goal.cannotEmpty'), 'err')
|
|
1177
|
+
await safeRpc('goal.edit', { sessionId: state.current, ref, objective: objective.trim() }, t('goal.updateFailed'))
|
|
1178
|
+
toast(t('goal.updated'), 'ok')
|
|
1179
|
+
refreshSessions()
|
|
1180
|
+
renderSessionCards()
|
|
1181
|
+
return
|
|
1182
|
+
}
|
|
1183
|
+
const map = { pause: 'goal.pause', resume: 'goal.resume', complete: 'goal.complete', clear: 'goal.clear' }
|
|
1184
|
+
const method = map[kind]
|
|
1185
|
+
if (!method) return
|
|
1186
|
+
if (kind === 'clear' && !confirm(t('goal.confirmClear'))) return
|
|
1187
|
+
if (kind === 'complete' && !confirm(t('goal.confirmComplete'))) return
|
|
1188
|
+
await safeRpc(method, { sessionId: state.current, ref }, t('goal.actionFailed'))
|
|
1189
|
+
if (kind === 'complete') setGoalPhaseLocal('complete')
|
|
1190
|
+
if (kind === 'clear') setGoalPhaseLocal('cleared')
|
|
1191
|
+
toast(t('goal.actionSubmitted'), 'ok')
|
|
1192
|
+
refreshSessions()
|
|
1193
|
+
renderSessionCards()
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function interruptSubagent(childId) {
|
|
1197
|
+
if (!confirm(t('subagent.confirmInterrupt'))) return
|
|
1198
|
+
await safeRpc('subagent.interrupt', { parentSessionId: state.current, childSessionId: childId, mode: 'continuable' }, t('subagent.interruptFailed'))
|
|
1199
|
+
toast(t('subagent.interruptSubmitted'), 'ok')
|
|
1200
|
+
setTimeout(renderSessionCards, 600)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
async function runSlashCommand(text) {
|
|
1204
|
+
const clean = String(text || '').trim()
|
|
1205
|
+
if (!clean.startsWith('/') || !state.current) return false
|
|
1206
|
+
try {
|
|
1207
|
+
const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
1208
|
+
? AbortSignal.timeout(20000)
|
|
1209
|
+
: undefined
|
|
1210
|
+
const res = await fetch(apiUrl('/remote/api/command'), {
|
|
1211
|
+
method: 'POST',
|
|
1212
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
1213
|
+
body: JSON.stringify({ sessionId: state.current, line: clean }),
|
|
1214
|
+
...(signal ? { signal } : {})
|
|
1215
|
+
})
|
|
1216
|
+
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return true }
|
|
1217
|
+
if (!res.ok) return false
|
|
1218
|
+
const data = await res.json().catch(() => null)
|
|
1219
|
+
if (data?.ok === false) return true
|
|
1220
|
+
return data?.ok === true && data.executed === true
|
|
1221
|
+
} catch (e) {
|
|
1222
|
+
console.error('slash command bridge failed', e)
|
|
1223
|
+
}
|
|
1224
|
+
return false
|
|
1225
|
+
}
|
|
1226
|
+
|
|
770
1227
|
async function sendMessage() {
|
|
771
1228
|
const input = $('composer')
|
|
772
1229
|
const text = input.value.trim()
|
|
773
1230
|
if (!text || !state.current) return
|
|
1231
|
+
if (await runSlashCommand(text)) { input.value = ''; return }
|
|
774
1232
|
input.value = ''
|
|
775
1233
|
const v = await safeRpc('session.prompt', { sessionId: state.current, text }, '')
|
|
776
1234
|
if (v) toast(t('ds.toastSent'), 'ok')
|
|
@@ -993,6 +1451,22 @@ function showView(id) {
|
|
|
993
1451
|
if (id === 'view-chat') { const s = state.byId.get(state.current); $('ds-title').textContent = s ? titleOf(s) : t('ds.sessions') }
|
|
994
1452
|
else $('ds-title').textContent = t(titles[id])
|
|
995
1453
|
if (id === 'view-files' && !state.fs.loaded) loadFs(null, true)
|
|
1454
|
+
if (id === 'view-settings') showSettingsHome()
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const SETTINGS_GROUPS = ['general', 'servers', 'theme', 'about']
|
|
1458
|
+
function showSettingsHome() {
|
|
1459
|
+
const home = $('settings-home')
|
|
1460
|
+
if (!home) return
|
|
1461
|
+
home.classList.remove('hidden')
|
|
1462
|
+
for (const name of SETTINGS_GROUPS) $('settings-page-' + name)?.classList.add('hidden')
|
|
1463
|
+
}
|
|
1464
|
+
function showSettingsPage(name) {
|
|
1465
|
+
const home = $('settings-home')
|
|
1466
|
+
if (!home || !SETTINGS_GROUPS.includes(name)) return
|
|
1467
|
+
home.classList.add('hidden')
|
|
1468
|
+
for (const g of SETTINGS_GROUPS) $('settings-page-' + g)?.classList.toggle('hidden', g !== name)
|
|
1469
|
+
if (name === 'general') renderPresetSummary()
|
|
996
1470
|
}
|
|
997
1471
|
function updateConn() {
|
|
998
1472
|
const el = $('conn-badge')
|
|
@@ -1033,8 +1507,52 @@ function bindUi() {
|
|
|
1033
1507
|
$('composer').addEventListener('keydown', (e) => {
|
|
1034
1508
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage() }
|
|
1035
1509
|
})
|
|
1510
|
+
renderPresetMenuDesktop()
|
|
1511
|
+
renderPresetSummary()
|
|
1512
|
+
$('btn-cmd').addEventListener('click', (e) => { e.stopPropagation(); toggleCmdMenuDesktop() })
|
|
1513
|
+
$('cmd-menu').addEventListener('click', (e) => {
|
|
1514
|
+
const item = e.target.closest('[data-ds-cmd]')
|
|
1515
|
+
if (item) {
|
|
1516
|
+
const input = $('composer')
|
|
1517
|
+
input.value = item.dataset.dsCmd + ' '
|
|
1518
|
+
input.focus()
|
|
1519
|
+
$('cmd-menu').classList.add('hidden')
|
|
1520
|
+
}
|
|
1521
|
+
})
|
|
1522
|
+
$('btn-preset').addEventListener('click', (e) => { e.stopPropagation(); togglePresetMenuDesktop() })
|
|
1523
|
+
$('preset-menu').addEventListener('click', (e) => {
|
|
1524
|
+
const item = e.target.closest('[data-ds-preset]')
|
|
1525
|
+
if (item) {
|
|
1526
|
+
const found = readPresets().find(x => x.id === item.dataset.dsPreset)
|
|
1527
|
+
if (found) {
|
|
1528
|
+
const input = $('composer')
|
|
1529
|
+
input.value = found.text
|
|
1530
|
+
input.focus()
|
|
1531
|
+
}
|
|
1532
|
+
$('preset-menu').classList.add('hidden')
|
|
1533
|
+
}
|
|
1534
|
+
})
|
|
1535
|
+
$('btn-model').addEventListener('click', (e) => { e.stopPropagation(); toggleModelMenuDesktop() })
|
|
1536
|
+
$('model-menu').addEventListener('click', (e) => {
|
|
1537
|
+
if (e.target.closest('[data-model]') || e.target.closest('[data-effort]')) e.stopPropagation()
|
|
1538
|
+
})
|
|
1539
|
+
document.addEventListener('click', (e) => {
|
|
1540
|
+
if (!e.target.closest('#preset-menu') && !e.target.closest('#btn-preset')) $('preset-menu')?.classList.add('hidden')
|
|
1541
|
+
if (!e.target.closest('#cmd-menu') && !e.target.closest('#btn-cmd')) $('cmd-menu')?.classList.add('hidden')
|
|
1542
|
+
if (!e.target.closest('#model-menu') && !e.target.closest('#btn-model')) $('model-menu')?.classList.add('hidden')
|
|
1543
|
+
})
|
|
1036
1544
|
$('btn-stats-top').addEventListener('click', toggleStatsDrawer)
|
|
1037
1545
|
$('stats-drawer-close').addEventListener('click', toggleStatsDrawer)
|
|
1546
|
+
// 赞赏支持
|
|
1547
|
+
$('btn-donate').addEventListener('click', openDonateModal)
|
|
1548
|
+
$('btn-donate-about').addEventListener('click', openDonateModal)
|
|
1549
|
+
$('btn-donate-close').addEventListener('click', () => $('modal-donate').classList.add('hidden'))
|
|
1550
|
+
// 更新内容弹窗
|
|
1551
|
+
$('notes-close').addEventListener('click', closeNotesModal)
|
|
1552
|
+
$('notes-prev').addEventListener('click', () => scrollNotes(-1))
|
|
1553
|
+
$('notes-next').addEventListener('click', () => scrollNotes(1))
|
|
1554
|
+
$('notes-pages').addEventListener('scroll', updateNotesPage)
|
|
1555
|
+
$('modal-notes').addEventListener('click', (e) => { if (e.target === $('modal-notes')) closeNotesModal() })
|
|
1038
1556
|
// 反馈
|
|
1039
1557
|
$('btn-feedback').addEventListener('click', (e) => { e.stopPropagation(); toggleFeedbackMenu() })
|
|
1040
1558
|
$('feedback-menu').addEventListener('click', (e) => {
|
|
@@ -1065,6 +1583,15 @@ function bindUi() {
|
|
|
1065
1583
|
})
|
|
1066
1584
|
$('stats-chart').addEventListener('mouseleave', hideTip)
|
|
1067
1585
|
|
|
1586
|
+
$('view-settings').addEventListener('click', (e) => {
|
|
1587
|
+
const group = e.target.closest('[data-settings-group]')
|
|
1588
|
+
if (group) { showSettingsPage(group.dataset.settingsGroup); return }
|
|
1589
|
+
if (e.target.closest('[data-settings-back]')) { showSettingsHome(); return }
|
|
1590
|
+
})
|
|
1591
|
+
$('btn-manage-presets').addEventListener('click', openPresetModal)
|
|
1592
|
+
$('btn-preset-add').addEventListener('click', addPreset)
|
|
1593
|
+
$('btn-presets-close').addEventListener('click', closePresetModal)
|
|
1594
|
+
$('modal-presets').addEventListener('click', (e) => { if (e.target === $('modal-presets')) closePresetModal() })
|
|
1068
1595
|
$('btn-server-speed').addEventListener('click', () => selectFastestServer({ silent: false }))
|
|
1069
1596
|
$('btn-server-add').addEventListener('click', addServer)
|
|
1070
1597
|
$('btn-group-add').addEventListener('click', addGroup)
|
|
@@ -1105,6 +1632,7 @@ async function start() {
|
|
|
1105
1632
|
$('token-desc').textContent = state.token ? '● ' + state.token.slice(0, 12) + '…' : t('ds.toastAuth')
|
|
1106
1633
|
bindUi()
|
|
1107
1634
|
updateConn()
|
|
1635
|
+
checkNotesOnStart()
|
|
1108
1636
|
if (state.token) {
|
|
1109
1637
|
if (state.servers.length) await selectFastestServer({ silent: true, reconnect: false })
|
|
1110
1638
|
openStreams()
|
|
Binary file
|