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/docs/CHANGELOG.md +4 -0
- package/docs/features.md +3 -2
- package/docs/usage.md +3 -2
- package/lib/client.js +27 -27
- package/lib/index.js +3 -3
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/note-widget.ts +5 -5
- package/src/client/settings-page-config.ts +21 -6
- package/src/client/settings-page-wikis.ts +6 -2
- package/src/client/sidebar-entry.ts +87 -16
- package/src/client/wiki-scope-dock.ts +58 -7
- package/src/host/seed-notes.ts +1 -1
- package/src/index-clip.ts +1 -1
- package/src/index-git.ts +1 -1
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
342
|
-
//
|
|
343
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 :
|
|
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:
|
|
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(
|
|
195
|
-
...wikis.map((wiki) => React.createElement(
|
|
245
|
+
...ordered.map((wiki) => React.createElement(
|
|
196
246
|
'option',
|
|
197
247
|
{ key: wiki.id, value: wiki.id },
|
|
198
|
-
|
|
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,
|
package/src/host/seed-notes.ts
CHANGED
|
@@ -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()
|