dsh-tiddlywiki 0.1.2 → 0.2.1

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.
@@ -79,6 +79,7 @@ interface StatusPayload {
79
79
  wikiPath?: string
80
80
  error?: string
81
81
  note?: { tag?: string }
82
+ ui?: { showPanelStatus?: boolean }
82
83
  }
83
84
 
84
85
  function conversationColumn(): HTMLElement | undefined {
@@ -115,6 +116,8 @@ export function mountPanel(state: PanelState): () => void {
115
116
  let frameArea: HTMLDivElement | undefined
116
117
  let errorArea: HTMLDivElement | undefined
117
118
  let chip: HTMLSpanElement | undefined
119
+ let statusDot: HTMLSpanElement | undefined
120
+ let statusFloater: HTMLDivElement | undefined
118
121
  let refreshTimer: number | undefined
119
122
  let refreshAttempts = 0
120
123
 
@@ -123,27 +126,6 @@ export function mountPanel(state: PanelState): () => void {
123
126
  view.dataset.dshTwView = ''
124
127
  view.className = 'dsh-tw-view'
125
128
 
126
- const bar = document.createElement('div')
127
- bar.className = 'dsh-tw-panel-bar'
128
- const title = document.createElement('span')
129
- title.className = 'dsh-tw-panel-title'
130
- title.textContent = 'TiddlyWiki 知识库'
131
- chip = document.createElement('span')
132
- chip.className = 'dsh-tw-status-chip'
133
- chip.dataset.state = 'unknown'
134
- chip.textContent = '—'
135
- const reload = document.createElement('button')
136
- reload.type = 'button'
137
- reload.textContent = '重载'
138
- reload.addEventListener('click', () => {
139
- if (iframe !== undefined && !iframe.hidden) iframe.src = iframe.src
140
- })
141
- const refresh = document.createElement('button')
142
- refresh.type = 'button'
143
- refresh.textContent = '状态'
144
- refresh.addEventListener('click', () => { void doRefresh() })
145
- bar.append(title, chip, refresh, reload)
146
-
147
129
  frameArea = document.createElement('div')
148
130
  frameArea.className = 'dsh-tw-panel-frame-wrap'
149
131
  frameArea.style.cssText = 'flex:1;min-height:0;display:flex;flex-direction:column'
@@ -157,14 +139,50 @@ export function mountPanel(state: PanelState): () => void {
157
139
  errorArea.className = 'dsh-tw-panel-error'
158
140
  errorArea.hidden = true
159
141
 
160
- view.append(bar, frameArea, errorArea)
142
+ view.append(frameArea, errorArea)
161
143
  return view
162
144
  }
163
145
 
146
+ /**
147
+ * Bottom-right floating status/reload buttons (same visual language as the
148
+ * quick-note toggle) — the top panel bar is gone, so TW's menubar reaches
149
+ * the very top of the panel and clears the host's top-right buttons. Mount
150
+ * is gated by `ui.showPanelStatus` (settings page toggle).
151
+ */
152
+ const buildStatusFloater = (): HTMLDivElement => {
153
+ const wrap = document.createElement('div')
154
+ wrap.className = 'dsh-tw-panel-status'
155
+
156
+ const statusBtn = document.createElement('button')
157
+ statusBtn.type = 'button'
158
+ statusBtn.className = 'dsh-tw-panel-status-btn'
159
+ statusBtn.title = '刷新状态'
160
+ statusDot = document.createElement('span')
161
+ statusDot.className = 'dsh-tw-status-dot'
162
+ statusDot.dataset.state = 'unknown'
163
+ chip = document.createElement('span')
164
+ chip.className = 'dsh-tw-panel-status-text'
165
+ chip.textContent = '—'
166
+ statusBtn.append(statusDot, chip)
167
+ statusBtn.addEventListener('click', () => { void doRefresh() })
168
+
169
+ const reloadBtn = document.createElement('button')
170
+ reloadBtn.type = 'button'
171
+ reloadBtn.className = 'dsh-tw-panel-status-btn'
172
+ reloadBtn.textContent = '重载'
173
+ reloadBtn.title = '重载 TiddlyWiki 面板'
174
+ reloadBtn.addEventListener('click', () => {
175
+ if (iframe !== undefined && !iframe.hidden) iframe.src = iframe.src
176
+ })
177
+
178
+ wrap.append(statusBtn, reloadBtn)
179
+ document.body.append(wrap)
180
+ return wrap
181
+ }
182
+
164
183
  const setChip = (stateName: string, text: string): void => {
165
- if (chip === undefined) return
166
- chip.dataset.state = stateName
167
- chip.textContent = text
184
+ if (statusDot !== undefined) statusDot.dataset.state = stateName
185
+ if (chip !== undefined) chip.textContent = text
168
186
  }
169
187
 
170
188
  /** Pin the overlay to the center column's current viewport rect. */
@@ -329,6 +347,13 @@ export function mountPanel(state: PanelState): () => void {
329
347
  ensure()
330
348
  applyActive()
331
349
 
350
+ // Bottom-right status/reload buttons, gated by ui.showPanelStatus.
351
+ void (async () => {
352
+ const payload = await fetchStatus()
353
+ if (payload?.ui?.showPanelStatus === false) return
354
+ statusFloater = buildStatusFloater()
355
+ })()
356
+
332
357
  return () => {
333
358
  if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
334
359
  window.clearInterval(syncInterval)
@@ -341,5 +366,6 @@ export function mountPanel(state: PanelState): () => void {
341
366
  unsubscribe()
342
367
  document.documentElement.removeAttribute(ACTIVE_ATTR)
343
368
  container?.remove()
369
+ statusFloater?.remove()
344
370
  }
345
371
  }
@@ -146,6 +146,7 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
146
146
  section.append(make('h3', 'dsh-tw-settings-h', '常规配置'))
147
147
  const note = (config.note ?? {}) as Record<string, unknown>
148
148
  const git = (config.git ?? {}) as Record<string, unknown>
149
+ const ui = (config.ui ?? {}) as Record<string, unknown>
149
150
  const fields: ConfigField[] = []
150
151
 
151
152
  const textField = (key: string, label: string, initial: string): void => {
@@ -180,6 +181,8 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
180
181
  numField('git.debounceMs', '自动 commit 防抖(ms)', typeof git.debounceMs === 'number' ? git.debounceMs : 60_000)
181
182
  textField('git.remote', 'git 远端(空=仅本地)', typeof git.remote === 'string' ? git.remote : '')
182
183
  textField('git.branch', 'git 分支', typeof git.branch === 'string' ? git.branch : 'main')
184
+ checkField('ui.showQuickNote', '显示「快速笔记」悬浮按钮', ui.showQuickNote !== false)
185
+ checkField('ui.showPanelStatus', '显示 TW 面板右下角「状态/重载」悬浮按钮', ui.showPanelStatus !== false)
183
186
  // 界面语言在下方「语言管理」区块设置(config 的 uiLanguage 仅供启动时自动应用)。
184
187
 
185
188
  const save = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '保存配置')
@@ -24,6 +24,21 @@ const CSS_TEXT = `
24
24
  .dsh-tw-entry[data-active="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); font-weight: 500; }
25
25
  .dsh-tw-entry svg { flex: none; }
26
26
  .dsh-tw-entry .dsh-tw-entry-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27
+ /* Collapsed rail: the shell narrows the sidebar to a 56px icon rail (dual
28
+ signals — the frame's data-sidebar-collapsed + the sidebar root's hashed
29
+ *_collapsed class, same doctrine as dsh-taskboard 0.4.3). Mirror the
30
+ native 36×36 icon-button geometry: center the icon, hide the label, and
31
+ scale it up to match the shell's 18px rail icons. */
32
+ [data-sidebar-collapsed] [data-dsh-tw-entry],
33
+ [class*="_collapsed"] [data-dsh-tw-entry] {
34
+ width: 36px; height: 36px; min-width: 36px;
35
+ margin: 0 0 12px; padding: 0;
36
+ justify-content: center; gap: 0; text-align: center;
37
+ }
38
+ [data-sidebar-collapsed] [data-dsh-tw-entry] .dsh-tw-entry-label,
39
+ [class*="_collapsed"] [data-dsh-tw-entry] .dsh-tw-entry-label { display: none; }
40
+ [data-sidebar-collapsed] [data-dsh-tw-entry] svg,
41
+ [class*="_collapsed"] [data-dsh-tw-entry] svg { width: 18px; height: 18px; }
27
42
 
28
43
  /* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
29
44
  .dsh-tw-view {
@@ -36,31 +51,31 @@ html[data-dsh-tw-active] [data-pane="conversation"] > :not([data-dsh-tw-view]),
36
51
  html[data-dsh-tw-active] [class*="centerCol"] > :not([data-dsh-tw-view]),
37
52
  html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view]) { display: none !important; }
38
53
 
39
- .dsh-tw-panel-bar {
40
- display: flex; align-items: center; gap: 8px; padding: 6px 10px;
41
- border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.18));
42
- background: var(--dsw-alias-bg-layer-1, transparent);
43
- font-size: 12px; color: var(--dsw-alias-label-secondary, inherit);
44
- flex: none;
45
- }
46
- .dsh-tw-panel-bar button {
47
- border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
48
- background: transparent; color: inherit; font: inherit; font-size: 12px;
49
- padding: 2px 8px; border-radius: 6px; cursor: pointer;
50
- }
51
- .dsh-tw-panel-bar button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
52
- .dsh-tw-panel-title { font-weight: 600; margin-right: auto; }
53
- /* dsh-better-sidebar coexistence: the floating toggle cluster (two 28px
54
- buttons at the viewport's top-right) lives above the panel (z-index is
55
- handled in JS — the panel stays below the host layer), so reserve the
56
- cluster's width at the panel bar's right end — mirroring how
57
- dsh-better-sidebar pads the DSH session header — and the panel's own
58
- 状态/重载 buttons never sit underneath it. */
59
- .dsh-tw-view[data-sidebar-host] .dsh-tw-panel-bar { padding-right: 80px; }
60
- .dsh-tw-status-chip { font-variant-numeric: tabular-nums; }
61
- .dsh-tw-status-chip[data-state="running"] { color: var(--dsw-alias-state-success-primary, #3eaa5f); }
62
- .dsh-tw-status-chip[data-state="starting"] { color: var(--dsw-alias-state-warning-primary, #d9822b); }
63
- .dsh-tw-status-chip[data-state="failed"], .dsh-tw-status-chip[data-state="stopped"] { color: var(--dsw-alias-state-error-primary, #d13b3b); }
54
+ /* ── bottom-right status / reload floating buttons ───────────────────
55
+ The top panel bar (title + status + reload row) is gone so TW's menubar
56
+ reaches the panel top; status/reload now live here as floating buttons,
57
+ same visual language as the quick-note toggle. The ui.showPanelStatus
58
+ setting controls whether they mount at all. */
59
+ .dsh-tw-panel-status {
60
+ /* 状态/重载悬浮按钮:右对齐到与快速笔记按钮同一条竖线(right:24),
61
+ 垂直排在快速笔记按钮(bottom:88,高约 38px)上方;
62
+ 两个按钮竖直依次排列(状态在上、重载在下),与其他插件按钮列对齐、不堆叠。 */
63
+ position: fixed; right: 24px; bottom: 140px; z-index: 950;
64
+ display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
65
+ }
66
+ .dsh-tw-panel-status-btn {
67
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
68
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
69
+ font: inherit; font-size: 12px; padding: 7px 12px; border-radius: 999px;
70
+ box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
71
+ display: inline-flex; align-items: center; gap: 6px;
72
+ transition: background-color 120ms ease;
73
+ }
74
+ .dsh-tw-panel-status-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
75
+ .dsh-tw-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
76
+ .dsh-tw-status-dot[data-state="running"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
77
+ .dsh-tw-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
78
+ .dsh-tw-status-dot[data-state="failed"], .dsh-tw-status-dot[data-state="stopped"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
64
79
 
65
80
  .dsh-tw-panel-frame { flex: 1; min-height: 0; border: 0; width: 100%; display: block; background: #fff; }
66
81
  /* [hidden] must beat the author display rules above (UA hidden is overridden). */
@@ -139,6 +154,34 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
139
154
  }
140
155
  .dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
141
156
  .dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
157
+ /* ── Multi-tag chip editor (quick-note tags) ─────────────────────────────── */
158
+ .dsh-tw-note-tags { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
159
+ .dsh-tw-note-chips { display: flex; flex-wrap: wrap; gap: 4px; }
160
+ .dsh-tw-note-tagchip {
161
+ display: inline-flex; align-items: center; gap: 4px;
162
+ padding: 2px 6px 2px 8px; border-radius: 999px; font-size: 11px; line-height: 1.4;
163
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 10%, var(--dsw-alias-bg-layer-1, #fff));
164
+ border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 30%, transparent);
165
+ color: var(--dsw-alias-label-primary, #222);
166
+ }
167
+ .dsh-tw-note-tagchip-x {
168
+ cursor: pointer; font-size: 12px; line-height: 1; padding: 0 2px; border-radius: 50%;
169
+ color: var(--dsw-alias-label-secondary, #888);
170
+ }
171
+ .dsh-tw-note-tagchip-x:hover { color: var(--dsw-alias-state-error-primary, #d13b3b); }
172
+ .dsh-tw-note-taginput { width: 100%; box-sizing: border-box; }
173
+ .dsh-tw-note-tagsuggest {
174
+ position: absolute; left: 0; right: 0; top: calc(100% + 2px); z-index: 60;
175
+ max-height: 168px; overflow-y: auto; border-radius: 8px; padding: 4px;
176
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
177
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
178
+ box-shadow: var(--dsw-shadow-lv2, 0 4px 16px rgba(0,0,0,.16));
179
+ font-size: 12px;
180
+ }
181
+ .dsh-tw-note-tagsuggest-item {
182
+ padding: 6px 9px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
183
+ }
184
+ .dsh-tw-note-tagsuggest-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
142
185
  /* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
143
186
  .dsh-tw-editor-popup {
144
187
  position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
@@ -20,6 +20,12 @@ export const CONFIG_TIDDLER = '$:/plugins/dsh-tiddlywiki/config'
20
20
  export interface PluginConfigShape {
21
21
  note?: { tag?: string }
22
22
  git?: { autoCommit?: boolean; debounceMs?: number; remote?: string; branch?: string }
23
+ ui?: {
24
+ /** 是否在界面右下角显示「快速笔记」悬浮按钮(默认 true)。 */
25
+ showQuickNote?: boolean
26
+ /** 是否显示 TW 面板右下角的「状态/重载」悬浮按钮(默认 true)。 */
27
+ showPanelStatus?: boolean
28
+ }
23
29
  uiLanguage?: string
24
30
  [key: string]: unknown
25
31
  }
@@ -39,6 +39,7 @@ export interface RouteDeps {
39
39
  git: GitFace
40
40
  autoCommit: () => void
41
41
  noteDefaults: () => { tag: string }
42
+ uiDefaults: () => { showQuickNote: boolean; showPanelStatus: boolean }
42
43
  getWikiPath: () => string
43
44
  }
44
45
 
@@ -86,10 +87,10 @@ export async function openInTwEditor(
86
87
  client: TiddlyWebClient,
87
88
  title: string,
88
89
  text: string,
89
- tag: string,
90
+ tags: string[],
90
91
  ): Promise<{ title: string; draftTitle: string }> {
91
92
  if (text.trim().length > 0) {
92
- await client.put({ title, text, tags: [tag] })
93
+ await client.put({ title, text, tags })
93
94
  }
94
95
  // Draft content: the provided text, else the existing tiddler's content.
95
96
  let draftText = text
@@ -115,6 +116,22 @@ export async function openInTwEditor(
115
116
  return { title, draftTitle }
116
117
  }
117
118
 
119
+ /** Resolve note tags from the request body: `tags` array wins, then the
120
+ * legacy single `tag` string, then the configured default tag. */
121
+ function resolveTags(
122
+ body: { tag?: unknown; tags?: unknown },
123
+ defaultTag: string,
124
+ ): string[] {
125
+ if (Array.isArray(body.tags)) {
126
+ const tags = body.tags.filter((t): t is string => typeof t === 'string' && t.trim().length > 0).map((t) => t.trim())
127
+ if (tags.length > 0) return tags
128
+ }
129
+ if (typeof body.tag === 'string' && body.tag.trim().length > 0) {
130
+ return body.tag.trim().split(/\s+/).filter(Boolean)
131
+ }
132
+ return [defaultTag]
133
+ }
134
+
118
135
  export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDeps): () => void {
119
136
  const handleStatus = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
120
137
  const view = deps.server.status()
@@ -124,12 +141,12 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
124
141
  } catch {
125
142
  gitSummary = null
126
143
  }
127
- json(res, { ok: true, ...view, git: gitSummary, note: { tag: deps.noteDefaults().tag } })
144
+ json(res, { ok: true, ...view, git: gitSummary, note: { tag: deps.noteDefaults().tag }, ui: deps.uiDefaults() })
128
145
  }
129
146
 
130
147
  const handleNote = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
131
148
  try {
132
- const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; text?: unknown }
149
+ const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; tags?: unknown; text?: unknown }
133
150
  const text = typeof body.text === 'string' && body.text.trim().length > 0 ? body.text.trim() : null
134
151
  if (text === null) {
135
152
  json(res, { ok: false, error: 'text is required' }, 400)
@@ -141,10 +158,10 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
141
158
  return
142
159
  }
143
160
  const title = typeof body.title === 'string' && body.title.trim().length > 0 ? body.title.trim() : timestampTitle()
144
- const tag = typeof body.tag === 'string' && body.tag.trim().length > 0 ? body.tag.trim() : deps.noteDefaults().tag
145
- await client.put({ title, text, tags: [tag] })
161
+ const tags = resolveTags(body, deps.noteDefaults().tag)
162
+ await client.put({ title, text, tags })
146
163
  deps.autoCommit()
147
- json(res, { ok: true, title, tag, text })
164
+ json(res, { ok: true, title, tag: tags.join(' '), tags, text })
148
165
  } catch (err) {
149
166
  json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
150
167
  }
@@ -152,16 +169,16 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
152
169
 
153
170
  const handleEdit = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
154
171
  try {
155
- const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; text?: unknown }
172
+ const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; tags?: unknown; text?: unknown }
156
173
  const client = deps.getClient()
157
174
  if (client === undefined) {
158
175
  json(res, { ok: false, error: 'wiki service is not running' }, 503)
159
176
  return
160
177
  }
161
178
  const title = typeof body.title === 'string' && body.title.trim().length > 0 ? body.title.trim() : timestampTitle()
162
- const tag = typeof body.tag === 'string' && body.tag.trim().length > 0 ? body.tag.trim() : deps.noteDefaults().tag
179
+ const tags = resolveTags(body, deps.noteDefaults().tag)
163
180
  const text = typeof body.text === 'string' ? body.text : ''
164
- const result = await openInTwEditor(client, title, text, tag)
181
+ const result = await openInTwEditor(client, title, text, tags)
165
182
  deps.autoCommit()
166
183
  json(res, { ok: true, ...result, twUrl: deps.server.url })
167
184
  } catch (err) {
@@ -169,6 +186,27 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
169
186
  }
170
187
  }
171
188
 
189
+ /** Distinct non-system tags for the quick-note tag autocomplete. */
190
+ const handleTags = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
191
+ const client = deps.getClient()
192
+ if (client === undefined) {
193
+ json(res, { ok: false, error: 'wiki service is not running' }, 503)
194
+ return
195
+ }
196
+ try {
197
+ const items = await client.list(undefined, false)
198
+ const tags = new Set<string>()
199
+ for (const item of items) {
200
+ for (const tag of item.tags ?? []) {
201
+ if (tag.length > 0 && !tag.startsWith('$:/')) tags.add(tag)
202
+ }
203
+ }
204
+ json(res, { ok: true, tags: [...tags].sort((a, b) => a.localeCompare(b, 'zh')) })
205
+ } catch (err) {
206
+ json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
207
+ }
208
+ }
209
+
172
210
  const handleRestart = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
173
211
  try {
174
212
  await deps.server.restart()
@@ -212,6 +250,7 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
212
250
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/status`, handler: (req, res) => { void handleStatus(req, res) } }),
213
251
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/note`, handler: (req, res) => { void handleNote(req, res) } }),
214
252
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/edit`, handler: (req, res) => { void handleEdit(req, res) } }),
253
+ ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/tags`, handler: (req, res) => { void handleTags(req, res) } }),
215
254
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/restart`, handler: (req, res) => { void handleRestart(req, res) } }),
216
255
  ctx.webServer.register({ kind: 'prefix', path: `${ROUTE_PREFIX}/api`, handler: (req, res) => { void handleApiProxy(req, res) } }),
