dsh-remote-plugin 0.6.5 → 0.6.7
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/README.en.md +1 -1
- package/README.md +1 -1
- package/apk/dsh-remote.apk +0 -0
- package/gateway-stats.cjs +9 -2
- package/gateway.cjs +292 -7
- package/index.mjs +61 -12
- package/package.json +1 -1
- package/public/app.js +647 -54
- package/public/desktop/desktop.css +79 -5
- package/public/desktop/desktop.html +75 -7
- package/public/desktop/desktop.js +418 -22
- package/public/desktop/i18n.js +33 -1
- package/public/index.html +108 -13
- package/public/styles.css +82 -4
- package/public/update.json +12 -4
- package/public/version.json +1 -1
|
@@ -49,6 +49,7 @@ const state = {
|
|
|
49
49
|
serverLatency: {},
|
|
50
50
|
selectingServer: false,
|
|
51
51
|
sessions: [],
|
|
52
|
+
sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
|
|
52
53
|
byId: new Map(),
|
|
53
54
|
current: null,
|
|
54
55
|
history: { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity },
|
|
@@ -61,6 +62,8 @@ const state = {
|
|
|
61
62
|
pollSeq: { mux: 0, host: 0 },
|
|
62
63
|
fs: { path: null, initial: null, loaded: false },
|
|
63
64
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
65
|
+
wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
|
|
66
|
+
archivedIds: [],
|
|
64
67
|
view: 'sessions'
|
|
65
68
|
}
|
|
66
69
|
const streams = {}
|
|
@@ -495,12 +498,16 @@ function hideTip() { const tip = $('ds-tip'); if (tip) tip.classList.add('hidden
|
|
|
495
498
|
|
|
496
499
|
/* ---------------- API ---------------- */
|
|
497
500
|
function apiUrl(path) { return (state.server || '') + path }
|
|
498
|
-
async function rpc(method, payload = {}) {
|
|
499
|
-
const
|
|
501
|
+
async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
502
|
+
const opts = {
|
|
500
503
|
method: 'POST',
|
|
501
504
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
502
505
|
body: JSON.stringify({ type: 'client-request', rpcId: uuid(), method, payload })
|
|
503
|
-
}
|
|
506
|
+
}
|
|
507
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
508
|
+
opts.signal = AbortSignal.timeout(timeoutMs)
|
|
509
|
+
}
|
|
510
|
+
const res = await fetch(apiUrl('/api/' + method), opts)
|
|
504
511
|
if (res.status === 401) throw new Error('AUTH')
|
|
505
512
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
506
513
|
const full = await res.json()
|
|
@@ -509,11 +516,15 @@ async function rpc(method, payload = {}) {
|
|
|
509
516
|
return full.result.value
|
|
510
517
|
}
|
|
511
518
|
async function respond(rpcId, value) {
|
|
512
|
-
const
|
|
519
|
+
const opts = {
|
|
513
520
|
method: 'POST',
|
|
514
521
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
515
522
|
body: JSON.stringify({ type: 'client-response', rpcId, result: { ok: true, value } })
|
|
516
|
-
}
|
|
523
|
+
}
|
|
524
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
525
|
+
opts.signal = AbortSignal.timeout(15000)
|
|
526
|
+
}
|
|
527
|
+
const res = await fetch(apiUrl('/api/respond'), opts)
|
|
517
528
|
if (res.status === 401) throw new Error('AUTH')
|
|
518
529
|
const receipt = await res.json()
|
|
519
530
|
return receipt?.accepted === true
|
|
@@ -610,7 +621,8 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
|
610
621
|
let best = null
|
|
611
622
|
let ms = Infinity
|
|
612
623
|
if (state.autoSelect[state.activeGroup] !== false) {
|
|
613
|
-
|
|
624
|
+
const measured = await Promise.all(candidates.map(async (u) => [u, await pingServer(u)]))
|
|
625
|
+
for (const [u, latency] of measured) state.serverLatency[u] = latency
|
|
614
626
|
best = candidates.filter(u => Number.isFinite(state.serverLatency[u])).sort((a, b) => state.serverLatency[a] - state.serverLatency[b])[0] || null
|
|
615
627
|
chosen = best || (state.server || '')
|
|
616
628
|
ms = best ? state.serverLatency[best] : Infinity
|
|
@@ -972,7 +984,12 @@ async function pollKind(kind) {
|
|
|
972
984
|
let data
|
|
973
985
|
try { data = await res.json() } catch { return }
|
|
974
986
|
if (!data || !Array.isArray(data.events)) return
|
|
975
|
-
|
|
987
|
+
const reset = data.truncated === true || (typeof data.latestSeq === 'number' && data.latestSeq < since)
|
|
988
|
+
if (reset) {
|
|
989
|
+
state.pollSeq[kind] = 0
|
|
990
|
+
if (kind === 'mux') renderNotifStack()
|
|
991
|
+
refreshSessions()
|
|
992
|
+
}
|
|
976
993
|
for (const item of data.events) {
|
|
977
994
|
if (item.seq > (state.pollSeq[kind] || 0)) {
|
|
978
995
|
state.pollSeq[kind] = item.seq
|
|
@@ -1056,7 +1073,7 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
1056
1073
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) refreshSessions()
|
|
1057
1074
|
}
|
|
1058
1075
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1059
|
-
function titleOf(s) { return proj(s, 'title') || short(s.sessionId) }
|
|
1076
|
+
function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('ds.sessions')) }
|
|
1060
1077
|
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
1061
1078
|
function isGoalTerminal(goal) {
|
|
1062
1079
|
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
@@ -1087,19 +1104,76 @@ async function refreshSessions() {
|
|
|
1087
1104
|
state.sessions = v.items || []
|
|
1088
1105
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1089
1106
|
renderSessions()
|
|
1107
|
+
scheduleWorkbenchRefresh()
|
|
1108
|
+
}
|
|
1109
|
+
function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
|
|
1110
|
+
function sessionWorkspaceLabel(s) {
|
|
1111
|
+
const cwd = sessionCwd(s)
|
|
1112
|
+
return cwd || t('ds.workspaceUnknown')
|
|
1113
|
+
}
|
|
1114
|
+
function workspaceDisplayName(label) {
|
|
1115
|
+
const value = String(label || '').trim()
|
|
1116
|
+
if (!value || value === t('ds.workspaceUnknown')) return value || t('ds.workspaceUnknown')
|
|
1117
|
+
const clean = value.replace(/[\\/]+$/, '')
|
|
1118
|
+
const parts = clean.split(/[\\/]/).filter(Boolean)
|
|
1119
|
+
return parts[parts.length - 1] || value
|
|
1120
|
+
}
|
|
1121
|
+
function sortedSessions() {
|
|
1122
|
+
const items = [...state.sessions]
|
|
1123
|
+
if (state.sessionSort === 'workspace') {
|
|
1124
|
+
return items.sort((a, b) => {
|
|
1125
|
+
const aw = sessionCwd(a) || '\uffff'
|
|
1126
|
+
const bw = sessionCwd(b) || '\uffff'
|
|
1127
|
+
const byWorkspace = aw.localeCompare(bw, undefined, { numeric: true, sensitivity: 'base' })
|
|
1128
|
+
return byWorkspace || ((b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1129
|
+
})
|
|
1130
|
+
}
|
|
1131
|
+
return items.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1090
1132
|
}
|
|
1091
1133
|
function renderSessions() {
|
|
1092
|
-
const
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1134
|
+
const allItems = sortedSessions()
|
|
1135
|
+
const wbIds = new Set()
|
|
1136
|
+
if (state.wb.bound && state.wb.projects) for (const w of state.wb.projects) for (const id of (w.sessionIds || [])) wbIds.add(id)
|
|
1137
|
+
const root = state.wb.bound ? state.wb.path : ''
|
|
1138
|
+
const visible = allItems.filter(s => !(state.wb.bound && (wbIds.has(s.sessionId) || wbStrictInside(s.cwd, root))))
|
|
1139
|
+
const archivedSet = new Set(state.archivedIds || [])
|
|
1140
|
+
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1141
|
+
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1142
|
+
const showArchived = LS.get('dsShowArchivedV1', '0') === '1'
|
|
1143
|
+
const renderItems = (items) => {
|
|
1144
|
+
let lastWorkspace = null
|
|
1145
|
+
const rows = []
|
|
1146
|
+
for (const s of items) {
|
|
1147
|
+
const workspace = sessionWorkspaceLabel(s)
|
|
1148
|
+
const workspaceName = workspaceDisplayName(workspace)
|
|
1149
|
+
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1150
|
+
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>`)
|
|
1151
|
+
lastWorkspace = workspace
|
|
1152
|
+
}
|
|
1153
|
+
const title = titleOf(s)
|
|
1154
|
+
rows.push(`<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1155
|
+
<span class="ds-session-title">${esc(title)}</span>
|
|
1156
|
+
<span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
|
|
1157
|
+
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(s.updatedAt)}</span>
|
|
1158
|
+
</button>`)
|
|
1159
|
+
}
|
|
1160
|
+
return rows.join('')
|
|
1161
|
+
}
|
|
1162
|
+
const divider = archived.length ? `<button class="ds-archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1163
|
+
const hiddenByWorkbench = allItems.length - visible.length
|
|
1164
|
+
const html = renderItems(main) + divider + (showArchived ? renderItems(archived) : '') || `<div class="ds-empty">${esc(hiddenByWorkbench ? t('wb.flatHidden', { n: hiddenByWorkbench }) : t('ds.sessionsEmpty'))}</div>`
|
|
1100
1165
|
$('session-list').innerHTML = html
|
|
1101
1166
|
$('mobile-session-list').innerHTML = html
|
|
1167
|
+
$('session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1168
|
+
$('mobile-session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1169
|
+
const sort = $('session-sort')
|
|
1170
|
+
if (sort) sort.value = state.sessionSort
|
|
1171
|
+
document.querySelectorAll('[data-archived-toggle]').forEach(b => b.addEventListener('click', () => {
|
|
1172
|
+
LS.set('dsShowArchivedV1', LS.get('dsShowArchivedV1', '0') === '1' ? '0' : '1')
|
|
1173
|
+
renderSessions()
|
|
1174
|
+
}))
|
|
1102
1175
|
document.querySelectorAll('[data-id]').forEach(b => b.addEventListener('click', () => openSession(b.dataset.id)))
|
|
1176
|
+
renderWorkbench()
|
|
1103
1177
|
}
|
|
1104
1178
|
|
|
1105
1179
|
async function openSession(id) {
|
|
@@ -1271,7 +1345,8 @@ async function goalAction(kind) {
|
|
|
1271
1345
|
const objective = prompt(t('goal.editPrompt'), goal.objective || '')
|
|
1272
1346
|
if (objective === null) return
|
|
1273
1347
|
if (!objective.trim()) return toast(t('goal.cannotEmpty'), 'err')
|
|
1274
|
-
await safeRpc('goal.edit', { sessionId: state.current, ref, objective: objective.trim() }, t('goal.updateFailed'))
|
|
1348
|
+
const result = await safeRpc('goal.edit', { sessionId: state.current, ref, objective: objective.trim() }, t('goal.updateFailed'))
|
|
1349
|
+
if (result == null) return
|
|
1275
1350
|
toast(t('goal.updated'), 'ok')
|
|
1276
1351
|
refreshSessions()
|
|
1277
1352
|
renderSessionCards()
|
|
@@ -1282,7 +1357,8 @@ async function goalAction(kind) {
|
|
|
1282
1357
|
if (!method) return
|
|
1283
1358
|
if (kind === 'clear' && !confirm(t('goal.confirmClear'))) return
|
|
1284
1359
|
if (kind === 'complete' && !confirm(t('goal.confirmComplete'))) return
|
|
1285
|
-
await safeRpc(method, { sessionId: state.current, ref }, t('goal.actionFailed'))
|
|
1360
|
+
const result = await safeRpc(method, { sessionId: state.current, ref }, t('goal.actionFailed'))
|
|
1361
|
+
if (result == null) return
|
|
1286
1362
|
if (kind === 'complete') setGoalPhaseLocal('complete')
|
|
1287
1363
|
if (kind === 'clear') setGoalPhaseLocal('cleared')
|
|
1288
1364
|
toast(t('goal.actionSubmitted'), 'ok')
|
|
@@ -1292,7 +1368,8 @@ async function goalAction(kind) {
|
|
|
1292
1368
|
|
|
1293
1369
|
async function interruptSubagent(childId) {
|
|
1294
1370
|
if (!confirm(t('subagent.confirmInterrupt'))) return
|
|
1295
|
-
await safeRpc('subagent.interrupt', { parentSessionId: state.current, childSessionId: childId, mode: 'continuable' }, t('subagent.interruptFailed'))
|
|
1371
|
+
const result = await safeRpc('subagent.interrupt', { parentSessionId: state.current, childSessionId: childId, mode: 'continuable' }, t('subagent.interruptFailed'))
|
|
1372
|
+
if (result == null) return
|
|
1296
1373
|
toast(t('subagent.interruptSubmitted'), 'ok')
|
|
1297
1374
|
setTimeout(renderSessionCards, 600)
|
|
1298
1375
|
}
|
|
@@ -1384,7 +1461,14 @@ function renderNotifStack() {
|
|
|
1384
1461
|
async function approveApproval(id, allow) {
|
|
1385
1462
|
const a = state.approvals.find(x => x.approvalId === id)
|
|
1386
1463
|
if (!a) return
|
|
1387
|
-
|
|
1464
|
+
let ok
|
|
1465
|
+
try {
|
|
1466
|
+
ok = await respond(a.rpcId, { sessionId: a.sessionId, approvalId: a.approvalId, outcome: allow ? 'allowed-once' : 'rejected' })
|
|
1467
|
+
} catch (e) {
|
|
1468
|
+
if (e.message === 'AUTH') toast(t('ds.toastAuth'), 'err')
|
|
1469
|
+
else toast(t('ds.pendingSubmitFailed', { msg: e.message || t('ds.feedbackNetworkError') }), 'err')
|
|
1470
|
+
return
|
|
1471
|
+
}
|
|
1388
1472
|
toast(ok ? (allow ? t('ds.allowed') : t('ds.rejected')) : t('ds.stale'), ok ? 'ok' : 'err')
|
|
1389
1473
|
state.approvals = state.approvals.filter(x => x.approvalId !== id)
|
|
1390
1474
|
renderNotifStack()
|
|
@@ -1413,7 +1497,14 @@ async function submitQuestion() {
|
|
|
1413
1497
|
return ans
|
|
1414
1498
|
}).filter(Boolean)
|
|
1415
1499
|
if (!answers.length) return toast(t('ds.questionNeedAnswer'), 'err')
|
|
1416
|
-
|
|
1500
|
+
let ok
|
|
1501
|
+
try {
|
|
1502
|
+
ok = await respond(q.rpcId, { sessionId: q.sessionId, answer: { answers } })
|
|
1503
|
+
} catch (e) {
|
|
1504
|
+
if (e.message === 'AUTH') toast(t('ds.toastAuth'), 'err')
|
|
1505
|
+
else toast(t('ds.pendingSubmitFailed', { msg: e.message || t('ds.feedbackNetworkError') }), 'err')
|
|
1506
|
+
return
|
|
1507
|
+
}
|
|
1417
1508
|
if (ok) {
|
|
1418
1509
|
toast(t('ds.questionSubmitted'), 'ok')
|
|
1419
1510
|
$('modal-question').classList.add('hidden')
|
|
@@ -1439,6 +1530,48 @@ function fsParent(p) {
|
|
|
1439
1530
|
parts.pop()
|
|
1440
1531
|
return parts.length ? '/' + parts.join('/') : '/'
|
|
1441
1532
|
}
|
|
1533
|
+
async function openWorkspaceModal() {
|
|
1534
|
+
if (!state.token) { toast(t('ds.toastAuth'), 'err'); showView('view-settings'); return }
|
|
1535
|
+
if (!state.fs.path) await loadFs(null, true)
|
|
1536
|
+
$('workspace-parent-path').textContent = state.fs.path || '~'
|
|
1537
|
+
$('workspace-name').value = ''
|
|
1538
|
+
$('modal-workspace').classList.remove('hidden')
|
|
1539
|
+
setTimeout(() => $('workspace-name').focus(), 50)
|
|
1540
|
+
}
|
|
1541
|
+
function closeWorkspaceModal() { $('modal-workspace').classList.add('hidden') }
|
|
1542
|
+
async function createWorkspace() {
|
|
1543
|
+
if (createWorkspace.busy) return
|
|
1544
|
+
const name = $('workspace-name').value.trim()
|
|
1545
|
+
if (!name) { toast(t('ds.workspaceNameRequired'), 'err'); $('workspace-name').focus(); return }
|
|
1546
|
+
createWorkspace.busy = true
|
|
1547
|
+
const parent = state.fs.path || ''
|
|
1548
|
+
const button = $('workspace-create')
|
|
1549
|
+
button.disabled = true
|
|
1550
|
+
try {
|
|
1551
|
+
const res = await fetch(fsApiUrl('/mkdir', { path: parent, name }), { method: 'POST', headers: fsHeaders() })
|
|
1552
|
+
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
1553
|
+
const data = await res.json().catch(() => ({}))
|
|
1554
|
+
if (!res.ok) {
|
|
1555
|
+
const msg = data.error === 'exists' ? t('ds.workspaceExists') : data.error === 'bad-name' ? t('ds.workspaceInvalidName') : data.error || ('HTTP ' + res.status)
|
|
1556
|
+
throw new Error(msg)
|
|
1557
|
+
}
|
|
1558
|
+
closeWorkspaceModal()
|
|
1559
|
+
await loadFs(parent || null, true)
|
|
1560
|
+
const v = await safeRpc('session.create', { cwd: data.path }, t('ds.toastOpFailed'))
|
|
1561
|
+
await refreshSessions()
|
|
1562
|
+
if (v?.sessionId) {
|
|
1563
|
+
toast(t('ds.workspaceCreated'), 'ok')
|
|
1564
|
+
openSession(v.sessionId)
|
|
1565
|
+
} else {
|
|
1566
|
+
toast(t('ds.workspaceCreatedNoSession'), 'ok')
|
|
1567
|
+
}
|
|
1568
|
+
} catch (e) {
|
|
1569
|
+
toast(`${t('ds.workspaceCreateFailed')}:${e.message || t('ds.feedbackNetworkError')}`, 'err')
|
|
1570
|
+
} finally {
|
|
1571
|
+
createWorkspace.busy = false
|
|
1572
|
+
button.disabled = false
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1442
1575
|
async function loadFs(dir, silent) {
|
|
1443
1576
|
if (!state.token) {
|
|
1444
1577
|
$('fs-path').textContent = t('ds.toastAuth')
|
|
@@ -1480,6 +1613,228 @@ function fsUp() {
|
|
|
1480
1613
|
}
|
|
1481
1614
|
}
|
|
1482
1615
|
|
|
1616
|
+
/* ---------------- 工作台绑定 / 项目会话 ---------------- */
|
|
1617
|
+
function wbPathKey(p) {
|
|
1618
|
+
let value = String(p || '').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1619
|
+
if (value.length > 1) value = value.replace(/\/+$/, '')
|
|
1620
|
+
const windows = /^[A-Za-z]:\//.test(value) || /Windows/i.test(navigator.platform || navigator.userAgent || '')
|
|
1621
|
+
return windows ? value.toLowerCase() : value
|
|
1622
|
+
}
|
|
1623
|
+
function wbBaseName(p) {
|
|
1624
|
+
const value = String(p || '').replace(/[\\/]+$/, '')
|
|
1625
|
+
return value.split(/[\\/]/).pop() || value
|
|
1626
|
+
}
|
|
1627
|
+
function wbStrictInside(pathValue, rootValue) {
|
|
1628
|
+
const pathKey = wbPathKey(pathValue)
|
|
1629
|
+
const rootKey = wbPathKey(rootValue)
|
|
1630
|
+
if (!pathKey || !rootKey || pathKey === rootKey) return false
|
|
1631
|
+
return pathKey.startsWith(rootKey.endsWith('/') ? rootKey : rootKey + '/')
|
|
1632
|
+
}
|
|
1633
|
+
function wbJoin(root, name) {
|
|
1634
|
+
const raw = String(root || '')
|
|
1635
|
+
const separator = raw.includes('\\') ? '\\' : '/'
|
|
1636
|
+
return raw.replace(/[\\/]+$/, '') + separator + String(name || '')
|
|
1637
|
+
}
|
|
1638
|
+
function wbFsParent(p) {
|
|
1639
|
+
if (!p) return null
|
|
1640
|
+
const raw = String(p)
|
|
1641
|
+
const separator = raw.includes('\\') ? '\\' : '/'
|
|
1642
|
+
const index = raw.lastIndexOf(separator)
|
|
1643
|
+
if (index <= 0 || /^[A-Za-z]:$/.test(raw.slice(0, index))) return null
|
|
1644
|
+
return raw.slice(0, index)
|
|
1645
|
+
}
|
|
1646
|
+
async function wbGateway(method, pathname, body) {
|
|
1647
|
+
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' } }
|
|
1648
|
+
if (body !== undefined) {
|
|
1649
|
+
options.headers['content-type'] = 'application/json'
|
|
1650
|
+
options.body = JSON.stringify(body)
|
|
1651
|
+
}
|
|
1652
|
+
const res = await fetch(apiUrl(pathname), options)
|
|
1653
|
+
if (res.status === 401) throw new Error('AUTH')
|
|
1654
|
+
const data = await res.json().catch(() => ({}))
|
|
1655
|
+
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
1656
|
+
return data
|
|
1657
|
+
}
|
|
1658
|
+
async function refreshWorkbench({ silent = false } = {}) {
|
|
1659
|
+
if (!state.token) { renderWorkbench(); return }
|
|
1660
|
+
let wb = null
|
|
1661
|
+
try {
|
|
1662
|
+
wb = await wbGateway('GET', '/workbench')
|
|
1663
|
+
state.wb.apiMissing = false
|
|
1664
|
+
} catch (e) {
|
|
1665
|
+
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return }
|
|
1666
|
+
if (!silent) toast(t('wb.loadFailed', { msg: e.message }), 'err')
|
|
1667
|
+
if (!state.wb.bound && /404/.test(e.message)) state.wb.apiMissing = true
|
|
1668
|
+
}
|
|
1669
|
+
const wl = await safeRpc('workspace.list', {}, '')
|
|
1670
|
+
state.archivedIds = wl && Array.isArray(wl.archivedSessionIds) ? wl.archivedSessionIds : []
|
|
1671
|
+
if (!wb) { renderWorkbench(); renderSessions(); return }
|
|
1672
|
+
if (!wb.bound) {
|
|
1673
|
+
state.wb = { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false }
|
|
1674
|
+
renderWorkbench()
|
|
1675
|
+
renderSessions()
|
|
1676
|
+
return
|
|
1677
|
+
}
|
|
1678
|
+
state.wb.bound = true
|
|
1679
|
+
state.wb.path = wb.path || ''
|
|
1680
|
+
state.wb.title = wb.title || ''
|
|
1681
|
+
if (!wl) { state.wb.projects = []; renderWorkbench(); renderSessions(); return }
|
|
1682
|
+
const items = Array.isArray(wl.items) ? wl.items.slice() : []
|
|
1683
|
+
try {
|
|
1684
|
+
const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
1685
|
+
if (listRes.ok) {
|
|
1686
|
+
const listData = await listRes.json().catch(() => ({}))
|
|
1687
|
+
const have = new Set(items.map(w => wbPathKey(w.path)))
|
|
1688
|
+
for (const entry of listData.entries || []) {
|
|
1689
|
+
if (entry.type !== 'dir') continue
|
|
1690
|
+
const projectPath = wbJoin(state.wb.path, entry.name)
|
|
1691
|
+
if (have.has(wbPathKey(projectPath))) continue
|
|
1692
|
+
try {
|
|
1693
|
+
const created = await rpc('workspace.create', { path: projectPath })
|
|
1694
|
+
if (created?.workspace) { items.push(created.workspace); have.add(wbPathKey(projectPath)) }
|
|
1695
|
+
} catch {}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
} catch {}
|
|
1699
|
+
state.wb.projects = items
|
|
1700
|
+
.filter(w => wbStrictInside(w.path, state.wb.path))
|
|
1701
|
+
.sort((a, b) => String(a.title || wbBaseName(a.path)).localeCompare(String(b.title || wbBaseName(b.path)), 'zh-CN', { numeric: true }))
|
|
1702
|
+
renderWorkbench()
|
|
1703
|
+
renderSessions()
|
|
1704
|
+
}
|
|
1705
|
+
function renderWorkbench() {
|
|
1706
|
+
const box = $('workbench-box')
|
|
1707
|
+
if (!box) return
|
|
1708
|
+
const unbound = $('wb-unbound')
|
|
1709
|
+
const bound = $('wb-bound')
|
|
1710
|
+
const hint = $('wb-api-hint')
|
|
1711
|
+
if (!state.wb.bound) {
|
|
1712
|
+
unbound.classList.remove('hidden')
|
|
1713
|
+
bound.classList.add('hidden')
|
|
1714
|
+
hint?.classList.toggle('hidden', !state.wb.apiMissing)
|
|
1715
|
+
return
|
|
1716
|
+
}
|
|
1717
|
+
unbound.classList.add('hidden')
|
|
1718
|
+
bound.classList.remove('hidden')
|
|
1719
|
+
$('wb-head-text').textContent = t('wb.bound', { title: state.wb.title || wbBaseName(state.wb.path) })
|
|
1720
|
+
$('wb-head').setAttribute('aria-expanded', state.wb.expanded ? 'true' : 'false')
|
|
1721
|
+
$('wb-caret').textContent = state.wb.expanded ? '▾' : '▸'
|
|
1722
|
+
const panel = $('wb-panel')
|
|
1723
|
+
panel.classList.toggle('hidden', !state.wb.expanded)
|
|
1724
|
+
if (!state.wb.expanded) return
|
|
1725
|
+
const projects = state.wb.projects || []
|
|
1726
|
+
let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
|
|
1727
|
+
html += projects.length ? projects.map(w => {
|
|
1728
|
+
const id = String(w.workspaceId || '')
|
|
1729
|
+
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1730
|
+
const open = state.wb.open === id
|
|
1731
|
+
return `<div class="ds-wb-project ${open ? 'open' : ''}">
|
|
1732
|
+
<button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
|
|
1733
|
+
<span class="ds-wb-caret" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
1734
|
+
<span class="ds-wb-project-title" title="${esc(w.path)}">${esc(w.title || wbBaseName(w.path) || short(id))}</span>
|
|
1735
|
+
<span class="ds-wb-project-count">${sessions.length}</span>
|
|
1736
|
+
</button>
|
|
1737
|
+
<div class="ds-wb-project-body ${open ? '' : 'hidden'}">
|
|
1738
|
+
<button type="button" class="ds-mini-btn ds-wb-new-session" data-wb-new="${esc(id)}">+ ${esc(t('wb.newSession'))}</button>
|
|
1739
|
+
${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>`}
|
|
1740
|
+
</div>
|
|
1741
|
+
</div>`
|
|
1742
|
+
}).join('') : `<div class="ds-wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
1743
|
+
html += `<button type="button" class="ds-mini-btn ds-wb-unbind-panel" data-wb-unbind-panel>${esc(t('wb.unbind'))}</button>`
|
|
1744
|
+
panel.innerHTML = html
|
|
1745
|
+
panel.querySelectorAll('[data-wb-head]').forEach(button => button.addEventListener('click', () => {
|
|
1746
|
+
state.wb.open = state.wb.open === button.dataset.wbHead ? null : button.dataset.wbHead
|
|
1747
|
+
renderWorkbench()
|
|
1748
|
+
}))
|
|
1749
|
+
panel.querySelectorAll('[data-wb-new]').forEach(button => button.addEventListener('click', async () => {
|
|
1750
|
+
const value = await safeRpc('session.create', { workspaceId: button.dataset.wbNew }, '')
|
|
1751
|
+
if (value?.sessionId) { await refreshSessions(); openSession(value.sessionId) }
|
|
1752
|
+
}))
|
|
1753
|
+
panel.querySelectorAll('[data-wb-session]').forEach(button => button.addEventListener('click', () => openSession(button.dataset.wbSession)))
|
|
1754
|
+
panel.querySelectorAll('[data-wb-unbind-panel]').forEach(button => button.addEventListener('click', unbindWorkbench))
|
|
1755
|
+
}
|
|
1756
|
+
const wbFs = { path: null, initial: null }
|
|
1757
|
+
function openWorkbenchModal() {
|
|
1758
|
+
$('modal-workbench').classList.remove('hidden')
|
|
1759
|
+
wbFs.path = null
|
|
1760
|
+
wbFs.initial = null
|
|
1761
|
+
wbFsLoad(null)
|
|
1762
|
+
setTimeout(() => $('wb-path-input').focus(), 50)
|
|
1763
|
+
}
|
|
1764
|
+
function closeWorkbenchModal() { $('modal-workbench').classList.add('hidden') }
|
|
1765
|
+
async function wbFsLoad(dir) {
|
|
1766
|
+
const box = $('wb-fs-list')
|
|
1767
|
+
const target = dir ?? wbFs.path ?? ''
|
|
1768
|
+
box.innerHTML = `<div class="ds-empty">${esc(t('ds.loading'))}</div>`
|
|
1769
|
+
$('wb-fs-path').textContent = target ? '…' + target.slice(-40) : '~'
|
|
1770
|
+
try {
|
|
1771
|
+
const res = await fetch(fsApiUrl('/list', target ? { path: target } : {}), { headers: fsHeaders() })
|
|
1772
|
+
const data = await res.json().catch(() => ({}))
|
|
1773
|
+
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error || ('HTTP ' + res.status))
|
|
1774
|
+
wbFs.path = data.path
|
|
1775
|
+
if (!wbFs.initial) wbFs.initial = data.path
|
|
1776
|
+
$('wb-fs-path').textContent = data.path
|
|
1777
|
+
const dirs = (data.entries || []).filter(e => e.type === 'dir')
|
|
1778
|
+
box.innerHTML = dirs.length ? dirs.map(e => {
|
|
1779
|
+
const p = wbJoin(data.path, e.name)
|
|
1780
|
+
return `<div class="ds-wb-fs-row" data-wb-dir="${esc(p)}"><span>📁</span><span class="ds-wb-fs-name">${esc(e.name)}</span><button type="button" class="ds-btn ds-wb-select" data-wb-select="${esc(p)}">${esc(t('wb.selectDir'))}</button></div>`
|
|
1781
|
+
}).join('') : `<div class="ds-empty">${esc(t('wb.empty'))}</div>`
|
|
1782
|
+
box.querySelectorAll('[data-wb-dir]').forEach(row => row.addEventListener('click', e => { if (!e.target.closest('[data-wb-select]')) wbFsLoad(row.dataset.wbDir) }))
|
|
1783
|
+
box.querySelectorAll('[data-wb-select]').forEach(button => button.addEventListener('click', () => bindWorkbench(button.dataset.wbSelect)))
|
|
1784
|
+
} catch (e) {
|
|
1785
|
+
$('wb-fs-path').textContent = target || '~'
|
|
1786
|
+
box.innerHTML = `<div class="ds-empty">${esc(e.message || t('ds.toastConnFailed'))}</div>`
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
function wbFsUp() {
|
|
1790
|
+
if (wbFs.path && wbFs.initial && wbFs.path !== wbFs.initial) {
|
|
1791
|
+
const parent = wbFsParent(wbFs.path)
|
|
1792
|
+
if (parent) wbFsLoad(parent)
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
async function bindWorkbench(rawPath) {
|
|
1796
|
+
const value = String(rawPath || '').trim()
|
|
1797
|
+
if (!value) return toast(t('wb.pathEmpty'), 'err')
|
|
1798
|
+
try {
|
|
1799
|
+
const wb = await wbGateway('POST', '/workbench/bind', { path: value })
|
|
1800
|
+
state.wb = { bound: true, path: wb.path || value, title: wb.title || '', expanded: true, projects: null, open: null, apiMissing: false }
|
|
1801
|
+
const paths = [state.wb.path]
|
|
1802
|
+
try {
|
|
1803
|
+
const res = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
1804
|
+
const data = await res.json().catch(() => ({}))
|
|
1805
|
+
for (const e of data.entries || []) if (e.type === 'dir') paths.push(wbJoin(state.wb.path, e.name))
|
|
1806
|
+
} catch {}
|
|
1807
|
+
for (const projectPath of paths) {
|
|
1808
|
+
try { await rpc('workspace.create', { path: projectPath }) } catch {}
|
|
1809
|
+
}
|
|
1810
|
+
closeWorkbenchModal()
|
|
1811
|
+
await refreshWorkbench({ silent: true })
|
|
1812
|
+
await refreshSessions()
|
|
1813
|
+
toast(t('wb.boundOk', { path: state.wb.path }), 'ok')
|
|
1814
|
+
} catch (e) {
|
|
1815
|
+
if (e.message === 'AUTH') return toast(t('ds.toastAuth'), 'err')
|
|
1816
|
+
toast(t('wb.bindFailed', { msg: e.message }), 'err')
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
async function unbindWorkbench() {
|
|
1820
|
+
if (!confirm(t('wb.unbindConfirm'))) return
|
|
1821
|
+
try {
|
|
1822
|
+
await wbGateway('POST', '/workbench/unbind')
|
|
1823
|
+
state.wb = { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false }
|
|
1824
|
+
renderWorkbench()
|
|
1825
|
+
renderSessions()
|
|
1826
|
+
toast(t('wb.unboundOk'), 'ok')
|
|
1827
|
+
} catch (e) {
|
|
1828
|
+
if (e.message === 'AUTH') return toast(t('ds.toastAuth'), 'err')
|
|
1829
|
+
toast(t('wb.unbindFailed', { msg: e.message }), 'err')
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
let wbRefreshTimer = null
|
|
1833
|
+
function scheduleWorkbenchRefresh() {
|
|
1834
|
+
clearTimeout(wbRefreshTimer)
|
|
1835
|
+
wbRefreshTimer = setTimeout(() => refreshWorkbench({ silent: true }), 400)
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1483
1838
|
/* ---------------- 统计 ---------------- */
|
|
1484
1839
|
function bucketTokens(b) { return (b.input || 0) + (b.cacheRead || 0) + (b.cacheWrite || 0) + (b.output || 0) }
|
|
1485
1840
|
let statsDrawerOpened = false
|
|
@@ -1615,18 +1970,52 @@ function updateConn() {
|
|
|
1615
1970
|
/* ---------------- 初始化 ---------------- */
|
|
1616
1971
|
function bindUi() {
|
|
1617
1972
|
$('btn-new-session').addEventListener('click', async () => {
|
|
1618
|
-
|
|
1973
|
+
let payload = {}
|
|
1974
|
+
// 与移动端保持一致:新会话继承 DSH 当前工作目录;查询失败时兼容回退。
|
|
1975
|
+
try {
|
|
1976
|
+
const host = await rpc('host.describe', {}, 5000)
|
|
1977
|
+
const cwd = typeof host?.cwd === 'string' ? host.cwd.trim() : ''
|
|
1978
|
+
if (cwd) payload = { cwd }
|
|
1979
|
+
} catch {}
|
|
1980
|
+
const v = await safeRpc('session.create', payload, '')
|
|
1619
1981
|
if (v?.sessionId) { await refreshSessions(); openSession(v.sessionId) }
|
|
1620
1982
|
})
|
|
1983
|
+
$('btn-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
1984
|
+
$('session-sort')?.addEventListener('change', (e) => {
|
|
1985
|
+
state.sessionSort = e.target.value === 'workspace' ? 'workspace' : 'time'
|
|
1986
|
+
LS.set('sessionSort', state.sessionSort)
|
|
1987
|
+
renderSessions()
|
|
1988
|
+
})
|
|
1621
1989
|
$('btn-mobile-nav').addEventListener('click', () => {
|
|
1622
1990
|
const list = $('mobile-session-list')
|
|
1623
1991
|
list.style.display = list.style.display === 'none' ? 'flex' : 'none'
|
|
1624
1992
|
})
|
|
1625
1993
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.addEventListener('click', () => showView(b.dataset.view)))
|
|
1626
1994
|
$('session-list').addEventListener('click', (e) => {
|
|
1995
|
+
if (e.target.closest('[data-archived-toggle]')) {
|
|
1996
|
+
LS.set('dsShowArchivedV1', LS.get('dsShowArchivedV1', '0') === '1' ? '0' : '1')
|
|
1997
|
+
renderSessions()
|
|
1998
|
+
return
|
|
1999
|
+
}
|
|
1627
2000
|
const item = e.target.closest('[data-id]')
|
|
1628
2001
|
if (item) openSession(item.dataset.id)
|
|
1629
2002
|
})
|
|
2003
|
+
$('btn-wb-bind').addEventListener('click', openWorkbenchModal)
|
|
2004
|
+
$('btn-wb-bind-manual').addEventListener('click', () => bindWorkbench($('wb-path-input').value))
|
|
2005
|
+
$('wb-path-input').addEventListener('keydown', e => {
|
|
2006
|
+
if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); bindWorkbench($('wb-path-input').value) }
|
|
2007
|
+
})
|
|
2008
|
+
$('wb-fs-up').addEventListener('click', wbFsUp)
|
|
2009
|
+
$('wb-fs-home').addEventListener('click', () => wbFsLoad(wbFs.initial || null))
|
|
2010
|
+
$('btn-wb-modal-close').addEventListener('click', closeWorkbenchModal)
|
|
2011
|
+
$('modal-workbench').addEventListener('click', e => { if (e.target === $('modal-workbench')) closeWorkbenchModal() })
|
|
2012
|
+
$('wb-head').addEventListener('click', () => {
|
|
2013
|
+
state.wb.expanded = !state.wb.expanded
|
|
2014
|
+
if (state.wb.expanded && !state.wb.projects) refreshWorkbench({ silent: false })
|
|
2015
|
+
else renderWorkbench()
|
|
2016
|
+
})
|
|
2017
|
+
$('btn-wb-path').addEventListener('click', () => { if (state.wb.path) toast(t('wb.boundPath', { path: state.wb.path }), 'ok') })
|
|
2018
|
+
$('btn-wb-unbind').addEventListener('click', unbindWorkbench)
|
|
1630
2019
|
$('btn-send').addEventListener('click', sendMessage)
|
|
1631
2020
|
$('composer').addEventListener('keydown', (e) => {
|
|
1632
2021
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage() }
|
|
@@ -1676,6 +2065,10 @@ function bindUi() {
|
|
|
1676
2065
|
$('notes-prev').addEventListener('click', () => scrollNotes(-1))
|
|
1677
2066
|
$('notes-next').addEventListener('click', () => scrollNotes(1))
|
|
1678
2067
|
$('notes-pages').addEventListener('scroll', updateNotesPage)
|
|
2068
|
+
$('workspace-cancel').addEventListener('click', closeWorkspaceModal)
|
|
2069
|
+
$('workspace-create').addEventListener('click', createWorkspace)
|
|
2070
|
+
$('workspace-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') createWorkspace() })
|
|
2071
|
+
$('modal-workspace').addEventListener('click', (e) => { if (e.target === $('modal-workspace')) closeWorkspaceModal() })
|
|
1679
2072
|
$('modal-notes').addEventListener('click', (e) => { if (e.target === $('modal-notes')) closeNotesModal() })
|
|
1680
2073
|
// 反馈
|
|
1681
2074
|
$('btn-feedback').addEventListener('click', (e) => { e.stopPropagation(); toggleFeedbackMenu() })
|
|
@@ -1738,6 +2131,7 @@ function bindUi() {
|
|
|
1738
2131
|
renderServers(); renderSessions(); updateConn(); themeApply()
|
|
1739
2132
|
})
|
|
1740
2133
|
$('fs-up').addEventListener('click', fsUp)
|
|
2134
|
+
$('fs-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
1741
2135
|
$('fs-refresh').addEventListener('click', () => loadFs(state.fs.path || null))
|
|
1742
2136
|
$('btn-question-submit').addEventListener('click', submitQuestion)
|
|
1743
2137
|
$('btn-question-cancel').addEventListener('click', () => { $('modal-question').classList.add('hidden'); toast(t('ds.ignored'), 'ok') })
|
|
@@ -1755,12 +2149,14 @@ async function start() {
|
|
|
1755
2149
|
}
|
|
1756
2150
|
$('token-desc').textContent = state.token ? '● ' + state.token.slice(0, 12) + '…' : t('ds.toastAuth')
|
|
1757
2151
|
bindUi()
|
|
2152
|
+
renderWorkbench()
|
|
1758
2153
|
updateConn()
|
|
1759
2154
|
checkNotesOnStart()
|
|
1760
2155
|
if (state.token) {
|
|
1761
2156
|
if (state.servers.length) await selectFastestServer({ silent: true, reconnect: false })
|
|
1762
2157
|
openStreams()
|
|
1763
2158
|
refreshSessions()
|
|
2159
|
+
refreshWorkbench({ silent: true })
|
|
1764
2160
|
}
|
|
1765
2161
|
}
|
|
1766
2162
|
|
package/public/desktop/i18n.js
CHANGED
|
@@ -10,6 +10,38 @@
|
|
|
10
10
|
if (saved === 'zh' || saved === 'en') return saved
|
|
11
11
|
return (navigator.language || 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
12
12
|
}
|
|
13
|
+
const BUILTIN = {
|
|
14
|
+
zh: {
|
|
15
|
+
'wb.unbound': 'DSH Remote(未绑定)', 'wb.bind': '绑定工作台', 'wb.bound': 'DSH Remote(绑定 {title})',
|
|
16
|
+
'wb.viewPath': '已绑定 workspace', 'wb.unbind': '解绑', 'wb.unbindConfirm': '确定解绑当前工作台?解绑后这些会话将回到扁平会话列表。',
|
|
17
|
+
'wb.modalTitle': '绑定工作台', 'wb.modalDesc': '选择工作台根目录,其下每个子文件夹会自动成为项目工作区。',
|
|
18
|
+
'wb.up': '上级', 'wb.home': '根目录', 'wb.selectDir': '选择此目录', 'wb.pathPlaceholder': '或手动输入绝对路径…',
|
|
19
|
+
'wb.pathEmpty': '请输入要绑定的路径', 'wb.empty': '此目录没有子文件夹', 'wb.boundOk': '工作台已绑定:{path}',
|
|
20
|
+
'wb.bindFailed': '绑定失败:{msg}', 'wb.unboundOk': '已解绑工作台', 'wb.unbindFailed': '解绑失败:{msg}',
|
|
21
|
+
'wb.boundPath': '绑定路径:{path}', 'wb.projects': '项目', 'wb.noProjects': '暂无项目(根目录下新建文件夹后会自动出现)',
|
|
22
|
+
'wb.newSession': '新会话', 'wb.noSessions': '暂无会话', 'wb.flatHidden': '工作台会话已收起({n} 个在工作台面板)',
|
|
23
|
+
'wb.apiMissing': '工作台接口不可用(需更新网关)', 'wb.loadFailed': '工作台加载失败:{msg}',
|
|
24
|
+
'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------'
|
|
25
|
+
},
|
|
26
|
+
en: {
|
|
27
|
+
'wb.unbound': 'DSH Remote (unbound)', 'wb.bind': 'Bind workbench', 'wb.bound': 'DSH Remote (bound {title})',
|
|
28
|
+
'wb.viewPath': 'Bound workspace', 'wb.unbind': 'Unbind', 'wb.unbindConfirm': 'Unbind the current workbench? These sessions will return to the flat list.',
|
|
29
|
+
'wb.modalTitle': 'Bind workbench', 'wb.modalDesc': 'Choose the workbench root; each subfolder becomes a project workspace automatically.',
|
|
30
|
+
'wb.up': 'Up', 'wb.home': 'Root', 'wb.selectDir': 'Select this folder', 'wb.pathPlaceholder': 'Or type an absolute path…',
|
|
31
|
+
'wb.pathEmpty': 'Enter a path to bind', 'wb.empty': 'No subfolders in this folder', 'wb.boundOk': 'Workbench bound: {path}',
|
|
32
|
+
'wb.bindFailed': 'Bind failed: {msg}', 'wb.unboundOk': 'Workbench unbound', 'wb.unbindFailed': 'Unbind failed: {msg}',
|
|
33
|
+
'wb.boundPath': 'Bound path: {path}', 'wb.projects': 'Projects', 'wb.noProjects': 'No projects yet',
|
|
34
|
+
'wb.newSession': 'New session', 'wb.noSessions': 'No sessions', 'wb.flatHidden': 'Workbench sessions folded ({n} in the workbench panel)',
|
|
35
|
+
'wb.apiMissing': 'Workbench API unavailable (update gateway)', 'wb.loadFailed': 'Workbench load failed: {msg}',
|
|
36
|
+
'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function mergeDicts(strings) {
|
|
40
|
+
return {
|
|
41
|
+
zh: Object.assign({}, BUILTIN.zh, strings?.zh || {}),
|
|
42
|
+
en: Object.assign({}, BUILTIN.en, strings?.en || {})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
13
45
|
let dict = null
|
|
14
46
|
let lang = detect()
|
|
15
47
|
|
|
@@ -46,7 +78,7 @@
|
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
window.I18N = {
|
|
49
|
-
init(strings) { dict = strings || dict; return apply(document) },
|
|
81
|
+
init(strings) { dict = mergeDicts(strings || dict || {}); return apply(document) },
|
|
50
82
|
t, setLang, apply,
|
|
51
83
|
get lang() { return lang }
|
|
52
84
|
}
|