memory-eternal 0.15.0 → 0.17.0

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.
@@ -2,6 +2,7 @@
2
2
  // Memory Atlas TUI —— 完整交互原型
3
3
  // node dsh-tui-plugin/demo.mjs
4
4
  // 静态帧:node dsh-tui-plugin/demo.mjs --once --no-color
5
+ // 定位列表:node dsh-tui-plugin/demo.mjs --once --no-color --screen=library --index=900
5
6
  // 全选预览:node dsh-tui-plugin/demo.mjs --once --no-color --select-all
6
7
  //
7
8
  // 示例数据,不连接真实 vault,不写任何卡片。
@@ -179,6 +180,8 @@ const writeMode = argv.has('--write')
179
180
  const API = process.env.MEMORY_ETERNAL_API || 'http://127.0.0.1:7999/memory-eternal/api'
180
181
  const screenArg = (process.argv.find((a) => a.startsWith('--screen=')) || '').split('=')[1]
181
182
  const scrollArg = Number((process.argv.find((a) => a.startsWith('--scroll=')) || '').split('=')[1] || 0)
183
+ // --index=N:设定初始选中项;列表窗口会跟着它滚动,便于自动化验收位置
184
+ const indexArg = Number((process.argv.find((a) => a.startsWith('--index=')) || '').split('=')[1])
182
185
  const useColor = !argv.has('--no-color') && !process.env.NO_COLOR
183
186
 
184
187
  const sgr = (code, text) => (useColor ? code + text + C.reset : text)