217
256
  ]
@@ -1,62 +1,79 @@
1
- /**
2
- * Built-in doc note for the plugin (design doc §14): a short user-facing
3
- * "how to use dsh-tiddlywiki" note that is seeded into the wiki on first run.
4
- *
5
- * Idempotent seed (create-if-missing): on every plugin start we check whether
6
- * the note tiddler exists and only write it when it is absent — deleting it
7
- * and restarting dsh web recreates it, but editing it never gets overwritten.
8
- *
9
- * @module dsh-tiddlywiki/host/seed-notes
10
- */
11
- import type { TiddlyWebClient } from './tw-api.ts'
12
-
13
- /** Note tiddler title (a normal, searchable note — not a system tiddler). */
14
- export const DOC_NOTE_TITLE = 'dsh-tiddlywiki 插件说明'
15
-
16
- /** Tag that makes the note easy to find via `tiddlywiki_search tag=docs`. */
17
- export const DOC_NOTE_TAG = 'docs'
18
-
19
- /** The note body, TiddlyWiki wiki-text. */
20
- export const DOC_NOTE_TEXT = `! dsh-tiddlywiki 插件说明
21
-
22
- 本插件把 **TiddlyWiki 5** 作为 DSH 的持久知识库(wiki 文件夹本身就是一个 git 仓库,随内容自动提交/同步)。
23
-
24
- !! 它能做什么
25
-
26
- * **5 个 agent 工具**:\`tiddlywiki_search\`(检索)/ \`tiddlywiki_get\`(读)/ \`tiddlywiki_put\`(写)/ \`tiddlywiki_delete\`(删)/ \`tiddlywiki_git_sync\`(git 同步)。
27
- * **TW 编辑器面板**:侧边栏「TiddlyWiki」按钮 → 在界面中央打开完整版 TW 编辑器。
28
- * **快速笔记**:右下角悬浮「📝 快速笔记」写随手记,\`Ctrl+Enter\` 保存;点「✏️ 在 TW 中编辑」会弹出独立小窗用 TW 原生编辑器编辑。
29
- * **git 同步**:写入自动防抖 commit(默认 60 秒);手动 \`tiddlywiki_git_sync action=sync\` 做 pull → commit → push。
30
- * **设置页**:DSH 设置 → 「TiddlyWiki 知识库」管理插件/主题/语言与运行配置。
31
-
32
- !! 知识库纪律(三条)
33
-
34
- 1. 开工先 \`tiddlywiki_git_sync action=pull\`(rebase + autostash,真冲突会自动 abort 并报文件)。
35
- 2. 收工 \`tiddlywiki_git_sync action=sync\`。
36
- 3. 插件自动 commit 兜底,手动 sync 用于需要主动推送的场合。
37
-
38
- !! 主题与语言
39
-
40
- * **主题**分两层:每行一个「☑ 加载」(多选 = TW 里可用的主题,依赖链自动带上)和「◉ 活动」(单选 = 当前视觉主题)。应用后自动重启 TW。
41
- * **语言**:设置页勾选 \`zh-Hans\`(简体)并应用,TW 界面即切换为中文。
42
-
43
- !! 说明
44
-
45
- * 本笔记由插件在首次启动时自动写入 wiki(幂等:不存在才写)。删除后重启 dsh web 会重建;手动编辑过的内容不会被覆盖。
46
- * 更多细节见插件仓库 README。`
47
-
48
- /**
49
- * Seed the doc note when it is absent. Returns whether a note was written.
50
- * Never throws (missing wiki or note already present → no-op / false).
51
- */
52
- export async function seedDocNote(client: TiddlyWebClient): Promise<boolean> {
53
- const existing = await client.get(DOC_NOTE_TITLE).catch(() => undefined)
54
- if (existing !== undefined) return false
55
- await client.put({
56
- title: DOC_NOTE_TITLE,
57
- text: DOC_NOTE_TEXT,
58
- type: 'text/vnd.tiddlywiki',
59
- tags: [DOC_NOTE_TAG],
60
- })
61
- return true
62
- }
1
+ /**
2
+ * Built-in doc note for the plugin (design doc §14): a short user-facing
3
+ * "how to use dsh-tiddlywiki" note seeded into the wiki the first time the
4
+ * plugin runs on a wiki.
5
+ *
6
+ * ONE-SHOT seed: a marker tiddler (`seed-doc-note`) records that the note has
7
+ * been offered once; after that the note is the user's own content — deleting
8
+ * it and restarting dsh web does NOT recreate it, and edits are never
9
+ * overwritten.
10
+ *
11
+ * @module dsh-tiddlywiki/host/seed-notes
12
+ */
13
+ import type { TiddlyWebClient } from './tw-api.ts'
14
+
15
+ /** Note tiddler title (a normal, searchable note — not a system tiddler). */
16
+ export const DOC_NOTE_TITLE = 'dsh-tiddlywiki 插件说明'
17
+
18
+ /** Tag that makes the note easy to find via `tiddlywiki_search tag=docs`. */
19
+ export const DOC_NOTE_TAG = 'docs'
20
+
21
+ /** One-time marker: its presence means "the note was offered once — hands off". */
22
+ export const SEED_MARKER_TITLE = '$:/plugins/dsh-tiddlywiki/seed-doc-note'
23
+
24
+ /** The note body, TiddlyWiki wiki-text. */
25
+ export const DOC_NOTE_TEXT = `! dsh-tiddlywiki 插件说明
26
+
27
+ 本插件把 **TiddlyWiki 5** 作为 DSH 的持久知识库(wiki 文件夹本身就是一个 git 仓库,随内容自动提交/同步)。
28
+
29
+ !! 它能做什么
30
+
31
+ * **5 个 agent 工具**:\`tiddlywiki_search\`(检索)/ \`tiddlywiki_get\`(读)/ \`tiddlywiki_put\`(写)/ \`tiddlywiki_delete\`(删)/ \`tiddlywiki_git_sync\`(git 同步)。
32
+ * **TW 编辑器面板**:侧边栏「TiddlyWiki」按钮 → 在界面中央打开完整版 TW 编辑器。
33
+ * **快速笔记**:右下角悬浮「📝 快速笔记」写随手记(可多选/自动补全 tag),\`Ctrl+Enter\` 保存;点「✏️ 在 TW 中编辑」会弹出独立小窗用 TW 原生编辑器编辑。
34
+ * **git 同步**:写入自动防抖 commit(默认 60 秒);手动 \`tiddlywiki_git_sync action=sync\` 做 pull → commit → push。
35
+ * **设置页**:DSH 设置 → 「TiddlyWiki 知识库」管理插件/主题/语言与运行配置(含「快速笔记」显示开关、面板「状态/重载」悬浮按钮开关)。
36
+
37
+ !! 知识库纪律(三条)
38
+
39
+ 1. 开工先 \`tiddlywiki_git_sync action=pull\`(rebase + autostash,真冲突会自动 abort 并报文件)。
40
+ 2. 收工 \`tiddlywiki_git_sync action=sync\`。
41
+ 3. 插件自动 commit 兜底,手动 sync 用于需要主动推送的场合。
42
+
43
+ !! 主题与语言
44
+
45
+ * **主题**分两层:每行一个「☑ 加载」(多选 = TW 里可用的主题,依赖链自动带上)和「◉ 活动」(单选 = 当前视觉主题)。应用后自动重启 TW。
46
+ * **语言**:设置页勾选 \`zh-Hans\`(简体)并应用,TW 界面即切换为中文。
47
+
48
+ !! 说明
49
+
50
+ * 本笔记由插件在**首次启动**时自动写入(一次性:只写一次)。删除后重启 dsh web **不会自动恢复**——它从此归你所有。
51
+ * 更多细节见插件仓库 README。`
52
+
53
+ /**
54
+ * Seed the doc note exactly once per wiki. A marker tiddler records that the
55
+ * note has been offered; from then on the note is user-owned and is never
56
+ * re-created (deleting it survives restarts). Returns whether a note was
57
+ * written this call. Never throws.
58
+ */
59
+ export async function seedDocNote(client: TiddlyWebClient): Promise<boolean> {
60
+ const marker = await client.get(SEED_MARKER_TITLE).catch(() => undefined)
61
+ if (marker !== undefined) return false
62
+ const existing = await client.get(DOC_NOTE_TITLE).catch(() => undefined)
63
+ let wrote = false
64
+ if (existing === undefined) {
65
+ await client.put({
66
+ title: DOC_NOTE_TITLE,
67
+ text: DOC_NOTE_TEXT,
68
+ type: 'text/vnd.tiddlywiki',
69
+ tags: [DOC_NOTE_TAG],
70
+ })
71
+ wrote = true
72
+ }
73
+ // Record the offer regardless, so an existing note (upgrade from an older
74
+ // create-if-missing version) also becomes user-owned from here on.
75
+ await client
76
+ .put({ title: SEED_MARKER_TITLE, text: 'seeded-once', type: 'text/plain', tags: [] })
77
+ .catch(() => undefined)
78
+ return wrote
79
+ }
package/src/index.ts CHANGED
@@ -55,6 +55,7 @@ export interface TiddlywikiConfig {
55
55
  port?: number
56
56
  git?: { autoCommit?: boolean; debounceMs?: number; remote?: string; branch?: string }
57
57
  note?: { tag?: string }
58
+ ui?: { showQuickNote?: boolean; showPanelStatus?: boolean }
58
59
  auth?: { username?: string; password?: string }
59
60
  }
