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/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.13",
4
+ "version": "0.28.14",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "dsh",
@@ -76,6 +76,7 @@ const PANEL_SKIN: TwFrameSkin = {
76
76
  frameWrapStyle: 'flex:1;min-height:0;display:flex;flex-direction:column',
77
77
  frame: 'dsh-tw-panel-frame',
78
78
  error: 'dsh-tw-panel-error',
79
+ loading: 'dsh-tw-loading',
79
80
  }
80
81
 
81
82
  /**
@@ -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
- const unsubscribeFocus = subscribeFocusWiki(() => { void applyRoster() })
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
@@ -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. */
@@ -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
- const showFrame = (url: string, wiki: string | undefined): void => {
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 bases = twProxyFor(payload.mode, hooks.wikiId?.(), payload.twProxy, payload.twProxyAbsolute)
376
- showFrame(resolveTwUrl(bases.relative, bases.absolute), hooks.wikiId?.())
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
- showFrame(payload.url, hooks.wikiId?.())
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 (!disposed && visible) void doRefresh()
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
- viewEl.append(wrapEl, errorEl)
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
  /**