dsh-tiddlywiki 0.16.21 → 0.16.23
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 +311 -271
- package/docs/seed-initialization.md +47 -22
- package/lib/client.bundle.js +26 -26
- package/lib/client.js +26 -26
- package/lib/index.js +395 -17
- package/lib/index.js.map +1 -1
- package/package.json +86 -86
- package/src/client/better-sidebar-tab.ts +136 -0
- package/src/client/index.ts +30 -0
- package/src/client/panel.ts +5 -4
- package/src/client/rightbar-tab.ts +20 -299
- package/src/client/settings-page.ts +1 -0
- package/src/client/tw-frame.ts +334 -0
- package/src/client/ui-config.ts +5 -2
- package/src/host/config.ts +8 -0
- package/src/host/routes.ts +2 -0
- package/src/host/seed-home.ts +96 -92
- package/src/host/seed-notes.ts +8 -1
- package/src/host/seed-starter-docs.ts +251 -0
- package/src/host/seed-ui-styles.ts +74 -0
- package/src/host/seeds.ts +71 -25
- package/src/index.ts +7 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Right-Sidebar TiddlyWiki tab type (v0.17.0).
|
|
2
|
+
* Right-Sidebar TiddlyWiki tab type (v0.17.0; shared TW frame v0.16.23).
|
|
3
3
|
*
|
|
4
4
|
* DSH's new right column (dsh-client-ui-sidebar-right) lets plugins register
|
|
5
5
|
* "tab types": stage 1 registers the type into `ctx.sidebarRightTabs` — a
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* the whole integration is optional (older DSH / rightbar absent → no-op).
|
|
10
10
|
*
|
|
11
11
|
* The body embeds the SAME-ORIGIN TW proxy (`/dsh-tiddlywiki/tw/`) in an
|
|
12
|
-
* iframe
|
|
13
|
-
*
|
|
14
|
-
* overlay
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* iframe via the shared frame machinery (tw-frame.ts), exactly like the
|
|
13
|
+
* center-column panel. One TW client at a time is enforced: when this tab
|
|
14
|
+
* becomes visible the center overlay (and any sibling overlay panel) closes
|
|
15
|
+
* via the existing `dsh-panel-activate` protocol, and when another panel
|
|
16
|
+
* activates this tab closes itself. Wiki links in the reply stream route here
|
|
17
|
+
* while the tab is visible (tw-frame.ts' live-frame registry, see panel.ts).
|
|
17
18
|
*
|
|
18
19
|
* The tab body is React only because the slot runtime renders React: the frame
|
|
19
20
|
* is plain DOM built inside a ref host, mirroring the plugin's DOM style.
|
|
@@ -21,8 +22,14 @@
|
|
|
21
22
|
* @module dsh-tiddlywiki/client/rightbar-tab
|
|
22
23
|
*/
|
|
23
24
|
import * as React from 'react'
|
|
24
|
-
import {
|
|
25
|
-
|
|
25
|
+
import {
|
|
26
|
+
ACTIVATE_EVENT,
|
|
27
|
+
createTwFrameController,
|
|
28
|
+
getTabLabel,
|
|
29
|
+
TwTabIcon,
|
|
30
|
+
type TwFrameController,
|
|
31
|
+
warmTabLabel,
|
|
32
|
+
} from './tw-frame.ts'
|
|
26
33
|
|
|
27
34
|
/** The tab kind this package owns; what `openTab` names and the guide entry opens. */
|
|
28
35
|
export const TW_RIGHTBAR_KIND = 'dsh-tiddlywiki'
|
|
@@ -30,21 +37,6 @@ export const TW_RIGHTBAR_KIND = 'dsh-tiddlywiki'
|
|
|
30
37
|
export const TW_RIGHTBAR_ID = 'dsh-tiddlywiki'
|
|
31
38
|
/** Activation-event name for THIS tab (distinct from the center panel's 'dsh-tiddlywiki'). */
|
|
32
39
|
const RIGHTBAR_PANEL_NAME = 'dsh-tiddlywiki/rightbar'
|
|
33
|
-
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
34
|
-
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
35
|
-
/** The "知识库" FAB's reload event; the rightbar frame reloads with the center one. */
|
|
36
|
-
const PANEL_RELOAD_EVENT = 'dsh-tw-panel-reload'
|
|
37
|
-
|
|
38
|
-
const RESTART_ENDPOINT = '/dsh-tiddlywiki/restart'
|
|
39
|
-
|
|
40
|
-
/** Tab chip / guide copy default (label refreshed from `/status` ui.tabLabel). */
|
|
41
|
-
let rightbarLabel = '知识库'
|
|
42
|
-
|
|
43
|
-
/** Update the tab chip label from the live config (ui.tabLabel). */
|
|
44
|
-
function setLabel(label: string): void {
|
|
45
|
-
const trimmed = typeof label === 'string' ? label.trim() : ''
|
|
46
|
-
if (trimmed.length > 0) rightbarLabel = trimmed
|
|
47
|
-
}
|
|
48
40
|
|
|
49
41
|
/* ── structural faces over the rightbar contract (no @deepseek-ai imports) ── */
|
|
50
42
|
|
|
@@ -77,274 +69,6 @@ export interface RightbarSlotsFace {
|
|
|
77
69
|
): () => void
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
interface StatusPayload {
|
|
81
|
-
ok?: boolean
|
|
82
|
-
status: string
|
|
83
|
-
url?: string
|
|
84
|
-
/** Same-origin TW proxy path (e.g. /dsh-tiddlywiki/tw/); the iframe base. */
|
|
85
|
-
twProxy?: string
|
|
86
|
-
wikiPath?: string
|
|
87
|
-
error?: string
|
|
88
|
-
ui?: { followDshTheme?: boolean; darkPalette?: string; tabLabel?: string }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function fetchStatus(): Promise<StatusPayload | null> {
|
|
92
|
-
try {
|
|
93
|
-
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(8_000) })
|
|
94
|
-
if (!res.ok) return null
|
|
95
|
-
return (await res.json()) as StatusPayload
|
|
96
|
-
} catch {
|
|
97
|
-
return null
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function requestRestart(): Promise<boolean> {
|
|
102
|
-
try {
|
|
103
|
-
const res = await fetch(RESTART_ENDPOINT, { method: 'POST', signal: AbortSignal.timeout(8_000) })
|
|
104
|
-
return res.ok
|
|
105
|
-
} catch {
|
|
106
|
-
return false
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Guide entry glyph: a wiki page with a TiddlyWiki-style "T" (same as the entry). */
|
|
111
|
-
function GuideIcon({ size = 16, className }: { size?: number; className?: string }): React.ReactElement {
|
|
112
|
-
return React.createElement(
|
|
113
|
-
'svg',
|
|
114
|
-
{ width: size, height: size, className, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
|
|
115
|
-
React.createElement('path', { d: 'M4 2.5h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z' }),
|
|
116
|
-
React.createElement('path', { d: 'M6 6h4M6 8.5h2.5' }),
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** The plain-DOM TW frame controller owned by the React body. */
|
|
121
|
-
interface FrameController {
|
|
122
|
-
/** Reflect the tab's visibility; loads lazily on the first show. */
|
|
123
|
-
setVisible(visible: boolean): void
|
|
124
|
-
isVisible(): boolean
|
|
125
|
-
/** Open a tiddler by title; false when this controller cannot serve it. */
|
|
126
|
-
openTiddler(title: string): boolean
|
|
127
|
-
dispose(): void
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Live controller handle for center-panel link routing (openTiddlerInRightbar). */
|
|
131
|
-
let liveFrame: FrameController | undefined
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Ask the live rightbar TW tab to open a tiddler. False when no visible tab
|
|
135
|
-
* is there, so the center panel handles the request as before.
|
|
136
|
-
*/
|
|
137
|
-
export function openTiddlerInRightbar(title: string): boolean {
|
|
138
|
-
if (liveFrame === undefined) return false
|
|
139
|
-
return liveFrame.openTiddler(title)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function createFrameController(host: HTMLElement, signal: AbortSignal): FrameController {
|
|
143
|
-
let visible = false
|
|
144
|
-
let started = false
|
|
145
|
-
let disposed = false
|
|
146
|
-
let refreshTimer: number | undefined
|
|
147
|
-
let refreshAttempts = 0
|
|
148
|
-
let frameLoaded = false
|
|
149
|
-
let pendingHash: string | null = null
|
|
150
|
-
let themeSyncDispose: (() => void) | undefined
|
|
151
|
-
|
|
152
|
-
const view = document.createElement('div')
|
|
153
|
-
view.className = 'dsh-tw-rightbar-view'
|
|
154
|
-
|
|
155
|
-
const frameArea = document.createElement('div')
|
|
156
|
-
frameArea.className = 'dsh-tw-rightbar-frame-wrap'
|
|
157
|
-
const frame = document.createElement('iframe')
|
|
158
|
-
frame.className = 'dsh-tw-rightbar-frame'
|
|
159
|
-
frame.title = 'TiddlyWiki'
|
|
160
|
-
frame.hidden = true
|
|
161
|
-
frameArea.append(frame)
|
|
162
|
-
|
|
163
|
-
const errorArea = document.createElement('div')
|
|
164
|
-
errorArea.className = 'dsh-tw-rightbar-error'
|
|
165
|
-
errorArea.hidden = true
|
|
166
|
-
|
|
167
|
-
view.append(frameArea, errorArea)
|
|
168
|
-
host.append(view)
|
|
169
|
-
|
|
170
|
-
frame.addEventListener('load', () => {
|
|
171
|
-
frameLoaded = true
|
|
172
|
-
applyPendingHash()
|
|
173
|
-
})
|
|
174
|
-
themeSyncDispose = attachThemeSync(frame)
|
|
175
|
-
|
|
176
|
-
const showError = (message: string): void => {
|
|
177
|
-
frame.hidden = true
|
|
178
|
-
errorArea.hidden = false
|
|
179
|
-
errorArea.replaceChildren()
|
|
180
|
-
const p = document.createElement('div')
|
|
181
|
-
p.textContent = 'TiddlyWiki 服务不可用'
|
|
182
|
-
const code = document.createElement('code')
|
|
183
|
-
code.textContent = message
|
|
184
|
-
const retry = document.createElement('button')
|
|
185
|
-
retry.type = 'button'
|
|
186
|
-
retry.textContent = '重试'
|
|
187
|
-
retry.addEventListener('click', () => {
|
|
188
|
-
retry.disabled = true
|
|
189
|
-
retry.textContent = '重启中…'
|
|
190
|
-
void requestRestart().finally(() => { void doRefresh() })
|
|
191
|
-
})
|
|
192
|
-
errorArea.append(p, code, retry)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const showStarting = (): void => {
|
|
196
|
-
frame.hidden = true
|
|
197
|
-
errorArea.hidden = false
|
|
198
|
-
errorArea.replaceChildren()
|
|
199
|
-
const p = document.createElement('div')
|
|
200
|
-
p.textContent = 'TiddlyWiki 服务正在启动…'
|
|
201
|
-
errorArea.append(p)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const showFrame = (url: string): void => {
|
|
205
|
-
errorArea.hidden = true
|
|
206
|
-
// Only reveal the frame while the tab is on screen.
|
|
207
|
-
frame.hidden = !visible
|
|
208
|
-
// Set the src only when the url changed, so an editor never loses unsaved
|
|
209
|
-
// state on a status refresh.
|
|
210
|
-
if (frame.dataset.loaded !== url) {
|
|
211
|
-
frame.dataset.loaded = url
|
|
212
|
-
frameLoaded = false
|
|
213
|
-
frame.src = url
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** Apply a pending tiddler-hash once the frame is ready (see panel.ts). */
|
|
218
|
-
const applyPendingHash = (): void => {
|
|
219
|
-
if (pendingHash === null || !frameLoaded) return
|
|
220
|
-
const hash = pendingHash
|
|
221
|
-
const win = frame.contentWindow
|
|
222
|
-
if (win === null) {
|
|
223
|
-
fallbackLoad(hash)
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
const tryOnce = (attempt: number): void => {
|
|
227
|
-
if (disposed) return
|
|
228
|
-
if (pendingHash !== hash) return
|
|
229
|
-
const frameTw = win as { $tw?: unknown }
|
|
230
|
-
if (typeof frameTw.$tw !== 'object' || frameTw.$tw === null) {
|
|
231
|
-
if (attempt < 40) {
|
|
232
|
-
window.setTimeout(() => tryOnce(attempt + 1), 150)
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
fallbackLoad(hash)
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
pendingHash = null
|
|
239
|
-
try {
|
|
240
|
-
if (win.location.hash !== hash) win.location.hash = hash
|
|
241
|
-
} catch {
|
|
242
|
-
fallbackLoad(hash)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
tryOnce(0)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const fallbackLoad = (hash: string): void => {
|
|
249
|
-
if (disposed) return
|
|
250
|
-
if (pendingHash === hash) pendingHash = null
|
|
251
|
-
const base = frame.src.split('#')[0]
|
|
252
|
-
if (frame.src !== `${base}${hash}`) frame.src = `${base}${hash}`
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const doRefresh = async (): Promise<void> => {
|
|
256
|
-
if (refreshTimer !== undefined) {
|
|
257
|
-
window.clearTimeout(refreshTimer)
|
|
258
|
-
refreshTimer = undefined
|
|
259
|
-
}
|
|
260
|
-
const payload = await fetchStatus()
|
|
261
|
-
if (disposed) return
|
|
262
|
-
if (payload === null) {
|
|
263
|
-
showError('无法访问 /dsh-tiddlywiki/status')
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
if (payload.ui !== undefined) {
|
|
267
|
-
setLabel(payload.ui.tabLabel ?? rightbarLabel)
|
|
268
|
-
setThemeSyncConfig({
|
|
269
|
-
enabled: payload.ui.followDshTheme !== false,
|
|
270
|
-
darkPalette: payload.ui.darkPalette,
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
if (payload.status === 'running') {
|
|
274
|
-
refreshAttempts = 0
|
|
275
|
-
if (typeof payload.twProxy === 'string') {
|
|
276
|
-
showFrame(new URL(payload.twProxy, window.location.origin).href)
|
|
277
|
-
} else if (typeof payload.url === 'string') {
|
|
278
|
-
showFrame(payload.url)
|
|
279
|
-
} else {
|
|
280
|
-
showError('服务未返回编辑器地址')
|
|
281
|
-
}
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
if (payload.status === 'starting') {
|
|
285
|
-
showStarting()
|
|
286
|
-
if (refreshAttempts < 30) {
|
|
287
|
-
refreshAttempts++
|
|
288
|
-
refreshTimer = window.setTimeout(() => { void doRefresh() }, 1_500)
|
|
289
|
-
}
|
|
290
|
-
return
|
|
291
|
-
}
|
|
292
|
-
refreshAttempts = 0
|
|
293
|
-
showError(payload.error ?? `服务状态:${payload.status}`)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const onReloadRequest = (): void => {
|
|
297
|
-
if (!frame.hidden) frame.src = frame.src
|
|
298
|
-
}
|
|
299
|
-
document.addEventListener(PANEL_RELOAD_EVENT, onReloadRequest)
|
|
300
|
-
|
|
301
|
-
const onAbort = (): void => controller.dispose()
|
|
302
|
-
signal.addEventListener('abort', onAbort, { once: true })
|
|
303
|
-
|
|
304
|
-
const controller: FrameController = {
|
|
305
|
-
setVisible(next: boolean): void {
|
|
306
|
-
visible = next
|
|
307
|
-
if (!started) {
|
|
308
|
-
// Nothing loaded yet: stay hidden until the first show kicks the load.
|
|
309
|
-
frame.hidden = true
|
|
310
|
-
view.dataset.visible = next ? '1' : '0'
|
|
311
|
-
if (next) {
|
|
312
|
-
started = true
|
|
313
|
-
void doRefresh()
|
|
314
|
-
}
|
|
315
|
-
return
|
|
316
|
-
}
|
|
317
|
-
frame.hidden = !next
|
|
318
|
-
view.dataset.visible = next ? '1' : '0'
|
|
319
|
-
},
|
|
320
|
-
isVisible(): boolean {
|
|
321
|
-
return visible
|
|
322
|
-
},
|
|
323
|
-
openTiddler(title: string): boolean {
|
|
324
|
-
if (disposed || !visible) return false
|
|
325
|
-
pendingHash = `#${encodeURIComponent(title)}`
|
|
326
|
-
if (!started) {
|
|
327
|
-
started = true
|
|
328
|
-
void doRefresh()
|
|
329
|
-
} else {
|
|
330
|
-
applyPendingHash()
|
|
331
|
-
}
|
|
332
|
-
return true
|
|
333
|
-
},
|
|
334
|
-
dispose(): void {
|
|
335
|
-
if (disposed) return
|
|
336
|
-
disposed = true
|
|
337
|
-
if (liveFrame === controller) liveFrame = undefined
|
|
338
|
-
document.removeEventListener(PANEL_RELOAD_EVENT, onReloadRequest)
|
|
339
|
-
signal.removeEventListener('abort', onAbort)
|
|
340
|
-
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
|
341
|
-
themeSyncDispose?.()
|
|
342
|
-
view.remove()
|
|
343
|
-
},
|
|
344
|
-
}
|
|
345
|
-
return controller
|
|
346
|
-
}
|
|
347
|
-
|
|
348
72
|
/**
|
|
349
73
|
* The tab body (stage 2): a thin React wrapper that mounts the TW iframe into
|
|
350
74
|
* a plain-DOM host. One mount per tab record; mutual exclusion rides the
|
|
@@ -355,14 +79,13 @@ export function TwRightbarTabBody(props: RightbarTabBodyProps): React.ReactEleme
|
|
|
355
79
|
const info = useTabInfo()
|
|
356
80
|
const tab = info.tab
|
|
357
81
|
const hostRef = React.useRef<HTMLDivElement | null>(null)
|
|
358
|
-
const controllerRef = React.useRef<
|
|
82
|
+
const controllerRef = React.useRef<TwFrameController | null>(null)
|
|
359
83
|
|
|
360
84
|
React.useEffect(() => {
|
|
361
85
|
const host = hostRef.current
|
|
362
86
|
if (host === null) return
|
|
363
|
-
const controller =
|
|
87
|
+
const controller = createTwFrameController(host, tab.signal)
|
|
364
88
|
controllerRef.current = controller
|
|
365
|
-
liveFrame = controller
|
|
366
89
|
return () => {
|
|
367
90
|
controller.dispose()
|
|
368
91
|
controllerRef.current = null
|
|
@@ -397,13 +120,13 @@ export function rightbarDefinition(): Record<string, unknown> {
|
|
|
397
120
|
return {
|
|
398
121
|
id: TW_RIGHTBAR_ID,
|
|
399
122
|
kind: TW_RIGHTBAR_KIND,
|
|
400
|
-
title: (): string =>
|
|
123
|
+
title: (): string => getTabLabel(),
|
|
401
124
|
guide: [
|
|
402
125
|
{
|
|
403
126
|
order: 10,
|
|
404
127
|
title: (): string => 'TiddlyWiki 知识库',
|
|
405
128
|
description: (): string => '在右侧边栏打开 TiddlyWiki 编辑器(与聊天并排)',
|
|
406
|
-
icon:
|
|
129
|
+
icon: TwTabIcon,
|
|
407
130
|
},
|
|
408
131
|
],
|
|
409
132
|
}
|
|
@@ -431,9 +154,7 @@ export function mountRightbarTab(
|
|
|
431
154
|
}
|
|
432
155
|
// Warm the chip label from the live config (ui.tabLabel) so the first open
|
|
433
156
|
// already shows the right name; the frame refreshes it on every status call.
|
|
434
|
-
|
|
435
|
-
if (payload?.ui !== undefined) setLabel(payload.ui.tabLabel ?? rightbarLabel)
|
|
436
|
-
})
|
|
157
|
+
warmTabLabel()
|
|
437
158
|
return () => {
|
|
438
159
|
for (const dispose of disposers.splice(0)) dispose()
|
|
439
160
|
}
|
|
@@ -255,6 +255,7 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
|
|
|
255
255
|
textField('ui.tabLabel', '会话顶部「知识库」Tab 名称', typeof ui.tabLabel === 'string' && ui.tabLabel.trim().length > 0 ? ui.tabLabel.trim() : '知识库')
|
|
256
256
|
checkField('ui.showSessionTab', '显示会话顶部「知识库」Tab(本会话相关 wiki 笔记汇总)', ui.showSessionTab !== false)
|
|
257
257
|
checkField('ui.showRightbarTab', '在 DSH 右侧边栏提供 TiddlyWiki 入口/Tab(与聊天并排)', ui.showRightbarTab !== false)
|
|
258
|
+
checkField('ui.showBetterSidebarTab', '在 DSH Better Sidebar 侧边栏提供 TiddlyWiki Tab(与聊天并排)', ui.showBetterSidebarTab !== false)
|
|
258
259
|
const sendToAgent = (ui.sendToAgent ?? {}) as Record<string, unknown>
|
|
259
260
|
checkField('ui.sendToAgent.enabled', '启用「发送给 Agent」(TW 笔记 → DSH 会话注入)', sendToAgent.enabled !== false)
|
|
260
261
|
textField('ui.sendToAgent.endpoint', 'TW 端请求基址(空=自动取当前 DSH origin)', typeof sendToAgent.endpoint === 'string' ? sendToAgent.endpoint : '')
|