60
61
 
@@ -75,6 +76,7 @@ interface ResolvedConfig {
75
76
  port: number
76
77
  git: { autoCommit: boolean; debounceMs: number; remote: string; branch: string }
77
78
  note: { tag: string }
79
+ ui: { showQuickNote: boolean; showPanelStatus: boolean }
78
80
  auth: { username?: string; password?: string }
79
81
  }
80
82
 
@@ -84,6 +86,7 @@ const DEFAULTS: ResolvedConfig = {
84
86
  port: 0,
85
87
  git: { autoCommit: true, debounceMs: 60_000, remote: '', branch: 'main' },
86
88
  note: { tag: 'inbox' },
89
+ ui: { showQuickNote: true, showPanelStatus: true },
87
90
  auth: { username: '', password: '' },
88
91
  }
89
92
 
@@ -163,6 +166,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
163
166
  port: rawConfig.port ?? DEFAULTS.port,
164
167
  git: { ...DEFAULTS.git, ...(rawConfig.git ?? {}) },
165
168
  note: { ...DEFAULTS.note, ...(rawConfig.note ?? {}) },
169
+ ui: { ...DEFAULTS.ui, ...(rawConfig.ui ?? {}) },
166
170
  auth: { ...DEFAULTS.auth, ...(rawConfig.auth ?? {}) },