@@ -227,13 +230,55 @@ const wrap = (text, n) => {
227
230
  function panel(title, lines, w, h, accent = C.border) {
228
231
  const inner = Math.max(1, w - 2)
229
232
  const t = ' ' + title + ' '
230
- const top = sgr(accent, '╭─') + sgr(accent, t) + sgr(accent, '─'.repeat(Math.max(0, inner - width(t))) + '╮')
233
+ // 顶框总宽必须等于内框 + 左右边框(= w):标题两侧各多占 1 列,否则右边会多出 1 列
234
+ const top = sgr(accent, '╭─') + sgr(accent, t) + sgr(accent, '─'.repeat(Math.max(0, inner - width(t) - 1)) + '╮')
231
235
  const bottom = sgr(accent, '╰' + '─'.repeat(inner) + '╯')
232
236
  const body = []
233
237
  for (let i = 0; i < h - 2; i++) body.push(sgr(C.border, '│') + pad(clip(lines[i] || '', inner), inner) + sgr(C.border, '│'))
234
238
  return [top, ...body, bottom].join('\n')
235
239
  }
236
240
 
241
+ // ── 列表窗口 + 滚动条 ────────────────────────────────────────────────
242
+ // 记忆库 / 待审队列不再全量渲染:只渲染 listTop 起的一屏,右侧 1 列画
243
+ // 滚动条(滑轨 │ / 滑块 █),并保证 state.index 始终落在窗口内。
244
+ const SCROLL_THUMB = '█'
245
+ const SCROLL_TRACK = '│'
246
+
247
+ function windowInfo(state, total, visible) {
248
+ const count = Math.max(0, Number.isFinite(total) ? Math.floor(total) : 0)
249
+ const vis = Math.max(1, Number.isFinite(visible) ? Math.floor(visible) : 1)
250
+ let index = Math.max(0, Math.min(Math.floor(Number(state.index) || 0), Math.max(0, count - 1)))
251
+ let top = Math.max(0, Math.floor(Number(state.listTop) || 0))
252
+ if (index < top) top = index
253
+ else if (index >= top + vis) top = index - vis + 1
254
+ top = Math.min(top, Math.max(0, count - vis))
255
+ state.index = index
256
+ state.listTop = top
257
+ return { total: count, visible: vis, index, top, end: Math.min(count, top + vis) }
258
+ }
259
+
260
+ function scrollChar(row, rows, view) {
261
+ if (!view || view.total <= view.visible || rows <= 0) return ' '
262
+ const thumbH = Math.min(rows, Math.max(1, Math.round((view.visible / view.total) * rows)))
263
+ const maxTop = Math.max(1, view.total - view.visible)
264
+ const pos = Math.round((Math.min(view.top, maxTop) / maxTop) * (rows - thumbH))
265
+ return row >= pos && row < pos + thumbH ? sgr(C.ember, SCROLL_THUMB) : sgr(C.faint, SCROLL_TRACK)
266
+ }
267
+
268
+ // panel 会把每行 clip/pad 到 inner 宽度:这里先把正文压到 inner-1,再补滚动条列,
269
+ // 所以右边框不会被挤掉;不需要滚动时该列留空。
270
+ function scrollPanel(title, lines, w, h, accent, view) {
271
+ const inner = Math.max(1, w - 2)
272
+ const rows = Math.max(0, h - 2)
273
+ if (!view) return panel(title, lines, w, h, accent)
274
+ const cw = Math.max(1, inner - 1)
275
+ const composed = []
276
+ for (let i = 0; i < rows; i++) {
277
+ composed.push(pad(clip(lines[i] || '', cw), cw) + scrollChar(i, rows, view))
278
+ }
279
+ return panel(title, composed, w, h, accent)
280
+ }
281
+
237
282
  const kindOf = (c) => KIND[c.kind] || KIND.project
238
283
  const filtered = (state) => {
239
284
  const q = state.query.trim().toLowerCase()
@@ -261,14 +306,18 @@ function searchRow(state, w) {
261
306
  return ' ' + sgr(C.faint, '/ ') + sgr(state.searchMode ? C.text : C.faint, text) + ' ' + sgr(C.faint, 'Enter 确认 · Esc 清空')
262
307
  }
263
308
 
264
- function queueLines(cards, state, w) {
309
+ function queueLines(cards, state, w, view) {
265
310
  if (!cards.length) return ['', sgr(C.green, ' ✓ 没有匹配的卡片'), '', sgr(C.dim, ' 按 Esc 清空搜索 / 筛选')]
266
311
  const lines = []
267
312
  const chosen = cards.filter((c) => state.marked.has(c.id)).length
268
313
  const all = cards.length > 0 && chosen === cards.length
269
314
  lines.push(` ${all ? sgr(C.ember, '▣') : sgr(C.faint, '☐')} ${sgr(C.dim, all ? '反选' : '全选')} ${cards.length} ${chosen ? sgr(C.amber, '已选 ' + chosen) : sgr(C.faint, '已选 0')} ${keycap('s', all ? '反选' : '全选')}`)
270
315
  lines.push('')
271
- cards.forEach((card, i) => {
316
+ // 只渲染窗口内的项:view.top..view.end(0 张 / 全量时退化为原行为)
317
+ const from = view ? view.top : 0
318
+ const to = view ? view.end : cards.length
319
+ for (let i = from; i < to; i++) {
320
+ const card = cards[i]
272
321
  const k = kindOf(card)
273
322
  const selected = i === state.index
274
323
  const marked = state.marked.has(card.id)
@@ -282,7 +331,7 @@ function queueLines(cards, state, w) {
282
331
  if (selected) meta = tone(pad(clip(meta, w), w), '', C.selectedBg)
283
332
  lines.push(clip(meta, w))
284
333
  lines.push('')
285
- })
334
+ }
286
335
  return lines
287
336
  }
288
337
 
@@ -313,11 +362,17 @@ function previewLines(card, w) {
313
362
 
314
363
  function auditView(state, w, h) {
315
364
  const cards = filtered(state)
316
- const selected = cards[state.index]
317
365
  const qw = Math.max(40, Math.floor((w - 1) * 0.4))
318
366
  const pw = w - qw - 1
319
367
  const ph = h - 2
320
- const q = panel(`队列 ${cards.length}${state.marked.size ? ' ◉' + state.marked.size : ''}`, queueLines(cards, state, qw - 2), qw, ph)
368
+ // 队列面板内高 = ph-2;前 2 行是全选/已选说明,其余按每卡 3 行开窗
369
+ const rows = Math.max(0, ph - 2)
370
+ const visible = Math.max(1, Math.floor(Math.max(0, rows - 2) / 3) || 1)
371
+ const view = windowInfo(state, cards.length, visible)
372
+ state.listPage = view.visible
373
+ const selected = cards[view.index]
374
+ const pos = cards.length ? ` · ${view.top + 1}-${view.end}/${cards.length}` : ''
375
+ const q = scrollPanel(`队列 ${cards.length}${state.marked.size ? ' ◉' + state.marked.size : ''}${pos}`, queueLines(cards, state, qw - 3, view), qw, ph, C.border, view)
321
376
  const p = panel('判词', previewLines(selected, pw - 2), pw, ph, C.ember)
322
377
  const ql = q.split('\n'), pl = p.split('\n')
323
378
  const out = [typeRow(state, w), '']
@@ -336,19 +391,33 @@ function cardRow(card, selected, w, compact) {
336
391
 
337
392
  function libraryView(state, w, h) {
338
393
  const cards = filtered(state)
339
- const lines = [typeRow(state, w), '', searchRow(state, w), '']
394
+ const inner = Math.max(1, w - 2)
395
+ // 右侧留 1 列给滚动条:正文宽度 = 内框宽度 - 1
396
+ const cw = Math.max(1, inner - 1)
397
+ const bodyRows = Math.max(0, h - 2)
340
398
  const feature = cards[0] || CARDS[0]
341
- lines.push(`${sgr(C.ember, '✦ 今日回看')} ${sgr(C.text, feature.title)}`)
342
- lines.push(` ${sgr(C.dim, feature.summary)}`)
343
- lines.push(` ${feature.tags.map((t) => tone(' #' + t + ' ', C.ember, C.chipBg)).join(' ')}`)
344
- lines.push('', sgr(C.border, '─'.repeat(Math.max(1, w - 2))), '')
345
- lines.push(`${sgr(C.dim, '全部记忆')} ${sgr(C.faint, cards.length + ' 张 · 按 Enter 读卡')}`)
346
- lines.push('')
347
- cards.forEach((card, i) => {
348
- lines.push(...cardRow(card, i === state.index, w - 2, true))
399
+ const header = [
400
+ typeRow(state, cw), '',
401
+ searchRow(state, cw), '',
402
+ `${sgr(C.ember, '✦ 今日回看')} ${sgr(C.text, feature.title)}`,
403
+ ` ${sgr(C.dim, feature.summary)}`,
404
+ ` ${feature.tags.map((t) => tone(' #' + t + ' ', C.ember, C.chipBg)).join(' ')}`,
405
+ '', sgr(C.border, '─'.repeat(Math.max(1, cw))), '',
406
+ `${sgr(C.dim, '全部记忆')} ${sgr(C.faint, cards.length + ' 张 · 按 Enter 读卡')}`,
407
+ '',
408
+ ]
409
+ const listRows = Math.max(0, bodyRows - header.length)
410
+ // 每张卡固定 2 行正文 + 1 行空行;只渲染这一屏
411
+ const visible = Math.max(1, Math.floor(listRows / 3) || 1)
412
+ const view = windowInfo(state, cards.length, visible)
413
+ state.listPage = view.visible
414
+ const lines = header.slice()
415
+ for (let i = view.top; i < view.end; i++) {
416
+ lines.push(...cardRow(cards[i], i === view.index, cw, true))
349
417
  lines.push('')
350
- })
351
- return [panel('记忆库', lines, w, h, C.border)]
418
+ }
419
+ const title = cards.length ? `记忆库 · ${view.top + 1}-${view.end}/${cards.length}` : '记忆库 · 0/0'
420
+ return [scrollPanel(title, lines, w, h, C.border, view)]
352
421
  }
353
422
 
354
423
  function readerView(state, w, h) {
@@ -478,6 +547,8 @@ function helpView(w, h) {
478
547
  ` ${keycap('1')}–${keycap('6')} 记忆库 / 待审 / 星图 / 状态 / 回收 / 设置`,
479
548
  ` ${keycap('Tab')} 切换页面`,
480
549
  ` ${keycap('↑↓')} ${keycap('jk')} 移动`,
550
+ ` ${keycap('PgUp/PgDn')} 列表翻页(记忆库 / 待审)`,
551
+ ` ${keycap('g/G')} 列表首 / 尾`,
481
552
  ` ${keycap('←→')} ${keycap('t')} 切换类型`,
482
553
  ` ${keycap('Enter')} 读卡 / 切换设置`,
483
554
  ` ${keycap('Space')} 多选 / 切换设置`,
@@ -501,16 +572,19 @@ function footer(state, w) {
501
572
  if (state.screen === 'settings') return ' ' + [keycap('↑↓', '移动'), keycap('Enter', '切换'), keycap('Space', '切换'), keycap('Esc', '返回')].join(' ')
502
573
  if (state.screen === 'recycle') return ' ' + [keycap('↑↓', '移动'), keycap('r', '恢复'), keycap('d', '删除')].join(' ')
503
574
  if (state.screen === 'audit' && state.marked.size) return ' ' + [keycap('a', '批准 ' + state.marked.size), keycap('r', '驳回'), keycap('s', '反选'), keycap('Esc', '清空')].join(' ')
504
- if (state.screen === 'audit') return ' ' + [keycap('↑↓', '移动'), keycap('←→', '类型'), keycap('Space', '多选'), keycap('s', '全选'), keycap('a', '批准'), keycap('r', '驳回'), keycap('Enter', '读卡'), keycap('1-6', '页面'), keycap('?', '帮助')].join(' ')
505
- if (state.screen === 'library') return ' ' + [keycap('↑↓', '移动'), keycap('←→', '类型'), keycap('/', '搜索'), keycap('Enter', '读卡'), keycap('1-6', '页面')].join(' ')
575
+ if (state.screen === 'audit') return ' ' + [keycap('↑↓', '移动'), keycap('PgUp/PgDn', '翻页'), keycap('g/G', '首/尾'), keycap('←→', '类型'), keycap('Space', '多选'), keycap('s', '全选'), keycap('a', '批准'), keycap('r', '驳回'), keycap('Enter', '读卡'), keycap('1-6', '页面'), keycap('?', '帮助')].join(' ')
576
+ if (state.screen === 'library') return ' ' + [keycap('↑↓', '移动'), keycap('PgUp/PgDn', '翻页'), keycap('g/G', '首/尾'), keycap('←→', '类型'), keycap('/', '搜索'), keycap('Enter', '读卡'), keycap('1-6', '页面')].join(' ')
506
577
  if (state.screen === 'graph') return ' ' + [keycap('↑↓', '切换卡片'), keycap('Enter', '读卡'), keycap('1-6', '页面')].join(' ')
507
578
  if (state.screen === 'status') return ' ' + [keycap('1-6', '页面'), keycap('Tab', '切换'), keycap('?', '帮助')].join(' ')
508
579
  return ' ' + [keycap('1-6', '页面'), keycap('Tab', '切换'), keycap('?', '帮助')].join(' ')
509
580
  }
510
581
 
511
582
  function frame(state) {
512
- const w = Math.max(72, Math.min(useColor ? 140 : 120, process.stdout.columns || 120))
513
- const rows = Math.max(24, process.stdout.rows || 36)
583
+ // 非 TTY(管道 / --once 重定向)时退化到 COLUMNS/LINES,便于自动化验收终端尺寸
584
+ const cols = Number(process.stdout.columns) || Number(process.env.COLUMNS) || 0
585
+ const rowsRaw = Number(process.stdout.rows) || Number(process.env.LINES) || 0
586
+ const w = Math.max(72, Math.min(useColor ? 140 : 120, cols || 120))
587
+ const rows = Math.max(24, rowsRaw || 36)
514
588
  const screen = state.help ? 'help' : state.screen
515
589
  const label = screen === 'help' ? '帮助' : SCREENS[screen]?.label || '记忆库'
516
590
  const totalNow = state.screen === 'library' ? (state.libraryCards || state.cards).length : state.cards.length
@@ -615,6 +689,7 @@ async function loadLive(state) {
615
689
  path: c.path,
616
690
  }))
617
691
  state.index = 0
692
+ state.listTop = 0
618
693
  state.marked.clear()
619
694
  if (selectAllAtStart) state.cards.forEach((c) => state.marked.add(c.id))
620
695
  if (!state.cards.length) {
@@ -639,7 +714,12 @@ async function main() {
639
714
  settings: SETTINGS.map((s) => ({ ...s })), settingsIndex: 0, help: screenArg === 'help', toast: null,
640
715
  live: false, liveError: '', overview: null, libraryCards: CARDS.slice(), graphData: null, graphIndex: 0,
641
716
  }
717
+ state.initialIndex = Number.isFinite(indexArg) && indexArg > 0 ? Math.floor(indexArg) : 0
718
+ state.listTop = 0
719
+ state.listPage = 1
642
720
  if (liveMode) await loadLive(state)
721
+ // --index=N 在实时数据加载后仍要生效(loadLive 内部会把 index 归零)
722
+ if (state.initialIndex) state.index = state.initialIndex
643
723
  if (once) { draw(state); return }
644
724
  if (!process.stdout.isTTY) { process.stderr.write('请在有 TTY 的终端里运行;预览用 --once\n'); process.exitCode = 1; return }
645
725
 
@@ -695,6 +775,7 @@ async function main() {
695
775
  const ids = new Set(targets.map((c) => c.id))
696
776
  state.cards = state.cards.filter((c) => !ids.has(c.id))
697
777
  state.index = 0
778
+ state.listTop = 0
698
779
  state.marked.clear()
699
780
  toast(`${decision === 'a' ? '✓ 已批准' : '✕ 已驳回'} ${targets.length} 张:${targets[0].title}`, decision === 'a')
700
781
  }
@@ -725,6 +806,7 @@ async function main() {
725
806
  const i = ids.indexOf(state.screen === 'reader' ? state.readerBack : state.screen)
726
807
  state.screen = ids[(i + dir + ids.length) % ids.length]
727
808
  state.index = 0
809
+ state.listTop = 0
728
810
  draw(state)
729
811
  }
730
812
 
@@ -745,7 +827,7 @@ async function main() {
745
827
  if (key.name === 'tab') { cycle(key.shift ? -1 : 1); return }
746
828
  if (/^[1-6]$/.test(str)) {
747
829
  const id = ['library', 'audit', 'graph', 'status', 'recycle', 'settings'][Number(str) - 1]
748
- state.screen = id; state.index = 0; draw(state); return
830
+ state.screen = id; state.index = 0; state.listTop = 0; draw(state); return
749
831
  }
750
832
  if (state.screen === 'reader') {
751
833
  if (key.name === 'escape') { state.screen = state.readerBack; draw(state); return }
@@ -790,22 +872,28 @@ async function main() {
790
872
  if (str === 't' || key.name === 'right' || str === 'l') {
791
873
  const keys = ['all', 'project', 'knowledge', 'mistake', 'content', 'tool', 'prompt']
792
874
  state.kind = keys[(keys.indexOf(state.kind) + 1) % keys.length]
793
- state.index = 0; state.marked.clear(); draw(state); return
875
+ state.index = 0; state.listTop = 0; state.marked.clear(); draw(state); return
794
876
  }
795
877
  if (key.name === 'left' || str === 'h') {
796
878
  const keys = ['all', 'project', 'knowledge', 'mistake', 'content', 'tool', 'prompt']
797
879
  state.kind = keys[(keys.indexOf(state.kind) - 1 + keys.length) % keys.length]
798
- state.index = 0; state.marked.clear(); draw(state); return
880
+ state.index = 0; state.listTop = 0; state.marked.clear(); draw(state); return
799
881
  }
800
882
  if (state.screen === 'audit' && (str === 's' || (key.ctrl && key.name === 'a'))) {
801
883
  const all = cards.length > 0 && cards.every((c) => state.marked.has(c.id))
802
884
  cards.forEach((c) => all ? state.marked.delete(c.id) : state.marked.add(c.id))
803
885
  draw(state); return
804
886
  }
887
+ // 列表翻页:一次走一屏(state.listPage 由 libraryView / auditView 渲染时写入)
888
+ const pageStep = Math.max(1, state.listPage || 1)
889
+ if (key.name === 'pageup') { state.index = Math.max(0, state.index - pageStep); draw(state); return }
890
+ if (key.name === 'pagedown') { state.index = Math.min(Math.max(0, cards.length - 1), state.index + pageStep); draw(state); return }
891
+ if (str === 'g') { state.index = 0; draw(state); return }
892
+ if (str === 'G') { state.index = Math.max(0, cards.length - 1); draw(state); return }
805
893
  if (key.name === 'up' || str === 'k') { state.index = Math.max(0, state.index - 1); draw(state); return }
806
894
  if (key.name === 'down' || str === 'j') { state.index = Math.min(Math.max(0, cards.length - 1), state.index + 1); draw(state); return }
807
895
  if (key.name === 'return' || key.name === 'enter') { openReader(); return }
808
- if (key.name === 'escape') { state.query = ''; state.kind = 'all'; state.marked.clear(); state.index = 0; draw(state); return }
896
+ if (key.name === 'escape') { state.query = ''; state.kind = 'all'; state.marked.clear(); state.index = 0; state.listTop = 0; draw(state); return }
809
897
  if (state.screen === 'audit' && (key.name === 'space' || str === ' ')) {
810
898
  const c = cards[state.index]; if (c) { state.marked.has(c.id) ? state.marked.delete(c.id) : state.marked.add(c.id) } draw(state); return
811
899
  }
@@ -1,42 +1,42 @@
1
- {
2
- "$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
3
- "id": "memory-eternal-tui",
4
- "name": "memory-eternal-tui",
5
- "version": "0.15.0",
6
- "manifestVersion": "0.15",
7
- "description": "Open Memory Atlas from dsh-TUI: native /audit scene plus browser launcher.",
8
- "facets": {
9
- "host": {
10
- "entry": "main.js",
11
- "apiVersion": "v1alpha1"
12
- }
13
- },
14
- "requires": {
15
- "contracts": [
16
- {
17
- "apiVersion": "commands.dsh/v1alpha1",
18
- "kind": "Command"
19
- }
20
- ]
21
- },
22
- "permissions": [],
23
- "contributes": {
24
- "commands": [
25
- {
26
- "id": "memory-eternal-tui.open",
27
- "title": "memory",
28
- "description": "Open Memory Atlas in the browser"
29
- },
30
- {
31
- "id": "memory-eternal-tui.audit",
32
- "title": "audit",
33
- "description": "Open the native Memory Atlas audit scene"
34
- }
35
- ]
36
- },
37
- "subscriptions": [],
38
- "license": "MIT",
39
- "source": {
40
- "repository": "https://github.com/EternalNight996/memory-eternal"
41
- }
42
- }
1
+ {
2
+ "$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
3
+ "id": "memory-eternal-tui",
4
+ "name": "memory-eternal-tui",
5
+ "version": "0.17.0",
6
+ "manifestVersion": "0.15",
7
+ "description": "Open Memory Atlas from dsh-TUI: native /audit scene plus browser launcher.",
8
+ "facets": {
9
+ "host": {
10
+ "entry": "main.js",
11
+ "apiVersion": "v1alpha1"
12
+ }
13
+ },
14
+ "requires": {
15
+ "contracts": [
16
+ {
17
+ "apiVersion": "commands.dsh/v1alpha1",
18
+ "kind": "Command"
19
+ }
20
+ ]
21
+ },
22
+ "permissions": [],
23
+ "contributes": {
24
+ "commands": [
25
+ {
26
+ "id": "memory-eternal-tui.open",
27
+ "title": "memory",
28
+ "description": "Open Memory Atlas in the browser"
29
+ },
30
+ {
31
+ "id": "memory-eternal-tui.audit",
32
+ "title": "audit",
33
+ "description": "Open the native Memory Atlas audit scene"
34
+ }
35
+ ]
36
+ },
37
+ "subscriptions": [],
38
+ "license": "MIT",
39
+ "source": {
40
+ "repository": "https://github.com/EternalNight996/memory-eternal"
41
+ }
42
+ }
@@ -41,17 +41,72 @@ function openMemory() {
41
41
  }
42
42
 
43
43
  export function apply(ctx) {
44
- const host = ctx.get('tuiPluginHost', false)
45
- if (!host) return
44
+ // ── 为什么不能「拿不到 tuiPluginHost 就直接 return」────────────────────────
45
+ // tuiPluginHost / tuiScenes 都是**后挂载**的 TUI seam:cordis 只保证插件被拉起来,
46
+ // 不保证它激活时宿主 seam 已经就绪。旧实现 `const host = ctx.get('tuiPluginHost', false); if (!host) return`
47
+ // 一旦抢跑就永久放弃 —— 表现为命令面板里根本没有 /memory、/audit(issue #50 用户实测)。
48
+ // 参照 dsh-tui-find(同宿主同 API):软探测 + 50ms 小步轮询,挂上再注册;10s 还没挂
49
+ // 就安静收手(确实没有 TUI 运行时的组合不该报错)。
50
+ const HOST_POLL_MS = 50
51
+ const HOST_MAX_ATTEMPTS = 200 // ≈10s
52
+ let disposed = false
53
+ let hostTimer = null
54
+ let hostAttempts = 0
55
+ let hostDisposer = null
46
56
 
47
- // tuiScenes can mount after this activation on a cold boot. Soft-probe and
48
- // retry briefly instead of silently leaving /audit dead for the session.
57
+ const probe = (name) => {
58
+ try { return ctx.get(name, false) } catch { return false }
59
+ }
60
+
61
+ const registerCommands = (host) => {
62
+ const disposers = []
63
+ disposers.push(host.registerCommand(ctx, COMMAND_ID, {
64
+ recordInput: false,
65
+ handler: () => {
66
+ openMemory()
67
+ return { kind: 'success', text: `Memory Atlas 已打开:${URL}` }
68
+ },
69
+ }))
70
+ disposers.push(host.registerCommand(ctx, AUDIT_COMMAND_ID, {
71
+ recordInput: false,
72
+ handler: () => {
73
+ const runtime = probe('tuiScenes')
74
+ if (!runtime) return { kind: 'error', text: '当前 dsh-tui 没有 scenes 运行时' }
75
+ if (runtime.active?.id === AUDIT_SCENE_ID) runtime.close()
76
+ const opened = runtime.open(AUDIT_SCENE_ID)
77
+ return opened
78
+ ? { kind: 'success', text: 'Memory Atlas / 待审 已打开' }
79
+ : { kind: 'error', text: '审核场景未注册' }
80
+ },
81
+ }))
82
+ return () => { for (const d of disposers) { try { if (typeof d === 'function') d() } catch { /* 卸载失败不影响其他 */ } } }
83
+ }
84
+
85
+ const tryRegisterCommands = () => {
86
+ hostTimer = null
87
+ if (disposed) return
88
+ const host = probe('tuiPluginHost')
89
+ if (!host) {
90
+ hostAttempts += 1
91
+ if (hostAttempts < HOST_MAX_ATTEMPTS) hostTimer = setTimeout(tryRegisterCommands, HOST_POLL_MS)
92
+ return
93
+ }
94
+ try {
95
+ hostDisposer = registerCommands(host)
96
+ } catch (error) {
97
+ console.warn('[memory-eternal-tui] command registration failed:', error)
98
+ }
99
+ }
100
+ tryRegisterCommands()
101
+
102
+ // tuiScenes 同样可能晚于本插件挂载:沿用原来的软探测 + 50ms 重试(最多 ~2s)。
49
103
  let sceneDisposer = null
50
104
  let sceneTimer = null
51
105
  let attempts = 0
52
106
  const registerScene = () => {
53
107
  sceneTimer = null
54
- const scenes = ctx.get('tuiScenes', false)
108
+ if (disposed) return
109
+ const scenes = probe('tuiScenes')
55
110
  if (scenes && !sceneDisposer) {
56
111
  try {
57
112
  sceneDisposer = scenes.register({ id: AUDIT_SCENE_ID, title: 'Memory Atlas / 待审', component: AuditScene }, ctx)
@@ -65,33 +120,12 @@ export function apply(ctx) {
65
120
  }
66
121
  }
67
122
  registerScene()
68
- ctx.effect(() => {
123
+
124
+ ctx.effect(() => () => {
125
+ disposed = true
126
+ if (hostTimer) clearTimeout(hostTimer)
69
127
  if (sceneTimer) clearTimeout(sceneTimer)
128
+ if (hostDisposer) hostDisposer()
70
129
  if (sceneDisposer) sceneDisposer()
71
130
  })
72
-
73
- const dispose = host.registerCommand(ctx, COMMAND_ID, {
74
- recordInput: false,
75
- handler: () => {
76
- openMemory()
77
- return { kind: 'success', text: `Memory Atlas 已打开:${URL}` }
78
- },
79
- })
80
- ctx.effect(() => dispose)
81
-
82
- const disposeAudit = host.registerCommand(ctx, AUDIT_COMMAND_ID, {
83
- recordInput: false,
84
- handler: () => {
85
- const runtime = ctx.get('tuiScenes', false)
86
- if (!runtime) return { kind: 'error', text: '当前 dsh-tui 没有 scenes 运行时' }
87
- if (runtime.active?.id === AUDIT_SCENE_ID) runtime.close()
88
- const opened = runtime.open(AUDIT_SCENE_ID)
89
- return opened
90
- ? { kind: 'success', text: 'Memory Atlas / 待审 已打开' }
91
- : { kind: 'error', text: '审核场景未注册' }
92
- },
93
- })
94
- ctx.effect(() => disposeAudit)
95
131
  }
96
-
97
- export default { name, inject, apply }
@@ -1,31 +1,51 @@
1
- {
2
- "name": "memory-eternal-tui",
3
- "version": "0.15.0",
4
- "description": "Open Memory Atlas from dsh-TUI with /memory.",
5
- "type": "module",
6
- "main": "main.js",
7
- "files": [
8
- "main.js",
9
- "scene.js",
10
- "demo.mjs",
11
- "dsh-plugin.json",
12
- "cordis.patch.yml",
13
- "README.md"
14
- ],
15
- "keywords": [
16
- "dsh",
17
- "dsh-tui",
18
- "memory-eternal",
19
- "memory",
20
- "plugin"
21
- ],
22
- "license": "MIT",
23
- "dsh": {
24
- "bundle": {
25
- "patch": "./cordis.patch.yml"
26
- }
27
- },
28
- "peerDependencies": {
29
- "@deepseek-ai/cordis": "^4.0.1"
30
- }
31
- }
1
+ {
2
+ "name": "memory-eternal-tui",
3
+ "version": "0.17.0",
4
+ "description": "Memory Atlas for dsh-TUI: /audit scene with reason/severity, batch confirmation and reject reasons.",
5
+ "type": "module",
6
+ "main": "main.js",
7
+ "files": [
8
+ "main.js",
9
+ "scene.js",
10
+ "demo-audit.mjs",
11
+ "demo.mjs",
12
+ "dsh-plugin.json",
13
+ "cordis.patch.yml",
14
+ "README.md"
15
+ ],
16
+ "keywords": [
17
+ "dsh",
18
+ "dsh-tui",
19
+ "dsh-tui-plugin",
20
+ "memory-eternal",
21
+ "memory",
22
+ "audit",
23
+ "plugin"
24
+ ],
25
+ "license": "MIT",
26
+ "author": "EternalNight996",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/EternalNight996/memory-eternal.git",
30
+ "directory": "dsh-tui-plugin"
31
+ },
32
+ "homepage": "https://github.com/EternalNight996/memory-eternal/tree/main/dsh-tui-plugin",
33
+ "bugs": {
34
+ "url": "https://github.com/EternalNight996/memory-eternal/issues"
35
+ },
36
+ "publishConfig": {
37
+ "registry": "https://registry.npmjs.org/",
38
+ "access": "public"
39
+ },
40
+ "engines": {
41
+ "node": ">=22"
42
+ },
43
+ "dsh": {
44
+ "bundle": {
45
+ "patch": "./cordis.patch.yml"
46
+ }
47
+ },
48
+ "peerDependencies": {
49
+ "@deepseek-ai/cordis": "^4.0.1"
50
+ }
51
+ }