dsh-tiddlywiki 0.16.20 → 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/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.20",
4
+ "version": "0.16.21",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -24,6 +24,7 @@ import { mountSessionSummaryView } from './session-summary.ts'
24
24
  import { disposeEditorPopup } from './editor-popup.ts'
25
25
  import { SettingsSection } from './settings-page.ts'
26
26
  import { registerToolViews, installWikiLinkInterceptor } from './tool-views.ts'
27
+ import { mountRightbarTab, type RightbarSlotsFace } from './rightbar-tab.ts'
27
28
 
28
29
  /** Client plugin name. */
29
30
  export const name = 'dsh-tiddlywiki/client'
@@ -40,9 +41,16 @@ interface ClientContextFace {
40
41
  component: unknown,
41
42
  ): () => void
42
43
  }
44
+ /** Optional service read without the inject requirement (cordis ctx.get). */
45
+ get?: (name: string) => unknown
43
46
  effect?(fn: () => unknown, label?: string): void
44
47
  }
45
48
 
49
+ /** Structural face over the rightbar tab registry (dsh-client-ui-sidebar-right). */
50
+ interface SidebarRightTabsFace {
51
+ register(definition: unknown): () => void
52
+ }
53
+
46
54
  /**
47
55
  * Client entry: installs styles and mounts the DOM seats + settings page.
48
56
  * @param ctx - the cordis client context.
@@ -91,6 +99,34 @@ export function apply(ctx: ClientContextFace): void {
91
99
  // DOM failures degrade the plugin, never the GUI.
92
100
  console.error('[dsh-tiddlywiki] mount failed:', error)
93
101
  }
102
+ try {
103
+ // 右侧边栏(DSH new rightbar)集成:可选挂载——仅当
104
+ // dsh-client-ui-sidebar-right 提供了 sidebarRightTabs 服务时才启用
105
+ // (老版本 DSH / 无右侧栏时静默跳过,插件其余功能不受影响)。注册 TW
106
+ // tab 类型 + guide 首页入口盒;由 ui.showRightbarTab 控制(默认开)。
107
+ let retryTimer: number | undefined
108
+ const tryMountRightbar = (attempt: number): void => {
109
+ const tabs = ctx.get?.('sidebarRightTabs') as SidebarRightTabsFace | undefined
110
+ if (tabs === undefined) {
111
+ // rightbar 插件可能在本次 apply 之后才就绪:有限重试几次即可。
112
+ if (attempt < 6 && !clientDisposed) {
113
+ retryTimer = window.setTimeout(() => tryMountRightbar(attempt + 1), 500 * (attempt + 1))
114
+ }
115
+ return
116
+ }
117
+ void fetchUiConfig().then((cfg) => {
118
+ if (clientDisposed) return
119
+ if (!cfg.showRightbarTab) return
120
+ const removeRightbar = mountRightbarTab(tabs, ctx.slots as unknown as RightbarSlotsFace)
121
+ if (removeRightbar !== undefined) disposers.push(removeRightbar)
122
+ })
123
+ }
124
+ tryMountRightbar(0)
125
+ disposers.push(() => { if (retryTimer !== undefined) window.clearTimeout(retryTimer) })
126
+ } catch (error) {
127
+ // 右侧栏集成失败只影响该功能本身,绝不让整个插件挂掉。
128
+ console.error('[dsh-tiddlywiki] rightbar mount failed:', error)
129
+ }
94
130
  try {
95
131
  // Reply-stream native tool cards: keyed `tool.call.toolview` slots for
96
132
  // every tiddlywiki_* tool (additive — our keys are unclaimed).
@@ -23,6 +23,7 @@
23
23
  import type { PanelState } from './state.ts'
24
24
  import { ENTRY_SELECTOR } from './sidebar-entry.ts'
25
25
  import { attachThemeSync, setThemeSyncConfig } from './theme-sync.ts'
26
+ import { openTiddlerInRightbar } from './rightbar-tab.ts'
26
27
 
27
28
  export const PANEL_RELOAD_EVENT = 'dsh-tw-panel-reload'
28
29
 
@@ -316,6 +317,9 @@ export function mountPanel(state: PanelState): () => void {
316
317
  const detail = (event as CustomEvent).detail as { title?: unknown } | undefined
317
318
  const title = typeof detail?.title === 'string' && detail.title.length > 0 ? detail.title : ''
318
319
  if (title.length === 0) return
320
+ // 右侧栏的 TW tab 可见时,链接直接在那里打开(与聊天并排);否则退回
321
+ // 中央面板。互斥由 rightbar-tab 的 dsh-panel-activate 协议保证。
322
+ if (openTiddlerInRightbar(title)) return
319
323
  pendingHash = `#${encodeURIComponent(title)}`
320
324
  state.openPanel()
321
325
  applyPendingHash()
@@ -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
+ }
@@ -254,6 +254,7 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
254
254
  textField('ui.darkPalette', '暗色时 TW palette(tiddler 标题)', typeof ui.darkPalette === 'string' && ui.darkPalette.trim().length > 0 ? ui.darkPalette.trim() : '$:/palettes/CupertinoDark')
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
+ checkField('ui.showRightbarTab', '在 DSH 右侧边栏提供 TiddlyWiki 入口/Tab(与聊天并排)', ui.showRightbarTab !== false)
257
258
  const sendToAgent = (ui.sendToAgent ?? {}) as Record<string, unknown>
258
259
  checkField('ui.sendToAgent.enabled', '启用「发送给 Agent」(TW 笔记 → DSH 会话注入)', sendToAgent.enabled !== false)
259
260
  textField('ui.sendToAgent.endpoint', 'TW 端请求基址(空=自动取当前 DSH origin)', typeof sendToAgent.endpoint === 'string' ? sendToAgent.endpoint : '')
@@ -69,6 +69,33 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
69
69
  .dsh-tw-panel-error button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
70
70
  .dsh-tw-panel-error code { font-size: 11px; opacity: .8; max-width: 80%; overflow-wrap: anywhere; }
71
71
 
72
+ /* ── right-sidebar tab (DSH new rightbar) ──────────────────────
73
+ The React body host fills the rightbar pane; the TW iframe lives inside
74
+ it and follows the DSH theme like the center-column panel. */
75
+ .dsh-tw-rightbar-tab {
76
+ position: relative; display: flex; flex-direction: column;
77
+ width: 100%; height: 100%; min-height: 0; box-sizing: border-box;
78
+ background: var(--dsw-alias-bg-layer-1, var(--dsw-bg, #fff));
79
+ }
80
+ .dsh-tw-rightbar-view { flex: 1; min-height: 0; display: flex; flex-direction: column; }
81
+ .dsh-tw-rightbar-frame-wrap { flex: 1; min-height: 0; display: flex; flex-direction: column; }
82
+ .dsh-tw-rightbar-frame {
83
+ flex: 1; min-height: 0; border: 0; width: 100%; display: block;
84
+ background: var(--dsw-alias-bg-layer-1, #fff);
85
+ }
86
+ .dsh-tw-rightbar-frame[hidden],
87
+ .dsh-tw-rightbar-error[hidden] { display: none !important; }
88
+ .dsh-tw-rightbar-error {
89
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
90
+ gap: 10px; color: var(--dsw-alias-label-secondary, #666); font-size: 13px; text-align: center; padding: 20px;
91
+ }
92
+ .dsh-tw-rightbar-error button {
93
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25)); background: transparent;
94
+ color: inherit; font: inherit; padding: 6px 14px; border-radius: 8px; cursor: pointer;
95
+ }
96
+ .dsh-tw-rightbar-error button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
97
+ .dsh-tw-rightbar-error code { font-size: 11px; opacity: .8; max-width: 80%; overflow-wrap: anywhere; }
98
+
72
99
  /* ── floating quick-note widget ────────────────────────────────
73
100
  Positioned ABOVE the shutdown launcher FAB (fixed right:24 bottom:24,
74
101
  z-index 900, 46px) and BELOW its confirm overlay (z-index 1000).
@@ -24,9 +24,11 @@ export interface UiConfig {
24
24
  tabLabel: string
25
25
  /** 是否在会话顶部显示「知识库」Tab(会话相关 wiki 汇总,默认 true)。 */
26
26
  showSessionTab: boolean
27
+ /** 是否在 DSH 右侧边栏提供 TiddlyWiki 入口/Tab(默认 true)。 */
28
+ showRightbarTab: boolean
27
29
  }
28
30
 
29
- const FALLBACK: UiConfig = { showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', tabLabel: '知识库', showSessionTab: true }
31
+ const FALLBACK: UiConfig = { showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', tabLabel: '知识库', showSessionTab: true, showRightbarTab: true }
30
32
 
31
33
  const CACHE_TTL_MS = 15_000
32
34
  let cache: { at: number; value: UiConfig } | undefined
@@ -41,7 +43,7 @@ export async function fetchUiConfig(opts: { force?: boolean } = {}): Promise<UiC
41
43
  try {
42
44
  const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
43
45
  if (!res.ok) return FALLBACK
44
- const p = (await res.json()) as { ui?: { showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; tabLabel?: string; showSessionTab?: boolean } }
46
+ const p = (await res.json()) as { ui?: { showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; tabLabel?: string; showSessionTab?: boolean; showRightbarTab?: boolean } }
45
47
  const value: UiConfig = {
46
48
  showQuickNoteDock: p.ui?.showQuickNoteDock !== false,
47
49
  quickNoteMode: p.ui?.quickNoteMode === 'card' ? 'card' : 'native',
@@ -52,6 +54,7 @@ export async function fetchUiConfig(opts: { force?: boolean } = {}): Promise<UiC
52
54
  ? p.ui.tabLabel.trim()
53
55
  : '知识库',
54
56
  showSessionTab: p.ui?.showSessionTab !== false,
57
+ showRightbarTab: p.ui?.showRightbarTab !== false,
55
58
  }
56
59
  cache = { at: Date.now(), value }
57
60
  return value
@@ -81,6 +81,13 @@ export interface PluginConfigShape {
81
81
  tabLabel?: string
82
82
  /** 是否在会话顶部显示「知识库」Tab(会话相关 wiki 汇总,默认 true)。 */
83
83
  showSessionTab?: boolean
84
+ /**
85
+ * 是否在 DSH 右侧边栏(new rightbar)提供 TiddlyWiki 入口/Tab(默认
86
+ * true):在 rightbar 首页(guide 页)注册「TiddlyWiki 知识库」入口盒,
87
+ * 点击即在右侧栏以 tab 形式打开完整 TW 编辑器(与聊天并排)。老版本
88
+ * DSH 无 rightbar 时该配置不生效(客户端自动跳过)。
89
+ */
90
+ showRightbarTab?: boolean
84
91
  }
85
92
  uiLanguage?: string
86
93
  [key: string]: unknown
@@ -151,6 +151,8 @@ export interface UiDefaultsPublic {
151
151
  tabLabel: string
152
152
  /** 是否在会话顶部显示「知识库」Tab(会话相关 wiki 汇总,默认 true)。 */
153
153
  showSessionTab: boolean
154
+ /** 是否在 DSH 右侧边栏提供 TiddlyWiki 入口/Tab(默认 true)。 */
155
+ showRightbarTab: boolean
154
156
  }
155
157
 
156
158
  export interface RouteDeps {
package/src/index.ts CHANGED
@@ -66,7 +66,7 @@ export interface TiddlywikiConfig {
66
66
  port?: number
67
67
  git?: { autoCommit?: boolean; debounceMs?: number; remote?: string; branch?: string }
68
68
  note?: { tag?: string }
69
- ui?: { showQuickNote?: boolean; showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; showPanelStatus?: boolean; showSyncButton?: boolean; followDshTheme?: boolean; darkPalette?: string; tabLabel?: string; showSessionTab?: boolean; sendToAgent?: { enabled?: boolean; endpoint?: string; token?: string }; allArticles?: { pageSize?: number } }
69
+ ui?: { showQuickNote?: boolean; showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; showPanelStatus?: boolean; showSyncButton?: boolean; followDshTheme?: boolean; darkPalette?: string; tabLabel?: string; showSessionTab?: boolean; showRightbarTab?: boolean; sendToAgent?: { enabled?: boolean; endpoint?: string; token?: string }; allArticles?: { pageSize?: number } }
70
70
  /** 启动时自动启用的 TW 语言代码(如 "zh-Hans"),也受配置 tiddler 覆盖。 */
71
71
  uiLanguage?: string
72
72
  auth?: { username?: string; password?: string }
@@ -89,7 +89,7 @@ interface ResolvedConfig {
89
89
  port: number
90
90
  git: { autoCommit: boolean; debounceMs: number; remote: string; branch: string }
91
91
  note: { tag: string }
92
- ui: { showQuickNote: boolean; showQuickNoteDock: boolean; quickNoteMode: 'native' | 'card'; sidebarLabel: string; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; tabLabel: string; showSessionTab: boolean; sendToAgent: { enabled: boolean; endpoint?: string; token?: string }; allArticles: { pageSize: number } }
92
+ ui: { showQuickNote: boolean; showQuickNoteDock: boolean; quickNoteMode: 'native' | 'card'; sidebarLabel: string; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; tabLabel: string; showSessionTab: boolean; showRightbarTab: boolean; sendToAgent: { enabled: boolean; endpoint?: string; token?: string }; allArticles: { pageSize: number } }
93
93
  uiLanguage: string
94
94
  auth: { username?: string; password?: string }
95
95
  }
@@ -100,7 +100,7 @@ const DEFAULTS: ResolvedConfig = {
100
100
  port: 0,
101
101
  git: { autoCommit: true, debounceMs: 60_000, remote: '', branch: 'main' },
102
102
  note: { tag: 'inbox' },
103
- ui: { showQuickNote: true, showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', showPanelStatus: true, showSyncButton: true, followDshTheme: true, darkPalette: DARK_PALETTE_DEFAULT, tabLabel: '知识库', showSessionTab: true, sendToAgent: { enabled: true }, allArticles: { pageSize: 10 } },
103
+ ui: { showQuickNote: true, showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', showPanelStatus: true, showSyncButton: true, followDshTheme: true, darkPalette: DARK_PALETTE_DEFAULT, tabLabel: '知识库', showSessionTab: true, showRightbarTab: true, sendToAgent: { enabled: true }, allArticles: { pageSize: 10 } },
104
104
  uiLanguage: '',
105
105
  auth: { username: '', password: '' },
106
106
  }
@@ -259,7 +259,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
259
259
  const tag = eff().note?.tag
260
260
  return typeof tag === 'string' && tag.trim().length > 0 ? tag : config.note.tag
261
261
  }
262
- const effectiveUi = (): { showQuickNote: boolean; showQuickNoteDock: boolean; quickNoteMode: 'native' | 'card'; sidebarLabel: string; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; tabLabel: string; showSessionTab: boolean } => {
262
+ const effectiveUi = (): { showQuickNote: boolean; showQuickNoteDock: boolean; quickNoteMode: 'native' | 'card'; sidebarLabel: string; showPanelStatus: boolean; showSyncButton: boolean; followDshTheme: boolean; darkPalette: string; tabLabel: string; showSessionTab: boolean; showRightbarTab: boolean } => {
263
263
  const ui = eff().ui ?? {}
264
264
  const palette = typeof ui.darkPalette === 'string' && ui.darkPalette.trim().length > 0 ? ui.darkPalette.trim() : DARK_PALETTE_DEFAULT
265
265
  const label = typeof ui.sidebarLabel === 'string' && ui.sidebarLabel.trim().length > 0 ? ui.sidebarLabel.trim() : config.ui.sidebarLabel
@@ -276,6 +276,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
276
276
  darkPalette: palette,
277
277
  tabLabel,
278
278
  showSessionTab: ui.showSessionTab !== false,
279
+ showRightbarTab: ui.showRightbarTab !== false,
279
280
  }
280
281
  }
281
282