dsh-remote-plugin 0.6.18 → 0.6.19

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-plugin",
3
- "version": "0.6.18",
3
+ "version": "0.6.19",
4
4
  "description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
package/public/admin.html CHANGED
@@ -3,9 +3,9 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <meta name="theme-color" content="#05348B">
6
+ <meta name="theme-color" content="#0D1117">
7
7
  <title>DSH Remote · 管理</title>
8
- <script>/* 首帧前应用皮肤 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
8
+ <script>/* 首帧前应用皮肤 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'&&t!=='mono'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
9
9
  <link rel="stylesheet" href="../styles.css">
10
10
  <style>
11
11
  .admin-wrap { max-width: 1180px; margin: 0 auto; padding: calc(env(safe-area-inset-top, 0px) + 14px) 14px 40px; }
@@ -87,8 +87,8 @@
87
87
  .device-key-toggle-actions { display:flex; align-items:center; gap:8px; flex:none; }
88
88
  .device-key-switch { position:relative; width:42px; height:24px; flex:none; }
89
89
  .device-key-switch input { position:absolute; opacity:0; pointer-events:none; }
90
- .device-key-switch span { position:absolute; inset:0; border:1px solid var(--dsr-line); border-radius:999px; background:var(--dsr-bg-2); cursor:pointer; transition:.18s ease; }
91
- .device-key-switch span::after { content:''; position:absolute; width:18px; height:18px; left:2px; top:2px; border-radius:50%; background:var(--dsr-muted); transition:.18s ease; }
90
+ .device-key-switch span { position:absolute; inset:0; border:1px solid var(--dsr-line); border-radius:999px; background:var(--dsr-bg-2); cursor:pointer; transition:background-color .18s ease,border-color .18s ease; }
91
+ .device-key-switch span::after { content:''; position:absolute; width:18px; height:18px; left:2px; top:2px; border-radius:50%; background:var(--dsr-muted); transition:transform .18s ease,background-color .18s ease; }
92
92
  .device-key-switch input:checked + span { border-color:var(--dsr-accent-line); background:var(--dsr-accent-soft); }
93
93
  .device-key-switch input:checked + span::after { transform:translateX(18px); background:var(--dsr-accent-strong); }
94
94
  .device-key-switch input:focus-visible + span { outline:2px solid var(--dsr-accent-strong); outline-offset:2px; }
@@ -126,8 +126,8 @@
126
126
  .host-ip-use { color: var(--dsr-muted); }
127
127
  .host-ip-switch { position: relative; display: inline-block; width: 38px; height: 22px; vertical-align: middle; }
128
128
  .host-ip-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
129
- .host-ip-switch span { position: absolute; inset: 0; border: 1px solid var(--dsr-line); border-radius: 999px; background: var(--dsr-bg-2); cursor: pointer; transition: .18s ease; }
130
- .host-ip-switch span::after { content: ''; position: absolute; width: 16px; height: 16px; left: 2px; top: 2px; border-radius: 50%; background: var(--dsr-muted); transition: .18s ease; }
129
+ .host-ip-switch span { position: absolute; inset: 0; border: 1px solid var(--dsr-line); border-radius: 999px; background: var(--dsr-bg-2); cursor: pointer; transition: background-color .18s ease, border-color .18s ease; }
130
+ .host-ip-switch span::after { content: ''; position: absolute; width: 16px; height: 16px; left: 2px; top: 2px; border-radius: 50%; background: var(--dsr-muted); transition: transform .18s ease, background-color .18s ease; }
131
131
  .host-ip-switch input:checked + span { border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
132
132
  .host-ip-switch input:checked + span::after { transform: translateX(16px); background: var(--dsr-accent-strong); }
133
133
  .host-ip-switch input:focus-visible + span { outline: 2px solid var(--dsr-accent-strong); outline-offset: 2px; }
@@ -186,7 +186,6 @@
186
186
  width: 248px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 14px;
187
187
  box-shadow: 0 12px 34px var(--dsr-shadow); padding: 6px;
188
188
  display: flex; flex-direction: column; gap: 2px;
189
- animation: fb-in .18s ease;
190
189
  }
191
190
  .fb-item {
192
191
  min-height: 44px; display: flex; align-items: center; gap: 10px;
@@ -204,7 +203,6 @@
204
203
  .fb-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
205
204
  .fb-name { font-weight: 600; }
206
205
  .fb-desc { font-size: 11px; color: var(--dsr-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
207
- @keyframes fb-in { from { opacity: 0; transform: translateY(-6px) } to { opacity: 1; transform: translateY(0) } }
208
206
  /* 窄屏压缩顶栏: 图标化文字按钮, 保证全部按钮完整可见 */
