dsh-tiddlywiki 0.16.2 → 0.16.4

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 as the DSH persistent knowledge base: tiddlywiki_* agent tools (search with filters/recent/tags/batch/rename + git sync & conflict resolve), a full TiddlyWiki editor embedded in the GUI center column (same-origin proxy, works over Tailscale/LAN/domain), a quick-note card with draft auto-save & recent-notes picker behind a single knowledge FAB, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
4
- "version": "0.16.2",
4
+ "version": "0.16.4",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -18,6 +18,8 @@ import { mountPanel } from './panel.ts'
18
18
  import { createNoteWidget } from './note-widget.ts'
19
19
  import { createSyncController } from './sync-button.ts'
20
20
  import { mountKnowledgeFab } from './knowledge-fab.ts'
21
+ import { createQuickNoteDock } from './quick-note-dock.ts'
22
+ import { fetchUiConfig } from './ui-config.ts'
21
23
  import { disposeEditorPopup } from './editor-popup.ts'
22
24
  import { SettingsSection } from './settings-page.ts'
23
25
  import { registerToolViews, installWikiLinkInterceptor } from './tool-views.ts'
@@ -49,6 +51,7 @@ export function apply(ctx: ClientContextFace): void {
49
51
  injectStyles()
50
52
  const state = new PanelState()
51
53
  const disposers: Array<() => void> = []
54
+ let clientDisposed = false
52
55
  try {
53
56
  // v0.5: the quick-note card and the sync logic are owned by controllers;
54
57
  // the single "知识库" FAB drives them (three old floating controls merged).
@@ -60,6 +63,22 @@ export function apply(ctx: ClientContextFace): void {
60
63
  disposers.push(mountPanel(state))
61
64
  disposers.push(mountKnowledgeFab(state, note, sync))
62
65
  disposers.push(disposeEditorPopup)
66
+ // 输入框上方「快速笔记」快捷按钮(conversation.input.dock 槽位)。该槽位
67
+ // 是官方为「输入框上方的全宽条目」预留的挂载点,todo/cost-meter/goal/
68
+ // queue/git-graph 等插件内容都渲染在这里、按纵向 flex 排列,天然不重叠。
69
+ // 由 ui.showQuickNoteDock 配置控制(默认开)。
70
+ if (ctx.slots !== undefined) {
71
+ void fetchUiConfig().then((cfg) => {
72
+ if (clientDisposed || !cfg.showQuickNoteDock) return
73
+ const removeDock = ctx.slots?.inject('conversation.input.dock', () =>
74
+ ctx.slots?.register(
75
+ { name: 'conversation.input.dock', id: 'quick-note', order: 8, label: '快速笔记' },
76
+ createQuickNoteDock(note),
77
+ ),
78
+ )
79
+ if (removeDock !== undefined) disposers.push(removeDock)
80
+ })
81
+ }
63
82
  } catch (error) {
64
83
  // DOM failures degrade the plugin, never the GUI.
65
84
  console.error('[dsh-tiddlywiki] mount failed:', error)
@@ -90,6 +109,7 @@ export function apply(ctx: ClientContextFace): void {
90
109
  console.error('[dsh-tiddlywiki] settings section failed:', error)
91
110
  }
92
111
  ctx.effect?.(() => () => {
112
+ clientDisposed = true
93
113
  for (const dispose of disposers.splice(0)) dispose()
94
114
  }, 'dsh-tiddlywiki: client mount')
95
115
  } catch (error) {
@@ -5,11 +5,13 @@
5
5
  *
6
6
  * - The FAB carries a live git status dot (from SyncController).
7
7
  * - The menu (opens upward) exposes, gated by the same ui.* settings:
8
- * · TW service status line + git status line
8
+ * · TW 服务状态行(一行;悬停弹出详细状态 tip,含 git 状态与最近日志)
9
9
  * · 📝 快速笔记 (if ui.showQuickNote) → opens the note card
10
10
  * · 🖥 打开/收起 TW 面板 (if ui.showPanelStatus) → toggles the center panel
11
11
  * · 🔄 重载 TW 面板 (if ui.showPanelStatus) → reloads the panel iframe
12
12
  * · 🔁 同步 (if ui.showSyncButton) → pull→commit→push
13
+ * - 旧版菜单里的第二行(git/知识库状态行)已去掉:git 状态由 FAB 上的状态点
14
+ * 展示,详细内容并入 TW 状态行的悬停 tip。
13
15
  * - When ALL three ui flags are off, no DOM is created.
14
16
  *
15
17
  * @module dsh-tiddlywiki/client/knowledge-fab
@@ -40,17 +42,21 @@ async function fetchUiFlags(): Promise<UiFlags> {
40
42
  }
41
43
  }
42
44
 
43
- /** Fetch the TW service health for the menu's status line. */
44
- async function fetchTwStatus(): Promise<{ state: string; text: string }> {
45
+ /** TW service health snapshot for the menu's status line + hover tip. */
46
+ interface TwHealth { state: string; text: string; logs: string[] }
47
+
48
+ /** Fetch the TW service health (state line text + recent logs for the tip). */
49
+ async function fetchTwHealth(): Promise<TwHealth> {
45
50
  try {
46
51
  const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(8_000) })
47
- if (!res.ok) return { state: 'failed', text: '状态不可达' }
48
- const p = (await res.json()) as { status?: string; url?: string; error?: string }
49
- if (p.status === 'running') return { state: 'running', text: `TW 在线 · ${p.url ?? ''}` }
50
- if (p.status === 'starting') return { state: 'starting', text: 'TW 启动中…' }
51
- return { state: 'failed', text: p.error ?? `TW 状态:${p.status ?? '?'}` }
52
+ if (!res.ok) return { state: 'failed', text: '状态不可达', logs: [] }
53
+ const p = (await res.json()) as { status?: string; url?: string; error?: string; logs?: string[] }
54
+ const logs = Array.isArray(p.logs) ? p.logs.filter((l): l is string => typeof l === 'string') : []
55
+ if (p.status === 'running') return { state: 'running', text: `TW 在线 · ${p.url ?? ''}`, logs }
56
+ if (p.status === 'starting') return { state: 'starting', text: 'TW 启动中…', logs }
57
+ return { state: 'failed', text: p.error ?? `TW 状态:${p.status ?? '?'}`, logs }
52
58
  } catch {
53
- return { state: 'failed', text: '状态不可达' }
59
+ return { state: 'failed', text: '状态不可达', logs: [] }
54
60
  }
55
61
  }
56
62
 
@@ -66,31 +72,45 @@ export function mountKnowledgeFab(state: PanelState, note: NoteWidgetHandle, syn
66
72
  let menu: HTMLDivElement | undefined
67
73
  let twStatusEl: HTMLSpanElement | undefined
68
74
  let twDot: HTMLSpanElement | undefined
75
+ let tip: HTMLDivElement | undefined
69
76
  let panelLabel: HTMLSpanElement | undefined
70
77
  let menuOpen = false
71
- let gitRowDot: HTMLSpanElement | undefined
72
- let gitRowText: HTMLSpanElement | undefined
78
+ /** Latest TW health snapshot, merged into the hover tip. */
79
+ let twHealth: TwHealth = { state: 'unknown', text: 'TiddlyWiki 服务…', logs: [] }
73
80
 
74
81
  const closeMenu = (): void => {
75
82
  menuOpen = false
76
83
  if (menu !== undefined) menu.hidden = true
84
+ if (tip !== undefined) tip.hidden = true
85
+ }
86
+
87
+ /** Rebuild the TW status row's hover tip (TW 服务 + git 状态 + 最近日志). */
88
+ const renderTip = (): void => {
89
+ if (tip === undefined) return
90
+ const lines: string[] = [`TiddlyWiki 服务:${twHealth.text}`]
91
+ lines.push(`知识库同步:${sync.getState().tooltip}`)
92
+ if (twHealth.logs.length > 0) {
93
+ lines.push('最近日志:')
94
+ lines.push(...twHealth.logs.slice(-3))
95
+ }
96
+ tip.textContent = lines.join('\n')
77
97
  }
78
98
 
79
- /** Refresh the git dot + tooltip from the controller (FAB dot + menu row). */
99
+ /** Refresh the FAB git dot + its title tooltip (and the menu tip's git line). */
80
100
  const renderDot = (): void => {
81
101
  const s = sync.getState()
82
102
  if (dot !== undefined) dot.dataset.state = s.state
83
103
  if (fabBtn !== undefined) fabBtn.title = `知识库 · ${s.tooltip}`
84
- if (gitRowDot !== undefined) gitRowDot.dataset.state = s.state
85
- if (gitRowText !== undefined) gitRowText.textContent = s.tooltip
104
+ renderTip()
86
105
  }
87
106
 
88
107
  /** Refresh the TW service status line in the menu. */
89
108
  const refreshTwStatus = async (): Promise<void> => {
90
- const info = await fetchTwStatus()
109
+ twHealth = await fetchTwHealth()
91
110
  if (disposed) return
92
- if (twStatusEl !== undefined) twStatusEl.textContent = info.text
93
- if (twDot !== undefined) twDot.dataset.state = info.state
111
+ if (twStatusEl !== undefined) twStatusEl.textContent = twHealth.text
112
+ if (twDot !== undefined) twDot.dataset.state = twHealth.state
113
+ renderTip()
94
114
  }
95
115
 
96
116
  const build = (flags: UiFlags): void => {
@@ -102,10 +122,11 @@ export function mountKnowledgeFab(state: PanelState, note: NoteWidgetHandle, syn
102
122
  menu.className = 'dsh-tw-fab-menu'
103
123
  menu.hidden = true
104
124
 
105
- // Status section (TW service + git).
125
+ // 状态区:仅保留一行 TW 服务状态(第二行 git 状态行已去掉,git 详情并入
126
+ // 这一行的悬停 tip;FAB 上的状态点仍展示 git 颜色)。
106
127
  if (flags.showPanelStatus) {
107
128
  const twRow = document.createElement('div')
108
- twRow.className = 'dsh-tw-fab-status'
129
+ twRow.className = 'dsh-tw-fab-status dsh-tw-fab-status-tiprow'
109
130
  twDot = document.createElement('span')
110
131
  twDot.className = 'dsh-tw-fab-status-dot'
111
132
  twDot.dataset.state = 'unknown'
@@ -113,20 +134,20 @@ export function mountKnowledgeFab(state: PanelState, note: NoteWidgetHandle, syn
113
134
  twStatusEl.className = 'dsh-tw-fab-status-text'
114
135
  twStatusEl.textContent = 'TiddlyWiki 服务…'
115
136
  twRow.append(twDot, twStatusEl)
137
+ // 悬停详细状态 tip(TW 服务 + git + 最近日志)。
138
+ tip = document.createElement('div')
139
+ tip.className = 'dsh-tw-fab-tip'
140
+ tip.hidden = true
141
+ twRow.append(tip)
142
+ twRow.addEventListener('mouseenter', () => {
143
+ renderTip()
144
+ if (tip !== undefined) tip.hidden = false
145
+ })
146
+ twRow.addEventListener('mouseleave', () => {
147
+ if (tip !== undefined) tip.hidden = true
148
+ })
116
149
  menu.append(twRow)
117
150
  }
118
- if (flags.showSyncButton) {
119
- const gitRow = document.createElement('div')
120
- gitRow.className = 'dsh-tw-fab-status'
121
- gitRowDot = document.createElement('span')
122
- gitRowDot.className = 'dsh-tw-fab-status-dot'
123
- gitRowDot.dataset.state = sync.getState().state
124
- gitRowText = document.createElement('span')
125
- gitRowText.className = 'dsh-tw-fab-status-text'
126
- gitRowText.textContent = sync.getState().tooltip
127
- gitRow.append(gitRowDot, gitRowText)
128
- menu.append(gitRow)
129
- }
130
151
 
131
152
  if (flags.showQuickNote) {
132
153
  const item = document.createElement('button')
@@ -135,7 +156,8 @@ export function mountKnowledgeFab(state: PanelState, note: NoteWidgetHandle, syn
135
156
  item.textContent = '📝 快速笔记'
136
157
  item.addEventListener('click', () => {
137
158
  closeMenu()
138
- void note.toggle()
159
+ // open-only:弹窗只能由卡片上的 ✕ 关闭,触发按钮不负责收起。
160
+ void note.open()
139
161
  })
140
162
  menu.append(item)
141
163
  }
@@ -324,6 +324,17 @@ export function createNoteWidget(): NoteWidgetHandle {
324
324
  let draftTimer: number | undefined
325
325
  let recentOpen = false
326
326
 
327
+ /**
328
+ * Broadcast the card's open/close state to the rest of the page (the input-
329
+ * dock quick-note button highlights while the card is open). Plain DOM
330
+ * CustomEvent — no shared service needed.
331
+ */
332
+ const emitState = (open: boolean): void => {
333
+ try {
334
+ window.dispatchEvent(new CustomEvent('dsh-tw-note-state', { detail: { open } }))
335
+ } catch { /* event dispatch is best-effort */ }
336
+ }
337
+
327
338
  const resetTitle = (): void => {
328
339
  if (ui !== undefined) ui.titleInput.value = timestampTitle()
329
340
  }
@@ -564,6 +575,7 @@ export function createNoteWidget(): NoteWidgetHandle {
564
575
  opened = false
565
576
  root.hidden = true
566
577
  closeRecent()
578
+ emitState(false)
567
579
  }
568
580
 
569
581
  const saveDone = (): void => {
@@ -651,14 +663,9 @@ export function createNoteWidget(): NoteWidgetHandle {
651
663
  editBtn.addEventListener('click', () => { void doEdit() })
652
664
  closeBtn.addEventListener('click', close)
653
665
 
654
- // Clicking outside the card collapses it (but keeps the draft).
655
- document.addEventListener('click', (event) => {
656
- if (!opened) return
657
- const target = event.target as Node
658
- if (root.contains(target)) return
659
- if (recentWrap.contains(target)) return
660
- close()
661
- }, true)
666
+ // 快速笔记弹窗只允许「手动点关闭按钮(✕)」关闭:刻意移除「点击卡片外部
667
+ // 即收起」的监听(用户反馈会误关、丢失正在编辑的内容)。草稿仍会防抖
668
+ // 自动保存,重开时原样恢复。
662
669
 
663
670
  // Discard draft (banner button).
664
671
  discardBtn.addEventListener('click', () => {
@@ -679,6 +686,7 @@ export function createNoteWidget(): NoteWidgetHandle {
679
686
  if (ui === undefined || opened) return
680
687
  opened = true
681
688
  ui.root.hidden = false
689
+ emitState(true)
682
690
  const draft = loadDraft()
683
691
  if (draft !== null && (draft.text.trim().length > 0 || draft.title.trim().length > 0)) {
684
692
  // Restore the autosaved draft (survives reload / accidental close).
@@ -698,6 +706,7 @@ export function createNoteWidget(): NoteWidgetHandle {
698
706
  opened = false
699
707
  ui.root.hidden = true
700
708
  closeRecent()
709
+ emitState(false)
701
710
  },
702
711
  async toggle() {
703
712
  if (opened) this.close()
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Quick-note button above the chat input box (需求: 输入框上方快速笔记按钮).
3
+ *
4
+ * Registers into the shell's `conversation.input.dock` slot — the sanctioned
5
+ * "Full-width entries above the composer card" seat that sibling plugins
6
+ * (todo / cost-meter / goal / queue / git-graph) already use. Entries render as
7
+ * a flex column, so this strip stacks with theirs and can never overlap or
8
+ * occlude anything. Gated by `ui.showQuickNoteDock` (default on), honored at
9
+ * registration time in client/index.ts.
10
+ *
11
+ * Clicking opens the floating quick-note card (open-only; the card itself can
12
+ * only be dismissed with its ✕ button). While the card is open the button
13
+ * highlights, driven by the card's `dsh-tw-note-state` CustomEvent.
14
+ *
15
+ * @module dsh-tiddlywiki/client/quick-note-dock
16
+ */
17
+ import * as React from 'react'
18
+ import type { NoteWidgetHandle } from './note-widget.ts'
19
+
20
+ /** Event name the note card dispatches on open/close (detail: { open }). */
21
+ export const NOTE_STATE_EVENT = 'dsh-tw-note-state'
22
+
23
+ /**
24
+ * Build the dock entry component bound to one note-widget handle. Called once
25
+ * per client mount; the returned component is what the slot renders.
26
+ */
27
+ export function createQuickNoteDock(note: NoteWidgetHandle): () => React.ReactElement {
28
+ return function QuickNoteDock() {
29
+ const [open, setOpen] = React.useState(false)
30
+ React.useEffect(() => {
31
+ const onState = (event: Event): void => {
32
+ const detail = (event as CustomEvent<{ open?: boolean }>).detail
33
+ setOpen(detail?.open === true)
34
+ }
35
+ window.addEventListener(NOTE_STATE_EVENT, onState)
36
+ return () => window.removeEventListener(NOTE_STATE_EVENT, onState)
37
+ }, [])
38
+ return React.createElement(
39
+ 'div',
40
+ { className: 'dsh-tw-dock-note' },
41
+ React.createElement(
42
+ 'button',
43
+ {
44
+ type: 'button',
45
+ className: open ? 'dsh-tw-dock-note-btn dsh-tw-dock-note-btn-active' : 'dsh-tw-dock-note-btn',
46
+ title: open ? '快速笔记已打开(点卡片右上角 ✕ 收起)' : '打开快速笔记(草稿自动保存,点别处不会关闭)',
47
+ onClick: () => { void note.open() },
48
+ },
49
+ React.createElement('span', { className: 'dsh-tw-dock-note-icon', 'aria-hidden': 'true' }, '📝'),
50
+ React.createElement('span', { className: 'dsh-tw-dock-note-label' }, open ? '快速笔记(已打开)' : '快速笔记'),
51
+ ),
52
+ )
53
+ }
54
+ }
@@ -227,6 +227,8 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
227
227
  textField('git.remote', 'git 远端(空=仅本地)', typeof git.remote === 'string' ? git.remote : '')
228
228
  textField('git.branch', 'git 分支', typeof git.branch === 'string' ? git.branch : 'main')
229
229
  checkField('ui.showQuickNote', '显示「知识库」按钮里的「快速笔记」入口', ui.showQuickNote !== false)
230
+ checkField('ui.showQuickNoteDock', '显示聊天输入框上方的「快速笔记」快捷按钮', ui.showQuickNoteDock !== false)
231
+ textField('ui.sidebarLabel', '侧边栏 TW 入口显示名称', typeof ui.sidebarLabel === 'string' && ui.sidebarLabel.trim().length > 0 ? ui.sidebarLabel.trim() : 'TiddlyWiki')
230
232
  checkField('ui.showPanelStatus', '显示「知识库」按钮里的 TW 面板/重载入口与状态行', ui.showPanelStatus !== false)
231
233
  checkField('ui.showSyncButton', '显示「知识库」按钮里的「同步」入口与 git 状态点', ui.showSyncButton !== false)
232
234
  checkField('ui.followDshTheme', '嵌入式 TW 跟随 DSH 深浅主题(暗色时自动切深色 palette,不写回 wiki)', ui.followDshTheme !== false)
@@ -46,15 +46,21 @@ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
46
46
  }
47
47
 
48
48
  /** Build the entry row (a detached button; insert once the shell is up). */
49
- function createEntry(state: PanelState): HTMLButtonElement {
49
+ function createEntry(state: PanelState, label: string): { entry: HTMLButtonElement; labelEl: HTMLSpanElement } {
50
50
  const entry = document.createElement('button')
51
51
  entry.type = 'button'
52
52
  entry.dataset.dshTwEntry = ''
53
53
  entry.className = 'dsh-tw-entry'
54
54
  entry.setAttribute('aria-label', 'TiddlyWiki 知识库')
55
- entry.innerHTML = `<span class="dsh-tw-entry-icon">${ICON}</span><span class="dsh-tw-entry-label">TiddlyWiki</span>`
55
+ const icon = document.createElement('span')
56
+ icon.className = 'dsh-tw-entry-icon'
57
+ icon.innerHTML = ICON
58
+ const labelEl = document.createElement('span')
59
+ labelEl.className = 'dsh-tw-entry-label'
60
+ labelEl.textContent = label
61
+ entry.append(icon, labelEl)
56
62
  entry.addEventListener('click', () => { state.toggle() })
57
- return entry
63
+ return { entry, labelEl }
58
64
  }
59
65
 
60
66
  /** Re-insert the entry before the whole family block (stable ordering). */
@@ -78,12 +84,28 @@ interface TwDebug { attempts: number; found: boolean; placed: boolean }
78
84
 
79
85
  /**
80
86
  * Mount the sidebar entry, waiting for the shell and self-healing on later
81
- * re-renders.
87
+ * re-renders. The row label starts at `initialLabel` and is refreshed from the
88
+ * live config (`ui.sidebarLabel`) as soon as /status answers.
82
89
  * @param state - the shared panel state the entry toggles.
90
+ * @param initialLabel - default display name before config loads.
83
91
  * @returns disposer removing the entry and its observers.
84
92
  */
85
- export function mountSidebarEntry(state: PanelState): () => void {
86
- const entry = createEntry(state)
93
+ export function mountSidebarEntry(state: PanelState, initialLabel = 'TiddlyWiki'): () => void {
94
+ const { entry, labelEl } = createEntry(state, initialLabel)
95
+ // 自定义显示名:/status 返回 ui.sidebarLabel(设置页「侧边栏入口显示名称」),
96
+ // 异步到达后原地更新,无需重建 DOM(旧 host 无该字段时保持默认名)。
97
+ void (async () => {
98
+ try {
99
+ const res = await fetch('/dsh-tiddlywiki/status', { signal: AbortSignal.timeout(5_000) })
100
+ if (!res.ok) return
101
+ const p = (await res.json()) as { ui?: { sidebarLabel?: string } }
102
+ const label = p.ui?.sidebarLabel
103
+ if (typeof label === 'string' && label.trim().length > 0) {
104
+ labelEl.textContent = label.trim()
105
+ entry.setAttribute('aria-label', label.trim())
106
+ }
107
+ } catch { /* keep the initial label */ }
108
+ })()
87
109
  const debug: TwDebug = { attempts: 0, found: false, placed: false }
88
110
  const host = globalThis.location?.hostname
89
111
  if (host === 'localhost' || host === '127.0.0.1') {
@@ -427,8 +427,29 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
427
427
  border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.08));
428
428
  margin-bottom: 4px;
429
429
  }
430
- .dsh-tw-fab-status + .dsh-tw-fab-status { border-bottom: 0; margin-bottom: 4px; }
430
+ /* 状态行是悬停 tip 的定位锚点(菜单里只剩这一行状态)。 */
431
+ .dsh-tw-fab-status-tiprow { position: relative; }
431
432
  .dsh-tw-fab-status-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
433
+ /* 详细状态 tip:悬停第一行(TW 服务状态)时弹出,显示 TW 服务 + git 状态 +
434
+ 最近日志,取代旧版菜单里的第二行 git 状态行。 */
435
+ .dsh-tw-fab-tip {
436
+ position: absolute; right: 0; bottom: calc(100% + 8px);
437
+ z-index: 70; max-width: 300px; min-width: 200px;
438
+ padding: 8px 10px; border-radius: 10px;
439
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
440
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
441
+ box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
442
+ font-size: 11px; line-height: 1.6; white-space: pre-line; word-break: break-word;
443
+ text-align: left;
444
+ }
445
+ .dsh-tw-fab-tip[hidden] { display: none; }
446
+ .dsh-tw-fab-tip::after {
447
+ content: ''; position: absolute; right: 16px; bottom: -5px;
448
+ width: 10px; height: 10px; transform: rotate(45deg);
449
+ background: var(--dsw-alias-bg-layer-2, #fff);
450
+ border-right: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
451
+ border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
452
+ }
432
453
  .dsh-tw-fab-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
433
454
  .dsh-tw-fab-status-dot[data-state="running"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
434
455
  .dsh-tw-fab-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
@@ -445,6 +466,30 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
445
466
  .dsh-tw-fab-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
446
467
  .dsh-tw-fab-item:active { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); }
447
468
 
469
+ /* ── input-dock quick-note button(聊天输入框上方,conversation.input.dock)──
470
+ 该槽位渲染为「输入框上方的全宽条目」纵向 flex 排列(todo/cost-meter/goal/
471
+ queue/git-graph 等插件同区),此按钮以紧凑小胶囊呈现,不与其他条目重叠。 */
472
+ .dsh-tw-dock-note {
473
+ display: flex; align-items: center; gap: 8px;
474
+ padding: 4px 6px 0; box-sizing: border-box; width: 100%;
475
+ }
476
+ .dsh-tw-dock-note-btn {
477
+ display: inline-flex; align-items: center; gap: 6px;
478
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
479
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
480
+ font: inherit; font-size: 12px; padding: 5px 12px; border-radius: 999px;
481
+ cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,.06);
482
+ transition: background-color 120ms ease, border-color 120ms ease;
483
+ }
484
+ .dsh-tw-dock-note-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
485
+ .dsh-tw-dock-note-btn-active {
486
+ border-color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 45%, transparent);
487
+ color: var(--dsw-alias-brand-primary, #3e63dd);
488
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 8%, var(--dsw-alias-bg-layer-2, #fff));
489
+ }
490
+ .dsh-tw-dock-note-icon { display: inline-flex; flex: none; }
491
+ .dsh-tw-dock-note-label { white-space: nowrap; }
492
+
448
493
  /* ── quick-note restored-draft banner ─────────────────────────────────────── */
449
494
  .dsh-tw-note-draft {
450
495
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared client-side read of the effective UI config (mirrors UiDefaultsPublic
3
+ * on the host). Used by the input-dock quick-note button and any other client
4
+ * mount that needs a config value before building DOM.
5
+ *
6
+ * @module dsh-tiddlywiki/client/ui-config
7
+ */
8
+
9
+ const STATUS_ENDPOINT = '/dsh-tiddlywiki/status'
10
+
11
+ export interface UiConfig {
12
+ /** 聊天输入框上方的「快速笔记」快捷按钮是否显示(默认 true)。 */
13
+ showQuickNoteDock: boolean
14
+ /** 左侧侧边栏 TW 入口的显示名称(默认「TiddlyWiki」)。 */
15
+ sidebarLabel: string
16
+ }
17
+
18
+ const FALLBACK: UiConfig = { showQuickNoteDock: true, sidebarLabel: 'TiddlyWiki' }
19
+
20
+ /** Fetch /status once and read the ui.* fields (backward compatible: a host
21
+ * that predates a field simply falls back to the default). */
22
+ export async function fetchUiConfig(): Promise<UiConfig> {
23
+ try {
24
+ const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
25
+ if (!res.ok) return FALLBACK
26
+ const p = (await res.json()) as { ui?: { showQuickNoteDock?: boolean; sidebarLabel?: string } }
27
+ return {
28
+ showQuickNoteDock: p.ui?.showQuickNoteDock !== false,
29
+ sidebarLabel: typeof p.ui?.sidebarLabel === 'string' && p.ui.sidebarLabel.trim().length > 0
30
+ ? p.ui.sidebarLabel.trim()
31
+ : 'TiddlyWiki',
32
+ }
33
+ } catch {
34
+ return FALLBACK
35
+ }
36
+ }
@@ -35,6 +35,14 @@ export interface PluginConfigShape {
35
35
  ui?: {
36
36
  /** 是否在界面右下角显示「快速笔记」悬浮按钮(默认 true)。 */
37
37
  showQuickNote?: boolean
38
+ /**
39
+ * 是否在聊天输入框上方(conversation.input.dock 槽位)显示「快速笔记」
40
+ * 快捷按钮(默认 true)。与其它插件(todo/cost-meter/goal/queue/git-graph
41
+ * 等)注入的内容同处一个纵向排列的 dock 区,天然不重叠。
42
+ */
43
+ showQuickNoteDock?: boolean
44
+ /** 左侧侧边栏 TW 入口的显示名称(默认「TiddlyWiki」,可自定义)。 */
45
+ sidebarLabel?: string
38
46
  /** 是否显示 TW 面板右下角的「状态/重载」悬浮按钮(默认 true)。 */
39
47
  showPanelStatus?: boolean
40
48
  /** 是否在界面右下角显示「同步」悬浮按钮(默认 true)。 */
@@ -139,6 +139,10 @@ export interface SessionsFace {
139
139
  /** Effective UI flags returned by /status (mirror index.ts). */
140
140
  export interface UiDefaultsPublic {
141
141
  showQuickNote: boolean
142
+ /** 聊天输入框上方的「快速笔记」快捷按钮(conversation.input.dock 槽位)。 */
143
+ showQuickNoteDock: boolean
144
+ /** 左侧侧边栏 TW 入口的显示名称。 */
145
+ sidebarLabel: string
142
146
  showPanelStatus: boolean
143
147
  showSyncButton: boolean
144
148
  /** 嵌入式 TW 是否跟随 DSH 深浅主题(false 时客户端停止 palette 同步)。 */