dsh-tiddlywiki 0.26.0 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-tiddlywiki",
3
3
  "description": "TiddlyWiki 5 as the DSH persistent knowledge base: fifteen tiddlywiki_* agent tools (filtered search/recent/tags/batch/rewrite/trash/lint + git sync & conflict resolve), a full TiddlyWiki editor embedded in the GUI center column (same-origin proxy, works over Tailscale/LAN/domain), a right-side wiki tab, a per-session summary tab, a quick-note card with draft auto-save behind a single knowledge FAB, an optional clip bookmarklet that also saves selected images as attachments, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
4
- "version": "0.26.0",
4
+ "version": "0.26.2",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -169,8 +169,19 @@ function mountSettingsPage(container: HTMLElement): () => void {
169
169
  const disposers: Array<() => void> = []
170
170
  /** Config-section DOM + the server signature it was built from (see renderMain). */
171
171
  const configState: ConfigRenderState = {}
172
- /** 插件/主题/语言的「未应用勾选」(见 CatalogPending)。 */
173
- const catalogPending: CatalogPending = { plugins: new Set(), themes: new Set(), languages: new Set() }
172
+ /**
173
+ * 插件/主题/语言的「未应用勾选」(见 CatalogPending)。
174
+ *
175
+ * ⚠️ 必须是**空对象**,绝不能预置空 Set(v0.26.1 修 P0;回归由 v0.25.0 的
176
+ * commit 883196f 引入):契约是 `undefined` = 用户还没碰过、完全跟随服务器,
177
+ * 而 `desiredPlugins()` 等用 `??` 回退 —— **空 Set 不是 undefined**,预置成
178
+ * 空 Set 会让「没碰过」被读成「用户期望集合为空」,后果有两层:
179
+ * ① 设置页所有插件/主题/语言勾选框**全部显示为未勾选**(用户看到「装了却没勾」);
180
+ * ② 更严重——什么都没改就点「应用插件」,会 POST 一个**空数组**,把
181
+ * `tiddlywiki.info` 的插件清单整个清空(主题清单、语言同理)。
182
+ * 守门:`scripts/verify-plugin-runtime.mjs` 的「未碰过 ≠ 空集合」一节。
183
+ */
184
+ const catalogPending: CatalogPending = {}
174
185
  /** 首屏是否已成功渲染过内容:决定占位提示与失败时能不能清 body。 */
175
186
  let rendered = false
176
187
 
@@ -652,6 +663,13 @@ function renderCatalogSection(
652
663
  const listWrap = make('div', 'dsh-tw-settings-list')
653
664
  const applyPlugins = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用插件(重启 TW)')
654
665
  applyPlugins.type = 'button'
666
+ /**
667
+ * 没碰过勾选时禁用「应用」(v0.26.1):`pending.plugins === undefined` = 跟随服务器,
668
+ * 提交没有意义;把它当成「空期望集合」误提交就是清空整个启动清单。勾选变化时重算。
669
+ */
670
+ const syncApplyPlugins = (): void => {
671
+ applyPlugins.disabled = pending.plugins === undefined
672
+ }
655
673
 
656
674
  const renderPluginList = (needle: string): void => {
657
675
  listWrap.replaceChildren()
@@ -666,6 +684,7 @@ function renderCatalogSection(
666
684
  if (input.checked) next.add(plugin.name)
667
685
  else next.delete(plugin.name)
668
686
  pending.plugins = next
687
+ syncApplyPlugins()
669
688
  })
670
689
  const label = make('label', 'dsh-tw-settings-row dsh-tw-settings-plugin')
671
690
  const name = make('span', 'dsh-tw-settings-name', plugin.label)
@@ -675,12 +694,14 @@ function renderCatalogSection(
675
694
  // 状态徽标(v0.26.0):勾选只反映 tiddlywiki.info,运行时真相反映在这里。
676
695
  // ① 已在启动清单(勾选)但被 TW 禁用;② 不在启动清单但 wiki 内有同名 tiddler
677
696
  // 版本(TW 原生装的)。两种都常见,且不显示就会让用户误判「没装成功」。
697
+ // 判据取**服务器集合**而不是 desiredPlugins():徽标描述的是当前事实,
698
+ // 不该随「还没应用的勾选」抖动(v0.26.1)。
678
699
  if (runtimePlugins != null && disabledTitles.has(plugin.title)) {
679
700
  const badge = make('span', 'dsh-tw-settings-chip', 'TW 内已禁用')
680
701
  badge.dataset.state = 'disabled'
681
702
  badge.title = '该插件在 tiddlywiki.info 里,但已在 TW 控制面板被禁用($:/config/Plugins/Disabled)——去 TW 面板重新启用'
682
703
  label.append(badge)
683
- } else if (runtimePlugins != null && !desiredPlugins().has(plugin.name) && wikiTitles.has(plugin.title)) {
704
+ } else if (runtimePlugins != null && !serverPlugins.includes(plugin.name) && wikiTitles.has(plugin.title)) {
684
705
  const badge = make('span', 'dsh-tw-settings-chip', 'wiki 内已装')
685
706
  badge.dataset.state = 'update'
686
707
  badge.title = '不在启动清单,但 wiki 里存在同名插件 tiddler(经 TW 原生安装),TW 运行时已在用'
@@ -692,6 +713,7 @@ function renderCatalogSection(
692
713
  }
693
714
  search.addEventListener('input', () => renderPluginList(search.value))
694
715
  applyPlugins.addEventListener('click', () => {
716
+ if (pending.plugins === undefined) return
695
717
  applyPlugins.disabled = true
696
718
  void (async () => {
697
719
  try {
@@ -702,11 +724,12 @@ function renderCatalogSection(
702
724
  void refresh()
703
725
  } catch (err) {
704
726
  toast(`应用失败:${err instanceof Error ? err.message : String(err)}`)
705
- applyPlugins.disabled = false
727
+ syncApplyPlugins()
706
728
  }
707
729
  })()
708
730
  })
709
731
  renderPluginList('')
732
+ syncApplyPlugins()
710
733
  pluginSection.append(search, listWrap, applyPlugins)
711
734
  body.append(pluginSection)
712
735
 
@@ -782,6 +805,15 @@ function renderCatalogSection(
782
805
  : lastLoadedTheme ?? 'tiddlywiki/vanilla'
783
806
  /** 活动主题同样走 pending:单选按钮的未应用选择也要活过 refresh(见 CatalogPending)。 */
784
807
  const activeThemeName = (): string => pending.themeActive ?? serverActiveThemeName
808
+ const applyThemes = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用主题(重启 TW)')
809
+ applyThemes.type = 'button'
810
+ /**
811
+ * 没碰过主题的任何控件时禁用「应用」(v0.26.1,同插件管理):`pending.* === undefined`
812
+ * 才是「跟随服务器」;未碰过就提交会把「空期望集合」写成 `themes: []`,把主题清空。
813
+ */
814
+ const syncApplyThemes = (): void => {
815
+ applyThemes.disabled = pending.themes === undefined && pending.themeActive === undefined
816
+ }
785
817
  const themeWrap = make('div', 'dsh-tw-settings-list')
786
818
  for (const theme of themes) {
787
819
  const load = make('input', 'dsh-tw-settings-check')
@@ -793,6 +825,7 @@ function renderCatalogSection(
793
825
  if (load.checked) next.add(theme.name)
794
826
  else next.delete(theme.name)
795
827
  pending.themes = next
828
+ syncApplyThemes()
796
829
  })
797
830
  const act = make('input', 'dsh-tw-settings-check')
798
831
  act.type = 'radio'
@@ -801,6 +834,7 @@ function renderCatalogSection(
801
834
  act.title = '设为活动主题'
802
835
  act.addEventListener('change', () => {
803
836
  if (act.checked) pending.themeActive = theme.name
837
+ syncApplyThemes()
804
838
  })
805
839
  const name = make('span', 'dsh-tw-settings-name', theme.label)
806
840
  name.title = theme.name
@@ -809,9 +843,8 @@ function renderCatalogSection(
809
843
  row.append(load, act, name, desc)
810
844
  themeWrap.append(row)
811
845
  }
812
- const applyThemes = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用主题(重启 TW)')
813
- applyThemes.type = 'button'
814
846
  applyThemes.addEventListener('click', () => {
847
+ if (pending.themes === undefined && pending.themeActive === undefined) return
815
848
  applyThemes.disabled = true
816
849
  void (async () => {
817
850
  try {
@@ -827,10 +860,11 @@ function renderCatalogSection(
827
860
  void refresh()
828
861
  } catch (err) {
829
862
  toast(`应用失败:${err instanceof Error ? err.message : String(err)}`)
830
- applyThemes.disabled = false
863
+ syncApplyThemes()
831
864
  }
832
865
  })()
833
866
  })
867
+ syncApplyThemes()
834
868
  themeSection.append(themeHint, themeHead, themeWrap, applyThemes)
835
869
  body.append(themeSection)
836
870
 
@@ -839,6 +873,12 @@ function renderCatalogSection(
839
873
  langSection.append(make('h3', 'dsh-tw-settings-h', '语言管理(自带官方语言包)'))
840
874
  const langHint = make('div', 'dsh-tw-settings-muted', '勾选启用语言插件并重启 TW;如中文请选 zh-Hans(简体)或 zh-CN。')
841
875
  const langWrap = make('div', 'dsh-tw-settings-list')
876
+ const applyLangs = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用语言(重启 TW)')
877
+ applyLangs.type = 'button'
878
+ /** 没碰过语言勾选时禁用「应用」(v0.26.1,同插件管理):未碰过 ≠ 期望集合为空。 */
879
+ const syncApplyLangs = (): void => {
880
+ applyLangs.disabled = pending.languages === undefined
881
+ }
842
882
  for (const lang of languages) {
843
883
  const input = make('input', 'dsh-tw-settings-check')
844
884
  input.type = 'checkbox'
@@ -848,6 +888,7 @@ function renderCatalogSection(
848
888
  if (input.checked) next.add(lang.name)
849
889
  else next.delete(lang.name)
850
890
  pending.languages = next
891
+ syncApplyLangs()
851
892
  })
852
893
  const label = make('label', 'dsh-tw-settings-row dsh-tw-settings-plugin')
853
894
  const name = make('span', 'dsh-tw-settings-name', lang.label)
@@ -856,9 +897,8 @@ function renderCatalogSection(
856
897
  label.append(input, name, desc)
857
898
  langWrap.append(label)
858
899
  }
859
- const applyLangs = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '应用语言(重启 TW)')
860
- applyLangs.type = 'button'
861
900
  applyLangs.addEventListener('click', () => {
901
+ if (pending.languages === undefined) return
862
902
  applyLangs.disabled = true
863
903
  void (async () => {
864
904
  try {
@@ -868,10 +908,11 @@ function renderCatalogSection(
868
908
  void refresh()
869
909
  } catch (err) {
870
910
  toast(`应用失败:${err instanceof Error ? err.message : String(err)}`)
871
- applyLangs.disabled = false
911
+ syncApplyLangs()
872
912
  }
873
913
  })()
874
914
  })
915
+ syncApplyLangs()
875
916
  langSection.append(langHint, langWrap, applyLangs)
876
917
  body.append(langSection)
877
918
  }
package/src/host/admin.ts CHANGED
@@ -359,16 +359,25 @@ export async function bundledCatalog(twRoot: string): Promise<Catalog> {
359
359
  }
360
360
  const out: CatalogEntry[] = []
361
361
  for (const dir of dirs) {
362
- let info: { name?: string; description?: string; dependents?: string[] } = {}
362
+ let info: { title?: string; name?: string; description?: string; dependents?: string[] } = {}
363
363
  try {
364
364
  info = JSON.parse(await readFile(join(root, dir, 'plugin.info'), 'utf8')) as typeof info
365
365
  } catch {
366
366
  info = {}
367
367
  }
368
368
  if (sub === 'themes' && dir !== 'vanilla' && !(await themeHasCss(dir))) continue
369
+ // The tiddler title comes from plugin.info, NOT from the folder name
370
+ // (v0.26.2): they normally agree, but `plugins/tiddlywiki/codemirror-fullscreen-editing`
371
+ // declares `"title": "$:/plugins/tiddlywiki/codemirror-fullscreen"` — deriving
372
+ // the title from the directory made every title-keyed comparison miss that
373
+ // plugin (the settings page's 「TW 内已禁用」/「wiki 内已装」 badges match on
374
+ // the full tiddler title, so a disabled codemirror-fullscreen showed no badge).
375
+ // plugin.info IS the tiddler TW loads, so it is the source of truth; the
376
+ // folder-derived form stays only as a fallback for a missing/empty title.
377
+ const declaredTitle = typeof info.title === 'string' && info.title.length > 0 ? info.title : undefined
369
378
  out.push({
370
379
  name: `tiddlywiki/${dir}`,
371
- title: sub === 'plugins' ? `$:/plugins/tiddlywiki/${dir}` : `$:/themes/tiddlywiki/${dir}`,
380
+ title: declaredTitle ?? (sub === 'plugins' ? `$:/plugins/tiddlywiki/${dir}` : `$:/themes/tiddlywiki/${dir}`),
372
381
  label: info.name ?? dir,
373
382
  description: info.description ?? '',
374
383
  // plugin.info `dependents` are full plugin titles → convert to names.