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/README.md +4 -0
- package/lib/client.bundle.js +53 -26
- package/lib/client.js +53 -26
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +36 -0
- package/src/client/panel.ts +4 -0
- package/src/client/rightbar-tab.ts +440 -0
- package/src/client/settings-page.ts +1 -0
- package/src/client/styles.ts +27 -0
- package/src/client/ui-config.ts +5 -2
- package/src/host/config.ts +7 -0
- package/src/host/routes.ts +2 -0
- package/src/index.ts +5 -4
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.21",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
package/src/client/index.ts
CHANGED
|
@@ -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).
|
package/src/client/panel.ts
CHANGED
|
@@ -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 : '')
|
package/src/client/styles.ts
CHANGED
|
@@ -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).
|
package/src/client/ui-config.ts
CHANGED
|
@@ -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
|
package/src/host/config.ts
CHANGED
|
@@ -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
|
package/src/host/routes.ts
CHANGED
|
@@ -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
|
|