dsh-tiddlywiki 0.27.2 → 0.28.0

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.
Files changed (41) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -0
  3. package/cordis.patch.yml +13 -13
  4. package/docs/CHANGELOG.md +2 -1
  5. package/docs/code-review-2026-09-17.md +151 -0
  6. package/docs/features.md +5 -2
  7. package/docs/usage.md +34 -3
  8. package/docs/wiki-split.md +97 -0
  9. package/lib/client.js +54 -27
  10. package/lib/index.js +2803 -517
  11. package/lib/index.js.map +1 -1
  12. package/package.json +5 -4
  13. package/skills/wiki-split/SKILL.md +106 -0
  14. package/src/client/endpoints.ts +37 -0
  15. package/src/client/index.ts +14 -0
  16. package/src/client/knowledge-fab.ts +62 -3
  17. package/src/client/markdown-editor.ts +128 -128
  18. package/src/client/panel.ts +2 -1
  19. package/src/client/settings-page.ts +217 -3
  20. package/src/client/state.ts +39 -39
  21. package/src/client/status-cache.ts +17 -0
  22. package/src/client/styles.ts +27 -0
  23. package/src/client/toast.ts +22 -22
  24. package/src/client/tw-frame.ts +28 -4
  25. package/src/client/wiki-focus.ts +70 -0
  26. package/src/client/wiki-scope-dock.ts +116 -0
  27. package/src/host/admin.ts +175 -35
  28. package/src/host/git.ts +50 -5
  29. package/src/host/path-key.ts +35 -0
  30. package/src/host/prompt.ts +24 -0
  31. package/src/host/repo-committers.ts +149 -0
  32. package/src/host/routes.ts +254 -81
  33. package/src/host/seeds.ts +51 -10
  34. package/src/host/session-scope.ts +154 -0
  35. package/src/host/skill-install.ts +121 -0
  36. package/src/host/tools.ts +141 -49
  37. package/src/host/wiki-farm.ts +417 -0
  38. package/src/host/wiki-instance.ts +596 -0
  39. package/src/host/wiki-registry.ts +601 -0
  40. package/src/host/wiki.ts +19 -0
  41. package/src/index.ts +686 -459
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.27.2",
4
+ "version": "0.28.0",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -47,6 +47,7 @@
47
47
  "lib/client.js",
48
48
  "src",
49
49
  "docs",
50
+ "skills",
50
51
  "tools",
51
52
  "cordis.patch.yml",
52
53
  "LICENSE",
@@ -90,9 +91,9 @@
90
91
  "typecheck": "tsc --noEmit",
91
92
  "smoke:client": "node scripts/smoke-client.mjs",
92
93
  "selftest": "node scripts/selftest.mjs",
