dsh-sidebar-suite 0.2.7 → 0.2.9

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/lib/client.js CHANGED
@@ -179,6 +179,25 @@ window.__ModuleLoader__.load({
179
179
  'settings.support': '喜欢这个插件?请作者喝杯咖啡 · Enjoy it? Buy me a coffee',
180
180
  'settings.supportHint': '完全自愿,无任何回报承诺。用微信扫一扫即可;「设置 → 支持贡献者」里也登记了同一个码。 / Purely voluntary, no rewards promised. Scan with WeChat Pay; the same QR is registered under Settings → Support contributors.',
181
181
  'settings.qrAlt': '微信支付收款码 / WeChat Pay QR',
182
+ // 0.2.8 — every remaining config key becomes a switch on this page.
183
+ 'options.title': '侧栏与临时会话 · Sidebar & temp session options',
184
+ 'options.help': '每一项都会写入 temp-session-config.txt。也可以直接用记事本改那个文件,保存后约 3 秒自动生效,不用重启 DSH;两边是同一个文件,不会打架。 / Every switch writes temp-session-config.txt. You can also edit that file in Notepad — saves apply in about 3 seconds, no restart. Both paths share one file so they never disagree.',
185
+ 'options.showButton': '显示「临时」按钮 / Show the Temp button',
186
+ 'options.inChatMode': 'CHAT 官网模式 / In CHAT mode',
187
+ 'options.chatHide': '隐藏按钮 / Hide',
188
+ 'options.chatShow': '照常显示 / Show',
189
+ 'options.buttonStyle': '按钮摆法 / Button layout',
190
+ 'options.stylePair': '并排 / Pair',
191
+ 'options.styleOwn': '独占一行 / Own row',
192
+ 'options.expandAll': '会话默认全展开 / Always expand sessions',
193
+ 'options.expandHint': '关掉后恢复 DSH 原来的折叠行为;已经展开的列表要切换一次工作区才会收回去。 / Turning it off restores host folding; already-expanded groups collapse after you switch workspaces once.',
194
+ 'options.maxKept': '最多保留 / Keep at most',
195
+ 'options.maxKeptHint': '个临时会话(1–200) / temp sessions (1–200)',
196
+ 'options.purge': '立即清理临时会话 / Clean up now',
197
+ 'options.purging': '清理中… / Cleaning…',
198
+ 'options.purged': '已清理 / Removed ',
199
+ 'options.purgeFailed': '清理失败 / Cleanup failed: ',
200
+ 'options.rev': '设置版本 / Settings rev',
182
201
  'feedback.title': '反馈 · Feedback',
183
202
  'feedback.help': '遇到问题,或者想要某个功能?写在下面,然后选一个入口。 / Hit a bug or want a feature? Write it below, then pick a channel.',
184
203
  'feedback.placeholder': '例如:点了「临时」之后,侧栏没有出现新会话…… / e.g. clicking Temp adds no session to the sidebar…',
@@ -288,6 +307,55 @@ window.__ModuleLoader__.load({
288
307
  'html[data-dsbs-rail="true"] [data-dsbs-temp-row]::after{display:none}',
289
308
  '[data-dsbs-temp-toast]{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:9500;max-width:min(560px,calc(100vw - 40px));box-sizing:border-box;padding:9px 14px;border-radius:10px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base,#1f1f1f));color:var(--dsw-alias-label-primary,inherit);border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));box-shadow:0 12px 34px rgba(0,0,0,.34);font:inherit;font-size:12px;line-height:1.6;word-break:break-word}',
290
309
  '[data-dsbs-temp-toast][data-dsbs-temp-toast-error="true"]{color:var(--dsw-alias-state-business-danger,#d9534f);border-color:rgba(217,83,79,.45)}',
310
+ // ---- sidebar polish (0.2.9) --------------------------------------
311
+ // The three layout modes are mutually exclusive, so they share one
312
+ // segmented pill; the buttons that belong to the chosen layout sit on
313
+ // the far side of a divider, which keeps "pick a layout" and "act on
314
+ // it" visually distinct in a 280px column.
315
+ //
316
+ // Glyphs are CSS masks, not <svg> children: makeButton() owns a plain
317
+ // text node that relabel() rewrites (an inline <svg> would be wiped),
318
+ // and the test stubs have no createElementNS. A mask also paints in
319
+ // currentColor, so the icons follow the host theme on their own.
320
+ '[data-dsbs-bar]{display:flex;align-items:center;gap:6px;min-height:30px;flex-wrap:nowrap}',
321
+ '[data-dsbs-seg]{display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:9px;flex:none;background:color-mix(in srgb,currentColor 11%,transparent)}',
322
+ '[data-dsbs-bar] button[data-dsbs-glyph]{position:relative;display:flex;align-items:center;justify-content:center;width:28px;height:26px;min-width:0;padding:0!important;border:0;border-radius:7px;font-size:0!important;line-height:0!important;background:transparent;color:inherit;cursor:pointer;transition:background-color .14s ease,opacity .14s ease}',
323
+ '[data-dsbs-bar] button[data-dsbs-glyph]:not([aria-pressed="true"]):hover{background:color-mix(in srgb,currentColor 12%,transparent)}',
324
+ '[data-dsbs-seg] button[aria-pressed="true"]{background:color-mix(in srgb,currentColor 20%,transparent);box-shadow:0 1px 2px rgba(0,0,0,.22)}',
325
+ '[data-dsbs-seg] button[aria-pressed="false"]{opacity:.70}',
326
+ '[data-dsbs-seg] button[aria-pressed="false"]:hover{opacity:.9;background:color-mix(in srgb,currentColor 9%,transparent)}',
327
+ '[data-dsbs-bar] button[data-dsbs-glyph]::before{content:"";position:absolute;inset:0;margin:auto;width:16px;height:16px;background:currentColor;opacity:.88;-webkit-mask-image:var(--dsbs-icon);-webkit-mask-size:16px 16px;-webkit-mask-position:center;-webkit-mask-repeat:no-repeat;mask-image:var(--dsbs-icon);mask-size:16px 16px;mask-position:center;mask-repeat:no-repeat}',
328
+ // a — one column of stacked rows
329
+ '[data-dsbs-bar] [data-dsbs-glyph="a"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%222.5%22 y=%222.5%22 width=%2211%22 height=%2211%22 rx=%222.5%22/%3E%3Cpath d=%22M5.5 6.5h5M5.5 9.5h3%22/%3E%3C/svg%3E")}',
330
+ // b — one column split down the middle
331
+ '[data-dsbs-bar] [data-dsbs-glyph="b"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%222.5%22 y=%222.5%22 width=%2211%22 height=%2211%22 rx=%222.5%22/%3E%3Cpath d=%22M8 3v10%22/%3E%3C/svg%3E")}',
332
+ // c — a panel floating above the column
333
+ '[data-dsbs-bar] [data-dsbs-glyph="c"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%222.5%22 y=%226.5%22 width=%229%22 height=%227%22 rx=%221.8%22/%3E%3Cpath d=%22M7.2 6.5V4.4a1.6 1.6 0 011.6-1.6h3.1a1.6 1.6 0 011.6 1.6v3.9%22/%3E%3C/svg%3E")}',
334
+ // fold — collapse one list (single chevron)
335
+ '[data-dsbs-bar] [data-dsbs-glyph="fold"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3 3.5h10M8 6v6M5.6 9.6L8 12l2.4-2.4%22/%3E%3C/svg%3E")}',
336
+ // fold all — two chevrons, so the two fold buttons never look alike
337
+ '[data-dsbs-bar] [data-dsbs-glyph="fold2"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3 3h10M5.6 6.4L8 8.8l2.4-2.4M5.6 10.2L8 12.6l2.4-2.4%22/%3E%3C/svg%3E")}',
338
+ // swap — trade the two tabs
339
+ '[data-dsbs-bar] [data-dsbs-glyph="swap"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M2.5 5.5h9.5M9.5 3.2l2.5 2.3-2.5 2.3%22/%3E%3Cpath d=%22M13.5 10.5H4M6.5 8.2l-2.5 2.3 2.5 2.3%22/%3E%3C/svg%3E")}',
340
+ // pop — detach the panel
341
+ '[data-dsbs-bar] [data-dsbs-glyph="pop"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%222.5%22 y=%222.5%22 width=%228%22 height=%228%22 rx=%221.8%22/%3E%3Cpath d=%22M9.5 13.5h3.5v-3.5M13 13l-4-4%22/%3E%3C/svg%3E")}',
342
+ // cycle — walk the three layouts in order
343
+ '[data-dsbs-bar] [data-dsbs-glyph="quick"]{--dsbs-icon:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.5%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M13.4 7A5.5 5.5 0 004 4.2M2.6 9A5.5 5.5 0 0012 11.8%22/%3E%3Cpath d=%22M12.6 1.8v3h-3M3.4 14.2v-3h3%22/%3E%3C/svg%3E")}',
344
+ // The divider keeps the action buttons apart from the mode pill; it
345
+ // rides along to the right edge so the row never looks lopsided.
346
+ '[data-dsbs-bar]>span{margin-left:auto;margin-right:4px;height:16px;width:1px;opacity:.26;background:currentColor;flex:none}',
347
+ '[data-dsbs-bar]>span[hidden]{display:none!important}',
348
+ // Wide column: the action buttons win their words back (the mode pill
349
+ // stays iconic — its three options are read as one control). Only the
350
+ // direct children grow, so the pill is untouched.
351
+ 'html:not([data-dsbs-narrow="true"]) [data-dsbs-bar]>button[data-dsbs-glyph]{width:auto!important;min-width:max-content!important;max-width:none!important;box-sizing:border-box;white-space:nowrap;padding:0 11px 0 31px!important;font-size:12px!important;line-height:1!important;justify-content:flex-start}',
352
+ 'html:not([data-dsbs-narrow="true"]) [data-dsbs-bar]>button[data-dsbs-glyph]::before{inset:auto;left:9px;top:50%;margin:0;transform:translateY(-50%)}',
353
+ // Host session rows: a 2px accent rail on the active row, drawn with an
354
+ // inset shadow so no box changes size, plus a soft transition. Both are
355
+ // read-only decoration on the host's own markup.
356
+ '[data-row-key^="session:"]{transition:background-color .13s ease,box-shadow .13s ease,color .13s ease;font-variant-numeric:tabular-nums}',
357
+ '[data-row-key^="session:"][aria-selected="true"]{box-shadow:inset 2px 0 0 var(--dsw-alias-state-business-primary,var(--dsw-alias-label-primary,#4d6bfe))!important}',
358
+ 'html[data-dsbs-rail="true"] [data-row-key^="session:"][aria-selected="true"]{box-shadow:none!important}',
291
359
  ].join('\n')
292
360
 
293
361
  function assign(target) {
@@ -498,6 +566,10 @@ window.__ModuleLoader__.load({
498
566
  var tempSlot = null // wrapper that keeps the root's top-level <button> count at one
499
567
  var tempNewChatHost = null
500
568
  var tempChatPolicy = 'hide' // TEMP_IN_CHAT_MODE in temp-session-config.txt
569
+ // 0.2.8 hot reload: these are re-applied whenever the config file changes.
570
+ var tempShowButton = true
571
+ var tempConfigRevision = null
572
+ var configPollPending = false
501
573
  var tempChatGuard = null
502
574
  var tempIds = [] // session ids the Host half will delete on quit
503
575
  var tempBusy = false
@@ -802,9 +874,23 @@ window.__ModuleLoader__.load({
802
874
  return slot
803
875
  }
804
876
 
877
+ // 0.2.8: the config can change while DSH is running, so visibility is
878
+ // re-applied on every paint instead of being a one-way boot-time hide.
879
+ function applyTempButtonVisibility() {
880
+ var hidden = !tempShowButton
881
+ if (tempButton) {
882
+ if (hidden) tempButton.setAttribute('hidden', '')
883
+ else tempButton.removeAttribute('hidden')
884
+ }
885
+ if (tempSlot) {
886
+ if (hidden) tempSlot.setAttribute('hidden', '')
887
+ else tempSlot.removeAttribute('hidden')
888
+ }
889
+ }
890
+
805
891
  function hideTempButton() {
806
- if (tempButton) tempButton.setAttribute('hidden', '')
807
- if (tempSlot) tempSlot.setAttribute('hidden', '')
892
+ tempShowButton = false
893
+ applyTempButtonVisibility()
808
894
  }
809
895
 
810
896
  function placeTempButton() {
@@ -837,6 +923,7 @@ window.__ModuleLoader__.load({
837
923
  if (tempButton.textContent !== label) tempButton.textContent = label
838
924
  var hint = t('temp.hint')
839
925
  if (tempButton.title !== hint) tempButton.title = hint
926
+ applyTempButtonVisibility()
840
927
  return true
841
928
  }
842
929
 
@@ -849,6 +936,26 @@ window.__ModuleLoader__.load({
849
936
  setFlag(html, 'data-dsbs-expand-all', expandAllKnown && expandAll)
850
937
  }
851
938
 
939
+ /** Apply a Host status payload to the running sidebar. No restart. */
940
+ function applyTempSettings(data) {
941
+ if (runtime.disposed) return
942
+ tempShowButton = !data || data.showButton !== false
943
+ applyTempButtonVisibility()
944
+ if (data && data.chatModePolicy) tempChatPolicy = String(data.chatModePolicy)
945
+ if (data && data.buttonStyle) setFlag(html, 'data-dsbs-temp-style', String(data.buttonStyle) === 'own')
946
+ // Older Host halves (<= 0.2.3) do not send this key; default to on.
947
+ expandAll = !data || data.expandAllSessions !== false
948
+ expandAllKnown = true
949
+ applyExpandFlag()
950
+ syncChatMode()
951
+ expandAllSessions()
952
+ scheduleSync()
953
+ }
954
+
955
+ function rememberConfigRevision(data) {
956
+ if (data && typeof data.configRevision === 'number') tempConfigRevision = data.configRevision
957
+ }
958
+
852
959
  /**
853
960
  * The host keeps five session rows per workspace and renders one overflow
854
961
  * button for the rest: <button data-row-key="overflow:<group>">.
@@ -930,16 +1037,9 @@ window.__ModuleLoader__.load({
930
1037
  tempStatusPending = false
931
1038
  tempStatusLoaded = true
932
1039
  if (expandGateTimer) { clearTimeout(expandGateTimer); expandGateTimer = 0 }
933
- if (data && data.showButton === false) hideTempButton()
934
- if (data && data.chatModePolicy) tempChatPolicy = String(data.chatModePolicy)
935
- if (data && data.buttonStyle) setFlag(html, 'data-dsbs-temp-style', String(data.buttonStyle) === 'own')
936
- // Older Host halves (<= 0.2.3) do not send this key; default to on.
937
- expandAll = !data || data.expandAllSessions !== false
938
- expandAllKnown = true
939
- applyExpandFlag()
940
- syncChatMode()
1040
+ rememberConfigRevision(data)
1041
+ applyTempSettings(data)
941
1042
  refreshTempIds()
942
- scheduleSync()
943
1043
  }).catch(function (error) {
944
1044
  void error
945
1045
  tempStatusPending = false
@@ -980,21 +1080,30 @@ window.__ModuleLoader__.load({
980
1080
  tempChatGuard = setInterval(syncChatMode, 1500)
981
1081
  }
982
1082
 
983
- // One row of plain text buttons, the shape the bar had in its first
984
- // version: the filled button is the chosen layout, and everything after
985
- // the divider names the action its click performs in that layout.
1083
+ // 0.2.9: the three layouts are one segmented pill (they are mutually
1084
+ // exclusive), and the actions that belong to the chosen layout sit after
1085
+ // a divider. Glyphs are CSS pseudo elements, so makeButton() keeps owning
1086
+ // the plain text node that relabel() rewrites.
986
1087
  function buildBar() {
987
1088
  var bar = document.createElement('div')
988
1089
  bar.setAttribute('data-dsbs-bar', '')
989
1090
  bar.setAttribute('role', 'toolbar')
990
1091
 
1092
+ var seg = document.createElement('div')
1093
+ seg.setAttribute('data-dsbs-seg', '')
1094
+ seg.setAttribute('role', 'group')
1095
+ seg.setAttribute('aria-label', t('bar.label'))
1096
+
991
1097
  var i
992
1098
  for (i = 0; i < MODES.length; i++) {
993
1099
  var mode = MODES[i]
994
- bar.appendChild(makeButton('mode-' + mode, 'mode.' + mode, 'mode.' + mode + '.hint', (function (value) {
1100
+ var modeButton = makeButton('mode-' + mode, 'mode.' + mode, 'mode.' + mode + '.hint', (function (value) {
995
1101
  return function () { setMode(value) }
996
- })(mode)))
1102
+ })(mode))
1103
+ modeButton.setAttribute('data-dsbs-glyph', mode === 'a' ? 'a' : mode === 'b' ? 'b' : 'c')
1104
+ seg.appendChild(modeButton)
997
1105
  }
1106
+ bar.appendChild(seg)
998
1107
 
999
1108
  var separator = makeSeparator()
1000
1109
  bar.appendChild(separator)
@@ -1013,6 +1122,14 @@ window.__ModuleLoader__.load({
1013
1122
  var index = MODES.indexOf(config.values.mode)
1014
1123
  setMode(MODES[(index + 1) % MODES.length])
1015
1124
  })
1125
+ foldExt.setAttribute('data-dsbs-glyph', 'fold')
1126
+ // Two fold buttons sit side by side in layout a, so they must not
1127
+ // share a glyph — a single chevron vs. a double one is the only cue
1128
+ // available once the labels collapse to icons.
1129
+ foldWs.setAttribute('data-dsbs-glyph', 'fold2')
1130
+ tabSwap.setAttribute('data-dsbs-glyph', 'swap')
1131
+ pop.setAttribute('data-dsbs-glyph', 'pop')
1132
+ quick.setAttribute('data-dsbs-glyph', 'quick')
1016
1133
  bar.appendChild(foldExt)
1017
1134
  bar.appendChild(foldWs)
1018
1135
  bar.appendChild(tabSwap)
@@ -1155,6 +1272,7 @@ window.__ModuleLoader__.load({
1155
1272
  if (fx.nav && !fx.nav.hasAttribute('data-dsbs-ext')) fx.nav.setAttribute('data-dsbs-ext', '')
1156
1273
  ensureBar(fx)
1157
1274
  placeTempButton()
1275
+ applyTempButtonVisibility()
1158
1276
  loadTempStatus()
1159
1277
  // Bars left behind by an earlier client generation are dead weight and
1160
1278
  // read as a duplicated control; keep exactly one.
@@ -1584,6 +1702,10 @@ window.__ModuleLoader__.load({
1584
1702
  var saveState = ReactLib.useState('')
1585
1703
  var saveStatus = saveState[0]
1586
1704
  var setSaveStatus = saveState[1]
1705
+ // '' | 'busy' | a result line for the manual cleanup button.
1706
+ var purgeState = ReactLib.useState('')
1707
+ var purgeStatus = purgeState[0]
1708
+ var setPurgeStatus = purgeState[1]
1587
1709
 
1588
1710
  // ---- feedback -------------------------------------------------
1589
1711
  // A plugin bundle has no server to post to, and no mail client it can
@@ -1758,27 +1880,63 @@ window.__ModuleLoader__.load({
1758
1880
  }
1759
1881
 
1760
1882
  function adoptPolicy(data, fallbackTime) {
1883
+ var kept = Number(data && data.maxKept)
1761
1884
  setPolicy({
1762
1885
  deleteOnExit: data.deleteOnExit !== false,
1763
1886
  scheduleDelete: data.scheduleDelete === true,
1764
1887
  scheduleTime: typeof data.scheduleTime === 'string' && data.scheduleTime
1765
1888
  ? data.scheduleTime
1766
1889
  : (fallbackTime || '03:00'),
1890
+ // 0.2.8 — the remaining config keys, all editable on this page.
1891
+ showButton: data.showButton !== false,
1892
+ inChatMode: data.inChatMode === 'show' ? 'show' : 'hide',
1893
+ buttonStyle: data.buttonStyle === 'own' ? 'own' : 'pair',
1894
+ expandAllSessions: data.expandAllSessions !== false,
1895
+ maxKept: kept > 0 ? Math.min(Math.round(kept), 200) : 20,
1896
+ configRevision: typeof data.configRevision === 'number' ? data.configRevision : null,
1897
+ configFile: typeof data.configFile === 'string' ? data.configFile : '',
1767
1898
  })
1768
1899
  }
1769
1900
 
1770
1901
  ReactLib.useEffect(function () {
1771
1902
  var stopped = false
1772
- tempCall('api/dsh-temp-session.config')
1773
- .then(function (data) {
1774
- if (stopped) return
1775
- if (!data || data.ok !== true) { setPolicy(false); return }
1776
- adoptPolicy(data)
1777
- })
1778
- .catch(function () {
1779
- if (!stopped) setPolicy(false)
1780
- })
1781
- return function () { stopped = true }
1903
+ var loaded = false
1904
+ var revision = null
1905
+ function read(first) {
1906
+ tempCall('api/dsh-temp-session.config')
1907
+ .then(function (data) {
1908
+ if (stopped) return
1909
+ if (!data || data.ok !== true) {
1910
+ if (first) setPolicy(false)
1911
+ return
1912
+ }
1913
+ loaded = true
1914
+ var rev = typeof data.configRevision === 'number' ? data.configRevision : null
1915
+ // Only re-render on a real change. Re-rendering on every tick
1916
+ // would fight a user who is halfway through typing a time.
1917
+ if (first || rev === null || rev !== revision) {
1918
+ revision = rev
1919
+ adoptPolicy(data)
1920
+ }
1921
+ })
1922
+ .catch(function () {
1923
+ if (!stopped && first) setPolicy(false)
1924
+ })
1925
+ }
1926
+ read(true)
1927
+ // 0.2.8: a save in Notepad is picked up here within ~2.5 seconds.
1928
+ var timer = null
1929
+ if (typeof window.setInterval === 'function') {
1930
+ timer = window.setInterval(function () {
1931
+ if (stopped || !loaded) return
1932
+ if (typeof document.hidden === 'boolean' && document.hidden) return
1933
+ read(false)
1934
+ }, 2500)
1935
+ }
1936
+ return function () {
1937
+ stopped = true
1938
+ if (timer !== null && typeof window.clearInterval === 'function') window.clearInterval(timer)
1939
+ }
1782
1940
  }, [])
1783
1941
 
1784
1942
  function persist(patch) {
@@ -1804,6 +1962,20 @@ window.__ModuleLoader__.load({
1804
1962
  })
1805
1963
  }
1806
1964
 
1965
+ function purgeNow() {
1966
+ if (purgeStatus === 'busy') return
1967
+ setPurgeStatus('busy')
1968
+ tempCall('api/dsh-temp-session.purge', { method: 'POST' })
1969
+ .then(function (data) {
1970
+ if (!data || data.ok !== true) throw new Error((data && data.error) || 'purge failed')
1971
+ var n = typeof data.removed === 'number' ? data.removed : 0
1972
+ setPurgeStatus(t('options.purged') + n)
1973
+ })
1974
+ .catch(function (error) {
1975
+ setPurgeStatus(t('options.purgeFailed') + String((error && error.message) || error))
1976
+ })
1977
+ }
1978
+
1807
1979
  var available = !!configController()
1808
1980
  var policyReady = !!(policy && policy !== false)
1809
1981
  var dim = { fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary,inherit)' }
@@ -1835,6 +2007,26 @@ window.__ModuleLoader__.load({
1835
2007
  extra || null
1836
2008
  )
1837
2009
  }
2010
+ function optionRow(label, value, choices, onPick) {
2011
+ return h('div', {
2012
+ key: label,
2013
+ style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }
2014
+ },
2015
+ h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary,inherit)' } }, label),
2016
+ h('div', { style: { display: 'flex', gap: '4px' } },
2017
+ choices.map(function (choice) {
2018
+ var active = value === choice.value
2019
+ return h('button', {
2020
+ key: choice.value, type: 'button',
2021
+ 'aria-pressed': active ? 'true' : 'false',
2022
+ disabled: !policyReady,
2023
+ onClick: function () { onPick(choice.value) },
2024
+ style: assign({}, pillBase, { minWidth: '0' }, active ? pillOn : {})
2025
+ }, choice.label)
2026
+ })
2027
+ )
2028
+ )
2029
+ }
1838
2030
  var cardBase = {
1839
2031
  display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '5px',
1840
2032
  flex: '1 1 140px', minWidth: '140px', maxWidth: '230px', padding: '10px',
@@ -1875,6 +2067,60 @@ window.__ModuleLoader__.load({
1875
2067
  })
1876
2068
  ),
1877
2069
  available ? null : h('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary,inherit)' } }, t('settings.unavailable')),
2070
+ h('div', { 'data-dsbs-options': ENTRY, style: {
2071
+ display: 'flex', flexDirection: 'column', gap: '8px',
2072
+ paddingTop: '12px', marginTop: '2px',
2073
+ borderTop: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))'
2074
+ } },
2075
+ h('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('options.title')),
2076
+ h('div', { style: dim }, t('options.help')),
2077
+ policy === null
2078
+ ? h('div', { style: dim }, t('policy.loading'))
2079
+ : policy === false
2080
+ ? h('div', { style: dim }, t('policy.unavailable'))
2081
+ : h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
2082
+ policyRow(t('options.showButton'), policy.showButton === true, function (next) {
2083
+ persist({ showButton: next })
2084
+ }),
2085
+ optionRow(t('options.inChatMode'), policy.inChatMode, [
2086
+ { value: 'hide', label: t('options.chatHide') },
2087
+ { value: 'show', label: t('options.chatShow') },
2088
+ ], function (next) { persist({ inChatMode: next }) }),
2089
+ optionRow(t('options.buttonStyle'), policy.buttonStyle, [
2090
+ { value: 'pair', label: t('options.stylePair') },
2091
+ { value: 'own', label: t('options.styleOwn') },
2092
+ ], function (next) { persist({ buttonStyle: next }) }),
2093
+ policyRow(t('options.expandAll'), policy.expandAllSessions === true, function (next) {
2094
+ persist({ expandAllSessions: next })
2095
+ }),
2096
+ h('div', { style: dim }, t('options.expandHint')),
2097
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
2098
+ h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('options.maxKept')),
2099
+ h('input', {
2100
+ type: 'number', min: 1, max: 200, step: 1,
2101
+ value: String(policy.maxKept),
2102
+ disabled: !policyReady,
2103
+ 'aria-label': t('options.maxKept'),
2104
+ onChange: function (event) {
2105
+ var raw = event && event.target ? event.target.value : ''
2106
+ var n = Number(raw)
2107
+ if (!(n > 0)) return
2108
+ persist({ maxKept: Math.min(Math.round(n), 200) })
2109
+ },
2110
+ style: {
2111
+ font: 'inherit', fontSize: '12px', padding: '1px 6px', width: '64px',
2112
+ borderRadius: '6px', color: 'var(--dsw-alias-label-primary,inherit)',
2113
+ background: 'transparent',
2114
+ border: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3))'
2115
+ }
2116
+ }),
2117
+ h('span', { style: dim }, t('options.maxKeptHint'))
2118
+ ),
2119
+ h('div', { style: dim },
2120
+ t('options.rev') + ' ' + (policy.configRevision === null ? '-' : policy.configRevision) +
2121
+ (policy.configFile ? ' · ' + policy.configFile : ''))
2122
+ )
2123
+ ),
1878
2124
  h('div', { 'data-dsbs-temp-policy': ENTRY, style: {
1879
2125
  display: 'flex', flexDirection: 'column', gap: '8px',
1880
2126
  paddingTop: '12px', marginTop: '2px',
@@ -1922,7 +2168,20 @@ window.__ModuleLoader__.load({
1922
2168
  : 'var(--dsw-alias-text-error,#e5484d)'
1923
2169
  }
1924
2170
  }, saveStatus === 'saving' ? t('policy.saving') : saveStatus === 'saved' ? t('policy.saved') : saveStatus)
1925
- : null
2171
+ : null,
2172
+ h('div', {
2173
+ style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap', paddingTop: '2px' }
2174
+ },
2175
+ h('button', {
2176
+ type: 'button',
2177
+ disabled: !policyReady || purgeStatus === 'busy',
2178
+ onClick: purgeNow,
2179
+ style: assign({}, pillBase, { minWidth: '0' })
2180
+ }, purgeStatus === 'busy' ? t('options.purging') : t('options.purge')),
2181
+ purgeStatus && purgeStatus !== 'busy'
2182
+ ? h('span', { style: dim }, purgeStatus)
2183
+ : null
2184
+ )
1926
2185
  )
1927
2186
  ),
1928
2187
  h('div', { 'data-dsbs-feedback': ENTRY, style: {
@@ -2154,6 +2413,30 @@ window.__ModuleLoader__.load({
2154
2413
  }
2155
2414
  }, 'sidebar-suite: sync')
2156
2415
 
2416
+ effect(function () {
2417
+ // 0.2.8 hot reload. The Host stamps the config file and bumps a
2418
+ // monotone revision; one cheap status poll is enough to notice a save
2419
+ // in Notepad and re-apply every sidebar setting in place. Old Host
2420
+ // halves have no revision field — those are simply left on boot values.
2421
+ var timer = setInterval(function () {
2422
+ if (runtime.disposed || configPollPending) return
2423
+ if (typeof document.hidden === 'boolean' && document.hidden) return
2424
+ configPollPending = true
2425
+ tempCall('api/dsh-temp-session.status')
2426
+ .then(function (data) {
2427
+ configPollPending = false
2428
+ if (runtime.disposed || !data || data.ok !== true) return
2429
+ if (typeof data.configRevision !== 'number') return
2430
+ if (data.configRevision === tempConfigRevision) return
2431
+ rememberConfigRevision(data)
2432
+ applyTempSettings(data)
2433
+ })
2434
+ .catch(function () { configPollPending = false })
2435
+ }, 3000)
2436
+ if (timer && typeof timer.unref === 'function') timer.unref()
2437
+ return function () { clearInterval(timer) }
2438
+ }, 'sidebar-suite: config hot reload')
2439
+
2157
2440
  var teardown = null
2158
2441
  effect(function () {
2159
2442
  teardown = function () {