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.
@@ -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, 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).
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 { STATUS_ENDPOINT } from './endpoints.ts'
25
- import { attachThemeSync, setThemeSyncConfig } from './theme-sync.ts'
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<FrameController | null>(null)
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 = createFrameController(host, tab.signal)
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 => rightbarLabel,
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: GuideIcon,
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
- void fetchStatus().then((payload) => {
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 : '')