167
171
  }
168
172
  const wikiPath = join(config.wikiRoot, config.wiki)
@@ -171,12 +175,16 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
171
175
  // Runtime-editable config (settings page): the cordis `config:` block is the
172
176
  // BASE; a config tiddler ($:/plugins/dsh-tiddlywiki/config) written by the
173
177
  // settings page overlays it. Effective values come from configStore.get().
174
- const configStore = new ConfigStore({ note: config.note, git: config.git } satisfies PluginConfigShape)
178
+ const configStore = new ConfigStore({ note: config.note, git: config.git, ui: config.ui } satisfies PluginConfigShape)
175
179
  const eff = (): PluginConfigShape => configStore.get()
176
180
  const effectiveNoteTag = (): string => {
177
181
  const tag = eff().note?.tag
178
182
  return typeof tag === 'string' && tag.trim().length > 0 ? tag : config.note.tag
179
183
  }
184
+ const effectiveUi = (): { showQuickNote: boolean; showPanelStatus: boolean } => ({
185
+ showQuickNote: eff().ui?.showQuickNote !== false,
186
+ showPanelStatus: eff().ui?.showPanelStatus !== false,
187
+ })
180
188
 
181
189
  const disposers: Array<() => void> = []
182
190
  const disposeAll = (): void => {
@@ -265,11 +273,14 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
265
273
  try {
266
274
  await server.start()
267
275
  await configStore.load(client())
268
- // Seed the built-in doc note (idempotent, create-if-missing) so a fresh
269
- // wiki gets the plugin guide by default.
276
+ // Seeds (run after the effective config is loaded):
277
+ // - doc note: ONE-SHOT — a fresh wiki gets the plugin guide; deleting
278
+ // it afterwards survives restarts (see seedDocNote).
270
279
  try {
271
280
  const seedClient = client()
272
- if (seedClient !== undefined) await seedDocNote(seedClient)
281
+ if (seedClient !== undefined) {
282
+ await seedDocNote(seedClient)
283
+ }
273
284
  } catch (err) {
274
285
  console.warn('[dsh-tiddlywiki] seeding doc note:', err)
275
286
  }
@@ -307,6 +318,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
307
318
  git,
308
319
  autoCommit: () => committer?.touch(),
309
320
  noteDefaults: () => ({ tag: effectiveNoteTag() }),
321
+ uiDefaults: () => effectiveUi(),
310
322
  getWikiPath: () => wikiPath,
311
323
  })
312
324
  const adminDeps: AdminDeps = {