dsh-tiddlywiki 0.16.13 → 0.16.14
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 +2 -0
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +32 -26
- package/lib/client.js +32 -26
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/endpoints.ts +10 -10
- package/src/client/markdown-editor.ts +128 -128
- package/src/client/quick-note-dock.ts +133 -133
- package/src/client/session-summary.ts +223 -31
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +6 -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 +44 -44
- package/src/host/http.ts +57 -57
- package/src/host/seed-render.ts +66 -66
- package/src/host/seed-send-to-agent.ts +63 -63
- package/src/host/seeds.ts +299 -299
- package/src/host/session-summary.ts +422 -422
- package/src/host/tw-api.ts +231 -231
- package/src/host/wiki.ts +299 -299
- package/src/sdk.ts +204 -204
|
@@ -5,14 +5,27 @@
|
|
|
5
5
|
* 判定来源是 Host 路由 POST /dsh-tiddlywiki/session/summary:后端用 sessionQuery 读
|
|
6
6
|
* 本会话(含后代 subagent)的事件日志,按「产生/读取/检索」收集 tiddlywiki_* 笔记,
|
|
7
7
|
* 组装 TW wikitext 写入 volatile 的 `$:/temp/dsh/session-summary/<会话ID>`(不落盘、
|
|
8
|
-
* 不进 git
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* 不进 git),见 src/host/session-summary.ts。
|
|
9
|
+
*
|
|
10
|
+
* 渲染(v0.16.14 关键机制——为什么不能直接 `#<标题>` 直达):浏览器端 TW 的同步
|
|
11
|
+
* 机制天生看不到 `$:/temp`——服务端 recipe 列表默认 `[all[tiddlers]!is[system]]`
|
|
12
|
+
* (get-tiddlers-json.js),tiddlyweb adaptor 的请求过滤器又显式
|
|
13
|
+
* `-[prefix[$:/temp/]]`(tiddlywebadaptor.js getSkinnyTiddlers),而 lazyLoad 只补
|
|
14
|
+
* 「已知 skinny」不拉「完全缺失」——所以 iframe 里的 TW 永远拿不到 volatile 汇总
|
|
15
|
+
* 条目,`#<标题>` 直达只会渲染「佚失条目」(v0.16.11–13 的根因)。因此由本组件在
|
|
16
|
+
* iframe 就绪后【注入】:同源 `/get` 读回汇总字段 →
|
|
17
|
+
* `iframe.contentWindow.$tw.wiki.addTiddler(...)` 写进 iframe 的 TW store → 设置
|
|
18
|
+
* `contentWindow.location.hash` 走 TW 原生 hash 导航(与中央面板 openTiddler 同一
|
|
19
|
+
* 机制,落地前重试绕开 boot 时序竞争)。汇总条目始终只存在于服务端内存与 iframe
|
|
20
|
+
* 内存——不落盘、不进 git、重启即消失(§四 设计不变)。
|
|
21
|
+
*
|
|
22
|
+
* 自愈:30s 探测——① 服务端丢失(TW 重启清空 `$:/temp`)→ 重新生成(连续
|
|
23
|
+
* MAX_MISSES 次仍缺失交还手动「🔄 刷新」);② 服务端仍在而 iframe store 丢失
|
|
24
|
+
* (iframe 自身重载/重连)→ 重新注入并导航,不打扰用户在 iframe 内的浏览。
|
|
11
25
|
*
|
|
12
26
|
* 槽位注册(探查定稿):`conversation.view` kind=list scope=session replaceRisk=none,
|
|
13
27
|
* 官方留给第三方扩展;用新 id = 在「对话 | 轨迹」旁新增 tab(order 20),label 用
|
|
14
|
-
* thunk 跟随 `ui.tabLabel`
|
|
15
|
-
* 手动「刷新」。
|
|
28
|
+
* thunk 跟随 `ui.tabLabel` 配置(默认「知识库」)。
|
|
16
29
|
*
|
|
17
30
|
* @module dsh-tiddlywiki/client/session-summary
|
|
18
31
|
*/
|
|
@@ -25,6 +38,16 @@ const SUMMARY_ENDPOINT = '/dsh-tiddlywiki/session/summary'
|
|
|
25
38
|
/** Same-origin TW proxy base(与 host TW_PROXY_PATH 一致,client 不能 import 它)。 */
|
|
26
39
|
const TW_PROXY_BASE = '/dsh-tiddlywiki/tw/'
|
|
27
40
|
const SESSION_SUMMARY_LABEL_DEFAULT = '知识库'
|
|
41
|
+
/** iframe 内 TW 就绪探测节奏与上限(250ms × 80 ≈ 20s)。 */
|
|
42
|
+
const TW_READY_POLL_MS = 250
|
|
43
|
+
const TW_READY_TIMEOUT_MS = 20_000
|
|
44
|
+
/** hash 导航落地重试节奏与上限(500ms × 20 ≈ 10s)。 */
|
|
45
|
+
const NAV_RETRY_MS = 500
|
|
46
|
+
const NAV_RETRY_MAX = 20
|
|
47
|
+
/** 自愈探测周期。 */
|
|
48
|
+
const SELF_HEAL_MS = 30_000
|
|
49
|
+
/** 连续多少次「生成后服务端仍缺失」后停止自动重试,交还手动「🔄 刷新」。 */
|
|
50
|
+
const MAX_MISSES = 3
|
|
28
51
|
|
|
29
52
|
/** Tab 显示名缓存:客户端从 /status 读到 `ui.tabLabel` 后更新;默认「知识库」。 */
|
|
30
53
|
let tabLabel = SESSION_SUMMARY_LABEL_DEFAULT
|
|
@@ -45,16 +68,57 @@ interface SessionSummaryViewProps {
|
|
|
45
68
|
completeViewRequest?: () => void
|
|
46
69
|
}
|
|
47
70
|
|
|
48
|
-
/**
|
|
71
|
+
/** `/get` 读回的汇总字段子集(够渲染即可)。 */
|
|
72
|
+
interface SummaryFields {
|
|
73
|
+
title: string
|
|
74
|
+
text?: string
|
|
75
|
+
type?: string
|
|
76
|
+
tags?: string[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* iframe 内 TW 运行时的极小结构面——只声明注入/探测用到的成员(theme-sync 已有
|
|
81
|
+
* 访问 `iframe.contentWindow.$tw` 的先例,同一访问模式)。
|
|
82
|
+
*/
|
|
83
|
+
interface TwRuntime {
|
|
84
|
+
Tiddler: new (fields: Record<string, unknown>) => unknown
|
|
85
|
+
wiki: {
|
|
86
|
+
addTiddler(tiddler: unknown): void
|
|
87
|
+
tiddlerExists(title: string): boolean
|
|
88
|
+
getTiddlerList(title: string): string[]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 读取 iframe 内的 TW 运行时;未就绪(未启动/缺成员)返回 undefined。 */
|
|
93
|
+
function readTwRuntime(el: HTMLIFrameElement | null | undefined): TwRuntime | undefined {
|
|
94
|
+
const w = el?.contentWindow as (Window & { $tw?: unknown }) | null | undefined
|
|
95
|
+
const tw = w?.$tw as TwRuntime | undefined
|
|
96
|
+
if (tw === undefined || tw.wiki === undefined) return undefined
|
|
97
|
+
if (typeof tw.Tiddler !== 'function') return undefined
|
|
98
|
+
if (typeof tw.wiki.addTiddler !== 'function' || typeof tw.wiki.tiddlerExists !== 'function' || typeof tw.wiki.getTiddlerList !== 'function') {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
return tw
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sleep(ms: number): Promise<void> {
|
|
105
|
+
return new Promise((resolve) => window.setTimeout(resolve, ms))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** View 组件:生成汇总 → iframe 载入 TW → 注入 volatile 条目 → 原生 hash 导航。 */
|
|
49
109
|
function SessionSummaryView(props: SessionSummaryViewProps): React.ReactElement {
|
|
50
110
|
const { sessionId, viewRequest, completeViewRequest } = props
|
|
51
111
|
const [phase, setPhase] = React.useState<'loading' | 'ready' | 'error'>('loading')
|
|
52
|
-
const [
|
|
112
|
+
const [frameSrc, setFrameSrc] = React.useState<string | null>(null)
|
|
53
113
|
const [summaryTitle, setSummaryTitle] = React.useState<string | null>(null)
|
|
114
|
+
/** true = 汇总已在 iframe 内落地显示;false = 盖一层加载浮层(遮住 TW 默认页)。 */
|
|
115
|
+
const [navigated, setNavigated] = React.useState(false)
|
|
54
116
|
const [error, setError] = React.useState('')
|
|
55
117
|
const [frameNonce, setFrameNonce] = React.useState(0)
|
|
56
118
|
const genRef = React.useRef(0)
|
|
119
|
+
const missesRef = React.useRef(0)
|
|
57
120
|
const themeDisposeRef = React.useRef<(() => void) | undefined>(undefined)
|
|
121
|
+
const frameElRef = React.useRef<HTMLIFrameElement | null>(null)
|
|
58
122
|
|
|
59
123
|
const generate = React.useCallback(async (): Promise<void> => {
|
|
60
124
|
const gen = ++genRef.current
|
|
@@ -80,11 +144,18 @@ function SessionSummaryView(props: SessionSummaryViewProps): React.ReactElement
|
|
|
80
144
|
}
|
|
81
145
|
if (typeof data.title === 'string' && data.title.length > 0) {
|
|
82
146
|
setSummaryTitle(data.title)
|
|
83
|
-
|
|
84
|
-
//
|
|
147
|
+
// 注意:不带 #hash 挂载——volatile 条目不在浏览器 store,boot 期 hash 直达
|
|
148
|
+
// 只会渲染「佚失条目」;就绪后由 injectAndNavigate 注入再原生导航。
|
|
149
|
+
setFrameSrc(new URL(TW_PROXY_BASE, window.location.origin).href)
|
|
150
|
+
// 新 nonce 强制 iframe 重挂载 → 重新注入最新汇总(含刷新场景)。
|
|
85
151
|
setFrameNonce((n) => n + 1)
|
|
152
|
+
setNavigated(false)
|
|
153
|
+
setPhase('ready')
|
|
154
|
+
} else {
|
|
155
|
+
// ok 但缺 title:防御性报错,避免空 iframe + 永久加载浮层。
|
|
156
|
+
setPhase('error')
|
|
157
|
+
setError('响应缺少汇总标题')
|
|
86
158
|
}
|
|
87
|
-
setPhase('ready')
|
|
88
159
|
} catch (err) {
|
|
89
160
|
if (gen !== genRef.current) return
|
|
90
161
|
setPhase('error')
|
|
@@ -97,30 +168,135 @@ function SessionSummaryView(props: SessionSummaryViewProps): React.ReactElement
|
|
|
97
168
|
void generate()
|
|
98
169
|
}, [generate])
|
|
99
170
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
171
|
+
/** 服务端读回汇总字段(volatile 条目对 host 的 /get 永远可见)。 */
|
|
172
|
+
const fetchSummaryFields = React.useCallback(async (title: string): Promise<SummaryFields | 'missing' | null> => {
|
|
173
|
+
try {
|
|
174
|
+
const res = await fetch(`${GET_ENDPOINT}?title=${encodeURIComponent(title)}`, { signal: AbortSignal.timeout(8_000) })
|
|
175
|
+
if (res.status === 404) return 'missing'
|
|
176
|
+
const data = (await res.json().catch(() => null)) as { ok?: boolean; title?: string; text?: string; type?: string; tags?: string[] } | null
|
|
177
|
+
if (data?.ok !== true || typeof data.title !== 'string') return null
|
|
178
|
+
return { title: data.title, text: data.text, type: data.type, tags: data.tags }
|
|
179
|
+
} catch {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
}, [])
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 注入 + 原生导航:读回汇总字段 → 等 iframe 内 TW 启动 → addTiddler 进 iframe 的
|
|
186
|
+
* store → 设 contentWindow.location.hash 走 TW 原生导航,落地重试直到条目进入
|
|
187
|
+
* `$:/StoryList`(绕开「hash 设置早于 story 监听器注册」的 boot 时序竞争)。
|
|
188
|
+
*/
|
|
189
|
+
const injectAndNavigate = React.useCallback(async (): Promise<'ok' | 'missing' | 'timeout'> => {
|
|
190
|
+
if (summaryTitle === null) return 'missing'
|
|
191
|
+
const fields = await fetchSummaryFields(summaryTitle)
|
|
192
|
+
if (fields === 'missing') return 'missing'
|
|
193
|
+
if (fields === null) return 'timeout'
|
|
194
|
+
// 等 TW 启动(iframe 重挂载后 boot 需要一点时间)。
|
|
195
|
+
let tw: TwRuntime | undefined
|
|
196
|
+
const deadline = Date.now() + TW_READY_TIMEOUT_MS
|
|
197
|
+
while (Date.now() < deadline) {
|
|
198
|
+
tw = readTwRuntime(frameElRef.current)
|
|
199
|
+
if (tw !== undefined) break
|
|
200
|
+
await sleep(TW_READY_POLL_MS)
|
|
201
|
+
}
|
|
202
|
+
if (tw === undefined) {
|
|
203
|
+
setNavigated(true) // TW 一直没就绪:揭开 iframe 看真实状态(代理错误页等)
|
|
204
|
+
return 'timeout'
|
|
205
|
+
}
|
|
206
|
+
// 注入(幂等:同标题覆盖;SyncFilter 排除 $:/temp → iframe 不会把它存回服务端)。
|
|
207
|
+
try {
|
|
208
|
+
tw.wiki.addTiddler(new tw.Tiddler({
|
|
209
|
+
title: fields.title,
|
|
210
|
+
text: fields.text ?? '',
|
|
211
|
+
type: fields.type ?? 'text/vnd.tiddlywiki',
|
|
212
|
+
tags: fields.tags ?? [],
|
|
213
|
+
}))
|
|
214
|
+
} catch {
|
|
215
|
+
/* 注入失败也继续尝试导航(store 里可能有旧版本可显示) */
|
|
216
|
+
}
|
|
217
|
+
// hash 导航 + 落地重试。
|
|
218
|
+
const hash = '#' + encodeURIComponent(summaryTitle)
|
|
219
|
+
for (let i = 0; i < NAV_RETRY_MAX; i++) {
|
|
220
|
+
let landed = false
|
|
221
|
+
try {
|
|
222
|
+
landed = tw.wiki.getTiddlerList('$:/StoryList').indexOf(summaryTitle) !== -1
|
|
223
|
+
} catch {
|
|
224
|
+
landed = false
|
|
225
|
+
}
|
|
226
|
+
if (landed) {
|
|
227
|
+
setNavigated(true)
|
|
228
|
+
return 'ok'
|
|
229
|
+
}
|
|
230
|
+
const w = frameElRef.current?.contentWindow
|
|
231
|
+
if (w !== null && w !== undefined) {
|
|
232
|
+
try {
|
|
233
|
+
const cur = w.location.hash
|
|
234
|
+
// TW 的 permaview 会把地址栏改写成 `#<标题>:<story>`;只要当前 hash 不是
|
|
235
|
+
// 以我们的标题开头就重设,避免和地址栏更新互相打架。
|
|
236
|
+
if (cur !== hash && !cur.startsWith(hash + ':')) w.location.hash = hash
|
|
237
|
+
} catch {
|
|
238
|
+
/* 跨文档瞬间设置失败 → 下轮重试 */
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
await sleep(NAV_RETRY_MS)
|
|
242
|
+
}
|
|
243
|
+
setNavigated(true)
|
|
244
|
+
return 'timeout'
|
|
245
|
+
}, [summaryTitle, fetchSummaryFields])
|
|
246
|
+
|
|
247
|
+
// iframe 挂载后注入并导航;每次重挂载(生成/刷新)都重跑。服务端条目丢失
|
|
248
|
+
// (TW 重启)→ 计入 misses 自动重新生成,连续 MAX_MISSES 次仍缺失交还手动刷新。
|
|
249
|
+
React.useEffect(() => {
|
|
250
|
+
if (phase !== 'ready' || summaryTitle === null) return
|
|
251
|
+
let disposed = false
|
|
252
|
+
void (async () => {
|
|
253
|
+
const outcome = await injectAndNavigate()
|
|
254
|
+
if (disposed) return
|
|
255
|
+
if (outcome === 'missing') {
|
|
256
|
+
missesRef.current++
|
|
257
|
+
if (missesRef.current < MAX_MISSES) void generate()
|
|
258
|
+
}
|
|
259
|
+
})()
|
|
260
|
+
return () => {
|
|
261
|
+
disposed = true
|
|
262
|
+
}
|
|
263
|
+
}, [phase, summaryTitle, frameNonce, injectAndNavigate, generate])
|
|
264
|
+
|
|
265
|
+
// 自愈探测(30s):① 服务端丢失(TW 重启清空 volatile 条目)→ 重新生成;
|
|
266
|
+
// ② 服务端仍在而 iframe store 丢失(iframe 自身重载/重连)→ 重新注入并导航。
|
|
103
267
|
React.useEffect(() => {
|
|
104
268
|
if (phase !== 'ready' || summaryTitle === null) return
|
|
105
|
-
let misses = 0
|
|
106
269
|
const timer = window.setInterval(() => {
|
|
107
270
|
void (async () => {
|
|
271
|
+
let serverHas = false
|
|
108
272
|
try {
|
|
109
273
|
const res = await fetch(`${GET_ENDPOINT}?title=${encodeURIComponent(summaryTitle)}`, { signal: AbortSignal.timeout(8_000) })
|
|
110
|
-
|
|
111
|
-
if (res.status === 404 || data?.notFound === true) {
|
|
112
|
-
misses++
|
|
113
|
-
if (misses < 3) void generate()
|
|
114
|
-
} else {
|
|
115
|
-
misses = 0
|
|
116
|
-
}
|
|
274
|
+
serverHas = res.status !== 404
|
|
117
275
|
} catch {
|
|
118
|
-
|
|
276
|
+
return // 探测失败保持现状,下个周期再试
|
|
277
|
+
}
|
|
278
|
+
if (!serverHas) {
|
|
279
|
+
missesRef.current++
|
|
280
|
+
if (missesRef.current < MAX_MISSES) void generate()
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
missesRef.current = 0
|
|
284
|
+
const tw = readTwRuntime(frameElRef.current)
|
|
285
|
+
if (tw === undefined) return
|
|
286
|
+
let present = true
|
|
287
|
+
try {
|
|
288
|
+
present = tw.wiki.tiddlerExists(summaryTitle)
|
|
289
|
+
} catch {
|
|
290
|
+
return // store 暂不可读,保守不动
|
|
291
|
+
}
|
|
292
|
+
if (!present) {
|
|
293
|
+
setNavigated(false)
|
|
294
|
+
void injectAndNavigate()
|
|
119
295
|
}
|
|
120
296
|
})()
|
|
121
|
-
},
|
|
297
|
+
}, SELF_HEAL_MS)
|
|
122
298
|
return () => window.clearInterval(timer)
|
|
123
|
-
}, [phase, summaryTitle, generate])
|
|
299
|
+
}, [phase, summaryTitle, generate, injectAndNavigate])
|
|
124
300
|
|
|
125
301
|
// 一次性 focus 请求直接确认(本视图无可聚焦子目标,避免 shell 挂起)。
|
|
126
302
|
React.useEffect(() => {
|
|
@@ -130,10 +306,12 @@ function SessionSummaryView(props: SessionSummaryViewProps): React.ReactElement
|
|
|
130
306
|
// iframe 主题同步(同源代理,attachThemeSync 自处理 load/reapply)。
|
|
131
307
|
const frameCallback = React.useCallback((el: HTMLIFrameElement | null) => {
|
|
132
308
|
if (el === null) {
|
|
309
|
+
frameElRef.current = null
|
|
133
310
|
themeDisposeRef.current?.()
|
|
134
311
|
themeDisposeRef.current = undefined
|
|
135
312
|
return
|
|
136
313
|
}
|
|
314
|
+
frameElRef.current = el
|
|
137
315
|
themeDisposeRef.current?.()
|
|
138
316
|
themeDisposeRef.current = attachThemeSync(el)
|
|
139
317
|
}, [])
|
|
@@ -167,13 +345,27 @@ function SessionSummaryView(props: SessionSummaryViewProps): React.ReactElement
|
|
|
167
345
|
React.createElement('button', { type: 'button', className: 'dsh-tw-summary-btn', onClick: refresh }, '重试'),
|
|
168
346
|
)
|
|
169
347
|
} else {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
348
|
+
// iframe 一直在挂载状态启动 TW;注入落地前盖一层加载浮层(遮住 TW 默认页,
|
|
349
|
+
// 避免闪现主页/佚失条目)。TW 就绪超时时揭开浮层让用户看到 iframe 真实状态。
|
|
350
|
+
content = React.createElement(
|
|
351
|
+
'div',
|
|
352
|
+
{ className: 'dsh-tw-summary-body' },
|
|
353
|
+
React.createElement('iframe', {
|
|
354
|
+
key: frameNonce,
|
|
355
|
+
ref: frameCallback,
|
|
356
|
+
className: 'dsh-tw-summary-frame',
|
|
357
|
+
src: frameSrc ?? undefined,
|
|
358
|
+
title: tabLabel,
|
|
359
|
+
}),
|
|
360
|
+
navigated
|
|
361
|
+
? null
|
|
362
|
+
: React.createElement(
|
|
363
|
+
'div',
|
|
364
|
+
{ className: 'dsh-tw-summary-state dsh-tw-summary-state-over' },
|
|
365
|
+
React.createElement('div', { className: 'dsh-tw-summary-state-spin' }, '⏳'),
|
|
366
|
+
React.createElement('div', null, '正在载入会话 wiki 汇总…'),
|
|
367
|
+
),
|
|
368
|
+
)
|
|
177
369
|
}
|
|
178
370
|
|
|
179
371
|
return React.createElement(
|
package/src/client/state.ts
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tiny pub/sub panel state shared by the sidebar entry and the center panel.
|
|
3
|
-
*
|
|
4
|
-
* @module dsh-tiddlywiki/client/state
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export type PanelListener = (open: boolean) => void
|
|
8
|
-
|
|
9
|
-
export class PanelState {
|
|
10
|
-
private open = false
|
|
11
|
-
private readonly listeners = new Set<PanelListener>()
|
|
12
|
-
|
|
13
|
-
isOpen(): boolean {
|
|
14
|
-
return this.open
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
toggle(): void {
|
|
18
|
-
this.set(!this.open)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
openPanel(): void {
|
|
22
|
-
this.set(true)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
closePanel(): void {
|
|
26
|
-
this.set(false)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
set(value: boolean): void {
|
|
30
|
-
if (this.open === value) return
|
|
31
|
-
this.open = value
|
|
32
|
-
for (const listener of [...this.listeners]) listener(value)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
subscribe(listener: PanelListener): () => void {
|
|
36
|
-
this.listeners.add(listener)
|
|
37
|
-
return () => { this.listeners.delete(listener) }
|
|
38
|
-
}
|
|
39
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tiny pub/sub panel state shared by the sidebar entry and the center panel.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-tiddlywiki/client/state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type PanelListener = (open: boolean) => void
|
|
8
|
+
|
|
9
|
+
export class PanelState {
|
|
10
|
+
private open = false
|
|
11
|
+
private readonly listeners = new Set<PanelListener>()
|
|
12
|
+
|
|
13
|
+
isOpen(): boolean {
|
|
14
|
+
return this.open
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toggle(): void {
|
|
18
|
+
this.set(!this.open)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
openPanel(): void {
|
|
22
|
+
this.set(true)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
closePanel(): void {
|
|
26
|
+
this.set(false)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(value: boolean): void {
|
|
30
|
+
if (this.open === value) return
|
|
31
|
+
this.open = value
|
|
32
|
+
for (const listener of [...this.listeners]) listener(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
subscribe(listener: PanelListener): () => void {
|
|
36
|
+
this.listeners.add(listener)
|
|
37
|
+
return () => { this.listeners.delete(listener) }
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/client/styles.ts
CHANGED
|
@@ -593,6 +593,8 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
593
593
|
}
|
|
594
594
|
.dsh-tw-summary-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
595
595
|
.dsh-tw-summary-btn:disabled { opacity: .55; cursor: default; }
|
|
596
|
+
/* body 包一层 relative:注入落地前用绝对定位浮层遮住 TW 默认页(避免闪现主页)。 */
|
|
597
|
+
.dsh-tw-summary-body { position: relative; flex: 1 1 0; min-height: 0; display: flex; }
|
|
596
598
|
.dsh-tw-summary-frame {
|
|
597
599
|
flex: 1 1 0; min-height: 0; width: 100%; border: 0; display: block;
|
|
598
600
|
background: var(--dsw-alias-bg-layer-1, #fff);
|
|
@@ -602,6 +604,10 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
602
604
|
align-items: center; justify-content: center; gap: 10px; padding: 24px;
|
|
603
605
|
text-align: center; color: var(--dsw-alias-label-secondary, #666); font-size: 13px;
|
|
604
606
|
}
|
|
607
|
+
.dsh-tw-summary-state-over {
|
|
608
|
+
position: absolute; inset: 0; z-index: 1;
|
|
609
|
+
background: var(--dsw-alias-bg-layer-1, #fff);
|
|
610
|
+
}
|
|
605
611
|
.dsh-tw-summary-state-spin { font-size: 22px; }
|
|
606
612
|
.dsh-tw-summary-state-title { font-weight: 600; color: var(--dsw-alias-label-primary, #222); }
|
|
607
613
|
.dsh-tw-summary-state-detail { max-width: 420px; word-break: break-all; font-size: 12px; }
|