93
- "verify:static": "node scripts/verify-send-to-agent-bundle.mjs && node scripts/verify-render-bundle.mjs && node scripts/verify-wechat-publish-bundle.mjs && node scripts/verify-package-contents.mjs && node scripts/verify-version-consistency.mjs && node scripts/verify-constants.mjs",
94
- "verify:unit": "node scripts/verify-clip-bridge.mjs && node scripts/verify-seed-error-policy.mjs && node scripts/verify-render-sanitizer.mjs && node scripts/verify-secret-masking.mjs && node scripts/verify-write-policy.mjs && node scripts/verify-workspace.mjs && node scripts/verify-restart-drain.mjs && node scripts/verify-language-pin.mjs && node scripts/verify-prompt.mjs && node scripts/verify-conflict-config-guards.mjs && node scripts/verify-config-patch.mjs && node scripts/verify-tool-views.mjs && npx tsx scripts/verify-plugin-runtime.mjs && node scripts/verify-wechat-adapters.mjs && node scripts/verify-wechat-docs-seed.mjs && node scripts/verify-wechat-publish.mjs && npx tsx scripts/verify-tw-origin.mjs && node scripts/verify-frame-guards.mjs && npx tsx scripts/verify-ready-policy.mjs && npx tsx scripts/verify-frame-surface.mjs && npx tsx scripts/verify-editor-popup.mjs && npx tsx scripts/verify-status-cache.mjs",
95
- "verify:e2e": "node scripts/verify-auth.mjs && node scripts/verify-anon-username.mjs && node scripts/verify-tools.mjs && node scripts/verify-audit-fixes.mjs && node scripts/verify-git-resolve.mjs && node scripts/verify-resilience.mjs && node scripts/verify-seed-send-to-agent.mjs && node scripts/verify-seeds-admin.mjs && node scripts/verify-wiki-switch.mjs",
94
+ "verify:static": "node scripts/verify-send-to-agent-bundle.mjs && node scripts/verify-render-bundle.mjs && node scripts/verify-wechat-publish-bundle.mjs && node scripts/verify-package-contents.mjs && node scripts/verify-version-consistency.mjs && node scripts/verify-constants.mjs && node scripts/verify-doc-links.mjs",
95
+ "verify:unit": "node scripts/verify-clip-bridge.mjs && node scripts/verify-seed-error-policy.mjs && node scripts/verify-render-sanitizer.mjs && node scripts/verify-secret-masking.mjs && node scripts/verify-write-policy.mjs && node scripts/verify-workspace.mjs && node scripts/verify-wiki-registry.mjs && node scripts/verify-wiki-farm.mjs && node scripts/verify-session-scope.mjs && node scripts/verify-skill-install.mjs && node scripts/verify-repo-keying.mjs && node scripts/verify-restart-drain.mjs && node scripts/verify-language-pin.mjs && node scripts/verify-prompt.mjs && node scripts/verify-conflict-config-guards.mjs && node scripts/verify-config-patch.mjs && node scripts/verify-tool-views.mjs && npx tsx scripts/verify-plugin-runtime.mjs && node scripts/verify-wechat-adapters.mjs && node scripts/verify-wechat-docs-seed.mjs && node scripts/verify-wechat-publish.mjs && npx tsx scripts/verify-tw-origin.mjs && node scripts/verify-frame-guards.mjs && npx tsx scripts/verify-ready-policy.mjs && npx tsx scripts/verify-frame-surface.mjs && npx tsx scripts/verify-editor-popup.mjs && npx tsx scripts/verify-status-cache.mjs && npx tsx scripts/verify-wiki-focus.mjs",
96
+ "verify:e2e": "node scripts/verify-auth.mjs && node scripts/verify-anon-username.mjs && node scripts/verify-tools.mjs && node scripts/verify-audit-fixes.mjs && node scripts/verify-git-resolve.mjs && node scripts/verify-resilience.mjs && node scripts/verify-seed-send-to-agent.mjs && node scripts/verify-seeds-admin.mjs && node scripts/verify-wiki-switch.mjs && node scripts/verify-wiki-farm-boot.mjs",
96
97
  "verify:large": "node scripts/verify-large-wiki.mjs",
97
98
  "verify": "npm run verify:static && npm run verify:unit && npm run verify:e2e && npm run verify:large",
