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/README.md +24 -12
- package/lib/client.bundle.js +52 -6
- package/lib/client.js +52 -6
- package/lib/index.js +9 -2
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +20 -0
- package/src/client/knowledge-fab.ts +54 -32
- package/src/client/note-widget.ts +17 -8
- package/src/client/quick-note-dock.ts +54 -0
- package/src/client/settings-page.ts +2 -0
- package/src/client/sidebar-entry.ts +28 -6
- package/src/client/styles.ts +46 -1
- package/src/client/ui-config.ts +36 -0
- package/src/host/config.ts +8 -0
- package/src/host/routes.ts +4 -0
- package/src/host/seed-send-to-agent.ts +1 -1
- package/src/index.ts +16 -7
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.
|
|
4
|
+
"version": "0.16.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
if (p.status === '
|
|
51
|
-
return { state: '
|
|
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
|
-
|
|
72
|
-
let
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
twHealth = await fetchTwHealth()
|
|
91
110
|
if (disposed) return
|
|
92
|
-
if (twStatusEl !== undefined) twStatusEl.textContent =
|
|
93
|
-
if (twDot !== undefined) twDot.dataset.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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
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') {
|
package/src/client/styles.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/host/config.ts
CHANGED
|
@@ -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)。 */
|
package/src/host/routes.ts
CHANGED
|
@@ -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 同步)。 */
|