dsh-tiddlywiki 0.28.13 → 0.28.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/docs/CHANGELOG.md +2 -0
- package/docs/features.md +1 -1
- package/docs/usage.md +1 -0
- package/lib/client.js +25 -15
- package/package.json +1 -1
- package/src/client/panel.ts +1 -0
- package/src/client/sidebar-entry.ts +19 -5
- package/src/client/styles.ts +10 -0
- package/src/client/tw-frame.ts +109 -7
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tiddlywiki",
|
|
3
3
|
"description": "把 TiddlyWiki 5 变成你和 Agent 共用的本地文档中心(纯文本 + git):你在 DSH 界面里阅读、写作、记日记,Agent 用 15 个 tiddlywiki_* 工具读写同一批笔记;每条笔记都是你能读、能改、能审计、能随时带走的文件——不是自动记忆插件,不做后台抓取。可选一键发布到微信公众号草稿箱。 Turn TiddlyWiki 5 into a document center you and your agent share, in plain text under git — a knowledge base you can read, edit, audit and take with you, not an auto-memory plugin. Mounts via the official dsh plugin system; no DSH source changes.",
|
|
4
|
-
"version": "0.28.
|
|
4
|
+
"version": "0.28.14",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
package/src/client/panel.ts
CHANGED
|
@@ -247,16 +247,30 @@ export function mountSidebarEntry(state: PanelState): () => void {
|
|
|
247
247
|
// 图标每次同步(v0.28.4):在设置页改完图标,10s 内的轮询就会把它换过来。
|
|
248
248
|
applyWikiIcon(row.iconEl, w.icon)
|
|
249
249
|
row.entry.setAttribute('aria-label', `TiddlyWiki 知识库:${w.label}`)
|
|
250
|
-
// 当前焦点库高亮(与点击判定同源:`shownWiki()`,不会出现"高亮的行点一下反而关掉面板")
|
|
251
|
-
const focused = shownWiki() === w.id
|
|
252
|
-
if (focused) row.entry.dataset.focus = 'true'
|
|
253
|
-
else delete row.entry.dataset.focus
|
|
254
250
|
}
|
|
251
|
+
// 当前焦点库高亮:与点击判定同源(`shownWiki()`),不会出现"高亮的行点一下反而关掉面板"
|
|
252
|
+
paintFocus()
|
|
255
253
|
// 让放置逻辑把新行插进去(复用同一个 root)
|
|
256
254
|
if (root !== undefined) { placed = false; tryPlace() }
|
|
257
255
|
}
|
|
258
256
|
void applyRoster()
|
|
259
|
-
|
|
257
|
+
/**
|
|
258
|
+
* 焦点变化只需要重画**高亮**(v0.28.14)。
|
|
259
|
+
*
|
|
260
|
+
* 旧实现是 `subscribeFocusWiki(() => { void applyRoster() })`:每切一次库都**再打一次
|
|
261
|
+
* `/status`**(host 处理一次要跑最多 5 个 git 进程、实测 300–400ms)并把所有行重建一遍、
|
|
262
|
+
* 重新走一次放置逻辑。可是名册并没有变 —— 变的只是"哪一行亮着"。而那次往返正是
|
|
263
|
+
* 「点下去要等一下界面才动」的一部分(见 tw-frame.ts 的 switchFrameNow)。
|
|
264
|
+
*/
|
|
265
|
+
const paintFocus = (): void => {
|
|
266
|
+
const focus = shownWiki()
|
|
267
|
+
for (const [key, row] of rows) {
|
|
268
|
+
if (key === '') continue
|
|
269
|
+
if (key === focus) row.entry.dataset.focus = 'true'
|
|
270
|
+
else delete row.entry.dataset.focus
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const unsubscribeFocus = subscribeFocusWiki(paintFocus)
|
|
260
274
|
const rosterTimer = window.setInterval(() => { void applyRoster() }, 10_000)
|
|
261
275
|
let root: HTMLElement | undefined
|
|
262
276
|
let placed = false
|
package/src/client/styles.ts
CHANGED
|
@@ -88,6 +88,16 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
88
88
|
.dsh-tw-panel-error button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
89
89
|
.dsh-tw-panel-error code { font-size: 11px; opacity: .8; max-width: 80%; overflow-wrap: anywhere; }
|
|
90
90
|
|
|
91
|
+
/* 换库提示条(v0.28.14):换一份 wiki 文档要重新下载 10–30MB 且 TW 回 no-store(缓存不了),
|
|
92
|
+
那 1–2 秒里界面什么都不动 —— 这条就是"正在发生什么"的唯一说明。两个 TW 界面共用它:
|
|
93
|
+
它是 view 的最后一个孩子,在 flex column 里天然落在底部。 */
|
|
94
|
+
.dsh-tw-loading {
|
|
95
|
+
flex: 0 0 auto; padding: 6px 12px; font-size: 12px; text-align: center;
|
|
96
|
+
color: var(--dsw-alias-label-secondary, #666);
|
|
97
|
+
background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.08));
|
|
98
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.18));
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
/* ── right-sidebar tab (DSH new rightbar) ──────────────────────
|
|
92
102
|
The React body host fills the rightbar pane; the TW iframe lives inside
|
|
93
103
|
it and follows the DSH theme like the center-column panel. */
|
package/src/client/tw-frame.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import * as React from 'react'
|
|
30
30
|
import { RESTART_ENDPOINT, resolveTwUrl, twProxyFor } from './endpoints.ts'
|
|
31
|
-
import { fetchStatus } from './status-cache.ts'
|
|
31
|
+
import { fetchStatus, type StatusPayload } from './status-cache.ts'
|
|
32
32
|
import { getFocusWiki, subscribeFocusWiki } from './wiki-focus.ts'
|
|
33
33
|
import { attachThemeSync, setThemeSyncConfig } from './theme-sync.ts'
|
|
34
34
|
|
|
@@ -111,6 +111,8 @@ export interface TwFrameSkin {
|
|
|
111
111
|
frame: string
|
|
112
112
|
/** Error / starting panel. */
|
|
113
113
|
error: string
|
|
114
|
+
/** Hint shown while a DIFFERENT wiki's document is on the wire (v0.28.14). */
|
|
115
|
+
loading: string
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
/**
|
|
@@ -177,7 +179,19 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
177
179
|
let view: HTMLDivElement | undefined
|
|
178
180
|
let frame: HTMLIFrameElement | undefined
|
|
179
181
|
let errorArea: HTMLDivElement | undefined
|
|
182
|
+
let loadingEl: HTMLDivElement | undefined
|
|
180
183
|
let refreshTimer: number | undefined
|
|
184
|
+
/** "Switching wiki" hint timer (v0.28.14). */
|
|
185
|
+
let switchTimer: number | undefined
|
|
186
|
+
/**
|
|
187
|
+
* The last successful `/status` payload (v0.28.14).
|
|
188
|
+
*
|
|
189
|
+
* A wiki switch needs only two things from it — `mode` and the proxy bases — and we
|
|
190
|
+
* already have them from the refresh that opened the current wiki. Re-fetching costs
|
|
191
|
+
* a full round-trip (the host runs up to five `git` processes per `/status`, measured
|
|
192
|
+
* 300–400 ms here), which used to sit BETWEEN the click and the start of the switch.
|
|
193
|
+
*/
|
|
194
|
+
let lastStatus: StatusPayload | undefined
|
|
181
195
|
/** In-flight hash-readiness retry timers (cancelled by dispose, v0.19.1). */
|
|
182
196
|
const hashWaitTimers = new Set<number>()
|
|
183
197
|
let refreshAttempts = 0
|
|
@@ -209,6 +223,7 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
209
223
|
|
|
210
224
|
const showError = (message: string): void => {
|
|
211
225
|
if (frame === undefined || errorArea === undefined) return
|
|
226
|
+
hideSwitching()
|
|
212
227
|
frame.hidden = true
|
|
213
228
|
errorArea.hidden = false
|
|
214
229
|
errorArea.replaceChildren()
|
|
@@ -229,6 +244,7 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
229
244
|
|
|
230
245
|
const showStarting = (): void => {
|
|
231
246
|
if (frame === undefined || errorArea === undefined) return
|
|
247
|
+
hideSwitching()
|
|
232
248
|
frame.hidden = true
|
|
233
249
|
errorArea.hidden = false
|
|
234
250
|
errorArea.replaceChildren()
|
|
@@ -237,7 +253,44 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
237
253
|
errorArea.append(p)
|
|
238
254
|
}
|
|
239
255
|
|
|
240
|
-
|
|
256
|
+
/** Cancel the "switching wiki" hint's safety timer. */
|
|
257
|
+
const clearSwitchTimer = (): void => {
|
|
258
|
+
if (switchTimer !== undefined) {
|
|
259
|
+
window.clearTimeout(switchTimer)
|
|
260
|
+
switchTimer = undefined
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Drop the "loading another wiki" hint (the new document arrived, or we gave up). */
|
|
265
|
+
const hideSwitching = (): void => {
|
|
266
|
+
clearSwitchTimer()
|
|
267
|
+
if (loadingEl !== undefined) loadingEl.hidden = true
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Say 「正在载入知识库「X」…」 while a DIFFERENT wiki's document is on the wire.
|
|
272
|
+
*
|
|
273
|
+
* WHY (v0.28.14,作者报障「点击切换感觉会卡那么一下」): 一个 iframe 只能装一份 TW
|
|
274
|
+
* 文档,换库 = 重新下载并解析**整份** wiki。本机实测这份文档是 **9.9 MB / 29.9 MB /
|
|
275
|
+
* 28 MB**,而且 TW 自己回 `Cache-Control: no-store`、没有 ETag —— 既缓存不了也预取不了。
|
|
276
|
+
* 这段时间界面完全不动,用户只能感觉"卡住了"。加载时长不是这套代码能把控的,但**界面
|
|
277
|
+
* 必须说清正在发生什么**(否则一次正常的 1–2 秒等待看起来就是"卡死/没反应")。
|
|
278
|
+
*
|
|
279
|
+
* 兜底 15s:`load` 万一不来(库没起来 / 服务挂了),提示不能永久挂在界面上 —— 那条路径
|
|
280
|
+
* 由 showError/showStarting 接管。
|
|
281
|
+
*/
|
|
282
|
+
const showSwitching = (label: string): void => {
|
|
283
|
+
if (loadingEl === undefined) return
|
|
284
|
+
loadingEl.textContent = `正在载入知识库「${label}」…`
|
|
285
|
+
loadingEl.hidden = false
|
|
286
|
+
clearSwitchTimer()
|
|
287
|
+
switchTimer = window.setTimeout(() => { switchTimer = undefined; hideSwitching() }, 15_000)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** A wiki's display name, from the last roster we saw (falls back to its id). */
|
|
291
|
+
const wikiLabel = (id: string): string => lastStatus?.wikis?.find((w) => w.id === id)?.label ?? id
|
|
292
|
+
|
|
293
|
+
const showFrame = (url: string, wiki: string | undefined, label?: string): void => {
|
|
241
294
|
if (frame === undefined || errorArea === undefined) return
|
|
242
295
|
errorArea.hidden = true
|
|
243
296
|
// Only reveal the frame while the surface is on screen: doRefresh() can
|
|
@@ -252,6 +305,8 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
252
305
|
frame.dataset.loaded = url
|
|
253
306
|
frameLoaded = false
|
|
254
307
|
frame.src = url
|
|
308
|
+
// 换了文档 = 重新下载整份 wiki(10–30MB、no-store):这段时间必须给个可见说明。
|
|
309
|
+
if (label !== undefined) showSwitching(label)
|
|
255
310
|
}
|
|
256
311
|
// 换库这一步到此为止(url 没变也算了结:单库模式下两个 id 解析出同一条裸路径)。
|
|
257
312
|
// 迟到的 hash 请求现在才允许落地 —— src 真的换了就等 load 事件再调一次。
|
|
@@ -352,6 +407,8 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
352
407
|
showError('无法访问 /dsh-tiddlywiki/status')
|
|
353
408
|
return
|
|
354
409
|
}
|
|
410
|
+
// 记住这一份:换库要用它的 mode / 代理基址 / 名册(见 switchFrameNow 与 wikiLabel)。
|
|
411
|
+
lastStatus = payload
|
|
355
412
|
if (payload.ui !== undefined) {
|
|
356
413
|
// Shared surface label + theme config: both are module-level, so any
|
|
357
414
|
// surface that polls keeps them fresh for every other surface.
|
|
@@ -372,10 +429,12 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
372
429
|
// TW refuses to load its sync adaptor anywhere else — see resolveTwUrl.
|
|
373
430
|
// WHICH knowledge base (v0.28.0): the focused one in multi mode, else the
|
|
374
431
|
// bare path (= the default wiki, exactly as before).
|
|
375
|
-
const
|
|
376
|
-
|
|
432
|
+
const wiki = hooks.wikiId?.()
|
|
433
|
+
const bases = twProxyFor(payload.mode, wiki, payload.twProxy, payload.twProxyAbsolute)
|
|
434
|
+
showFrame(resolveTwUrl(bases.relative, bases.absolute), wiki, wiki === undefined ? undefined : wikiLabel(wiki))
|
|
377
435
|
} else if (typeof payload.url === 'string') {
|
|
378
|
-
|
|
436
|
+
const wiki = hooks.wikiId?.()
|
|
437
|
+
showFrame(payload.url, wiki, wiki === undefined ? undefined : wikiLabel(wiki))
|
|
379
438
|
} else {
|
|
380
439
|
showError('服务未返回编辑器地址')
|
|
381
440
|
}
|
|
@@ -410,14 +469,45 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
410
469
|
}
|
|
411
470
|
document.addEventListener(PANEL_RELOAD_EVENT, onReloadRequest)
|
|
412
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Point the frame at the focused wiki RIGHT NOW, using the last `/status` payload.
|
|
474
|
+
*
|
|
475
|
+
* v0.28.14(作者报障「点击切换感觉会卡那么一下」):旧实现只调 `doRefresh()`,而它要**先**
|
|
476
|
+
* 等一次 `/status` 往返才动 iframe。host 处理一次 `/status` 会跑最多五个 `git` 进程
|
|
477
|
+
* (本机实测 300–400 ms),于是"点下去"与"真的开始换库"之间空了 0.3–0.4 秒,紧接着才是
|
|
478
|
+
* 10–30MB 文档的下载与解析 —— 两段加起来就是那一下卡顿。换库需要的全部信息(`mode`、
|
|
479
|
+
* 代理基址、名册)上一次刷新已经拿到了,没有理由再等一次。
|
|
480
|
+
*
|
|
481
|
+
* 没有可用 payload(首次加载失败、服务在重启)时什么都不做:调用方的 `doRefresh()`
|
|
482
|
+
* 仍会照旧把整件事做完,行为与 v0.28.13 完全一致。
|
|
483
|
+
*/
|
|
484
|
+
const switchFrameNow = (): void => {
|
|
485
|
+
const payload = lastStatus
|
|
486
|
+
if (payload === undefined || payload.status !== 'running') return
|
|
487
|
+
const wiki = hooks.wikiId?.()
|
|
488
|
+
const label = wiki === undefined ? undefined : wikiLabel(wiki)
|
|
489
|
+
if (typeof payload.twProxy === 'string') {
|
|
490
|
+
const bases = twProxyFor(payload.mode, wiki, payload.twProxy, payload.twProxyAbsolute)
|
|
491
|
+
showFrame(resolveTwUrl(bases.relative, bases.absolute), wiki, label)
|
|
492
|
+
} else if (typeof payload.url === 'string') {
|
|
493
|
+
showFrame(payload.url, wiki, label)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
413
497
|
/**
|
|
414
498
|
* Switching the focused knowledge base needs a different proxy path, so the
|
|
415
499
|
* frame reloads (v0.28.0). TW reloads inside the iframe — one iframe cannot
|
|
416
500
|
* host two editors, and silently keeping the old wiki's data while the UI says
|
|
417
501
|
* otherwise is exactly the confusion this feature must not create.
|
|
502
|
+
*
|
|
503
|
+
* v0.28.14:**先立即换,再后台复探**。`switchFrameNow()` 用已有 payload 当场把 iframe
|
|
504
|
+
* 指过去(并把"正在载入…"挂上),`doRefresh()` 只负责随后的状态/配置对账,不再挡在
|
|
505
|
+
* 用户点击与界面反应之间。
|
|
418
506
|
*/
|
|
419
507
|
const unsubscribeFocus = subscribeFocusWiki(() => {
|
|
420
|
-
if (
|
|
508
|
+
if (disposed || !visible) return
|
|
509
|
+
switchFrameNow()
|
|
510
|
+
void doRefresh()
|
|
421
511
|
})
|
|
422
512
|
|
|
423
513
|
const build = (): HTMLDivElement => {
|
|
@@ -438,14 +528,22 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
438
528
|
const errorEl = document.createElement('div')
|
|
439
529
|
errorEl.className = skin.error
|
|
440
530
|
errorEl.hidden = true
|
|
441
|
-
|
|
531
|
+
// 换库提示(v0.28.14):**view 的最后一个孩子**。刻意不放进 wrap / 不改 frame 的
|
|
532
|
+
// 显隐规则 —— 那条"未载入 TW 地址就绝不显示 frame"的规则是 v0.22.3 的血债,不碰它。
|
|
533
|
+
const loadingEl2 = document.createElement('div')
|
|
534
|
+
loadingEl2.className = skin.loading
|
|
535
|
+
loadingEl2.hidden = true
|
|
536
|
+
viewEl.append(wrapEl, errorEl, loadingEl2)
|
|
442
537
|
view = viewEl
|
|
443
538
|
frame = frameEl
|
|
444
539
|
errorArea = errorEl
|
|
540
|
+
loadingEl = loadingEl2
|
|
445
541
|
// Track load so a pending tiddler-hash navigation can target a ready
|
|
446
542
|
// document (setting contentWindow.location.hash before load is a no-op).
|
|
447
543
|
frameEl.addEventListener('load', () => {
|
|
448
544
|
frameLoaded = true
|
|
545
|
+
// 新文档到了:收起"正在载入…"(幂等,普通刷新时它本来就是隐藏的)。
|
|
546
|
+
hideSwitching()
|
|
449
547
|
applyPendingHash()
|
|
450
548
|
})
|
|
451
549
|
// Embedded TW follows the DSH light/dark theme (non-persisting palette
|
|
@@ -476,6 +574,8 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
476
574
|
} else {
|
|
477
575
|
// Hidden surfaces stop polling and get a fresh budget on the next show.
|
|
478
576
|
clearRetry()
|
|
577
|
+
// 界面都不显示了,"正在载入…" 没有意义(也不该在再次打开时凭空挂着)。
|
|
578
|
+
hideSwitching()
|
|
479
579
|
refreshAttempts = 0
|
|
480
580
|
}
|
|
481
581
|
},
|
|
@@ -506,6 +606,7 @@ export function createTwFrameSurface(skin: TwFrameSkin, hooks: TwFrameHooks = {}
|
|
|
506
606
|
document.removeEventListener(PANEL_RELOAD_EVENT, onReloadRequest)
|
|
507
607
|
unsubscribeFocus()
|
|
508
608
|
clearRetry()
|
|
609
|
+
clearSwitchTimer()
|
|
509
610
|
for (const timer of hashWaitTimers) window.clearTimeout(timer)
|
|
510
611
|
hashWaitTimers.clear()
|
|
511
612
|
themeSyncDispose?.()
|
|
@@ -567,6 +668,7 @@ const RIGHTBAR_SKIN: TwFrameSkin = {
|
|
|
567
668
|
frameWrap: 'dsh-tw-rightbar-frame-wrap',
|
|
568
669
|
frame: 'dsh-tw-rightbar-frame',
|
|
569
670
|
error: 'dsh-tw-rightbar-error',
|
|
671
|
+
loading: 'dsh-tw-loading',
|
|
570
672
|
}
|
|
571
673
|
|
|
572
674
|
/**
|