dsh-tiddlywiki 0.28.11 → 0.28.13

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.11",
4
+ "version": "0.28.13",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -513,18 +513,18 @@ export function createNoteWidget(): NoteWidgetHandle {
513
513
  if (!targetPicked) targetWiki = resolveFocusWiki(roster, defaultId)
514
514
  }
515
515
  followFocus()
516
+ // 「默认库」不再作为选项出现(作者 2026-09-29 报障):那个位置就是**被设为默认的那个
517
+ // 库本身**,用它自己的显示名,只加「(默认)」作为身份标记。原来那个 `value=''` 的
518
+ // 占位项其实是多余的 —— 清单里本来就有这个库,选中它写进去的就是它。
516
519
  wikiSelect.replaceChildren(
517
520
  ...roster.map((item) => {
518
521
  const option = document.createElement('option')
519
522
  option.value = item.id
520
- option.textContent = item.running ? item.label : `${item.label}(未运行)`
523
+ const mark = item.id === defaultId ? '(默认)' : ''
524
+ option.textContent = item.running ? `${item.label}${mark}` : `${item.label}${mark}(未运行)`
521
525
  return option
522
526
  }),
523
527
  )
524
- const fallback = document.createElement('option')
525
- fallback.value = ''
526
- fallback.textContent = '默认库'
527
- wikiSelect.append(fallback)
528
528
  wikiSelect.value = targetWiki ?? ''
529
529
  wikiField.hidden = false
530
530
  markStopped()