98
99
  "prepublishOnly": "npm run typecheck && npm run build && npm run verify:static && npm run verify:unit"
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: dsh-tiddlywiki-wiki-split
3
+ description: 把已有的 dsh-tiddlywiki 知识库按用户意愿拆成几个独立的库(语料/归档与工作集分开),并登记进插件的多知识库清单。当用户说「知识库太大了 / 检索被语料淹没 / 想把书和笔记分开 / 拆库 / 帮我拆开这个 wiki」时使用。
4
+ whenToUse: 用户要求拆分、整理或隔离一个已有的 TiddlyWiki 知识库,通常是因为 Agent 检索被只读语料淹没,或想分别备份到不同的 git 远端。
5
+ metadata:
6
+ dsh-tiddlywiki-skill: wiki-split
7
+ requires: dsh-tiddlywiki >= 0.28.0
8
+ ---
9
+
10
+ <!-- dsh-tiddlywiki-skill:wiki-split v1
11
+ 上面这行是"这份 SKILL.md 是插件写的"的标记:安装器只会覆盖带标记的文件,
12
+ 你自己改过的(去掉标记即可)永远不被覆盖。 -->
13
+
14
+ # 拆分知识库(把语料/归档从工作集里分出去)
15
+
16
+ 目标:把用户现有的一坨内容按**用途**拆成几个独立的库,每个库有自己的目录、自己的 git
17
+ 仓库(或共用仓库里的一个子目录)、自己的插件配置,以及**是否对 Agent 可见**。
18
+
19
+ ## 铁律(违反任何一条都停下,问用户)
20
+
21
+ 1. **确认前不动任何文件。** 先只读体检 + 出方案表格,等用户明确确认再执行。
22
+ 2. **先备份**:给现有仓库打一个 tag(或 clone 一份)。没有备份不动手。
23
+ 3. **旧库一个都不许删。** 拆分是"复制/移动 + 重新登记",不是删除。搬完让用户自己确认后再清理。
24
+ 4. **用 `git mv` 保住历史**,不要 `mv` + `git add`(那样历史会断)。
25
+ 5. **分步可中断**:每一步都告诉用户做到哪了、下一步是什么。
26
+
27
+ ## 步骤
28
+
29
+ ### 1. 只读体检(不改任何东西)
30
+
31
+ - **tag 分布**:`tiddlywiki_list_tags`(limit 调大),找出体量大又"只读"的簇(语料、导入归档)。
32
+ - **规模与体积**(shell):`tiddlers/` 下的文件数、`files/` 的体积、`git rev-parse --show-toplevel`
33
+ (判断是"独立仓库"还是"共用仓库")、`git count-objects -vH`(仓库体积)。
34
+ - **一句话结论**:向用户说明"多少条里有多少条是语料/归档,它们是不是只读"。
35
+
36
+ ### 2. 一次问完这五个问题(每个都给建议默认值)
37
+
38
+ | 问题 | 你要的答案 | 建议默认 |
39
+ |---|---|---|
40
+ | 拆成几个库,各叫什么? | 显示名 | 按体检发现的簇命名(书籍/工作/个人) |
41
+ | 每个库收什么? | 按 tag / 关键词 / 目录 / 时间段 | 语料按 tag;工作与个人按项目/主题 |
42
+ | git 怎么分? | 共用现有仓库(按文件夹物理划分)/ 各自独立仓库 | 只读语料独立仓库,其余共用 |
43
+ | 哪些库对 Agent 可见? | 可见 = 能检索/写入;隐身 = 永不触及 | **语料/归档建议隐身**(这正是拆分的目的) |
44
+ | 现在就搬,还是先出报告? | 先出报告 = 只读 dry-run | 先出报告 |
45
+
46
+ ### 3. 出方案表格,等确认
47
+
48
+ 每个库一行:**显示名 | 收纳规则 | 目标目录 | git 仓库 | agentVisible | autostart**,
49
+ 附"预计搬多少条",并明确指出**不可逆点**(哪些操作改历史、哪些不能自动回滚)。
50
+
51
+ ### 4. 备份
52
+
53
+ `git tag pre-split-<日期>`(或 clone 到另一个目录)。告诉用户怎么回滚:
54
+ `git reset --hard pre-split-<日期>`。
55
+
56
+ ### 5. 搬迁
57
+
58
+ - 建目标目录,必要时 `git init`(独立仓库)或直接作为现有仓库的子目录(共用仓库)。
59
+ - `git mv <旧路径> <新路径>` 逐批搬,**保持提交历史**。
60
+ - 每个库补一个 `tiddlywiki.info`(新库可以让插件首次启动时 `--init server` 自动生成)。
61
+ - 共用仓库的情况下:**不要**在两个子目录里各配一个远端(一个仓库只有一个 `origin`)。
62
+
63
+ ### 6. 登记进插件
64
+
65
+ **优先用设置页**:`知识库列表` → 「添加知识库」(根目录 + 文件夹名 + 显示名),然后逐行设置
66
+ 「设为默认 / 对 Agent 可见·隐身 / 开局自启」。运行模式选「多库」。
67
+
68
+ 需要手写时,文件是 `$DSH_HOME/dsh-tiddlywiki/wikis.json`:
69
+
70
+ ```json
71
+ {
72
+ "version": 1,
73
+ "mode": "multi",
74
+ "defaultId": "work",
75
+ "wikis": [
76
+ { "id": "work", "label": "工作", "root": "D:/notes", "name": "work", "agentVisible": true, "autostart": true },
77
+ { "id": "personal", "label": "个人", "root": "D:/notes", "name": "personal", "agentVisible": true, "autostart": true },
78
+ { "id": "books", "label": "书籍", "root": "D:/corpus", "name": "books", "agentVisible": false, "autostart": false }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ 约束(写错会被插件拒绝并告诉你原因):
84
+
85
+ - `id`:小写字母/数字开头,只含 `a-z0-9._-`,≤64 字符;**不能**是 `status` / `files` / `recipes`
86
+ / `bags` / `render` / `login` / `logout` / `index.html` / `favicon.ico`(会和 TW 自己的根路径撞名)。
87
+ - **两个库不能指向同一个目录**,**也不能互相嵌套**(外层库的 `git add -A` 会把内层内容一起提交)。
88
+ - `mode` 缺省是 `single`(只跑 `defaultId` 那个库)。
89
+
90
+ ### 7. 校验(逐条给用户看结果)
91
+
92
+ - 每个库的 tiddler 计数与方案里的预期一致。
93
+ - 工作库里 `tiddlywiki_search` **不再命中**语料(这是拆分的目的)。
94
+ - 每个库能独立打开编辑器、能提交(`tiddlywiki_git_sync action=status` 或看 git 状态)。
95
+ - Agent 侧:会话选择器里只出现 `agentVisible` 的库;选一个库后工具回执上会标着库名。
96
+
97
+ ### 8. 收尾
98
+
99
+ - 把体检数字与最终布局记成一条笔记(方便几个月后回看为什么这么分)。
100
+ - 提示用户:旧库还在,确认无误后再自行清理。
101
+
102
+ ## 旧版本(插件 < 0.28.0)
103
+
104
+ 没有多库模式,只有「切换知识库位置」(同一时刻只有一个库是活的)。降级做法:建好目录 →
105
+ 用 `git mv` 搬 → 在设置页「知识库位置」里指向它。**但它做不到同时在线**:用户切到语料库翻资料
106
+ 时 Agent 也跟着切过去了。先建议升级。
@@ -47,6 +47,14 @@ export const ADMIN_SEEDS_REMOVE_ENDPOINT = `${ROUTE_PREFIX}/admin/seeds/remove`
47
47
  export const ADMIN_WIKI_LOCATION_ENDPOINT = `${ROUTE_PREFIX}/admin/wiki/location`
48
48
  export const ADMIN_WIKI_SWITCH_ENDPOINT = `${ROUTE_PREFIX}/admin/wiki/switch`
49
49
  export const ADMIN_WIKI_RESET_ENDPOINT = `${ROUTE_PREFIX}/admin/wiki/reset`
50
+ /**
51
+ * The knowledge-base LIST (v0.28.0): GET reads the control file (mode + list +
52
+ * default), POST applies ONE action (add / update / remove / set-default /
53
+ * set-mode, plus the runtime-only start / stop).
54
+ */
55
+ export const ADMIN_WIKIS_ENDPOINT = `${ROUTE_PREFIX}/admin/wikis`
56
+ /** Per-session knowledge-base scope (v0.28.0). */
57
+ export const SESSION_WIKI_ENDPOINT = `${ROUTE_PREFIX}/session/wiki`
50
58
 
51
59
  /**
52
60
  * Resolve a TW proxy URL for THIS document (v0.26.7).
@@ -75,6 +83,35 @@ export function resolveTwUrl(relative: string, absolute?: string): string {
75
83
  return new URL(relative, origin).href
76
84
  }
77
85
 
86
+ /**
87
+ * Point BOTH proxy bases at one knowledge base (v0.28.0).
88
+ *
89
+ * In multi mode the panel must load `/tw/<id>/…`: a wiki's TW frontend builds
90
+ * every API URL from the base it was served under, and the bare path means "the
91
+ * default wiki" — so a non-default wiki embedded under it would show one wiki's
92
+ * UI while reading and writing another's data. Single mode (or no roster) keeps
93
+ * the bare path, so an existing install is untouched.
94
+ *
95
+ * The ABSOLUTE base carries the same suffix: it exists for the desktop app's
96
+ * `dsh-app:` renderer (see resolveTwUrl), and a per-wiki path is just as
97
+ * necessary there.
98
+ */
99
+ export function twProxyFor(
100
+ mode: string | undefined,
101
+ wikiId: string | undefined,
102
+ relative: string,
103
+ absolute?: string,
104
+ ): { relative: string; absolute?: string } {
105
+ if (mode !== 'multi' || typeof wikiId !== 'string' || wikiId.length === 0) {
106
+ return { relative, ...(absolute !== undefined ? { absolute } : {}) }
107
+ }
108
+ const suffix = `${encodeURIComponent(wikiId)}/`
109
+ return {
110
+ relative: `${relative}${suffix}`,
111
+ ...(absolute !== undefined ? { absolute: `${absolute}${suffix}` } : {}),
112
+ }
113
+ }
114
+
78
115
  /** The subset of the `/sync` JSON body both client callers report on. */
79
116
  export interface SyncResultPayload {
80
117
  ok?: boolean
@@ -19,6 +19,7 @@ import { createNoteWidget } from './note-widget.ts'
19
19
  import { createSyncController } from './sync-button.ts'
20
20
  import { mountKnowledgeFab } from './knowledge-fab.ts'
21
21
  import { createQuickNoteDock } from './quick-note-dock.ts'
22
+ import { createWikiScopeDock } from './wiki-scope-dock.ts'
22
23
  import { fetchUiConfig } from './ui-config.ts'
23
24
  import { mountSessionSummaryView } from './session-summary.ts'
24
25
  import { disposeEditorPopup } from './editor-popup.ts'
@@ -105,6 +106,19 @@ export function apply(ctx: ClientContextFace): void {
105
106
  // 是官方为「输入框上方的全宽条目」预留的挂载点,todo/cost-meter/goal/
106
107
  // queue/git-graph 等插件内容都渲染在这里、按纵向 flex 排列,天然不重叠。
107
108
  // 由 ui.showQuickNoteDock 配置控制(默认开)。
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
+ })
108
122
  if (ctx.slots !== undefined) {
109
123
  void fetchUiConfig().then((cfg) => {
110
124
  if (clientDisposed) return
@@ -20,6 +20,8 @@ import type { PanelState } from './state.ts'
20
20
  import type { NoteWidgetHandle } from './note-widget.ts'
21
21
  import type { SyncController } from './sync-button.ts'
22
22
  import { PANEL_RELOAD_EVENT } from './tw-frame.ts'
23
+ import { ROUTE_PREFIX } from './endpoints.ts'
24
+ import { resolveFocusWiki, setFocusWiki } from './wiki-focus.ts'
23
25
 
24
26
  import { fetchStatus } from './status-cache.ts'
25
27
  import { fetchUiConfig } from './ui-config.ts'
@@ -29,6 +31,28 @@ const BOOK_ICON = '<svg viewBox="0 0 16 16" width="17" height="17" fill="none" s
29
31
 
30
32
  interface UiFlags { showQuickNote: boolean; showPanelStatus: boolean; showSyncButton: boolean }
31
33
 
34
+ /** One knowledge base as the FAB's switcher lists it (v0.28.0). */
35
+ interface FabWiki { id: string; label: string; running: boolean; path: string }
36
+
37
+ /** The knowledge-base roster for the FAB switcher. */
38
+ interface FabRoster { mode: string; defaultId?: string; wikis: FabWiki[] }
39
+
40
+ /**
41
+ * Fetch the roster (v0.28.0). The switcher only appears when there is MORE THAN
42
+ * ONE wiki, so a single-wiki install keeps exactly the menu it always had.
43
+ */
44
+ async function fetchRoster(): Promise<FabRoster> {
45
+ const payload = await fetchStatus()
46
+ const wikis = Array.isArray(payload?.wikis)
47
+ ? payload.wikis.map((wiki) => ({ id: wiki.id, label: wiki.label, running: wiki.running, path: wiki.path }))
48
+ : []
49
+ return {
50
+ mode: typeof payload?.mode === 'string' ? payload.mode : 'single',
51
+ ...(typeof payload?.defaultId === 'string' ? { defaultId: payload.defaultId } : {}),
52
+ wikis,
53
+ }
54
+ }
55
+
32
56
  /** Read the three FAB-menu gates from the shared, TTL-cached ui config.
33
57
  *
34
58
  * Used to be a private `fetchStatus()` projection (v0.22.8): the `/status`
@@ -114,7 +138,7 @@ export function mountKnowledgeFab(state: PanelState, note: NoteWidgetHandle, syn
114
138
  renderTip()
115
139
  }
116
140
 
117
- const build = (flags: UiFlags): void => {
141
+ const build = (flags: UiFlags, roster: FabRoster): void => {
118
142
  if (disposed) return
119
143
  root = document.createElement('div')
120
144
  root.className = 'dsh-tw-fab-wrap'
@@ -169,6 +193,41 @@ export function mountKnowledgeFab(state: PanelState, note: NoteWidgetHandle, syn
169
193
  }
170
194
 
171
195
  if (flags.showPanelStatus) {
196
+ // 多知识库切换(v0.28.0):只有多于一个库时才长出这一段,单库安装的菜单不变。
197
+ // 点一下 = 把"焦点库"切过去并打开面板——面板会加载那个库自己的 /tw/<id>/。
198
+ if (roster.wikis.length > 1) {
199
+ const focus = resolveFocusWiki(roster.wikis, roster.defaultId)
200
+ const heading = document.createElement('div')
201
+ heading.className = 'dsh-tw-fab-group'
202
+ heading.textContent = '知识库'
203
+ menu.append(heading)
204
+ for (const wiki of roster.wikis) {
205
+ const item = document.createElement('button')
206
+ item.type = 'button'
207
+ item.className = 'dsh-tw-fab-item'
208
+ item.dataset.current = wiki.id === focus ? '1' : '0'
209
+ item.textContent = `${wiki.id === focus ? '●' : '○'} ${wiki.label}`
210
+ item.title = `${wiki.path}${wiki.running ? '' : '(未运行,打开会启动)'}`
211
+ item.addEventListener('click', () => {
212
+ closeMenu()
213
+ setFocusWiki(wiki.id)
214
+ // 打开面板前把它起起来(v0.28.0):`/tw/<id>/` 对没在跑的库回 503,
215
+ // 而这条菜单项的提示恰恰写着"打开会启动"。启动是显式 POST——代理路由
216
+ // 绝不能在 GET 里顺手 spawn 子进程。
217
+ if (!wiki.running) {
218
+ void fetch(`${ROUTE_PREFIX}/admin/wikis`, {
219
+ method: 'POST',
220
+ headers: { 'content-type': 'application/json' },
221
+ body: JSON.stringify({ action: 'start', id: wiki.id }),
222
+ signal: AbortSignal.timeout(120_000),
223
+ }).catch(() => undefined)
224
+ }
225
+ state.openPanel()
226
+ })
227
+ menu.append(item)
228
+ }
229
+ }
230
+
172
231
  panelLabel = document.createElement('span')
173
232
  panelLabel.textContent = '打开 TW 面板'
174
233
  const item = document.createElement('button')
@@ -240,10 +299,10 @@ export function mountKnowledgeFab(state: PanelState, note: NoteWidgetHandle, syn
240
299
  const unsubSync = sync.subscribe(renderDot)
241
300
 
242
301
  void (async () => {
243
- const flags = await fetchUiFlags()
302
+ const [flags, roster] = await Promise.all([fetchUiFlags(), fetchRoster()])
244
303
  if (disposed) return
245
304
  if (!flags.showQuickNote && !flags.showPanelStatus && !flags.showSyncButton) return
246
- build(flags)
305
+ build(flags, roster)
247
306
  })()
248
307
 
249
308
  return () => {
@@ -1,128 +1,128 @@
1
- /**
2
- * CodeMirror 6 Markdown editor for the quick-note widget (replaces the
3
- * hand-rolled regex highlighter in the old markdown.ts — real Lezer syntax
4
- * tree, proper GFM coverage, undo/redo, and a native editing surface).
5
- *
6
- * The widget stays a plain-DOM product: CodeMirror is framework-agnostic, so
7
- * no React is pulled in. The editor is built into a wrapper `<div>`
8
- * (`.dsh-tw-note-editor`) that the widget can append, drag-drop onto, and
9
- * re-read via `getValue()`/`setValue()`.
10
- *
11
- * Theming follows the DSH design tokens (`--dsw-alias-*`) through a
12
- * `HighlightStyle` that maps Markdown tags onto the same palette the old
13
- * `.md-*` rules used, so light/dark both look right.
14
- *
15
- * @module dsh-tiddlywiki/client/markdown-editor
16
- */
17
- import { EditorView, keymap, placeholder, drawSelection } from '@codemirror/view'
18
- import { EditorState } from '@codemirror/state'
19
- import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
20
- import { syntaxHighlighting, HighlightStyle } from '@codemirror/language'
21
- import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
22
- import { tags as t } from '@lezer/highlight'
23
-
24
- /** Design-token aliases (same values as styles.ts / the old .md-* rules). */
25
- const BRAND = 'var(--dsw-alias-brand-primary, #3e63dd)'
26
- const BRAND_DIM = 'color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent)'
27
- const SECONDARY = 'var(--dsw-alias-label-secondary, #888)'
28
- const DIMMED = 'var(--dsw-alias-label-dimmed, #999)'
29
- const MONO = 'ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace'
30
- const CODE_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 12%, transparent)'
31
- const CODE_BLOCK_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent)'
32
-
33
- /** Markdown token → design-token style map (mirrors the old .md-* palette). */
34
- const mdHighlight = HighlightStyle.define([
35
- // Headings (# … ######) — brand + bold.
36
- { tag: [t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], color: BRAND, fontWeight: '700' },
37
- // Markers: #, >, -, *, `, [ ] etc.
38
- { tag: t.processingInstruction, color: BRAND_DIM },
39
- // Inline code + fenced code text — monospace, tinted background.
40
- { tag: t.monospace, fontFamily: MONO, backgroundColor: CODE_BG, borderRadius: '4px', padding: '0 3px' },
41
- // Code block content (inside a fence) gets a slightly wider tint.
42
- { tag: t.content, fontFamily: MONO, backgroundColor: CODE_BLOCK_BG },
43
- { tag: t.strong, fontWeight: '700' },
44
- { tag: t.emphasis, fontStyle: 'italic' },
45
- { tag: t.strikethrough, textDecoration: 'line-through', opacity: '.75' },
46
- { tag: t.link, color: BRAND, textDecoration: 'underline' },
47
- { tag: t.url, color: BRAND_DIM, textDecoration: 'underline dotted' },
48
- { tag: t.quote, fontStyle: 'italic', color: SECONDARY },
49
- { tag: t.contentSeparator, color: DIMMED, textDecoration: 'line-through' },
50
- { tag: t.comment, color: DIMMED },
51
- ])
52
-
53
- /** Public editor surface the quick-note widget consumes. */
54
- export interface MarkdownEditor {
55
- /** Wrapper element (append/drag-target). */
56
- el: HTMLDivElement
57
- /** The CodeMirror EditorView (advanced use / future extensions). */
58
- view: EditorView
59
- getValue(): string
60
- setValue(value: string): void
61
- /** Insert a Markdown line at the current caret (used by file upload). */
62
- insertAtCaret(markdown: string): void
63
- focus(): void
64
- }
65
-
66
- export interface MarkdownEditorOptions {
67
- /** Placeholder text shown when the doc is empty. */
68
- placeholder?: string
69
- /** Called on Ctrl/Cmd+Enter (wired after save is defined by the widget). */
70
- onSave?: () => void
71
- /** Called after every doc change (used for draft auto-save). */
72
- onChange?: () => void
73
- }
74
-
75
- /**
76
- * Build a CodeMirror 6 Markdown editor inside `.dsh-tw-note-editor`.
77
- * GFM base language (strikethrough, tables, task lists, autolinks) — strictly
78
- * more coverage than the old regex highlighter.
79
- */
80
- export function buildMarkdownEditor(opts: MarkdownEditorOptions = {}): MarkdownEditor {
81
- const wrap = document.createElement('div')
82
- wrap.className = 'dsh-tw-note-editor'
83
-
84
- const view = new EditorView({
85
- parent: wrap,
86
- state: EditorState.create({
87
- doc: '',
88
- extensions: [
89
- EditorView.lineWrapping,
90
- history(),
91
- markdown({ base: markdownLanguage }),
92
- syntaxHighlighting(mdHighlight),
93
- drawSelection(),
94
- placeholder(opts.placeholder ?? ''),
95
- keymap.of([
96
- ...defaultKeymap,
97
- ...historyKeymap,
98
- { key: 'Mod-Enter', run: () => { opts.onSave?.(); return true } },
99
- ]),
100
- EditorView.updateListener.of((update) => {
101
- if (update.docChanged) opts.onChange?.()
102
- }),
103
- ],
104
- }),
105
- })
106
-
107
- return {
108
- el: wrap,
109
- view,
110
- getValue: () => view.state.doc.toString(),
111
- setValue(value: string) {
112
- view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } })
113
- },
114
- insertAtCaret(markdownLine: string) {
115
- const { from } = view.state.selection.main
116
- const line = view.state.doc.lineAt(from)
117
- const atLineStart = from === line.from
118
- const insert = `${atLineStart ? '' : '\n'}${markdownLine}\n`
119
- view.dispatch({
120
- changes: { from, insert },
121
- selection: { anchor: from + insert.length },
122
- scrollIntoView: true,
123
- })
124
- view.focus()
125
- },
126
- focus: () => view.focus(),
127
- }
128
- }
1
+ /**
2
+ * CodeMirror 6 Markdown editor for the quick-note widget (replaces the
3
+ * hand-rolled regex highlighter in the old markdown.ts — real Lezer syntax
4
+ * tree, proper GFM coverage, undo/redo, and a native editing surface).
5
+ *
6
+ * The widget stays a plain-DOM product: CodeMirror is framework-agnostic, so
7
+ * no React is pulled in. The editor is built into a wrapper `<div>`
8
+ * (`.dsh-tw-note-editor`) that the widget can append, drag-drop onto, and
9
+ * re-read via `getValue()`/`setValue()`.
10
+ *
11
+ * Theming follows the DSH design tokens (`--dsw-alias-*`) through a
12
+ * `HighlightStyle` that maps Markdown tags onto the same palette the old
13
+ * `.md-*` rules used, so light/dark both look right.
14
+ *
15
+ * @module dsh-tiddlywiki/client/markdown-editor
16
+ */
17
+ import { EditorView, keymap, placeholder, drawSelection } from '@codemirror/view'
18
+ import { EditorState } from '@codemirror/state'
19
+ import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
20
+ import { syntaxHighlighting, HighlightStyle } from '@codemirror/language'
21
+ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
22
+ import { tags as t } from '@lezer/highlight'
23
+
24
+ /** Design-token aliases (same values as styles.ts / the old .md-* rules). */
25
+ const BRAND = 'var(--dsw-alias-brand-primary, #3e63dd)'
26
+ const BRAND_DIM = 'color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent)'
27
+ const SECONDARY = 'var(--dsw-alias-label-secondary, #888)'
28
+ const DIMMED = 'var(--dsw-alias-label-dimmed, #999)'
29
+ const MONO = 'ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace'
30
+ const CODE_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 12%, transparent)'
31
+ const CODE_BLOCK_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent)'
32
+
33
+ /** Markdown token → design-token style map (mirrors the old .md-* palette). */
34
+ const mdHighlight = HighlightStyle.define([
35
+ // Headings (# … ######) — brand + bold.
36
+ { tag: [t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], color: BRAND, fontWeight: '700' },
37
+ // Markers: #, >, -, *, `, [ ] etc.
38
+ { tag: t.processingInstruction, color: BRAND_DIM },
39
+ // Inline code + fenced code text — monospace, tinted background.
40
+ { tag: t.monospace, fontFamily: MONO, backgroundColor: CODE_BG, borderRadius: '4px', padding: '0 3px' },
41
+ // Code block content (inside a fence) gets a slightly wider tint.
42
+ { tag: t.content, fontFamily: MONO, backgroundColor: CODE_BLOCK_BG },
43
+ { tag: t.strong, fontWeight: '700' },
44
+ { tag: t.emphasis, fontStyle: 'italic' },
45
+ { tag: t.strikethrough, textDecoration: 'line-through', opacity: '.75' },
46
+ { tag: t.link, color: BRAND, textDecoration: 'underline' },
47
+ { tag: t.url, color: BRAND_DIM, textDecoration: 'underline dotted' },
48
+ { tag: t.quote, fontStyle: 'italic', color: SECONDARY },
49
+ { tag: t.contentSeparator, color: DIMMED, textDecoration: 'line-through' },
50
+ { tag: t.comment, color: DIMMED },
51
+ ])
52
+
53
+ /** Public editor surface the quick-note widget consumes. */
54
+ export interface MarkdownEditor {
55
+ /** Wrapper element (append/drag-target). */
56
+ el: HTMLDivElement
57
+ /** The CodeMirror EditorView (advanced use / future extensions). */
58
+ view: EditorView
59
+ getValue(): string
60
+ setValue(value: string): void
61
+ /** Insert a Markdown line at the current caret (used by file upload). */
62
+ insertAtCaret(markdown: string): void
63
+ focus(): void
64
+ }
65
+
66
+ export interface MarkdownEditorOptions {
67
+ /** Placeholder text shown when the doc is empty. */
68
+ placeholder?: string
69
+ /** Called on Ctrl/Cmd+Enter (wired after save is defined by the widget). */
70
+ onSave?: () => void
71
+ /** Called after every doc change (used for draft auto-save). */
72
+ onChange?: () => void
73
+ }
74
+
75
+ /**
76
+ * Build a CodeMirror 6 Markdown editor inside `.dsh-tw-note-editor`.
77
+ * GFM base language (strikethrough, tables, task lists, autolinks) — strictly
78
+ * more coverage than the old regex highlighter.
79
+ */
80
+ export function buildMarkdownEditor(opts: MarkdownEditorOptions = {}): MarkdownEditor {
81
+ const wrap = document.createElement('div')
82
+ wrap.className = 'dsh-tw-note-editor'
83
+
84
+ const view = new EditorView({
85
+ parent: wrap,
86
+ state: EditorState.create({
87
+ doc: '',
88
+ extensions: [
89
+ EditorView.lineWrapping,
90
+ history(),
91
+ markdown({ base: markdownLanguage }),
92
+ syntaxHighlighting(mdHighlight),
93
+ drawSelection(),
94
+ placeholder(opts.placeholder ?? ''),
95
+ keymap.of([
96
+ ...defaultKeymap,
97
+ ...historyKeymap,
98
+ { key: 'Mod-Enter', run: () => { opts.onSave?.(); return true } },
99
+ ]),
100
+ EditorView.updateListener.of((update) => {
101
+ if (update.docChanged) opts.onChange?.()
102
+ }),
103
+ ],
104
+ }),
105
+ })
106
+
107
+ return {
108
+ el: wrap,
109
+ view,
110
+ getValue: () => view.state.doc.toString(),
111
+ setValue(value: string) {
112
+ view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } })
113
+ },
114
+ insertAtCaret(markdownLine: string) {
115
+ const { from } = view.state.selection.main
116
+ const line = view.state.doc.lineAt(from)
117
+ const atLineStart = from === line.from
118
+ const insert = `${atLineStart ? '' : '\n'}${markdownLine}\n`
119
+ view.dispatch({
120
+ changes: { from, insert },
121
+ selection: { anchor: from + insert.length },
122
+ scrollIntoView: true,
123
+ })
124
+ view.focus()
125
+ },
126
+ focus: () => view.focus(),
127
+ }
128
+ }
@@ -30,6 +30,7 @@ import type { PanelState } from './state.ts'
30
30
  import { ENTRY_SELECTOR } from './sidebar-entry.ts'
31
31
  // 事件名与 frame 生命周期助手都只有一份,住在 tw-frame.ts(内核)。
32
32
  import { ACTIVATE_EVENT, createTwFrameSurface, openTiddlerInLiveTab, type TwFrameSkin } from './tw-frame.ts'
33
+ import { getFocusWiki } from './wiki-focus.ts'
33
34
 
34
35
  /**
35
36
  * Center-column targets, most-specific shell generation first. The official
@@ -125,7 +126,7 @@ export function mountPanel(state: PanelState): () => void {
125
126
  let lastHasHost: boolean | undefined
126
127
 
127
128
  /** iframe + /status lifecycle (shared with the rightbar tab, tw-frame.ts). */
128
- const surface = createTwFrameSurface(PANEL_SKIN)
129
+ const surface = createTwFrameSurface(PANEL_SKIN, { wikiId: getFocusWiki })
129
130
 
130
131
  /** Pin the overlay to the center column's current viewport rect. */
131
132
  const syncRect = (): void => {