dsh-tiddlywiki 0.16.19 → 0.16.21
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/LICENSE +21 -21
- package/README.md +8 -1
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +53 -26
- package/lib/client.js +53 -26
- package/lib/index.js +136 -25
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/endpoints.ts +10 -10
- package/src/client/index.ts +36 -0
- package/src/client/markdown-editor.ts +128 -128
- package/src/client/panel.ts +4 -0
- package/src/client/rightbar-tab.ts +440 -0
- package/src/client/session-summary.ts +279 -279
- package/src/client/settings-page.ts +1 -0
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +27 -0
- package/src/client/theme-sync.ts +312 -312
- package/src/client/toast.ts +22 -22
- package/src/client/tool-views.ts +604 -604
- package/src/client/ui-config.ts +64 -61
- package/src/host/config.ts +7 -0
- package/src/host/http.ts +57 -57
- package/src/host/routes.ts +2 -0
- package/src/host/seed-home.ts +1 -1
- package/src/host/seed-render.ts +66 -66
- package/src/host/seed-send-to-agent.ts +63 -63
- package/src/host/tools.ts +40 -5
- package/src/host/tw-api.ts +108 -16
- package/src/host/wiki.ts +299 -299
- package/src/index.ts +9 -8
- package/src/sdk.ts +204 -204
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Right-Sidebar TiddlyWiki tab type (v0.17.0).
|
|
3
|
+
*
|
|
4
|
+
* DSH's new right column (dsh-client-ui-sidebar-right) lets plugins register
|
|
5
|
+
* "tab types": stage 1 registers the type into `ctx.sidebarRightTabs` — a
|
|
6
|
+
* guide entry box on the rightbar's home page is the entry point — and stage 2
|
|
7
|
+
* registers the tab's React body into the keyed `sidebar.right.pane.tab` seat
|
|
8
|
+
* under the definition's id. This module does both, guarded at the call site:
|
|
9
|
+
* the whole integration is optional (older DSH / rightbar absent → no-op).
|
|
10
|
+
*
|
|
11
|
+
* The body embeds the SAME-ORIGIN TW proxy (`/dsh-tiddlywiki/tw/`) in an
|
|
12
|
+
* iframe, exactly like the center-column panel. One TW client at a time is
|
|
13
|
+
* enforced: when this tab becomes visible the center overlay (and any sibling
|
|
14
|
+
* overlay panel) closes via the existing `dsh-panel-activate` protocol, and
|
|
15
|
+
* when another panel activates this tab closes itself. Wiki links in the reply
|
|
16
|
+
* stream route here while the tab is visible (see panel.ts openTiddler).
|
|
17
|
+
*
|
|
18
|
+
* The tab body is React only because the slot runtime renders React: the frame
|
|
19
|
+
* is plain DOM built inside a ref host, mirroring the plugin's DOM style.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-tiddlywiki/client/rightbar-tab
|
|
22
|
+
*/
|
|
23
|
+
import * as React from 'react'
|
|
24
|
+
import { STATUS_ENDPOINT } from './endpoints.ts'
|
|
25
|
+
import { attachThemeSync, setThemeSyncConfig } from './theme-sync.ts'
|
|
26
|
+
|
|
27
|
+
/** The tab kind this package owns; what `openTab` names and the guide entry opens. */
|
|
28
|
+
export const TW_RIGHTBAR_KIND = 'dsh-tiddlywiki'
|
|
29
|
+
/** This implementation's identity in the tab system: the key its body registers under. */
|
|
30
|
+
export const TW_RIGHTBAR_ID = 'dsh-tiddlywiki'
|
|
31
|
+
/** Activation-event name for THIS tab (distinct from the center panel's 'dsh-tiddlywiki'). */
|
|
32
|
+
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
|
+
|
|
49
|
+
/* ── structural faces over the rightbar contract (no @deepseek-ai imports) ── */
|
|
50
|
+
|
|
51
|
+
interface RightbarTabInfo {
|
|
52
|
+
sidebar: { expanded: boolean; fullscreen: boolean }
|
|
53
|
+
panel: { id: string }
|
|
54
|
+
tab: {
|
|
55
|
+
id: string
|
|
56
|
+
contentId: string
|
|
57
|
+
visible: boolean
|
|
58
|
+
navigation: { address: string; params?: unknown; revision: number }
|
|
59
|
+
signal: AbortSignal
|
|
60
|
+
actions: { close(): void; openTab?(kind: string, options?: unknown): void }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type UseRightbarTabInfo = () => RightbarTabInfo
|
|
65
|
+
|
|
66
|
+
export interface RightbarTabBodyProps {
|
|
67
|
+
useTabInfo: UseRightbarTabInfo
|
|
68
|
+
sessionId?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The slot-registry face this module needs (keyed seats register by `key`). */
|
|
72
|
+
export interface RightbarSlotsFace {
|
|
73
|
+
inject(name: string, callback: () => unknown): (() => void) | undefined
|
|
74
|
+
register(
|
|
75
|
+
opts: { name: string; key?: string; id?: string; order?: number; label?: string | (() => string) },
|
|
76
|
+
component: unknown,
|
|
77
|
+
): () => void
|
|
78
|
+
}
|
|
79
|
+
|
|
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
|
+
/**
|
|
349
|
+
* The tab body (stage 2): a thin React wrapper that mounts the TW iframe into
|
|
350
|
+
* a plain-DOM host. One mount per tab record; mutual exclusion rides the
|
|
351
|
+
* `dsh-panel-activate` protocol both ways.
|
|
352
|
+
*/
|
|
353
|
+
export function TwRightbarTabBody(props: RightbarTabBodyProps): React.ReactElement {
|
|
354
|
+
const { useTabInfo } = props
|
|
355
|
+
const info = useTabInfo()
|
|
356
|
+
const tab = info.tab
|
|
357
|
+
const hostRef = React.useRef<HTMLDivElement | null>(null)
|
|
358
|
+
const controllerRef = React.useRef<FrameController | null>(null)
|
|
359
|
+
|
|
360
|
+
React.useEffect(() => {
|
|
361
|
+
const host = hostRef.current
|
|
362
|
+
if (host === null) return
|
|
363
|
+
const controller = createFrameController(host, tab.signal)
|
|
364
|
+
controllerRef.current = controller
|
|
365
|
+
liveFrame = controller
|
|
366
|
+
return () => {
|
|
367
|
+
controller.dispose()
|
|
368
|
+
controllerRef.current = null
|
|
369
|
+
}
|
|
370
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one controller per tab record (id + signal are stable per occurrence).
|
|
371
|
+
}, [tab.id, tab.signal])
|
|
372
|
+
|
|
373
|
+
React.useEffect(() => {
|
|
374
|
+
controllerRef.current?.setVisible(tab.visible)
|
|
375
|
+
}, [tab.visible])
|
|
376
|
+
|
|
377
|
+
// When this tab becomes visible, close the center overlay / sibling panels.
|
|
378
|
+
// When any OTHER panel activates, close this tab (symmetry → one TW client).
|
|
379
|
+
const visible = tab.visible
|
|
380
|
+
const close = tab.actions.close
|
|
381
|
+
React.useEffect(() => {
|
|
382
|
+
if (!visible) return
|
|
383
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: RIGHTBAR_PANEL_NAME }))
|
|
384
|
+
const onActivate = (event: Event): void => {
|
|
385
|
+
const detail = (event as CustomEvent).detail
|
|
386
|
+
if (detail !== RIGHTBAR_PANEL_NAME) close()
|
|
387
|
+
}
|
|
388
|
+
document.addEventListener(ACTIVATE_EVENT, onActivate)
|
|
389
|
+
return () => document.removeEventListener(ACTIVATE_EVENT, onActivate)
|
|
390
|
+
}, [visible, close])
|
|
391
|
+
|
|
392
|
+
return React.createElement('div', { ref: hostRef, className: 'dsh-tw-rightbar-tab', 'data-dsh-tw-rightbar': '' })
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The tab definition (stage 1): a page type opened by kind, with a guide entry box. */
|
|
396
|
+
export function rightbarDefinition(): Record<string, unknown> {
|
|
397
|
+
return {
|
|
398
|
+
id: TW_RIGHTBAR_ID,
|
|
399
|
+
kind: TW_RIGHTBAR_KIND,
|
|
400
|
+
title: (): string => rightbarLabel,
|
|
401
|
+
guide: [
|
|
402
|
+
{
|
|
403
|
+
order: 10,
|
|
404
|
+
title: (): string => 'TiddlyWiki 知识库',
|
|
405
|
+
description: (): string => '在右侧边栏打开 TiddlyWiki 编辑器(与聊天并排)',
|
|
406
|
+
icon: GuideIcon,
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Mount the rightbar integration for the caller's lifetime: register the type
|
|
414
|
+
* and the body seat. Call only when `sidebarRightTabs` is actually available.
|
|
415
|
+
*/
|
|
416
|
+
export function mountRightbarTab(
|
|
417
|
+
tabs: { register(definition: unknown): () => void },
|
|
418
|
+
slots: RightbarSlotsFace,
|
|
419
|
+
): (() => void) | undefined {
|
|
420
|
+
const disposers: Array<() => void> = []
|
|
421
|
+
try {
|
|
422
|
+
disposers.push(tabs.register(rightbarDefinition()))
|
|
423
|
+
const removeBody = slots.inject('sidebar.right.pane.tab', () =>
|
|
424
|
+
slots.register({ name: 'sidebar.right.pane.tab', key: TW_RIGHTBAR_ID }, TwRightbarTabBody),
|
|
425
|
+
)
|
|
426
|
+
if (removeBody !== undefined) disposers.push(removeBody)
|
|
427
|
+
} catch (error) {
|
|
428
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
429
|
+
console.error('[dsh-tiddlywiki] rightbar tab mount failed:', error)
|
|
430
|
+
return undefined
|
|
431
|
+
}
|
|
432
|
+
// Warm the chip label from the live config (ui.tabLabel) so the first open
|
|
433
|
+
// already shows the right name; the frame refreshes it on every status call.
|
|
434
|
+
void fetchStatus().then((payload) => {
|
|
435
|
+
if (payload?.ui !== undefined) setLabel(payload.ui.tabLabel ?? rightbarLabel)
|
|
436
|
+
})
|
|
437
|
+
return () => {
|
|
438
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
439
|
+
}
|
|
440
|
+
}
|