@@ -338,31 +338,46 @@ export function renderConfigSection(body: HTMLElement, config: Record<string, un
338
338
  textField('bridge.tag', '剪藏笔记默认 tag', typeof bridge.tag === 'string' && bridge.tag.trim().length > 0 ? bridge.tag.trim() : 'clip')
339
339
  // 剪藏目标库(v0.28.8):剪藏桥自建 loopback 服务,请求走不到宿主的 `?wiki=` 解析,
340
340
  // 所以多库下**必须**在这里显式选一次,否则永远剪进默认库。选项要等 /admin/wikis
341
- // 回来才知道,所以先渲染一个只有「默认库」的 select,拿到列表后再补全;用户已选的值
342
- // 若不在列表里(库被移出清单)也保留一行,免得下拉把它静默抹掉。
343
- const clipWiki = selectField('bridge.wiki', '剪藏写入的知识库(默认库=不指定;多库时可指定某个库)', typeof bridge.wiki === 'string' ? bridge.wiki.trim() : '', [{ value: '', label: '默认库' }])
341
+ // 回来才知道,所以先渲染一个占位 select,拿到列表后补全;用户已选的值若不在列表里
342
+ // (库被移出清单)也保留一行。
343
+ //
344
+ // 空值 = 「不指定,写进默认的那个库」,但这一项的**名字**不再是「默认库」(v0.28.12,
345
+ // 作者 2026-09-29 要求:设置过默认库之后,界面上不该再出现「默认库」这个字样,应当
346
+ // 直接给出真实库名)——清单回来后把这一项就地改名成默认库自己的显示名 +「(默认)」,
347
+ // 并且**跳过**它自己那一行,免得同一个库在下拉里出现两次。
348
+ const clipWiki = selectField('bridge.wiki', '剪藏写入的知识库(不指定 = 写进知识库列表里带 ★ 的那个库;多库时可指定具体的库)', typeof bridge.wiki === 'string' ? bridge.wiki.trim() : '', [{ value: '', label: '(跟随默认的那个库)' }])
344
349
  void (async () => {
345
350
  try {
346
351
  const view = await fetchJson<WikisView>(WIKI_LIST_ENDPOINT)
347
352
  if (isDisposed()) return
348
353
  const items = Array.isArray(view.wikis) ? view.wikis : []
349
354
  const current = clipWiki.value
355
+ const defaultEntry = items.find((wiki) => wiki.id === view.defaultId)
356
+ const defaultLabel = defaultEntry?.label ?? undefined
357
+ // 空值那一项的**名字**:默认库的显示名 + 身份标记。读不到清单/没有默认库时保持
358
+ // 中性说法(绝不退回「默认库」这三个字)。
359
+ const placeholder = clipWiki.options.item(0)
360
+ if (placeholder !== null && defaultLabel !== undefined) placeholder.textContent = `${defaultLabel}(默认)`
350
361
  for (const wiki of items) {
362
+ // 默认库由空值那一项代表;再列一遍就是同一个库出现两次。
363
+ if (wiki.id === view.defaultId) continue
351
364
  const option = document.createElement('option')
352
365
  option.value = wiki.id
353
366
  option.textContent = `${wiki.label}(${wiki.id})${wiki.running ? '' : ' · 未运行'}`
354
367
  clipWiki.append(option)
355
368
  }
356
- // 当前值不在清单里(已移出):补一行,避免下拉静默把它抹成默认库。
369
+ // 当前值不在清单里(已移出):补一行,避免下拉静默把它抹成另一个库。
357
370
  if (current.length > 0 && !items.some((wiki) => wiki.id === current)) {
358
371
  const option = document.createElement('option')
359
372
  option.value = current
360
- option.textContent = `${current}(不在清单里,保存后回落默认库)`
373
+ option.textContent = defaultLabel !== undefined
374
+ ? `${current}(不在清单里,保存后写进 ${defaultLabel})`
375
+ : `${current}(不在清单里)`
361
376
  clipWiki.append(option)
362
377
  }
363
378
  clipWiki.value = current
364
379
  } catch {
365
- /* 读不到清单:只有「默认库」一项,单库安装本来就是这种形态 */
380
+ /* 读不到清单:保留中性占位项(单库安装本来就是这种形态) */
366
381
  }
367
382
  })()
368
383
  // 界面语言在下方「语言管理」区块设置(config 的 uiLanguage 仅供启动时自动应用)。
@@ -122,9 +122,13 @@ export function renderWikiListSection(body: HTMLElement, isDisposed: () => boole
122
122
 
123
123
  const render = (view: WikisView): void => {
124
124
  status.textContent = view.error !== undefined && view.error.length > 0 ? `⚠️ ${view.error}` : ''
125
+ // 「默认库」这个字样不再出现在界面上(v0.28.12,作者 2026-09-29):直接说**那个库的名字**,
126
+ // 读不到清单时用中性说法兜底,绝不退回「默认库」。
127
+ const defaultLabel = (view.wikis ?? []).find((wiki) => wiki.id === view.defaultId)?.label
128
+ const defaultName = defaultLabel !== undefined && defaultLabel.length > 0 ? defaultLabel : '默认的那个库'
125
129
  modeRow.replaceChildren(make('span', 'dsh-tw-settings-label', '运行模式:'))
126
130
  const modes: Array<[string, string]> = [
127
- ['single', '单库(只跑默认库)'],
131
+ ['single', `单库(只跑 ${defaultName})`],
128
132
  ['multi', '多库(清单里的库都可用)'],
129
133
  ]
130
134
  for (const [value, text] of modes) {
@@ -135,7 +139,7 @@ export function renderWikiListSection(body: HTMLElement, isDisposed: () => boole
135
139
  chip.addEventListener('click', () => {
136
140
  const ok = window.confirm(value === 'multi'
137
141
  ? '切换到多库模式:清单里标了「开局自启」的知识库都会启动(每个库一个 TW 子进程)。继续?'
138
- : '切换到单库模式:除默认库外的知识库会被停掉(内容与清单都不受影响)。继续?')
142
+ : `切换到单库模式:除 ${defaultName} 外的知识库会被停掉(内容与清单都不受影响)。继续?`)
139
143
  if (ok) void post({ action: 'set-mode', mode: value }, '切换模式中…')
140
144
  })
141
145
  modeRow.append(chip)
@@ -12,12 +12,41 @@
12
12
  import type { PanelState } from './state.ts'
13
13
  import { fetchUiConfig, subscribeUiConfig } from './ui-config.ts'
14
14
  import { fetchStatus } from './status-cache.ts'
15
- import { resolveFocusWiki, setFocusWiki, subscribeFocusWiki } from './wiki-focus.ts'
15
+ import { resolveFocusWiki, setFocusWiki, subscribeFocusWiki, type FocusableWiki } from './wiki-focus.ts'
16
16
  import { applyWikiIcon } from './wiki-icon.ts'
17
17
 
18
18
  /** Stable data attribute identifying this entry row. */
19
19
  export const ENTRY_SELECTOR = '[data-dsh-tw-entry]'
20
20
 
21
+ /** What a click on an entry row does — see {@link resolveEntryClick}. */
22
+ export type EntryClickAction = 'open' | 'close' | 'switch'
23
+
24
+ /**
25
+ * Decide what clicking an entry row should do (v0.28.13).
26
+ *
27
+ * 症状(作者 2026-09-29 报障):「左侧多 wiki 入口,有时候要点击两次才能切换到对应的库」。
28
+ * 根因:点行**一律** `state.toggle()` —— 面板开着 A 时点 B,`setFocusWiki(B)` 确实把焦点
29
+ * 换过去了(内核也开始换库),但同一击里的 toggle 又把面板关掉;用户看到的只是"面板没了",
30
+ * 得再点一次才看到 B。开着面板时点另一个库,用户要的是**切换**,不是"关面板"。
31
+ *
32
+ * 判定表:
33
+ * - 面板关着 → `open`(点哪个库就开哪个库);
34
+ * - 面板开着 && 就是这个库 → `close`(入口同时也是开关,再点一次收起);
35
+ * - 面板开着 && 是别的库 → `switch`(只切焦点库,面板保持打开);
36
+ * - 单库那一行(`wikiId === undefined`)逐字保留原来的 toggle 语义。
37
+ */
38
+ export function resolveEntryClick(input: {
39
+ open: boolean
40
+ /** 这一行绑定的库;`undefined` = 单库模式那唯一一行。 */
41
+ wikiId: string | undefined
42
+ /** 面板此刻**实际显示**的库(见 `mountSidebarEntry` 的 `shownWiki`)。 */
43
+ shown: string | undefined
44
+ }): EntryClickAction {
45
+ if (!input.open) return 'open'
46
+ if (input.wikiId === undefined) return 'close'
47
+ return input.wikiId === input.shown ? 'close' : 'switch'
48
+ }
49
+
21
50
 
22
51
  /** Family entries from sibling plugins, kept in a stable relative order. */
23
52
  const FAMILY_SELECTOR = '[data-dsh-tw-entry], [data-dsh-atb-entry], [data-dsh-taskboard-entry], [data-dsh-ssh-entry]'
@@ -47,15 +76,43 @@ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
47
76
  return buttons.find(button => !button.matches(ENTRY_SELECTOR) && /新会话|新建会话|new session/i.test(button.textContent ?? ''))
48
77
  }
49
78
 
79
+ /** What a row needs from its owner: the shared panel state and "which wiki is on screen". */
80
+ export interface EntryHooks {
81
+ state: PanelState
82
+ /** The wiki the panel is displaying right now (`resolveEntryClick` needs it). */
83
+ shownWiki: () => string | undefined
84
+ }
85
+
86
+ /**
87
+ * Run one entry-row click: decide, move the focus wiki, then open/close the panel.
88
+ *
89
+ * 顺序是本函数唯一容易写错的地方:`shownWiki()` 必须在 `setFocusWiki()` **之前**求值。
90
+ * 反过来的话,刚切过去的那个库会被 `resolveEntryClick` 当成"面板此刻显示的就是它",
91
+ * 于是判成 `close` —— 又变回"点两次才能切库"。
92
+ *
93
+ * @returns the action that was taken (the guards assert on it).
94
+ */
95
+ export function applyEntryClick(hooks: EntryHooks, wikiId: string | undefined): EntryClickAction {
96
+ const action = resolveEntryClick({ open: hooks.state.isOpen(), wikiId, shown: hooks.shownWiki() })
97
+ // 多库:先把这个库设为「焦点库」,面板随后加载它自己的 /tw/<id>/。
98
+ if (wikiId !== undefined) setFocusWiki(wikiId)
99
+ // 'open' 与 'switch' 都保持/变成打开:'switch' 时面板本来就开着,openPanel() 是空操作,
100
+ // 换库由焦点订阅驱动内核重载(tw-frame.ts),所以**绝不能**在这里 toggle —— 那正是
101
+ // 「点两次才能切库」的成因(v0.28.13)。
102
+ if (action === 'close') hooks.state.closePanel()
103
+ else hooks.state.openPanel()
104
+ return action
105
+ }
106
+
50
107
  /**
51
108
  * Build one entry row (a detached button; inserted once the shell is up).
52
109
  *
53
110
  * `wikiId` is optional and only used in multi-wiki mode: clicking the row focuses
54
- * that knowledge base before toggling the panel, so the row a user clicks is the
111
+ * that knowledge base before showing the panel, so the row a user clicks is the
55
112
  * wiki they get. Single-wiki installs pass nothing and behave exactly as before.
56
113
  */
57
114
  function createEntry(
58
- state: PanelState,
115
+ hooks: EntryHooks,
59
116
  label: string,
60
117
  wikiId?: string,
61
118
  icon?: string,
@@ -73,11 +130,7 @@ function createEntry(
73
130
  labelEl.className = 'dsh-tw-entry-label'
74
131
  labelEl.textContent = label
75
132
  entry.append(iconEl, labelEl)
76
- entry.addEventListener('click', () => {
77
- // 多库:先把这个库设为「焦点库」,面板随后加载它自己的 /tw/<id>/。
78
- if (wikiId !== undefined) setFocusWiki(wikiId)
79
- state.toggle()
80
- })
133
+ entry.addEventListener('click', () => { applyEntryClick(hooks, wikiId) })
81
134
  return { entry, labelEl, iconEl }
82
135
  }
83
136
 
@@ -117,7 +170,19 @@ function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
117
170
  export function mountSidebarEntry(state: PanelState): () => void {
118
171
  /** 多库:每个**在运行**的库一个入口行,用自己的显示名(作者 2026-09-28 要求)。 */
119
172
  const rows = new Map<string, { entry: HTMLButtonElement; labelEl: HTMLSpanElement; iconEl: HTMLSpanElement }>()
120
- const { entry, labelEl, iconEl } = createEntry(state, 'TiddlyWiki')
173
+ /**
174
+ * `/status` 最近一次给出的「在运行库」与默认库(v0.28.13)。
175
+ *
176
+ * 点击一个入口行时要判断「面板此刻显示的是哪个库」,才能区分"切换"与"关面板"
177
+ * (见 `resolveEntryClick`)——判定必须与面板/内核用**同一套解析**:
178
+ * 记忆值 → 默认库 → 第一个可用库(`resolveFocusWiki`)。
179
+ */
180
+ let runningWikis: FocusableWiki[] = []
181
+ let defaultWikiId: string | undefined
182
+ /** 面板此刻显示的那个库。 */
183
+ const shownWiki = (): string | undefined => resolveFocusWiki(runningWikis, defaultWikiId)
184
+ const entryHooks: EntryHooks = { state, shownWiki }
185
+ const { entry, labelEl, iconEl } = createEntry(entryHooks, 'TiddlyWiki')
121
186
  rows.set('', { entry, labelEl, iconEl })
122
187
  let disposed = false
123
188
  /**
@@ -149,6 +214,9 @@ export function mountSidebarEntry(state: PanelState): () => void {
149
214
  if (disposed) return
150
215
  const list = Array.isArray(payload?.wikis) ? payload.wikis : []
151
216
  const running = list.filter((w) => w.running)
217
+ // 点击判定与高亮都要用同一份名册(v0.28.13):存下来,`shownWiki()` 据此解析。
218
+ runningWikis = running
219
+ defaultWikiId = payload?.defaultId
152
220
  // 单库:不建额外行,标签仍归 ui.sidebarLabel 管。
153
221
  if (running.length <= 1) {
154
222
  for (const [key, row] of rows) {
@@ -172,15 +240,15 @@ export function mountSidebarEntry(state: PanelState): () => void {
172
240
  for (const w of running) {
173
241
  let row = rows.get(w.id)
174
242
  if (row === undefined) {
175
- row = createEntry(state, w.label, w.id, w.icon)
243
+ row = createEntry(entryHooks, w.label, w.id, w.icon)
176
244
  rows.set(w.id, row)
177
245
  }
178
246
  row.labelEl.textContent = w.label
179
247
  // 图标每次同步(v0.28.4):在设置页改完图标,10s 内的轮询就会把它换过来。
180
248
  applyWikiIcon(row.iconEl, w.icon)
181
249
  row.entry.setAttribute('aria-label', `TiddlyWiki 知识库:${w.label}`)
182
- // 当前焦点库高亮
183
- const focused = resolveFocusWiki(running, payload?.defaultId) === w.id
250
+ // 当前焦点库高亮(与点击判定同源:`shownWiki()`,不会出现"高亮的行点一下反而关掉面板")
251
+ const focused = shownWiki() === w.id
184
252
  if (focused) row.entry.dataset.focus = 'true'
185
253
  else delete row.entry.dataset.focus
186
254
  }
@@ -273,12 +341,15 @@ export function mountSidebarEntry(state: PanelState): () => void {
273
341
  * wiki — the focused one. Highlighting every row made it look like all three
274
342
  * were open (作者报障). The row the user clicked becomes the focus, and that is
275
343
  * the row that lights up.
344
+ *
345
+ * v0.28.13: the resolver is the SAME one the click handler uses (`shownWiki`),
346
+ * so "which row is lit" and "which row closes the panel when clicked" can never
347
+ * disagree. The old call passed `undefined` as the default id, which made the
348
+ * highlight fall back to the FIRST running row instead of the actual default
349
+ * wiki whenever no focus had been stored yet.
276
350
  */
277
351
  const syncActive = (): void => {
278
- const focus = resolveFocusWiki(
279
- activeRows().map((el) => ({ id: el.dataset.wiki ?? '' })).filter((w) => w.id.length > 0),
280
- undefined,
281
- )
352
+ const focus = shownWiki()
282
353
  for (const el of activeRows()) {
283
354
  const id = el.dataset.wiki
284
355
  // 单库默认行没有 data-wiki:它就是唯一那个库,面板开着时它就该亮。
@@ -29,6 +29,21 @@ import { isBlankSeatMounted, subscribeBlankSeat } from './scope-seat.ts'
29
29
 
30
30
  interface WikiOption { id: string; label: string; running: boolean }
31
31
 
32
+ /** `resolved` of `/session/wiki`: the wiki a session EFFECTIVELY uses. */
33
+ interface ResolvedWiki { id: string; label: string }
34
+
35
+ /**
36
+ * Read `{id,label}` out of a `/session/wiki` payload, tolerating every shape we
37
+ * may be handed (absent field, wrong types, id-only). `undefined` = unknown.
38
+ */
39
+ function readResolvedWiki(value: unknown): ResolvedWiki | undefined {
40
+ if (typeof value !== 'object' || value === null) return undefined
41
+ const id = (value as { id?: unknown }).id
42
+ if (typeof id !== 'string' || id.length === 0) return undefined
43
+ const label = (value as { label?: unknown }).label
44
+ return { id, label: typeof label === 'string' && label.length > 0 ? label : id }
45
+ }
46
+
32
47
  /** Props the shell passes to a `conversation.input.dock` entry (scope: session). */
33
48
  interface DockProps {
34
49
  sessionId?: string
@@ -107,6 +122,17 @@ export function createWikiScopeDock(options: WikiScopeDockOptions = {}): (props:
107
122
  const blankSeat = useBlankSeatMounted()
108
123
  const [wikis, setWikis] = React.useState<WikiOption[]>([])
109
124
  const [scope, setScope] = React.useState('')
125
+ /**
126
+ * The wiki this session effectively uses when no explicit scope is set —
127
+ * the host resolves it (registry default, or the first running visible one)
128
+ * and returns it as `resolved` (v0.28.12).
129
+ *
130
+ * WHY: the picker used to render a nameless 「默认库」 option. The author's
131
+ * 2026-09-29 report: with a default wiki configured, the UI should show
132
+ * THAT LIBRARY'S NAME everywhere (and a new session should already show it
133
+ * selected) — 「默认库」 as a name must not appear in the list at all.
134
+ */
135
+ const [defaultWiki, setDefaultWiki] = React.useState<ResolvedWiki | undefined>(undefined)
110
136
  const [note, setNote] = React.useState<string | undefined>(undefined)
111
137
  const [busy, setBusy] = React.useState(false)
112
138
  const wrapRef = React.useRef<HTMLDivElement | null>(null)
@@ -131,6 +157,7 @@ export function createWikiScopeDock(options: WikiScopeDockOptions = {}): (props:
131
157
  const visible = (status?.wikis ?? []).filter((wiki) => wiki.agentVisible)
132
158
  setWikis(visible.map((wiki) => ({ id: wiki.id, label: wiki.label, running: wiki.running })))
133
159
  setScope(typeof current?.scope === 'string' ? current.scope : '')
160
+ setDefaultWiki(readResolvedWiki(current?.resolved))
134
161
  setNote(typeof current?.reason === 'string' ? current.reason : undefined)
135
162
  })()
136
163
  return () => { alive = false }
@@ -146,7 +173,30 @@ export function createWikiScopeDock(options: WikiScopeDockOptions = {}): (props:
146
173
  // 空白会话里仍有唯一一个选择器。
147
174
  if (!blankOnly && blank && blankSeat) return null
148
175
 
176
+ // 「默认库」不再作为一个**名字**出现在这里(作者 2026-09-29 报障):那个选项就是被设为
177
+ // 默认的那个库本身,用它自己的显示名,后缀「(默认)」标示身份。选中它 = 清空显式
178
+ // 作用域(仍然跟随默认),所以以后改了默认库,没动过选择器的会话会跟着走 —— 语义与
179
+ // 原来那行 `value=''` 完全一致,只是不再用无名占位符。
180
+ const defaultId = defaultWiki?.id ?? ''
181
+ const listed = (id: string): boolean => wikis.some((wiki) => wiki.id === id)
182
+ const selected = scope.length > 0 && listed(scope) ? scope : defaultId
183
+
184
+ const options: WikiOption[] = [...wikis]
185
+ if (defaultId.length > 0 && !listed(defaultId)) {
186
+ // 默认库不在可见清单里(例如它被设成「对 Agent 隐身」)时必须**补一行**:
187
+ // 否则下拉的 value 指向一个不存在的 option,界面直接显示成空白。
188
+ // `running: true` 表示不额外标「未运行」—— 我们在这里拿不到它的运行态,
189
+ // 而能被解析成"本会话实际使用"的库本来就一定是正在跑的那个。
190
+ options.push({ id: defaultId, label: defaultWiki?.label ?? defaultId, running: true })
191
+ }
192
+ // 默认库排在最前(原来是那个无名选项的位置),其余保持清单顺序。
193
+ const ordered = defaultId.length > 0 && listed(defaultId)
194
+ ? [...options.filter((wiki) => wiki.id === defaultId), ...options.filter((wiki) => wiki.id !== defaultId)]
195
+ : options
196
+
149
197
  const apply = (next: string): void => {
198
+ // 选中默认库那一项 = 清空显式作用域,仍然跟随默认(默认库改了也跟着改)。
199
+ const wanted = next === defaultId ? '' : next
150
200
  setBusy(true)
151
201
  void (async () => {
152
202
  try {
@@ -156,15 +206,16 @@ export function createWikiScopeDock(options: WikiScopeDockOptions = {}): (props:
156
206
  // A generous budget: selecting a wiki that is not running STARTS it
157
207
  // host-side (the tools resolve the scope synchronously and never start
158
208
  // anything themselves), and a cold child can take tens of seconds.
159
- body: JSON.stringify({ session: sessionId, wiki: next.length > 0 ? next : null }),
209
+ body: JSON.stringify({ session: sessionId, wiki: wanted.length > 0 ? wanted : null }),
160
210
  signal: AbortSignal.timeout(120_000),
161
211
  })
162
- const payload = (await res.json().catch(() => ({}))) as { ok?: boolean; scope?: string; reason?: string; error?: string }
212
+ const payload = (await res.json().catch(() => ({}))) as { ok?: boolean; scope?: string; resolved?: unknown; reason?: string; error?: string }
163
213
  if (!res.ok || payload.ok !== true) {
164
214
  setNote(payload.error ?? `切换失败(HTTP ${res.status})`)
165
215
  return
166
216
  }
167
- setScope(typeof payload.scope === 'string' ? payload.scope : next)
217
+ setScope(typeof payload.scope === 'string' ? payload.scope : wanted)
218
+ setDefaultWiki(readResolvedWiki(payload.resolved) ?? defaultWiki)
168
219
  setNote(typeof payload.reason === 'string' ? payload.reason : undefined)
169
220
  // 立即让工具卡/会话汇总重新解析作用域(v0.28.8):它们按会话缓存 wiki id,
170
221
  // 不失效的话切库后已显示的卡片仍带着旧库的 ?wiki= 与库徽标。
@@ -185,17 +236,17 @@ export function createWikiScopeDock(options: WikiScopeDockOptions = {}): (props:
185
236
  'select',
186
237
  {
187
238
  className: 'dsh-tw-scope-select',
188
- value: scope,
239
+ value: selected,
189
240
  disabled: busy,
190
241
  'aria-label': '本会话使用的知识库',
191
242
  title: '本会话的 tiddlywiki_* 工具与注入提示词都作用于这个知识库(下一个模型步骤生效)',
192
243
  onChange: (event: React.ChangeEvent<HTMLSelectElement>) => { apply(event.target.value) },
193
244
  },
194
- React.createElement('option', { value: '' }, '默认库'),
195
- ...wikis.map((wiki) => React.createElement(
245
+ ...ordered.map((wiki) => React.createElement(
196
246
  'option',
197
247
  { key: wiki.id, value: wiki.id },
198
- `${wiki.label}${wiki.running ? '' : '(未运行,选中会启动)'}`,
248
+ // 「(默认)」是**身份标记**,不是库名:库名照旧是它自己的显示名。
249
+ `${wiki.label}${wiki.id === defaultId ? '(默认)' : ''}${wiki.running ? '' : '(未运行,选中会启动)'}`,
199
250
  )),
200
251
  ),
201
252
  note !== undefined ? React.createElement('span', { className: 'dsh-tw-scope-note' }, note) : null,
@@ -81,7 +81,7 @@ const DOC_NOTE_TAIL = `* **TW 编辑器面板**:侧边栏「TiddlyWiki」按
81
81
 
82
82
  * **想继续用单库**:什么都不用做。设置页里「知识库位置(可切换)」只在单库模式出现,用法和以前一样。
83
83
  * **想开始用多库**:去设置页「知识库列表」添加第二个库,再把「运行模式」切到**多库**。此后清单里的库都会可用(标了「开局自启」的随 DSH 启动,其余在你打开或选中它时启动)。
84
- * **控制文件在哪**:\`$DSH_HOME/dsh-tiddlywiki/wikis.json\`(在每本 wiki **之外**,所以它不会随某个库被搬走)。模式、默认库、库清单都在里面;也可以在设置页里改,效果一样。
84
+ * **控制文件在哪**:\`$DSH_HOME/dsh-tiddlywiki/wikis.json\`(在每本 wiki **之外**,所以它不会随某个库被搬走)。运行模式、库清单、以及哪个库是默认的,都在里面;也可以在设置页里改,效果一样。
85
85
  * **旧的「知识库位置」指针文件**(\`location.json\`)在单库模式下仍然是权威的——多库模式下它不再参与,改由清单决定。
86
86
  * **每个库要有自己的 \`tiddlywiki.info\`**:把一个大库拆成多个目录时,每个目录都要有一个(缺了 TW 认不出它是库)。从旧库整体搬过来的目录通常已经有了。
87
87
  * **拆分大库的注意**:书籍/语料这类只读内容建议**独立仓库 + 对 Agent 隐身**;工作与个人笔记可以共用一个仓库、按子目录划分。搬运用 \`git mv\` 保留历史,别用\"复制粘贴再删\"。
package/src/index-clip.ts CHANGED
@@ -71,7 +71,7 @@ export function createClipSurface(deps: ClipDeps): ClipSurface {
71
71
  const warnClipFallbackOnce = (wanted: string): void => {
72
72
  if (clipFallbackWarned.has(wanted)) return
73
73
  clipFallbackWarned.add(wanted)
74
- console.warn(`[dsh-tiddlywiki] clip bridge: 配置的 bridge.wiki「${wanted}」当前不在运行(或已移出清单),剪藏回落到默认库(设置页可改)`)
74
+ console.warn(`[dsh-tiddlywiki] clip bridge: 配置的 bridge.wiki「${wanted}」当前不在运行(或已移出清单),剪藏回落到默认的那个知识库(设置页可改)`)
75
75
  }
76
76
 
77
77
  const clipTarget = (): { runtime: WikiInstance; id: string } | undefined => {
package/src/index-git.ts CHANGED
@@ -134,7 +134,7 @@ export function createGitLayer(deps: GitLayerDeps): GitLayer {
134
134
  if (members.length > 1) {
135
135
  const fingerprints = new Set(members.map((entry) => JSON.stringify(runtimeConfig(entry)?.git ?? {})))
136
136
  if (fingerprints.size > 1) {
137
- console.warn(`[dsh-tiddlywiki] 知识库 ${members.map((entry) => entry.id).join('、')} 共用同一个仓库(${repoRoot}),但各自配了不同的 git.* —— 一个仓库只有一份 git 设置,本次以「${chosen?.id ?? '默认库'}」为准`)
137
+ console.warn(`[dsh-tiddlywiki] 知识库 ${members.map((entry) => entry.label ?? entry.id).join('、')} 共用同一个仓库(${repoRoot}),但各自配了不同的 git.* —— 一个仓库只有一份 git 设置,本次以「${chosen?.label ?? chosen?.id ?? '默认的那个库'}」为准`)
138
138
  }
139
139
  }
140
140
  const base = deps.gitConfig()