209
207
  @media (max-width: 900px) {
210
208
  .gh-label, .t-label { display: none; }
@@ -331,8 +329,7 @@
331
329
  @media (max-width: 900px) {
332
330
  .admin-title { display: grid; grid-template-columns: minmax(0, 1fr) auto; row-gap: 10px; }
333
331
  .admin-title .left { min-width: 0; }
334
- .admin-title .right { grid-column: 1 / -1; justify-content: flex-start; min-width: 0; overflow-x: auto; scrollbar-width: none; }
335
- .admin-title .right::-webkit-scrollbar { display: none; }
332
+ .admin-title .right { grid-column: 1 / -1; justify-content: flex-start; min-width: 0; flex-wrap: wrap; overflow: visible; }
336
333
  }
337
334
  @media (max-width: 440px) {
338
335
  .admin-title h1 { display: block; min-width: 0; overflow: visible; font-size: 18px; }
@@ -381,12 +378,12 @@
381
378
  </div>
382
379
  </div>
383
380
 
384
- <div id="login-view" class="login-card hidden">
385
- <input id="token-input" type="password" placeholder="输入网关访问令牌" autocomplete="off" data-i18n-placeholder="tokenPlaceholder">
381
+ <div id="login-view" class="login-card hidden" data-motion-view>
382
+ <input id="token-input" name="gateway-token" type="password" placeholder="输入网关访问令牌" autocomplete="off" aria-label="网关访问令牌" data-i18n-placeholder="tokenPlaceholder" data-i18n-aria="tokenPlaceholder">
386
383
  <button id="btn-login" class="mini-btn" data-i18n="enter">进入</button>
387
384
  </div>
388
385
 
389
- <div id="main-view" class="hidden">
386
+ <div id="main-view" class="hidden" data-motion-view>
390
387
  <section id="admin-hero" class="admin-hero" aria-live="polite">
391
388
  <div class="admin-hero-copy">
392
389
  <div class="admin-eyebrow" data-i18n="hero.eyebrow">GATEWAY CONTROL</div>
@@ -455,8 +452,8 @@
455
452
  </div>
456
453
 
457
454
  <div id="gateway-port-row" class="gateway-port-row hidden">
458
- <label data-i18n="gatewayPort">网关端口</label>
459
- <input id="gateway-port-input" type="number" min="1" max="65535" inputmode="numeric" placeholder="8787">
455
+ <label for="gateway-port-input" data-i18n="gatewayPort">网关端口</label>
456
+ <input id="gateway-port-input" name="gateway-port" type="number" min="1" max="65535" inputmode="numeric" autocomplete="off" placeholder="8787">
460
457
  <span id="gateway-port-current" class="muted">—</span>
461
458
  <button id="btn-save-port" class="mini-btn" data-i18n="savePort">保存</button>
462
459
  </div>
@@ -560,7 +557,7 @@
560
557
  'doctor.realtime': '实时消息通道', 'doctor.realtimePass': 'mux 与 host 通道均已连接', 'doctor.realtimeFail': '实时通道未全部建立;可先使用轮询,检查 DSH 后会自动恢复',
561
558
  'doctor.start': '启动', 'doctor.showQr': '显示二维码', 'doctor.copyAddress': '复制地址', 'doctor.copyCommand': '复制命令',
562
559
  'doctor.reportCopied': '诊断报告已复制', 'doctor.commandCopied': '防火墙命令已复制,请确认网络范围后手动执行', 'doctor.addressCopied': '服务器地址已复制',
563
- 'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔',
560
+ 'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔', 'theme.mono': '黑曜白',
564
561
  'theme.panelTitle': '选择配色', 'theme.close': '关闭',
565
562
  'repo': '仓库',
566
563
  'donateTitle': '赞赏支持', 'donateThanks': '感谢你的支持 ☕',
@@ -670,7 +667,7 @@
670
667
  'doctor.realtime': 'Realtime channels', 'doctor.realtimePass': 'Both mux and host channels are connected', 'doctor.realtimeFail': 'Realtime channels are incomplete. Polling remains available and recovery is automatic.',
671
668
  'doctor.start': 'Start', 'doctor.showQr': 'Show QR', 'doctor.copyAddress': 'Copy address', 'doctor.copyCommand': 'Copy command',
672
669
  'doctor.reportCopied': 'Diagnostic report copied', 'doctor.commandCopied': 'Firewall command copied. Review the network scope before running it.', 'doctor.addressCopied': 'Server address copied',
673
- 'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower',
670
+ 'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower', 'theme.mono': 'Monochrome',
674
671
  'theme.panelTitle': 'Choose theme', 'theme.close': 'Close',
675
672
  'repo': 'Repo',
676
673
  'donateTitle': 'Support', 'donateThanks': 'Thanks for your support ☕',
@@ -767,6 +764,8 @@
767
764
  <script src="../qrcode.min.js"></script>
768
765
  <script src="../i18n.js"></script>
769
766
  <script src="../theme.js"></script>
767
+ <script src="../vendor/gsap/gsap.min.js"></script>
768
+ <script src="../motion.js"></script>
770
769
  <script src="../admin.js"></script>
771
770
  </body>
772
771
  </html>
package/public/admin.js CHANGED
@@ -935,10 +935,11 @@ function renderLangBtn() {
935
935
  }
936
936
 
937
937
  const THEME_META = [
938
- { id: 'default', sw: ['#0B0E1A', '#151B33', '#5B8CFF'] },
939
- { id: 'dark', sw: ['#05348B', '#0D438F', '#F9A647'] },
940
- { id: 'light', sw: ['#EFEEEC', '#FAF8F5', '#E6BC7B'] },
941
- { id: 'neutral', sw: ['#DDD4B8', '#585818', '#832D15'] }
938
+ { id: 'default', sw: ['#0D1117', '#21262D', '#58A6FF'] },
939
+ { id: 'dark', sw: ['#161316', '#302323', '#FFB86B'] },
940
+ { id: 'light', sw: ['#F5F7F8', '#FFFFFF', '#176B87'] },
941
+ { id: 'neutral', sw: ['#EEF1E8', '#FAFBF6', '#47643C'] },
942
+ { id: 'mono', sw: ['#050505', '#333333', '#F5F5F5'] }
942
943
  ]
943
944
 
944
945
  function renderThemeBtn() {
@@ -957,7 +958,7 @@ function renderThemeOptions() {
957
958
  if (!box) return
958
959
  const cur = window.DSHTheme.get()
959
960
  box.innerHTML = THEME_META.map(m => `
960
- <button class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" title="${t('theme.' + m.id)}">
961
+ <button type="button" class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" aria-pressed="${m.id === cur}" title="${t('theme.' + m.id)}">
961
962
  <span class="theme-swatches">${m.sw.map(c => `<i style="background:${c}"></i>`).join('')}</span>
962
963
  <span class="theme-name">${t('theme.' + m.id)}</span>
963
964
  <span class="theme-check">${m.id === cur ? '✓' : ''}</span>
package/public/app.js CHANGED
@@ -1673,6 +1673,24 @@ function goalOf(s) {
1673
1673
  if (!p) return null
1674
1674
  return p.goal && typeof p.goal === 'object' ? p.goal : p
1675
1675
  }
1676
+ const COLLAPSED_GOALS_KEY = 'dshCollapsedGoalsV1'
1677
+ function collapsedGoals() {
1678
+ try {
1679
+ const value = JSON.parse(LS.get(COLLAPSED_GOALS_KEY, '[]'))
1680
+ return Array.isArray(value) ? value.filter(item => item && typeof item.sessionId === 'string' && typeof item.goalId === 'string') : []
1681
+ } catch { return [] }
1682
+ }
1683
+ function goalDisplayId(goal) { return String(goal?.id || '__current__') }
1684
+ function isGoalCollapsed(sessionId, goal) {
1685
+ const goalId = goalDisplayId(goal)
1686
+ return collapsedGoals().some(item => item.sessionId === sessionId && item.goalId === goalId)
1687
+ }
1688
+ function setGoalCollapsed(sessionId, goal, collapsed) {
1689
+ const goalId = goalDisplayId(goal)
1690
+ const next = collapsedGoals().filter(item => item.sessionId !== sessionId)
1691
+ if (collapsed) next.push({ sessionId, goalId })
1692
+ LS.set(COLLAPSED_GOALS_KEY, JSON.stringify(next.slice(-100)))
1693
+ }
1676
1694
 
1677
1695
  function updatePendingBadge() {
1678
1696
  const pending = state.approvals.length + state.questions.length
@@ -2684,15 +2702,23 @@ async function renderSessionCards() {
2684
2702
  let html = ''
2685
2703
 
2686
2704
  if (goal && !isGoalTerminal(goal)) {
2687
- html += `<div class="card"><div class="card-title">${t('goal.title')}</div>
2688
- <div class="goal-obj">${esc(goal.objective || '')}</div>
2689
- <div class="goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
2690
- <div class="goal-actions">
2691
- ${goal.phase === 'active' ? '<button class="mini-btn" data-goal="pause">' + t('goal.pause') + '</button>' : '<button class="mini-btn" data-goal="resume">' + t('goal.resume') + '</button>'}
2692
- <button class="mini-btn" data-goal="complete">${t('goal.complete')}</button>
2693
- <button class="mini-btn" data-goal="edit">${t('goal.edit')}</button>
2694
- <button class="mini-btn" data-goal="clear">${t('goal.clear')}</button>
2695
- </div></div>`
2705
+ const collapsed = isGoalCollapsed(sessionId, goal)
2706
+ html += `<div class="goal-disclosure${collapsed ? ' is-collapsed' : ''}">
2707
+ <div id="goal-panel-mobile" class="card goal-card${collapsed ? ' hidden' : ''}">
2708
+ <div class="goal-card-head"><div class="card-title">${t('goal.title')}</div>
2709
+ <button type="button" class="goal-collapse-btn" data-goal-collapse="1" aria-expanded="true" aria-controls="goal-panel-mobile" title="${esc(t('goal.collapse'))}"><span aria-hidden="true">›</span>${t('goal.collapseShort')}</button>
2710
+ </div>
2711
+ <div class="goal-obj">${esc(goal.objective || '')}</div>
2712
+ <div class="goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
2713
+ <div class="goal-actions">
2714
+ ${goal.phase === 'active' ? '<button class="mini-btn" data-goal="pause">' + t('goal.pause') + '</button>' : '<button class="mini-btn" data-goal="resume">' + t('goal.resume') + '</button>'}
2715
+ <button class="mini-btn" data-goal="complete">${t('goal.complete')}</button>
2716
+ <button class="mini-btn" data-goal="edit">${t('goal.edit')}</button>
2717
+ <button class="mini-btn" data-goal="clear">${t('goal.clear')}</button>
2718
+ </div>
2719
+ </div>
2720
+ <button type="button" class="goal-side-tab${collapsed ? '' : ' hidden'}" data-goal-collapse="0" aria-expanded="false" aria-controls="goal-panel-mobile" aria-label="${esc(t('goal.expand'))}" title="${esc(t('goal.expand'))}"><span aria-hidden="true">‹</span><span>${t('goal.title')}</span><small>${t('goal.collapsedHint')}</small></button>
2721
+ </div>`
2696
2722
  }
2697
2723
  if (todos?.items?.length) {
2698
2724
  html += `<div class="card"><div class="card-title">${t('todos.title')}</div>${todos.items.map(t =>
@@ -2702,6 +2728,15 @@ async function renderSessionCards() {
2702
2728
  box.innerHTML = html
2703
2729
  box.querySelectorAll('[data-goal]').forEach(btn =>
2704
2730
  btn.addEventListener('click', () => goalAction(btn.dataset.goal)))
2731
+ box.querySelectorAll('[data-goal-collapse]').forEach(btn =>
2732
+ btn.addEventListener('click', () => {
2733
+ const collapse = btn.dataset.goalCollapse === '1'
2734
+ const currentGoal = goalOf(state.byId.get(state.current))
2735
+ if (!currentGoal) return
2736
+ setGoalCollapsed(state.current, currentGoal, collapse)
2737
+ void renderSessionCards()
2738
+ requestAnimationFrame(() => box.querySelector(`[data-goal-collapse="${collapse ? '0' : '1'}"]`)?.focus())
2739
+ }))
2705
2740
 
2706
2741
  // 子代理
2707
2742
  const sub = await safeRpc('subagent.list', { parentSessionId: sessionId })
@@ -5728,6 +5763,8 @@ function showView(id) {
5728
5763
  // 离开会话页必须清掉 in-session, 否则其他页面顶栏被 body 样式隐藏
5729
5764
  document.body.classList.toggle('in-session', id === 'view-session')
5730
5765
  document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === id))
5766
+ // 主页已有上下文明确的刷新按钮,避免顶栏出现第二个同义入口。
5767
+ $('btn-refresh')?.classList.toggle('hidden', id === 'view-activity')
5731
5768
  window.DshMotion?.view($(id))
5732
5769
  window.scrollTo(0, 0)
5733
5770
  if (id === 'view-files' && !state.fs.loaded) {
@@ -6411,10 +6448,11 @@ function renderLangBtn() {
6411
6448
  }
6412
6449
 
6413
6450
  const THEME_META = [
6414
- { id: 'default', sw: ['#0B0E1A', '#151B33', '#5B8CFF'] },
6415
- { id: 'dark', sw: ['#05348B', '#0D438F', '#F9A647'] },
6416
- { id: 'light', sw: ['#EFEEEC', '#FAF8F5', '#E6BC7B'] },
6417
- { id: 'neutral', sw: ['#DDD4B8', '#585818', '#832D15'] }
6451
+ { id: 'default', sw: ['#0D1117', '#21262D', '#58A6FF'] },
6452
+ { id: 'dark', sw: ['#161316', '#302323', '#FFB86B'] },
6453
+ { id: 'light', sw: ['#F5F7F8', '#FFFFFF', '#176B87'] },
6454
+ { id: 'neutral', sw: ['#EEF1E8', '#FAFBF6', '#47643C'] },
6455
+ { id: 'mono', sw: ['#050505', '#333333', '#F5F5F5'] }
6418
6456
  ]
6419
6457
 
6420
6458
  function renderThemeBtn() {
@@ -6427,7 +6465,7 @@ function renderThemeOptions() {
6427
6465
  if (!box) return
6428
6466
  const cur = window.DSHTheme.get()
6429
6467
  box.innerHTML = THEME_META.map(m => `
6430
- <button class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" title="${t('theme.' + m.id)}">
6468
+ <button type="button" class="theme-option ${m.id === cur ? 'current' : ''}" data-theme="${m.id}" aria-pressed="${m.id === cur}" title="${t('theme.' + m.id)}">
6431
6469
  <span class="theme-swatches">${m.sw.map(c => `<i style="background:${c}"></i>`).join('')}</span>
6432
6470
  <span class="theme-name">${t('theme.' + m.id)}</span>
6433
6471
  <span class="theme-check">${m.id === cur ? '✓' : ''}</span>
@@ -8,6 +8,7 @@ html, body {
8
8
  .hidden { display: none !important; }
9
9
  .ds-app { position: fixed; inset: 0; display: flex; overflow: hidden; }
10
10
  .ds-sidebar { width: 280px; flex: none; display: flex; flex-direction: column; border-right: 1px solid var(--dsr-line); background: linear-gradient(180deg, var(--dsr-panel), var(--dsr-bg)); box-shadow: 12px 0 34px var(--dsr-shadow); overflow: hidden; }
11
+ .ds-sidebar-backdrop { display: none; }
11
12
  .ds-brand { display: flex; align-items: center; gap: 8px; padding: 17px 18px 12px; font-weight: 750; font-size: 15px; letter-spacing: .1px; }
12
13
  .ds-logo { color: var(--dsr-accent-strong); }
13
14
  .ds-brand-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -288,7 +289,30 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
288
289
  .ds-session-cards:empty { display: none; }
289
290
  .ds-card { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 12px; }
290
291
  .ds-card-title { font-size: 11px; font-weight: 700; color: var(--dsr-muted); letter-spacing: .6px; margin-bottom: 6px; text-transform: uppercase; }
291
- .ds-goal-obj { font-size: 13px; line-height: 1.55; word-break: break-word; }
292
+ .ds-goal-disclosure { width: 100%; min-width: 0; display: flex; justify-content: flex-end; }
293
+ .ds-goal-disclosure .ds-goal-card { width: 100%; min-width: 0; }
294
+ .ds-goal-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
295
+ .ds-goal-card-head .ds-card-title { min-width: 0; margin-bottom: 0; }
296
+ .ds-goal-collapse-btn,
297
+ .ds-goal-side-tab {
298
+ touch-action: manipulation; border: 1px solid var(--dsr-line); background: var(--dsr-bg-2); color: var(--dsr-muted);
299
+ font: inherit; cursor: pointer;
300
+ }
301
+ .ds-goal-collapse-btn { min-height: 32px; display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px; border-radius: 8px; font-size: 11.5px; }
302
+ .ds-goal-collapse-btn > span { color: var(--dsr-accent-strong); font-size: 17px; line-height: 1; }
303
+ .ds-goal-side-tab {
304
+ min-height: 42px; max-width: min(100%, 210px); display: inline-flex; align-items: center; gap: 7px; padding: 6px 12px 6px 10px;
305
+ border-right: 0; border-radius: 11px 0 0 11px; box-shadow: -5px 6px 18px var(--dsr-shadow); color: var(--dsr-text); font-size: 12px; font-weight: 700;
306
+ }
307
+ .ds-goal-side-tab > span:first-child { color: var(--dsr-accent-strong); font-size: 19px; line-height: 1; }
308
+ .ds-goal-side-tab small { min-width: 0; color: var(--dsr-muted); font-size: 10.5px; font-weight: 500; white-space: nowrap; }
309
+ .ds-goal-collapse-btn:hover,
310
+ .ds-goal-side-tab:hover { background: var(--dsr-panel-2); color: var(--dsr-text); }
311
+ .ds-goal-collapse-btn:active,
312
+ .ds-goal-side-tab:active { transform: translateY(1px); }
313
+ .ds-goal-collapse-btn:focus-visible,
314
+ .ds-goal-side-tab:focus-visible { outline: 2px solid var(--dsr-accent-strong); outline-offset: 2px; }
315
+ .ds-goal-obj { font-size: 13px; line-height: 1.55; word-break: break-word; overflow-wrap: anywhere; }
292
316
  .ds-goal-phase { font-size: 11px; color: var(--dsr-muted); margin-top: 3px; }
293
317
  .ds-goal-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
294
318
  .ds-mini-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 26px; padding: 2px 9px; border-radius: 7px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); color: var(--dsr-text); font: inherit; font-size: 11.5px; cursor: pointer; }
@@ -350,6 +374,10 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
350
374
  .ds-settings { max-width: 720px; display: flex; flex-direction: column; gap: 10px; }
351
375
  .ds-setting-row { display: flex; align-items: center; gap: 10px; background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 14px; }
352
376
  .ds-setting-row > div:first-child { flex: 1; min-width: 0; }
377
+ .ds-setting-link { width: 100%; color: var(--dsr-text); font: inherit; text-align: left; cursor: pointer; transition: background-color .18s ease, border-color .18s ease; }
378
+ .ds-setting-link:hover, .ds-setting-link:focus-visible { background: var(--dsr-bg-2); border-color: var(--dsr-accent-line); }
379
+ .ds-setting-link .ds-setting-arrow { transition: transform .18s ease, color .18s ease; }
380
+ .ds-setting-link:hover .ds-setting-arrow, .ds-setting-link:focus-visible .ds-setting-arrow { color: var(--dsr-accent-strong); transform: translateX(2px); }
353
381
  .ds-setting-arrow,
354
382
  #settings-home .ds-setting-row .ds-btn {
355
383
  display: inline-flex; align-items: center; justify-content: center;
@@ -397,8 +425,8 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
397
425
  .ds-mini:hover { color: var(--dsr-text); }
398
426
  .ds-switch { position: relative; display: inline-block; width: 36px; height: 21px; flex-shrink: 0; }
399
427
  .ds-switch input { opacity: 0; width: 0; height: 0; }
400
- .ds-switch .ds-slider { position: absolute; inset: 0; border-radius: 999px; background: var(--dsr-panel-2); border: 1px solid var(--dsr-line); transition: .2s; }
401
- .ds-switch .ds-slider::before { content: ''; position: absolute; width: 15px; height: 15px; border-radius: 50%; left: 2px; top: 2px; background: var(--dsr-muted); transition: .2s; }
428
+ .ds-switch .ds-slider { position: absolute; inset: 0; border-radius: 999px; background: var(--dsr-panel-2); border: 1px solid var(--dsr-line); transition: background-color .2s ease, border-color .2s ease; }
429
+ .ds-switch .ds-slider::before { content: ''; position: absolute; width: 15px; height: 15px; border-radius: 50%; left: 2px; top: 2px; background: var(--dsr-muted); transition: transform .2s ease, background-color .2s ease; }
402
430
  .ds-switch input:checked + .ds-slider { background: var(--dsr-accent-soft); border-color: var(--dsr-accent); }
403
431
  .ds-switch input:checked + .ds-slider::before { transform: translateX(15px); background: var(--dsr-accent); }
404
432
 
@@ -429,7 +457,7 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
429
457
  .ds-stats-note { font-size: 11px; color: var(--dsr-muted); line-height: 1.6; margin-top: 8px; }
430
458
 
431
459
  /* 统计抽屉(右侧滑出) */
432
- .ds-drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 110; width: min(540px, 100vw); background: var(--dsr-panel); border-left: 1px solid var(--dsr-line); box-shadow: -12px 0 32px rgba(0,0,0,.35); display: flex; flex-direction: column; }
460
+ .ds-drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 110; width: min(540px, 100vw); background: var(--dsr-panel); border-left: 1px solid var(--dsr-line); box-shadow: -12px 0 32px rgba(0,0,0,.35); display: flex; flex-direction: column; overscroll-behavior: contain; }
433
461
  .ds-drawer.hidden { display: none; }
434
462
  .ds-drawer-head { flex: none; display: flex; align-items: center; justify-content: space-between; padding: 13px 15px; border-bottom: 1px solid var(--dsr-line); font-weight: 700; font-size: 15px; }
435
463
  .ds-drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 15px; scrollbar-width: thin; scrollbar-color: var(--dsr-line) transparent; }
@@ -453,7 +481,7 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
453
481
  /* 模态 */
454
482
  .ds-modal { position: fixed; inset: 0; z-index: 120; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; padding: 16px; }
455
483
  .ds-modal.hidden { display: none; }
456
- .ds-modal-card { width: min(520px, 100%); background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 14px; padding: 16px; }
484
+ .ds-modal-card { width: min(520px, 100%); background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 18px; padding: 18px; box-shadow: 0 22px 60px var(--dsr-shadow); overscroll-behavior: contain; }
457
485
  .ds-modal-title { font-weight: 700; margin-bottom: 10px; }
458
486
  .ds-modal-body { display: flex; flex-direction: column; gap: 10px; max-height: 60vh; overflow-y: auto; }
459
487
  .ds-workspace-create-desc { margin: 0; color: var(--dsr-muted); line-height: 1.55; }
@@ -529,9 +557,22 @@ input:focus-visible, select:focus-visible, textarea:focus-visible {
529
557
  .ds-btn:hover { filter: brightness(1.08); }
530
558
  }
531
559
 
532
- /* 窄屏回退: 隐藏侧栏, 顶部导航, 通知卡片全宽 */
560
+ /* 窄屏回退: 侧栏变为可关闭抽屉,保留完整导航与会话上下文。 */
533
561
  @media (max-width: 1023px) {
534
- .ds-sidebar { display: none; }
562
+ .ds-sidebar {
563
+ position: fixed; inset: 0 auto 0 0; z-index: 90; display: flex;
564
+ width: min(320px, 86vw); max-width: 100%;
565
+ opacity: 0; pointer-events: none; transform: translateX(-104%);
566
+ transition: transform .24s ease, opacity .18s ease;
567
+ box-shadow: 18px 0 48px rgba(0,0,0,.38);
568
+ }
569
+ .ds-sidebar.mobile-open { opacity: 1; pointer-events: auto; transform: translateX(0); }
570
+ .ds-sidebar-backdrop {
571
+ position: fixed; inset: 0; z-index: 80; display: block;
572
+ border: 0; background: var(--dsr-overlay); cursor: default;
573
+ }
574
+ .ds-sidebar-backdrop.hidden { display: none; }
575
+ .ds-app.mobile-nav-open { overflow: hidden; }
535
576
  #btn-mobile-nav { display: inline-flex; }
536
577
  .ds-content { flex-direction: column; }
537
578
  .ds-view { padding: 10px; overflow-y: auto; }
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>DSH Remote · 桌面</title>
7
- <script>/* 首帧前应用皮肤 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
7
+ <script>/* 首帧前应用皮肤 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'&&t!=='mono'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
8
8
  <link rel="stylesheet" href="../theme-vars.css">
9
9
  <link rel="stylesheet" href="desktop.css">
10
10
  </head>
@@ -50,7 +50,7 @@
50
50
  </div>
51
51
  <button class="ds-nav-item" data-view="view-overview"><span class="ds-nav-ico">◌</span><span data-i18n="ds.overview">主页</span></button>
52
52
  <button class="ds-nav-item" data-view="view-files"><span class="ds-nav-ico">⇅</span><span data-i18n="ds.files">文件传输</span></button>
53
- <button class="ds-nav-item" data-view="view-settings"><span class="ds-nav-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-1.8 1.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5v.2h-2.5v-.2a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1-1.8-1.8.1-.1A1.7 1.7 0 0 0 8.1 15a1.7 1.7 0 0 0-1.5-1H6v-2.5h.2a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.9l-.1-.1 1.8-1.8.1.1a1.7 1.7 0 0 0 1.9.3 1.7 1.7 0 0 0 1-1.5v-.2h2.5v.2a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1 1.8 1.8-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.5 1h.2V14h-.2a1.7 1.7 0 0 0-1.5 1Z"/></svg></span><span data-i18n="ds.settings">设置</span></button>
53
+ <button class="ds-nav-item" data-view="view-settings"><span class="ds-nav-ico"><svg class="settings-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.09a2 2 0 0 1 1 1.73v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.51a2 2 0 0 1 1-1.73l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z"/><circle cx="12" cy="12" r="3"/></svg></span><span data-i18n="ds.settings">设置</span></button>
54
54
  <div class="ds-feedback">
55
55
  <button id="btn-feedback" class="ds-feedback-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="ds.feedback" data-i18n-aria="ds.feedback">
56
56
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-8 8H5l-1.5 2L4 20a8 8 0 0 1-1-4 8 8 0 0 1 18-4Z"/></svg>
@@ -81,10 +81,11 @@
81
81
  </div>
82
82
  </div>
83
83
  </aside>
84
+ <button id="ds-sidebar-backdrop" class="ds-sidebar-backdrop hidden" type="button" data-i18n-aria="ds.close" aria-label="关闭菜单"></button>
84
85
 
85
86
  <div class="ds-main">
86
87
  <header class="ds-topbar">
87
- <button id="btn-mobile-nav" class="ds-icon-btn" data-i18n-aria="ds.menu" data-i18n-title="ds.menu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
88
+ <button id="btn-mobile-nav" class="ds-icon-btn" aria-controls="ds-sidebar" aria-expanded="false" data-i18n-aria="ds.menu" data-i18n-title="ds.menu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
88
89
  <div class="ds-title" id="ds-title" data-i18n="ds.sessions">会话</div>
89
90
  <span id="ds-session-status" class="ds-session-status hidden"></span>
90
91
  <button id="btn-rename-session" class="ds-icon-btn hidden" data-i18n-title="ds.sessionRename" data-i18n-aria="ds.sessionRename">✎</button>
@@ -202,22 +203,22 @@
202
203
  <div class="ds-section-label" data-i18n="ds.settings">设置</div>
203
204
  <div id="settings-home">
204
205
  <div class="ds-settings">
205
- <div class="ds-setting-row" data-settings-group="general" style="cursor:pointer">
206
+ <button type="button" class="ds-setting-row ds-setting-link" data-settings-group="general">
206
207
  <div><div class="ds-setting-name" data-i18n="ds.groupGeneral">通用</div><div class="ds-setting-desc" data-i18n="ds.groupGeneralDesc">工具调用、预设提示词</div></div>
207
208
  <span class="ds-setting-arrow" aria-hidden="true">›</span>
208
- </div>
209
- <div class="ds-setting-row" data-settings-group="servers" style="cursor:pointer">
209
+ </button>
210
+ <button type="button" class="ds-setting-row ds-setting-link" data-settings-group="servers">
210
211
  <div><div class="ds-setting-name" data-i18n="ds.groupServers">服务器</div><div class="ds-setting-desc" data-i18n="ds.groupServersDesc">服务器地址</div></div>
211
212
  <span class="ds-setting-arrow" aria-hidden="true">›</span>
212
- </div>
213
- <div class="ds-setting-row" data-settings-group="theme" style="cursor:pointer">
213
+ </button>
214
+ <button type="button" class="ds-setting-row ds-setting-link" data-settings-group="theme">
214
215
  <div><div class="ds-setting-name" data-i18n="ds.groupTheme">皮肤</div><div class="ds-setting-desc" data-i18n="ds.groupThemeDesc">配色主题</div></div>
215
216
  <span class="ds-setting-arrow" aria-hidden="true">›</span>
216
- </div>
217
- <div class="ds-setting-row" data-settings-group="about" style="cursor:pointer">
217
+ </button>
218
+ <button type="button" class="ds-setting-row ds-setting-link" data-settings-group="about">
218
219
  <div><div class="ds-setting-name" data-i18n="ds.groupAbout">关于</div><div class="ds-setting-desc" data-i18n="ds.groupAboutDesc">版本信息</div></div>
219
220
  <span class="ds-setting-arrow" aria-hidden="true">›</span>
220
- </div>
221
+ </button>
221
222
  </div>
222
223
  <div class="ds-settings">
223
224
  <div class="ds-setting-row">
@@ -231,7 +232,7 @@
231
232
  </div>
232
233
  </div>
233
234
 
234
- <div id="settings-page-general" class="ds-settings hidden">
235
+ <div id="settings-page-general" class="ds-settings hidden" data-motion-panel>
235
236
  <div class="ds-setting-row" data-settings-back style="cursor:pointer">
236
237
  <button class="ds-btn" data-i18n="ds.back">‹ 返回</button>
237
238
  <div class="ds-setting-name" data-i18n="ds.groupGeneral">通用</div>
@@ -242,7 +243,7 @@
242
243
  </div>
243
244
  </div>
244
245
 
245
- <div id="settings-page-servers" class="ds-settings hidden">
246
+ <div id="settings-page-servers" class="ds-settings hidden" data-motion-panel>
246
247
  <div class="ds-setting-row" data-settings-back style="cursor:pointer">
247
248
  <button class="ds-btn" data-i18n="ds.back">‹ 返回</button>
248
249
  <div class="ds-setting-name" data-i18n="ds.groupServers">服务器</div>
@@ -266,18 +267,18 @@
266
267
  </div>
267
268
  </div>
268
269
 
269
- <div id="settings-page-theme" class="ds-settings hidden">
270
+ <div id="settings-page-theme" class="ds-settings hidden" data-motion-panel>
270
271
  <div class="ds-setting-row" data-settings-back style="cursor:pointer">
271
272
  <button class="ds-btn" data-i18n="ds.back">‹ 返回</button>
272
273
  <div class="ds-setting-name" data-i18n="ds.groupTheme">皮肤</div>
273
274
  </div>
274
275
  <div class="ds-setting-row">
275
- <div><div class="ds-setting-name" data-i18n="ds.themeTitle">皮肤</div><div class="ds-setting-desc" data-i18n="ds.themeDesc">四套皮肤</div></div>
276
+ <div><div class="ds-setting-name" data-i18n="ds.themeTitle">皮肤</div><div class="ds-setting-desc" data-i18n="ds.themeDesc">五套皮肤</div></div>
276
277
  <button id="btn-theme" class="ds-btn">默认深空</button>
277
278
  </div>
278
279
  </div>
279
280
 
280
- <div id="settings-page-about" class="ds-settings hidden">
281
+ <div id="settings-page-about" class="ds-settings hidden" data-motion-panel>
281
282
  <div class="ds-setting-row" data-settings-back style="cursor:pointer">
282
283
  <button class="ds-btn" data-i18n="ds.back">‹ 返回</button>
283
284
  <div class="ds-setting-name" data-i18n="ds.groupAbout">关于</div>
@@ -475,7 +476,7 @@
475
476
  'ds.back': '‹ 返回', 'ds.emptyGroup': '暂无设置项',
476
477
  'ds.serversTitle': '服务器(分组)', 'ds.speedTest': '测速', 'ds.currentGroup': '当前组',
477
478
  'ds.addGroup': '+ 组', 'ds.addServer': '添加', 'ds.tokenTitle': '访问令牌', 'ds.copyToken': '复制',
478
- 'ds.themeTitle': '皮肤', 'ds.themeDesc': '默认深空 / 落日 / 易北爱乐厅 / 草原孤塔', 'ds.langTitle': '语言 / Language',
479
+ 'ds.themeTitle': '皮肤', 'ds.themeDesc': '默认深空 / 落日 / 易北爱乐厅 / 草原孤塔 / 黑曜白', 'ds.langTitle': '语言 / Language',
479
480
  'ds.feedbackTitle': '反馈渠道', 'ds.feedbackDesc': 'GitHub / Gitee / B站:反馈 bug、提建议、唠嗑',
480
481
  'ds.feedback': '反馈', 'ds.feedbackGithubDesc': '反馈 bug / 提建议', 'ds.feedbackGiteeDesc': '国内镜像,无需代理',
481
482
  'ds.feedbackBiliDesc': 'UP 动态页交流', 'ds.feedbackCopyLink': '复制项目链接', 'ds.feedbackCopyDesc': '手动分享给朋友',
@@ -526,7 +527,7 @@
526
527
  'ds.groupsPromptRename': '重命名组「{group}」:', 'ds.groupsRenamed': '组已重命名',
527
528
  'ds.groupsCannotDeleteDefault': '默认组不可删除',
528
529
  'ds.groupsConfirmDelete': '删除组「{group}」?组内服务器将归入默认组。', 'ds.groupsDeleted': '组已删除',
529
- 'ds.theme.default': '默认深空', 'ds.theme.dark': '落日', 'ds.theme.light': '易北爱乐厅', 'ds.theme.neutral': '草原孤塔',
530
+ 'ds.theme.default': '默认深空', 'ds.theme.dark': '落日', 'ds.theme.light': '易北爱乐厅', 'ds.theme.neutral': '草原孤塔', 'ds.theme.mono': '黑曜白',
530
531
  'ds.role.me': '我', 'ds.role.dsh': 'DSH', 'ds.toolDefault': '工具',
531
532
  'ds.eventSystemReminder': '系统提示',
532
533
  'ds.questionOptions': '选项', 'ds.questionCustom': '自定义回答',
@@ -540,6 +541,7 @@
540
541
  'presets.nameEmpty': '名称不能为空', 'presets.nameTooLong': '名称不能超过 20 字', 'presets.textTooLong': '内容不能超过 2000 字', 'presets.limit': '最多 20 条预设', 'presets.added': '预设已添加',
541
542
  'presets.confirmDelete': '删除预设「{name}」?', 'presets.saved': '预设已保存', 'presets.deleted': '预设已删除',
542
543
  'goal.title': '目标', 'goal.pause': '暂停', 'goal.resume': '继续', 'goal.complete': '完成', 'goal.edit': '改目标', 'goal.clear': '清除',
544
+ 'goal.collapse': '收起目标到侧边', 'goal.collapseShort': '收起', 'goal.expand': '展开目标', 'goal.collapsedHint': '仍在运行',
543
545
  'goal.none': '当前会话没有目标', 'goal.confirmClear': '清除当前目标?(不会删除会话)', 'goal.confirmComplete': '将目标标记为完成?',
544
546
  'goal.actionFailed': '目标操作失败', 'goal.actionSubmitted': '目标操作已提交',
545
547
  'goal.cannotEmpty': '目标不能为空', 'goal.updateFailed': '更新失败', 'goal.updated': '目标已更新', 'goal.editPrompt': '修改目标:',
@@ -577,7 +579,7 @@
577
579
  'ds.back': '‹ Back', 'ds.emptyGroup': 'No settings here',
578
580
  'ds.serversTitle': 'Servers (groups)', 'ds.speedTest': 'Test', 'ds.currentGroup': 'Current group',
579
581
  'ds.addGroup': '+ Group', 'ds.addServer': 'Add', 'ds.tokenTitle': 'Access token', 'ds.copyToken': 'Copy',
580
- 'ds.themeTitle': 'Theme', 'ds.themeDesc': 'Deep Space / Sunset / Elbphilharmonie / Prairie Tower', 'ds.langTitle': 'Language',
582
+ 'ds.themeTitle': 'Theme', 'ds.themeDesc': 'Deep Space / Sunset / Elbphilharmonie / Prairie Tower / Monochrome', 'ds.langTitle': 'Language',
581
583
  'ds.feedbackTitle': 'Feedback', 'ds.feedbackDesc': 'GitHub / Gitee / Bilibili: report bugs, suggest features, or just chat',
582
584
  'ds.feedback': 'Feedback', 'ds.feedbackGithubDesc': 'Report bugs · suggest features', 'ds.feedbackGiteeDesc': 'Mirror in China, no proxy needed',
583
585
  'ds.feedbackBiliDesc': 'Chat on the UP\'s Bilibili page', 'ds.feedbackCopyLink': 'Copy project link', 'ds.feedbackCopyDesc': 'Share it manually',
@@ -628,7 +630,7 @@
628
630
  'ds.groupsPromptRename': 'Rename group "{group}":', 'ds.groupsRenamed': 'Group renamed',
629
631
  'ds.groupsCannotDeleteDefault': 'The default group cannot be deleted',
630
632
  'ds.groupsConfirmDelete': 'Delete group "{group}"? Its servers move to the default group.', 'ds.groupsDeleted': 'Group deleted',
631
- 'ds.theme.default': 'Deep Space', 'ds.theme.dark': 'Sunset', 'ds.theme.light': 'Elbphilharmonie', 'ds.theme.neutral': 'Prairie Tower',
633
+ 'ds.theme.default': 'Deep Space', 'ds.theme.dark': 'Sunset', 'ds.theme.light': 'Elbphilharmonie', 'ds.theme.neutral': 'Prairie Tower', 'ds.theme.mono': 'Monochrome',
632
634
  'ds.role.me': 'Me', 'ds.role.dsh': 'DSH', 'ds.toolDefault': 'tool',
633
635
  'ds.eventSystemReminder': 'System reminder',
634
636
  'ds.questionOptions': 'Options', 'ds.questionCustom': 'Custom answer',
@@ -642,6 +644,7 @@
642
644
  'presets.nameEmpty': 'Name cannot be empty', 'presets.nameTooLong': 'Name must be ≤20 characters', 'presets.textTooLong': 'Content must be ≤2000 characters', 'presets.limit': 'At most 20 presets', 'presets.added': 'Preset added',
643
645
  'presets.confirmDelete': 'Delete preset "{name}"?', 'presets.saved': 'Preset saved', 'presets.deleted': 'Preset deleted',
644
646
  'goal.title': 'Goal', 'goal.pause': 'Pause', 'goal.resume': 'Resume', 'goal.complete': 'Complete', 'goal.edit': 'Edit goal', 'goal.clear': 'Clear',
647
+ 'goal.collapse': 'Collapse goal to the side', 'goal.collapseShort': 'Collapse', 'goal.expand': 'Expand goal', 'goal.collapsedHint': 'Still running',
645
648
  'goal.none': 'This session has no goal', 'goal.confirmClear': 'Clear the current goal? (the session stays)', 'goal.confirmComplete': 'Mark the goal as completed?',
646
649
  'goal.actionFailed': 'Goal action failed', 'goal.actionSubmitted': 'Goal action submitted',
647
650
  'goal.cannotEmpty': 'Goal cannot be empty', 'goal.updateFailed': 'Update failed', 'goal.updated': 'Goal updated', 'goal.editPrompt': 'Edit goal:',