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.
@@ -27,10 +27,11 @@ const CAP = window.Capacitor || null
27
27
 
28
28
  /* ---------------- 皮肤 ---------------- */
29
29
  const THEME_META = [
30
- { id: 'default', sw: ['#0B0E1A', '#151B33', '#5B8CFF'] },
31
- { id: 'dark', sw: ['#05348B', '#0D438F', '#F9A647'] },
32
- { id: 'light', sw: ['#EFEEEC', '#FAF8F5', '#E6BC7B'] },
33
- { id: 'neutral', sw: ['#DDD4B8', '#585818', '#832D15'] }
30
+ { id: 'default', sw: ['#0D1117', '#21262D', '#58A6FF'] },
31
+ { id: 'dark', sw: ['#161316', '#302323', '#FFB86B'] },
32
+ { id: 'light', sw: ['#F5F7F8', '#FFFFFF', '#176B87'] },
33
+ { id: 'neutral', sw: ['#EEF1E8', '#FAFBF6', '#47643C'] },
34
+ { id: 'mono', sw: ['#050505', '#333333', '#F5F5F5'] }
34
35
  ]
35
36
  function themeGet() {
36
37
  let v = LS.get('dshTheme', '')
@@ -1381,6 +1382,24 @@ function goalOf(s) {
1381
1382
  if (!p) return null
1382
1383
  return p.goal && typeof p.goal === 'object' ? p.goal : p
1383
1384
  }
1385
+ const COLLAPSED_GOALS_KEY = 'dshCollapsedGoalsV1'
1386
+ function collapsedGoals() {
1387
+ try {
1388
+ const value = JSON.parse(LS.get(COLLAPSED_GOALS_KEY, '[]'))
1389
+ return Array.isArray(value) ? value.filter(item => item && typeof item.sessionId === 'string' && typeof item.goalId === 'string') : []
1390
+ } catch { return [] }
1391
+ }
1392
+ function goalDisplayId(goal) { return String(goal?.id || '__current__') }
1393
+ function isGoalCollapsed(sessionId, goal) {
1394
+ const goalId = goalDisplayId(goal)
1395
+ return collapsedGoals().some(item => item.sessionId === sessionId && item.goalId === goalId)
1396
+ }
1397
+ function setGoalCollapsed(sessionId, goal, collapsed) {
1398
+ const goalId = goalDisplayId(goal)
1399
+ const next = collapsedGoals().filter(item => item.sessionId !== sessionId)
1400
+ if (collapsed) next.push({ sessionId, goalId })
1401
+ LS.set(COLLAPSED_GOALS_KEY, JSON.stringify(next.slice(-100)))
1402
+ }
1384
1403
  function onSessionEvent(sessionId, event) {
1385
1404
  if (event?.type === 'turn/start' || event?.type === 'turn/end') {
1386
1405
  noteSessionTurnTime(sessionId, event)
@@ -1738,15 +1757,23 @@ async function renderSessionCards() {
1738
1757
  const todos = proj(s, 'todos')
1739
1758
  let html = ''
1740
1759
  if (goal && !isGoalTerminal(goal)) {
1741
- html += `<div class="ds-card ds-goal-card"><div class="ds-card-title">${t('goal.title')}</div>
1742
- <div class="ds-goal-obj">${esc(goal.objective || '')}</div>
1743
- <div class="ds-goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
1744
- <div class="ds-goal-actions">
1745
- ${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>`}
1746
- <button class="ds-mini-btn" data-goal="complete">${t('goal.complete')}</button>
1747
- <button class="ds-mini-btn" data-goal="edit">${t('goal.edit')}</button>
1748
- <button class="ds-mini-btn" data-goal="clear">${t('goal.clear')}</button>
1749
- </div></div>`
1760
+ const collapsed = isGoalCollapsed(sessionId, goal)
1761
+ html += `<div class="ds-goal-disclosure${collapsed ? ' is-collapsed' : ''}">
1762
+ <div id="goal-panel-desktop" class="ds-card ds-goal-card${collapsed ? ' hidden' : ''}">
1763
+ <div class="ds-goal-card-head"><div class="ds-card-title">${t('goal.title')}</div>
1764
+ <button type="button" class="ds-goal-collapse-btn" data-goal-collapse="1" aria-expanded="true" aria-controls="goal-panel-desktop" title="${esc(t('goal.collapse'))}"><span aria-hidden="true">›</span>${t('goal.collapseShort')}</button>
1765
+ </div>
1766
+ <div class="ds-goal-obj">${esc(goal.objective || '')}</div>
1767
+ <div class="ds-goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
1768
+ <div class="ds-goal-actions">
1769
+ ${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>`}
1770
+ <button class="ds-mini-btn" data-goal="complete">${t('goal.complete')}</button>
1771
+ <button class="ds-mini-btn" data-goal="edit">${t('goal.edit')}</button>
1772
+ <button class="ds-mini-btn" data-goal="clear">${t('goal.clear')}</button>
1773
+ </div>
1774
+ </div>
1775
+ <button type="button" class="ds-goal-side-tab${collapsed ? '' : ' hidden'}" data-goal-collapse="0" aria-expanded="false" aria-controls="goal-panel-desktop" 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>
1776
+ </div>`
1750
1777
  }
1751
1778
  if (todos?.items?.length) {
1752
1779
  html += `<div class="ds-card"><div class="ds-card-title">${t('todos.title')}</div>${todos.items.map(item =>
@@ -1756,6 +1783,15 @@ async function renderSessionCards() {
1756
1783
  box.innerHTML = html
1757
1784
  box.querySelectorAll('[data-goal]').forEach(btn =>
1758
1785
  btn.addEventListener('click', () => goalAction(btn.dataset.goal)))
1786
+ box.querySelectorAll('[data-goal-collapse]').forEach(btn =>
1787
+ btn.addEventListener('click', () => {
1788
+ const collapse = btn.dataset.goalCollapse === '1'
1789
+ const currentGoal = goalOf(state.byId.get(state.current))
1790
+ if (!currentGoal) return
1791
+ setGoalCollapsed(state.current, currentGoal, collapse)
1792
+ void renderSessionCards()
1793
+ requestAnimationFrame(() => box.querySelector(`[data-goal-collapse="${collapse ? '0' : '1'}"]`)?.focus())
1794
+ }))
1759
1795
 
1760
1796
  const sub = await safeRpc('subagent.list', { parentSessionId: sessionId }, '')
1761
1797
  if (renderGeneration !== sessionCardsRenderGeneration || state.current !== sessionId) return
@@ -2719,10 +2755,25 @@ function renderOverviewDesktop() {
2719
2755
  $('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
2720
2756
  }
2721
2757
 
2758
+ const narrowSidebarQuery = window.matchMedia('(max-width: 1023px)')
2759
+ function setMobileSidebar(open) {
2760
+ const sidebar = $('ds-sidebar')
2761
+ const backdrop = $('ds-sidebar-backdrop')
2762
+ const trigger = $('btn-mobile-nav')
2763
+ if (!sidebar || !backdrop || !trigger) return
2764
+ const visible = !!open && narrowSidebarQuery.matches
2765
+ sidebar.classList.toggle('mobile-open', visible)
2766
+ backdrop.classList.toggle('hidden', !visible)
2767
+ trigger.setAttribute('aria-expanded', String(visible))
2768
+ sidebar.inert = narrowSidebarQuery.matches && !visible
2769
+ $('ds-app')?.classList.toggle('mobile-nav-open', visible)
2770
+ }
2771
+
2722
2772
  function showView(id) {
2723
2773
  state.view = id
2724
2774
  for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
2725
2775
  document.querySelectorAll('.ds-nav-item').forEach(b => b.classList.toggle('active', b.dataset.view === id))
2776
+ if (narrowSidebarQuery.matches) setMobileSidebar(false)
2726
2777
  window.DshMotion?.view($(id))
2727
2778
  const titles = { 'view-overview': 'ds.overview', 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
2728
2779
  if (id === 'view-chat') { const s = state.byId.get(state.current); $('ds-title').textContent = s ? titleOf(s) : t('ds.sessions') }
@@ -2825,9 +2876,14 @@ function bindUi() {
2825
2876
  LS.set('sessionSort', state.sessionSort)
2826
2877
  renderSessions()
2827
2878
  })
2828
- $('btn-mobile-nav').addEventListener('click', () => {
2829
- const list = $('mobile-session-list')
2830
- list.style.display = list.style.display === 'none' ? 'flex' : 'none'
2879
+ $('btn-mobile-nav').addEventListener('click', () => setMobileSidebar(!$('ds-sidebar').classList.contains('mobile-open')))
2880
+ $('ds-sidebar-backdrop').addEventListener('click', () => setMobileSidebar(false))
2881
+ narrowSidebarQuery.addEventListener?.('change', () => setMobileSidebar(false))
2882
+ document.addEventListener('keydown', (event) => {
2883
+ if (event.key === 'Escape' && $('ds-sidebar').classList.contains('mobile-open')) {
2884
+ setMobileSidebar(false)
2885
+ $('btn-mobile-nav').focus()
2886
+ }
2831
2887
  })
2832
2888
  document.querySelectorAll('.ds-nav-item').forEach(b => b.addEventListener('click', () => showView(b.dataset.view)))
2833
2889
  $('ds-overview-refresh').addEventListener('click', async () => {
package/public/index.html CHANGED
@@ -3,12 +3,12 @@
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
  <meta name="mobile-web-app-capable" content="yes">
8
8
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
9
  <title>DSH Remote</title>
10
10
  <script>/* 统一入口: 桌面浏览器(宽屏且 UA 非移动)访问根地址自动切换到桌面端, 手机/窄窗口继续 App 界面 */ (function(){try{var proto=location.protocol;var ua=navigator.userAgent||'';var mobile=/Android|iPhone|iPad|iPod|Mobile|Windows Phone/i.test(ua);if((proto==='http:'||proto==='https:')&&!mobile&&window.innerWidth>=1024){var p=location.pathname;if(p==='/'||/\/index\.html$/.test(p)){location.replace('desktop/desktop.html'+location.search)}}}catch(e){}})()</script>
11
- <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>
11
+ <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>
12
12
  <link rel="manifest" href="manifest.webmanifest">
13
13
  <link rel="icon" href="icon.svg" type="image/svg+xml">
14
14
  <link rel="stylesheet" href="styles.css">
@@ -276,27 +276,27 @@
276
276
  <section id="view-settings" class="view hidden">
277
277
  <div id="settings-home">
278
278
  <div class="settings-group">
279
- <div class="setting-row" data-settings-group="general" style="cursor:pointer">
279
+ <button type="button" class="setting-row setting-link" data-settings-group="general">
280
280
  <div><div class="setting-name" data-i18n="settings.groupGeneral">通用</div><div class="setting-desc" data-i18n="settings.groupGeneralDesc">显示工具调用、预设提示词</div></div>
281
- </div>
282
- <div class="setting-row" data-settings-group="model" style="cursor:pointer">
281
+ </button>
282
+ <button type="button" class="setting-row setting-link" data-settings-group="model">
283
283
  <div><div class="setting-name" data-i18n="settings.groupModel">模型</div><div class="setting-desc" data-i18n="settings.groupModelDesc">提供方、API 密钥和模型目录</div></div>
284
- </div>
285
- <div class="setting-row" data-settings-group="tests" style="cursor:pointer">
284
+ </button>
285
+ <button type="button" class="setting-row setting-link" data-settings-group="tests">
286
286
  <div><div class="setting-name" data-i18n="settings.groupTests">功能测试</div><div class="setting-desc" data-i18n="settings.groupTestsDesc">验证 Android 和 DSH 原生能力</div></div>
287
- </div>
288
- <div class="setting-row" data-settings-group="servers" style="cursor:pointer">
287
+ </button>
288
+ <button type="button" class="setting-row setting-link" data-settings-group="servers">
289
289
  <div><div class="setting-name" data-i18n="settings.groupServers">服务器</div><div class="setting-desc" data-i18n="settings.groupServersDesc">服务器地址、扫码连接</div></div>
290
- </div>
291
- <div class="setting-row" data-settings-group="notify" style="cursor:pointer">
290
+ </button>
291
+ <button type="button" class="setting-row setting-link" data-settings-group="notify">
292
292
  <div><div class="setting-name" data-i18n="settings.groupNotify">通知</div><div class="setting-desc" data-i18n="settings.groupNotifyDesc">通知、提醒、后台轮询</div></div>
293
- </div>
294
- <div class="setting-row" data-settings-group="theme" style="cursor:pointer">
293
+ </button>
294
+ <button type="button" class="setting-row setting-link" data-settings-group="theme">
295
295
  <div><div class="setting-name" data-i18n="settings.groupTheme">皮肤</div><div class="setting-desc" data-i18n="settings.groupThemeDesc">配色主题</div></div>
296
- </div>
297
- <div class="setting-row" data-settings-group="about" style="cursor:pointer">
296
+ </button>
297
+ <button type="button" class="setting-row setting-link" data-settings-group="about">
298
298
  <div><div class="setting-name" data-i18n="settings.groupAbout">关于</div><div class="setting-desc" data-i18n="settings.groupAboutDesc">DSH 状态、检查更新</div></div>
299
- </div>
299
+ </button>
300
300
  </div>
301
301
  <div class="settings-group">
302
302
  <div class="setting-row">
@@ -336,7 +336,7 @@
336
336
  <p class="about" data-i18n="settings.about">DSH Remote · 局域网 / Tailscale 均可访问<br>仅作为 DSH 本机 3080 端口的带认证代理</p>
337
337
  </div>
338
338
 
339
- <div id="settings-page-general" class="settings-subpage hidden">
339
+ <div id="settings-page-general" class="settings-subpage hidden" data-motion-panel>
340
340
  <div class="settings-group">
341
341
  <div class="setting-row" data-settings-back style="cursor:pointer">
342
342
  <button class="mini-btn" data-i18n="settings.back">‹ 返回</button>
@@ -363,7 +363,7 @@
363
363
  </div>
364
364
  </div>
365
365
 
366
- <div id="settings-page-model" class="settings-subpage hidden">
366
+ <div id="settings-page-model" class="settings-subpage hidden" data-motion-panel>
367
367
  <div class="settings-group">
368
368
  <div class="setting-row" data-settings-back style="cursor:pointer">
369
369
  <button class="mini-btn" data-i18n="settings.back">‹ 返回</button>
@@ -386,7 +386,7 @@
386
386
  </div>
387
387
  </div>
388
388
 
389
- <div id="settings-page-tests" class="settings-subpage hidden">
389
+ <div id="settings-page-tests" class="settings-subpage hidden" data-motion-panel>
390
390
  <div class="settings-group">
391
391
  <div class="setting-row" data-settings-back style="cursor:pointer">
392
392
  <button class="mini-btn" data-i18n="settings.back">‹ 返回</button>
@@ -415,7 +415,7 @@
415
415
  </div>
416
416
  </div>
417
417
 
418
- <div id="settings-page-servers" class="settings-subpage hidden">
418
+ <div id="settings-page-servers" class="settings-subpage hidden" data-motion-panel>
419
419
  <div class="settings-group">
420
420
  <div class="setting-row" data-settings-back style="cursor:pointer">
421
421
  <button class="mini-btn" data-i18n="settings.back">‹ 返回</button>
@@ -452,7 +452,7 @@
452
452
  </div>
453
453
  </div>
454
454
 
455
- <div id="settings-page-notify" class="settings-subpage hidden">
455
+ <div id="settings-page-notify" class="settings-subpage hidden" data-motion-panel>
456
456
  <div class="settings-group">
457
457
  <div class="setting-row" data-settings-back style="cursor:pointer">
458
458
  <button class="mini-btn" data-i18n="settings.back">‹ 返回</button>
@@ -496,7 +496,7 @@
496
496
  </div>
497
497
  </div>
498
498
 
499
- <div id="settings-page-theme" class="settings-subpage hidden">
499
+ <div id="settings-page-theme" class="settings-subpage hidden" data-motion-panel>
500
500
  <div class="settings-group">
501
501
  <div class="setting-row" data-settings-back style="cursor:pointer">
502
502
  <button class="mini-btn" data-i18n="settings.back">‹ 返回</button>
@@ -505,13 +505,13 @@
505
505
  </div>
506
506
  <div class="settings-group">
507
507
  <div class="setting-row">
508
- <div><div class="setting-name" data-i18n="settings.themeTitle">皮肤</div><div class="setting-desc" data-i18n="settings.themeDesc">默认深空 / 落日 / 易北爱乐厅 / 草原孤塔</div></div>
508
+ <div><div class="setting-name" data-i18n="settings.themeTitle">皮肤</div><div class="setting-desc" data-i18n="settings.themeDesc">默认深空 / 落日 / 易北爱乐厅 / 草原孤塔 / 黑曜白</div></div>
509
509
  <button id="btn-theme" class="mini-btn" style="min-width:104px">默认深空</button>
510
510
  </div>
511
511
  </div>
512
512
  </div>
513
513
 
514
- <div id="settings-page-about" class="settings-subpage hidden">
514
+ <div id="settings-page-about" class="settings-subpage hidden" data-motion-panel>
515
515
  <div class="settings-group">
516
516
  <div class="setting-row" data-settings-back style="cursor:pointer">
517
517
  <button class="mini-btn" data-i18n="settings.back">‹ 返回</button>
@@ -607,7 +607,7 @@
607
607
  <span data-i18n="nav.stats">统计</span>
608
608
  </button>
609
609
  <button data-view="view-settings" class="nav-btn">
610
- <span class="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>
610
+ <span class="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>
611
611
  <span data-i18n="nav.settings">设置</span>
612
612
  </button>
613
613
  </nav>
@@ -975,6 +975,7 @@
975
975
  'stats.llmTime': '模型耗时', 'stats.minutes': ' 分钟', 'stats.outputCache': '输出 / 缓存读',
976
976
  'stats.ctxPressure': '上下文压力', 'stats.permission': '权限', 'stats.empty': '暂无统计数据',
977
977
  'goal.title': '目标', 'goal.pause': '暂停', 'goal.resume': '继续', 'goal.complete': '完成', 'goal.edit': '改目标', 'goal.clear': '清除',
978
+ 'goal.collapse': '收起目标到侧边', 'goal.collapseShort': '收起', 'goal.expand': '展开目标', 'goal.collapsedHint': '仍在运行',
978
979
  'goal.none': '当前会话没有目标', 'goal.confirmClear': '清除当前目标?(不会删除会话)', 'goal.confirmComplete': '将目标标记为完成?',
979
980
  'goal.actionFailed': '目标操作失败', 'goal.actionSubmitted': '目标操作已提交',
980
981
  'goal.cannotEmpty': '目标不能为空', 'goal.updateFailed': '更新失败', 'goal.updated': '目标已更新',
@@ -1087,8 +1088,8 @@
1087
1088
  'settings.toolsTitle': '显示工具调用', 'settings.toolsDesc': '关闭后隐藏 🔧/📦 工具调用与结果',
1088
1089
  'settings.mobileEnterTitle': '手机回车行为', 'settings.mobileEnterDesc': '选择手机端按 Enter 时换行或直接发送', 'settings.mobileEnterNewline': '回车换行', 'settings.mobileEnterSend': '回车发送',
1089
1090
  'settings.langTitle': '语言 / Language', 'settings.langDesc': '界面语言,跟随系统或手动切换',
1090
- 'settings.themeTitle': '皮肤', 'settings.themeDesc': '默认深空 / 落日 / 易北爱乐厅 / 草原孤塔',
1091
- 'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔',
1091
+ 'settings.themeTitle': '皮肤', 'settings.themeDesc': '默认深空 / 落日 / 易北爱乐厅 / 草原孤塔 / 黑曜白',
1092
+ 'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔', 'theme.mono': '黑曜白',
1092
1093
  'theme.panelTitle': '选择配色', 'theme.close': '关闭',
1093
1094
  'settings.tokenTitle': '访问令牌', 'settings.change': '更换',
1094
1095
  'settings.hostTitle': 'DSH 状态', 'settings.hostProbing': '探测中…', 'settings.probe': '探测', 'settings.probeFailed': '探测失败',
@@ -1204,6 +1205,7 @@
1204
1205
  'stats.llmTime': 'LLM time', 'stats.minutes': ' min', 'stats.outputCache': 'Output / cache read',
1205
1206
  'stats.ctxPressure': 'Context pressure', 'stats.permission': 'Permission', 'stats.empty': 'No stats yet',
1206
1207
  'goal.title': 'Goal', 'goal.pause': 'Pause', 'goal.resume': 'Resume', 'goal.complete': 'Complete', 'goal.edit': 'Edit goal', 'goal.clear': 'Clear',
1208
+ 'goal.collapse': 'Collapse goal to the side', 'goal.collapseShort': 'Collapse', 'goal.expand': 'Expand goal', 'goal.collapsedHint': 'Still running',
1207
1209
  'goal.none': 'This session has no goal', 'goal.confirmClear': 'Clear the current goal? (the session stays)', 'goal.confirmComplete': 'Mark the goal as completed?',
1208
1210
  'goal.actionFailed': 'Goal action failed', 'goal.actionSubmitted': 'Goal action submitted',
1209
1211
  'goal.cannotEmpty': 'Goal cannot be empty', 'goal.updateFailed': 'Update failed', 'goal.updated': 'Goal updated',
@@ -1316,8 +1318,8 @@
1316
1318
  'settings.toolsTitle': 'Show tool calls', 'settings.toolsDesc': 'Hide 🔧/📦 tool calls and results when off',
1317
1319
  'settings.mobileEnterTitle': 'Mobile Enter behavior', 'settings.mobileEnterDesc': 'Choose whether Enter inserts a newline or sends', 'settings.mobileEnterNewline': 'Enter = newline', 'settings.mobileEnterSend': 'Enter = send',
1318
1320
  'settings.langTitle': 'Language / 语言', 'settings.langDesc': 'UI language, follows system or switch manually',
1319
- 'settings.themeTitle': 'Theme', 'settings.themeDesc': 'Default deep space / Sunset / Elbphilharmonie / Prairie Tower',
1320
- 'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower',
1321
+ 'settings.themeTitle': 'Theme', 'settings.themeDesc': 'Deep Space / Sunset / Elbphilharmonie / Prairie Tower / Monochrome',
1322
+ 'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower', 'theme.mono': 'Monochrome',
1321
1323
  'theme.panelTitle': 'Choose theme', 'theme.close': 'Close',
1322
1324
  'settings.tokenTitle': 'Access token', 'settings.change': 'Change',
1323
1325
  'settings.hostTitle': 'DSH status', 'settings.hostProbing': 'Probing…', 'settings.probe': 'Probe', 'settings.probeFailed': 'Probe failed',
package/public/motion.js CHANGED
@@ -7,8 +7,21 @@
7
7
 
8
8
  const reduceQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)')
9
9
  let reduced = !!reduceQuery?.matches
10
- reduceQuery?.addEventListener?.('change', event => { reduced = !!event.matches })
11
- const activePulses = new WeakSet()
10
+ const activePulses = new Set()
11
+ const recentRuns = new WeakMap()
12
+
13
+ reduceQuery?.addEventListener?.('change', event => {
14
+ reduced = !!event.matches
15
+ if (!reduced) return
16
+ const animated = document.querySelectorAll('.view, .ds-view, [data-motion-view], .modal, .ds-modal, .sheet, .ds-drawer, [data-motion-panel]')
17
+ gsap.killTweensOf(animated)
18
+ clear(animated)
19
+ for (const node of activePulses) {
20
+ gsap.killTweensOf(node)
21
+ clear(node)
22
+ }
23
+ activePulses.clear()
24
+ })
12
25
 
13
26
  function clear(targets) {
14
27
  gsap.set(targets, { clearProps: 'opacity,visibility,transform,willChange' })
@@ -22,17 +35,32 @@
22
35
  return items.map(motionKey).join('|')
23
36
  }
24
37
 
38
+ function startedRecently(node, threshold = 48) {
39
+ const now = window.performance?.now?.() || Date.now()
40
+ const previous = recentRuns.get(node) || 0
41
+ recentRuns.set(node, now)
42
+ return previous > 0 && now - previous < threshold
43
+ }
44
+
25
45
  function view(view) {
26
- if (!view) return
27
- const children = [...view.children].filter(child => !child.classList.contains('hidden')).slice(0, 6)
28
- gsap.killTweensOf([view, ...children])
46
+ if (!view || startedRecently(view)) return
47
+ const children = [...view.children].filter(child => !child.classList.contains('hidden')).slice(0, 8)
48
+ const targets = children.length ? children : [view]
49
+ gsap.killTweensOf([view, ...targets])
50
+ gsap.set(view, { autoAlpha: 1, clearProps: 'transform' })
29
51
  if (reduced) {
30
- clear([view, ...children])
52
+ clear([view, ...targets])
31
53
  return
32
54
  }
33
- const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
34
- tl.fromTo(view, { autoAlpha: 0, y: 8 }, { autoAlpha: 1, y: 0, duration: 0.22, clearProps: 'transform' })
35
- .fromTo(children, { autoAlpha: 0, y: 6 }, { autoAlpha: 1, y: 0, duration: 0.16, stagger: 0.025, clearProps: 'transform' }, '<0.04')
55
+ gsap.fromTo(targets, { opacity: 0.72, y: 8 }, {
56
+ opacity: 1,
57
+ y: 0,
58
+ duration: 0.24,
59
+ ease: 'power2.out',
60
+ stagger: { each: 0.035, from: 'start' },
61
+ overwrite: 'auto',
62
+ clearProps: 'opacity,visibility,transform,willChange'
63
+ })
36
64
  }
37
65
 
38
66
  function list(container, selector) {
@@ -420,8 +448,57 @@
420
448
  registry.set(selector, state)
421
449
  }
422
450
 
451
+ function panel(node) {
452
+ if (!node || startedRecently(node)) return
453
+ gsap.killTweensOf(node)
454
+ if (reduced) return clear(node)
455
+ gsap.fromTo(node, { autoAlpha: 0, y: 8 }, {
456
+ autoAlpha: 1,
457
+ y: 0,
458
+ duration: 0.2,
459
+ ease: 'power2.out',
460
+ overwrite: 'auto',
461
+ clearProps: 'opacity,visibility,transform,willChange'
462
+ })
463
+ }
464
+
465
+ function popover(node) {
466
+ if (!node || startedRecently(node)) return
467
+ const opensUpward = !!(node.closest('.composer-wrap, .ds-composer') || node.style.bottom)
468
+ gsap.killTweensOf(node)
469
+ if (reduced) return clear(node)
470
+ gsap.fromTo(node, {
471
+ autoAlpha: 0,
472
+ y: opensUpward ? 7 : -7,
473
+ scale: 0.985,
474
+ transformOrigin: opensUpward ? 'bottom left' : 'top right'
475
+ }, {
476
+ autoAlpha: 1,
477
+ y: 0,
478
+ scale: 1,
479
+ duration: 0.18,
480
+ ease: 'power2.out',
481
+ overwrite: 'auto',
482
+ clearProps: 'opacity,visibility,transform,transformOrigin,willChange'
483
+ })
484
+ }
485
+
486
+ function activate(node) {
487
+ if (!node || reduced || startedRecently(node, 90)) return
488
+ const target = node.querySelector('.nav-home-orb, .nav-ico, .ds-nav-ico') || node
489
+ gsap.killTweensOf(target)
490
+ gsap.fromTo(target, { scale: 0.9, y: 2 }, {
491
+ scale: 1,
492
+ y: 0,
493
+ duration: 0.22,
494
+ ease: 'power2.out',
495
+ overwrite: 'auto',
496
+ clearProps: 'transform,willChange'
497
+ })
498
+ }
499
+
423
500
  function overlay(node) {
424
- if (!node) return
501
+ if (!node || startedRecently(node)) return
425
502
  const card = node.querySelector('.modal-card, .ds-modal-card, .sheet, .ds-drawer') || node
426
503
  gsap.killTweensOf([node, card])
427
504
  if (reduced) {
@@ -430,11 +507,18 @@
430
507
  }
431
508
  const isSideDrawer = card.classList.contains('ds-drawer')
432
509
  const isSheet = card.classList.contains('sheet')
510
+ if (card === node) {
511
+ gsap.fromTo(card,
512
+ { autoAlpha: 0, x: isSideDrawer ? 28 : 0, y: isSheet ? 24 : 8, scale: isSideDrawer || isSheet ? 1 : 0.985 },
513
+ { autoAlpha: 1, x: 0, y: 0, scale: 1, duration: isSheet ? 0.24 : 0.2, ease: 'power3.out', overwrite: 'auto', clearProps: 'opacity,visibility,transform,willChange' }
514
+ )
515
+ return
516
+ }
433
517
  const tl = gsap.timeline({ defaults: { ease: 'power3.out' } })
434
- tl.fromTo(node, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.16 })
518
+ tl.fromTo(node, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.14, overwrite: 'auto', clearProps: 'opacity,visibility' })
435
519
  .fromTo(card,
436
- { autoAlpha: 0, x: isSideDrawer ? 28 : 0, y: isSheet ? 24 : 8, scale: isSideDrawer || isSheet ? 1 : 0.985 },
437
- { autoAlpha: 1, x: 0, y: 0, scale: 1, duration: isSheet ? 0.24 : 0.2, clearProps: 'transform' },
520
+ { x: isSideDrawer ? 28 : 0, y: isSheet ? 24 : 8, scale: isSideDrawer || isSheet ? 1 : 0.985 },
521
+ { x: 0, y: 0, scale: 1, duration: isSheet ? 0.24 : 0.2, overwrite: 'auto', clearProps: 'transform,willChange' },
438
522
  '<'
439
523
  )
440
524
  }
@@ -449,10 +533,10 @@
449
533
  }
450
534
  }
451
535
 
452
- window.DshMotion = { gsap, reduced: () => reduced, view, list, relayout, bindLongPressReorder, overlay, pulse }
536
+ window.DshMotion = { gsap, reduced: () => reduced, view, list, relayout, bindLongPressReorder, overlay, panel, popover, activate, pulse }
453
537
 
454
538
  document.addEventListener('DOMContentLoaded', () => {
455
- document.querySelectorAll('.view:not(.hidden), .ds-view:not(.hidden)').forEach(node => view(node))
539
+ document.querySelectorAll('.view:not(.hidden), .ds-view:not(.hidden), [data-motion-view]:not(.hidden)').forEach(node => view(node))
456
540
  }, { once: true })
457
541
 
458
542
  const observer = new MutationObserver(records => {
@@ -460,9 +544,13 @@
460
544
  const node = record.target
461
545
  if (!(node instanceof HTMLElement)) continue
462
546
  const becameVisible = record.oldValue?.split(/\s+/).includes('hidden') && !node.classList.contains('hidden')
547
+ const becameActive = !record.oldValue?.split(/\s+/).includes('active') && node.classList.contains('active')
548
+ if (becameActive && node.matches('.nav-btn, .ds-nav-item')) activate(node)
463
549
  if (!becameVisible) continue
464
- if (node.matches('.view, .ds-view')) view(node)
465
- else if (node.matches('.modal, .ds-modal, .sheet, .ds-drawer')) overlay(node)
550
+ if (node.matches('.view, .ds-view, [data-motion-view]')) view(node)
551
+ else if (node.matches('.modal, .ds-modal, .sheet-backdrop, .sheet, .ds-drawer')) overlay(node)
552
+ else if (node.matches('.composer-menu, .ds-group-menu, .ds-feedback-menu, .fb-menu')) popover(node)
553
+ else if (node.matches('[data-motion-panel], .wb-panel, .ds-wb-panel, .doctor-body, .settings-subpage')) panel(node)
466
554
  }
467
555
  })
468
556
  observer.observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ['class'], attributeOldValue: true })
@@ -4,19 +4,19 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
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
  <style>
10
10
  :root { color-scheme: dark; }
11
11
  html[data-theme="light"], html[data-theme="neutral"] { color-scheme: light; }
12
- * { box-sizing: border-box; }
12
+ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
13
13
  body {
14
14
  margin: 0; min-width: 280px; color: var(--dsr-text); background:
15
15
  radial-gradient(420px 240px at 100% 0, var(--dsr-accent-soft), transparent 72%),
16
16
  var(--dsr-bg); font: 13px/1.45 system-ui, -apple-system, "PingFang SC", "Noto Sans CJK SC", sans-serif;
17
17
  }
18
18
  button, a { font: inherit; }
19
- button { cursor: pointer; }
19
+ button { cursor: pointer; touch-action: manipulation; }
20
20
  .plugin-shell { min-height: 100vh; padding: 18px 16px 16px; display: flex; flex-direction: column; gap: 14px; }
21
21
  .plugin-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
22
22
  .plugin-brand { display: flex; align-items: center; gap: 9px; min-width: 0; }
@@ -41,8 +41,9 @@
41
41
  .plugin-primary:hover, .plugin-primary:focus-visible { filter: brightness(1.08); transform: translateY(-1px); }
42
42
  .plugin-primary:disabled { cursor: wait; opacity: .65; transform: none; }
43
43
  .plugin-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
44
- .plugin-action { min-height: 42px; padding: 7px 5px; border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-panel); color: var(--dsr-text); font-size: 11px; }
44
+ .plugin-action { min-height: 42px; padding: 7px 5px; border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-panel); color: var(--dsr-text); font-size: 11px; transition: transform .18s ease, background-color .18s ease, border-color .18s ease; }
45
45
  .plugin-action:hover, .plugin-action:focus-visible { border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
46
+ .plugin-action:active { transform: scale(.98); }
46
47
  .plugin-action svg { display: block; width: 17px; height: 17px; margin: 0 auto 3px; color: var(--dsr-accent-strong); }
47
48
  .plugin-usage { padding: 14px; border: 1px solid var(--dsr-line); border-radius: 16px; background: linear-gradient(145deg, var(--dsr-panel), var(--dsr-bg-2)); }
48
49
  .plugin-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--dsr-text); font-size: 12px; font-weight: 750; }
@@ -87,7 +88,7 @@
87
88
  </style>
88
89
  </head>
89
90
  <body>
90
- <main class="plugin-shell">
91
+ <main class="plugin-shell" data-motion-view>
91
92
  <header class="plugin-head">
92
93
  <div class="plugin-brand">
93
94
  <span class="plugin-brand-mark" aria-hidden="true"></span>