dsh-tiddlywiki 0.28.6 → 0.28.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-tiddlywiki",
3
3
  "description": "把 TiddlyWiki 5 变成你和 Agent 共用的本地文档中心(纯文本 + git):你在 DSH 界面里阅读、写作、记日记,Agent 用 15 个 tiddlywiki_* 工具读写同一批笔记;每条笔记都是你能读、能改、能审计、能随时带走的文件——不是自动记忆插件,不做后台抓取。可选一键发布到微信公众号草稿箱。 Turn TiddlyWiki 5 into a document center you and your agent share, in plain text under git — a knowledge base you can read, edit, audit and take with you, not an auto-memory plugin. Mounts via the official dsh plugin system; no DSH source changes.",
4
- "version": "0.28.6",
4
+ "version": "0.28.7",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -107,18 +107,11 @@ export function apply(ctx: ClientContextFace): void {
107
107
  // queue/git-graph 等插件内容都渲染在这里、按纵向 flex 排列,天然不重叠。
108
108
  // 由 ui.showQuickNoteDock 配置控制(默认开)。
109
109
  //
110
- // 同一槽位还挂「会话级知识库选择器」(v0.28.0,需求:会话栏上面动态选 wiki):
111
- // 它**不需要**开关——组件自己在"可见库 ≤ 1"时返回 null,所以单库安装看不到任何
112
- // 东西,而多库安装一装上就能选。order 比快速笔记小,排在它前面。
113
- safeMount('wiki scope dock', () => {
114
- const removeDock = ctx.slots?.inject('conversation.input.dock', () =>
115
- ctx.slots?.register(
116
- { name: 'conversation.input.dock', id: 'wiki-scope', order: 4, label: '知识库作用域' },
117
- createWikiScopeDock(),
118
- ),
119
- )
120
- if (removeDock !== undefined) disposers.push(removeDock)
121
- })
110
+ // 「会话级知识库选择器」(v0.28.0,需求:会话栏上面动态选 wiki)**不再单独占一个
111
+ // 槽位条目**(v0.28.7):dock 是纵向 flex 列,一个条目 = 一整行,所以两个条目
112
+ // 各自量宽度、永远对不齐(作者连报两次)。现在它作为 `scope` 渲染在快速笔记按钮
113
+ // 同一行的前面,只有一个条目、一次测量。组件自己在"可见库 ≤ 1"时返回 null,
114
+ // 所以单库安装这一块 DOM 与以前逐字相同。
122
115
  if (ctx.slots !== undefined) {
123
116
  void fetchUiConfig().then((cfg) => {
124
117
  if (clientDisposed) return
@@ -128,7 +121,7 @@ export function apply(ctx: ClientContextFace): void {
128
121
  const removeDock = ctx.slots?.inject('conversation.input.dock', () =>
129
122
  ctx.slots?.register(
130
123
  { name: 'conversation.input.dock', id: 'quick-note', order: 8, label: '快速笔记' },
131
- createQuickNoteDock(widget),
124
+ createQuickNoteDock(widget, createWikiScopeDock()),
132
125
  ),
133
126
  )
134
127
  if (removeDock !== undefined) disposers.push(removeDock)
@@ -28,10 +28,18 @@ import { isEditorPopupOpen, isEditorPopupBlank, closeEditorPopup } from './edito
28
28
  * (Lexical contenteditable 所在、有边框圆角的可见「输入框」盒子)的右缘,
29
29
  * 把横条 paddingRight 设为差值,让按钮右缘与输入框右缘对齐——而不是悬在整列
30
30
  * 最右端。注意 dock 槽位会把各条目包在一个容器里、与输入栏是兄弟节点,所以
31
- * 必须逐级向上爬祖先才能找到输入框。
31
+ * 必须逐级往上爬祖先才能找到输入框。
32
+ *
33
+ * `scope`(v0.28.7)是同一行里渲染在按钮**前面**的元素(知识库选择器)。
34
+ * 为什么合并成一个条目:dock 槽位是纵向 flex 列,**每个条目 = 一整行**,所以
35
+ * "选择器一行、快速笔记一行"永远不可能对齐成一条 —— 各自量各自的宽度,视觉上
36
+ * 就是两行错位(作者连报两次)。合并后只有一个条目、一次测量,两者天然同基线。
32
37
  */
33
- export function createQuickNoteDock(note: NoteWidgetHandle): () => React.ReactElement {
34
- return function QuickNoteDock() {
38
+ export function createQuickNoteDock(
39
+ note: NoteWidgetHandle,
40
+ scope?: (props: { sessionId?: string }) => React.ReactElement | null,
41
+ ): (props: { sessionId?: string }) => React.ReactElement {
42
+ return function QuickNoteDock(props: { sessionId?: string }) {
35
43
  const [open, setOpen] = React.useState(false)
36
44
  // 点击行为由 ui.quickNoteMode 决定:native=直达 TW 原生编辑页(弹窗);
37
45
  // card=Markdown 卡片。null = 配置尚未加载(点击时按配置实时分发)。
@@ -71,6 +79,11 @@ export function createQuickNoteDock(note: NoteWidgetHandle): () => React.ReactEl
71
79
  return React.createElement(
72
80
  'div',
73
81
  { ref: wrapRef, className: 'dsh-tw-dock-note' },
82
+ // 知识库选择器排在按钮**前面**(同一行、同一基线)。它的 sessionId 必须
83
+ // 从本条目透传下去 —— 槽位的 scope=session,props 是发给**条目组件**的,
84
+ // 子元素不会自动拿到(v0.28.7:漏了这一步选择器会永远读不到会话 id)。
85
+ // scope 为 undefined 时(单库安装)什么都不渲染,DOM 与以前逐字相同。
86
+ scope?.({ ...(props.sessionId !== undefined ? { sessionId: props.sessionId } : {}) }) ?? null,
74
87
  React.createElement(
75
88
  'button',
76
89
  {
@@ -186,18 +186,37 @@ function mountSettingsPage(container: HTMLElement): () => void {
186
186
  const catalogPending: CatalogPending = {}
187
187
  /** 首屏是否已成功渲染过内容:决定占位提示与失败时能不能清 body。 */
188
188
  let rendered = false
189
+ /**
190
+ * 当前是单库还是多库(v0.28.7)。
191
+ *
192
+ * 为什么要在比 `/status` 更早的地方拿到它:有些配置项**只在单库模式下有意义**
193
+ * (「侧边栏 TW 入口显示名称」是最典型的一个——多库时侧边栏是每个库各占一行、
194
+ * 各用自己 `wikis.json` 里的 label,这个字段改不动任何东西)。`renderConfigSection`
195
+ * 是同步的、按顺序 append 字段,没法在中间 await,所以这里先把模式探好,
196
+ * 再交给它决定要不要渲染那个字段。
197
+ *
198
+ * `undefined` = 还没探测 / 探测失败 —— 按**单库**处理(保守:宁可多显示一个
199
+ * "确实存在且能用"的字段,也不要因为一次请求失败把它藏了,否则单库用户会
200
+ * 莫名其妙找不到设置项)。多库时才显式 'multi'。
201
+ */
202
+ let rosterMode: string | undefined
189
203
 
190
204
  const refresh = async (): Promise<void> => {
191
205
  // 首屏先给占位(v0.24.x):以前 await 期间什么都不画,整块白屏看起来像插件坏了。
192
206
  if (!rendered) body.replaceChildren(make('div', 'dsh-tw-settings-muted', '加载中…'))
193
207
  try {
208
+ // 与 /status 并行探模式:失败/超时不阻塞配置页渲染(回落成单库的保守视图)。
209
+ const modePromise = fetchJson<WikisView>(WIKI_LIST_ENDPOINT)
210
+ .then((v) => { rosterMode = typeof v?.mode === 'string' ? v.mode : undefined })
211
+ .catch(() => { rosterMode = undefined })
194
212
  const state = await fetchJson<AdminState>(withWiki(STATE_ENDPOINT))
213
+ await modePromise
195
214
  if (disposed) return
196
215
  rendered = true
197
216
  loadError.hidden = true
198
217
  loadError.replaceChildren()
199
218
  renderStatus(statusRow, state, refresh)
200
- renderMain(body, state, refresh, () => disposed, configState, catalogPending)
219
+ renderMain(body, state, refresh, () => disposed, configState, catalogPending, rosterMode)
201
220
  } catch (err) {
202
221
  if (disposed) return
203
222
  // 不再 body.replaceChildren() / statusRow.replaceChildren():那会把已经渲染出来的
@@ -288,7 +307,7 @@ function renderStatus(row: HTMLElement, state: AdminState, refresh: () => Promis
288
307
  }
289
308
 
290
309
  /** Config section: fields bound to effective config, changed-only save. */
291
- function renderConfigSection(body: HTMLElement, config: Record<string, unknown>, refresh: () => Promise<void>, configState: ConfigRenderState): void {
310
+ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>, refresh: () => Promise<void>, configState: ConfigRenderState, rosterMode?: string): void {
292
311
  const section = make('section', 'dsh-tw-settings-section')
293
312
  section.append(make('h3', 'dsh-tw-settings-h', '常规配置'))
294
313
  const note = (config.note ?? {}) as Record<string, unknown>
@@ -448,7 +467,15 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
448
467
  { value: 'native', label: '原生编辑器:直接弹出 TW 原生编辑页(新建/恢复草稿)' },
449
468
  { value: 'card', label: 'Markdown 卡片:弹出现有快速笔记卡片(CodeMirror 编辑器)' },
450
469
  ])
451
- textField('ui.sidebarLabel', '侧边栏 TW 入口显示名称', typeof ui.sidebarLabel === 'string' && ui.sidebarLabel.trim().length > 0 ? ui.sidebarLabel.trim() : 'TiddlyWiki')
470
+ // 「侧边栏 TW 入口显示名称」只在**单库**模式有意义(v0.28.7)。多库时侧边栏是每个库
471
+ // 各占一行、各用自己 `wikis.json` 里的 label,这个字段改不动任何东西——留着只会让人
472
+ // 「改了没生效」。所以多库时**不渲染这个字段**(连字段带保存都不参与),只留一行说明
473
+ // 该去哪儿改。判定用的模式来自 mountSettingsPage 的探测(读不到按单库,见那里的注释)。
474
+ if (rosterMode === 'multi') {
475
+ section.append(make('div', 'dsh-tw-settings-muted', '侧边栏入口:多库模式下每个知识库各占一行、各用自己的「显示名」(在「知识库列表」里改),不再是统一的一个名称,所以这里没有对应配置项。'))
476
+ } else {
477
+ textField('ui.sidebarLabel', '侧边栏 TW 入口显示名称', typeof ui.sidebarLabel === 'string' && ui.sidebarLabel.trim().length > 0 ? ui.sidebarLabel.trim() : 'TiddlyWiki')
478
+ }
452
479
  checkField('ui.showPanelStatus', '显示「知识库」按钮里的 TW 面板/重载入口与状态行', ui.showPanelStatus !== false)
453
480
  checkField('ui.showSyncButton', '显示「知识库」按钮里的「同步」入口与 git 状态点', ui.showSyncButton !== false)
454
481
  checkField('ui.followDshTheme', '嵌入式 TW 跟随 DSH 深浅主题(暗色时自动切深色 palette,不写回 wiki)', ui.followDshTheme !== false)
@@ -1619,7 +1646,7 @@ interface CatalogPending {
1619
1646
  themeActive?: string
1620
1647
  }
1621
1648
 
1622
- function renderMain(body: HTMLElement, state: AdminState, refresh: () => Promise<void>, isDisposed: () => boolean, configState: ConfigRenderState, catalogPending: CatalogPending): void {
1649
+ function renderMain(body: HTMLElement, state: AdminState, refresh: () => Promise<void>, isDisposed: () => boolean, configState: ConfigRenderState, catalogPending: CatalogPending, rosterMode?: string): void {
1623
1650
  body.replaceChildren()
1624
1651
  // Loud, above everything else: an unparseable config tiddler means the config
1625
1652
  // block below shows DEFAULTS that are not actually in effect, and saving is
@@ -1644,7 +1671,7 @@ function renderMain(body: HTMLElement, state: AdminState, refresh: () => Promise
1644
1671
  }
1645
1672
  if (configState.host === undefined || (serverChanged && !dirty)) {
1646
1673
  const host = make('div', 'dsh-tw-settings-confighost')
1647
- renderConfigSection(host, state.config ?? {}, refresh, configState)
1674
+ renderConfigSection(host, state.config ?? {}, refresh, configState, rosterMode)
1648
1675
  configState.host = host
1649
1676
  configState.signature = signature
1650
1677
  }
@@ -514,14 +514,15 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
514
514
  /* ── 会话级知识库选择器(v0.28.0,同一个 conversation.input.dock 槽位)──
515
515
  与快速笔记按钮同区、纵向 flex 排列;只在"多于一个可见库"时渲染(组件自己返回
516
516
  null),所以单库安装这一整块都不存在。 */
517
- /* ── 会话级知识库选择器(v0.28.0,同一个 conversation.input.dock 槽位)──
518
- 与快速笔记按钮同区、纵向 flex 排列;只在"多于一个可见库"时渲染(组件自己返回
519
- null),所以单库安装这一整块都不存在。
520
- justify-content: flex-end 与 JS 测得的 paddingRight 一起把右缘对齐 composer
521
- 输入框(见 client/dock-align.ts)—— 与 .dsh-tw-dock-note 同一套做法。 */
517
+ /* ── 会话级知识库选择器(v0.28.0;v0.28.7 起并入快速笔记那一行)──
518
+ 它现在渲染在 .dsh-tw-dock-note **里面**、按钮的前面,所以:
519
+ - 不再 width:100%(那是"独占一整行"的写法,会把按钮挤下去);
520
+ - 不再自己做 padding/justify —— 右对齐与"与 composer 输入框对齐"由父级
521
+ 统一负责(见 client/dock-align.ts),一行只测量一次、天然同一基线。
522
+ 只在"多于一个可见库"时渲染(组件自己返回 null),单库安装这一块 DOM 不存在。 */
522
523
  .dsh-tw-scope-dock {
523
- display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap;
524
- padding: 4px 8px 0; box-sizing: border-box; width: 100%; font-size: 12px;
524
+ display: inline-flex; align-items: center; gap: 6px;
525
+ font-size: 12px;
525
526
  }
526
527
  .dsh-tw-scope-label { opacity: .7; }
527
528
  .dsh-tw-scope-select {
@@ -21,7 +21,6 @@
21
21
  */
22
22
  import * as React from 'react'
23
23
  import { fetchStatus } from './status-cache.ts'
24
- import { alignDockEntry } from './dock-align.ts'
25
24
  import { SESSION_WIKI_ENDPOINT } from './endpoints.ts'
26
25
 
27
26
  interface WikiOption { id: string; label: string; running: boolean }
@@ -30,8 +29,14 @@ interface WikiOption { id: string; label: string; running: boolean }
30
29
  interface DockProps { sessionId?: string }
31
30
 
32
31
  /**
33
- * The dock entry. Returning `null` is a supported way to occupy the slot and
34
- * show nothing — which is what a single-wiki install gets.
32
+ * 会话级知识库选择器(v0.28.0)。
33
+ *
34
+ * 它**不是**一个独立的 dock 条目,而是渲染在快速笔记那一行**里面**的元素
35
+ * (v0.28.7):dock 是纵向 flex 列,一个条目 = 一整行,两个条目各自量宽度就永远
36
+ * 对不齐。所以这里返回的是"一行里的一个内联块",宽度由内容决定,不占满整行。
37
+ *
38
+ * 返回 `null` 表示"这一行不需要它"——单库安装(可见库 ≤1)时正是如此,DOM 与
39
+ * 加这个功能之前逐字相同。
35
40
  */
36
41
  export function createWikiScopeDock(): (props: DockProps) => React.ReactElement | null {
37
42
  return function WikiScopeDock(props: DockProps) {
@@ -42,14 +47,11 @@ export function createWikiScopeDock(): (props: DockProps) => React.ReactElement
42
47
  const [busy, setBusy] = React.useState(false)
43
48
  const wrapRef = React.useRef<HTMLDivElement | null>(null)
44
49
 
45
- // 与 composer 输入框右缘对齐(v0.28.2):dock 槽位渲染成输入框的**兄弟节点**,
46
- // 不做这一步这一行就悬在整列最右端(作者报障:"没有和输入框对齐")。测量逻辑
47
- // 与快速笔记共用一份 —— client/dock-align.ts。
48
- React.useLayoutEffect(() => {
49
- const wrap = wrapRef.current
50
- if (wrap === null) return
51
- return alignDockEntry(wrap)
52
- }, [])
50
+ // 注意(v0.28.7):这里**不再**自己调 alignDockEntry。选择器已经从独立 dock 条目
51
+ // 改成渲染在快速笔记那一行**里面**的元素,而那一行整行负责与输入框右缘对齐
52
+ // (quick-note-dock.ts 的 wrapRef)。如果这里再量一次、再补一次右内边距,就是
53
+ // 同一个右缘被补两遍 —— 选择器会被推进去一大截,正是"没和对话框对齐"的复现。
54
+ // 保留 wrapRef 是为了给根元素一个稳定引用做测量/调试锚点,不参与对齐计算。
53
55
 
54
56
  React.useEffect(() => {
55
57
  if (sessionId === undefined) return