dsh-lite-memory 0.16.4 → 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.
package/client.js CHANGED
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
23
23
  const KEEP_OFF = { ...KEEP_STYLE, opacity: 0.45, cursor: 'default' };
24
24
  const DROP = { flex: '0 0 auto', border: 'none', background: 'transparent', color: 'inherit', padding: '0 2px', fontSize: '12px', lineHeight: '18px', opacity: 0.55, cursor: 'pointer' };
25
25
  const DOCK = { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0, fontSize: '12px', lineHeight: '1.6' };
26
- const TAIL = { ...DOCK, margin: '4px 0 2px', opacity: 0.9 };
26
+ const TAIL = { ...DOCK, margin: '4px 0 2px' };
27
27
  const CHIP = { alignSelf: 'flex-start', border: 'none', background: 'transparent', color: 'inherit', padding: 0, fontSize: '12px', lineHeight: '1.6', opacity: 0.6, cursor: 'pointer', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
28
28
  const ITEM = { alignSelf: 'flex-start', border: 'none', background: 'transparent', color: 'inherit', padding: 0, fontSize: '12px', lineHeight: '1.6', opacity: 0.75, cursor: 'pointer', textAlign: 'left', width: '100%', minWidth: 0, maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
29
29
  const BODY = { margin: '2px 0 4px', padding: '4px 0 4px 8px', borderLeft: '1px solid currentColor', opacity: 0.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: '180px', overflowY: 'auto' };
@@ -98,6 +98,13 @@ window.__ModuleLoader__.load({
98
98
  origin: meta.origin === 'project_evolution' ? 'project_evolution' : 'user_explicit',
99
99
  kind,
100
100
  exposure: kind === 'memory-save' || kind === 'memory-forget' ? 'update' : 'reference',
101
+ reason: typeof meta.reason === 'string' ? meta.reason : '',
102
+ reasonCode: typeof meta.reasonCode === 'string' ? meta.reasonCode : '',
103
+ noTriggerReason: typeof meta.noTriggerReason === 'string' ? meta.noTriggerReason : '',
104
+ sourceExcerpt: meta.sourceExcerpt,
105
+ sourceRefs: meta.sourceRefs,
106
+ evidence: meta.evidence,
107
+ processingStatus: processingStatusOf(meta),
101
108
  };
102
109
  };
103
110
 
@@ -122,15 +129,22 @@ window.__ModuleLoader__.load({
122
129
  const citationsOfTurn = (nodes) => {
123
130
  if (!Array.isArray(nodes)) return EMPTY;
124
131
  const out = [];
132
+ let noTriggerReason = '';
125
133
  for (const node of nodes) {
126
134
  const root = node && node.root;
127
135
  if (root === null || typeof root !== 'object') continue;
128
136
  for (const block of [root, ...(Array.isArray(root.subCalls) ? root.subCalls : [])]) {
129
137
  const meta = metaOf(block);
130
138
  if (!meta) continue;
131
- out.push(...citationsOfMeta(meta));
139
+ const citations = citationsOfMeta(meta);
140
+ out.push(...citations);
141
+ if (citations.length === 0 && meta.kind === 'memory-search') {
142
+ const reason = [meta.noTriggerReason, meta.reason].find((value) => typeof value === 'string' && value);
143
+ if (reason) noTriggerReason = reason;
144
+ }
132
145
  }
133
146
  }
147
+ if (noTriggerReason) Object.defineProperty(out, 'noTriggerReason', { value: noTriggerReason, enumerable: false, configurable: true });
134
148
  return out;
135
149
  };
136
150
 
@@ -148,6 +162,9 @@ window.__ModuleLoader__.load({
148
162
  out[at] = item;
149
163
  }
150
164
  }
165
+ if (items && typeof items.noTriggerReason === 'string' && items.noTriggerReason) {
166
+ Object.defineProperty(out, 'noTriggerReason', { value: items.noTriggerReason, enumerable: false, configurable: true });
167
+ }
151
168
  return out;
152
169
  };
153
170
 
@@ -196,7 +213,9 @@ window.__ModuleLoader__.load({
196
213
  if (value !== lastNodes) {
197
214
  lastNodes = value;
198
215
  const next = dedupe(citationsOfTurn(value));
199
- if (!sameList(next, snapshot)) snapshot = next;
216
+ const nextReason = next.noTriggerReason || '';
217
+ const lastReason = snapshot.noTriggerReason || '';
218
+ if (!sameList(next, snapshot) || nextReason !== lastReason) snapshot = next;
200
219
  }
201
220
  return snapshot;
202
221
  };
@@ -308,7 +327,9 @@ window.__ModuleLoader__.load({
308
327
  setState('failed');
309
328
  });
310
329
  };
311
- const label = note || (firstLine(text) ?? RUNNING.memory_save);
330
+ const processing = processingStatusOf(meta);
331
+ const baseLabel = firstLine(text) ?? RUNNING.memory_save;
332
+ const label = note || `${baseLabel}${processing ? ` · ${PROCESSING_LABEL[processing]}` : ''}`;
312
333
  const button = changed && state !== 'dropped'
313
334
  ? h('button', { type: 'button', onClick: drop, disabled: !canDrop, 'aria-label': '撤销此次修改', style: state === 'idle' || state === 'failed' ? DROP : { ...DROP, opacity: 0.25, cursor: 'default' } }, '✕')
314
335
  : null;
@@ -316,16 +337,257 @@ window.__ModuleLoader__.load({
316
337
  };
317
338
 
318
339
  const scopeTag = (scope) => (scope === 'global' ? '[全局]' : '[项目]');
340
+
341
+ /** 处理状态只认服务端枚举,绝不从中文 reason 反推。 */
342
+ const PROCESSING = ['applied', 'unchanged', 'superseded', 'failed'];
343
+ const PROCESSING_LABEL = {
344
+ applied: '已应用', unchanged: '无变化', superseded: '已被新版本取代', failed: '处理失败',
345
+ };
346
+ const processingStatusOf = (source) => {
347
+ if (!source || typeof source !== 'object') return '';
348
+ for (const field of ['processingStatus', 'processing', 'outcome', 'status']) {
349
+ const value = source[field];
350
+ if (typeof value === 'string' && PROCESSING.includes(value)) return value;
351
+ if (value && typeof value === 'object' && typeof value.status === 'string' && PROCESSING.includes(value.status)) return value.status;
352
+ }
353
+ return '';
354
+ };
355
+
356
+ const recordOfPayload = (payload) =>
357
+ (payload && payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record) ? payload.record : null);
358
+
359
+ const bodyTextOf = (value) => {
360
+ if (typeof value === 'string') return value;
361
+ if (!value || typeof value !== 'object') return '';
362
+ for (const field of ['body', 'text', 'value']) {
363
+ if (typeof value[field] === 'string') return value[field];
364
+ }
365
+ return '';
366
+ };
367
+
368
+ /** 当前正文与历史版本分开,历史不拼进正文。 */
369
+ const currentBodyOf = (payload) => {
370
+ if (!payload || typeof payload !== 'object') return '';
371
+ if (typeof payload.currentBody === 'string') return payload.currentBody;
372
+ const record = recordOfPayload(payload);
373
+ if (record) {
374
+ const recordBody = bodyTextOf(record.current);
375
+ if (recordBody) return recordBody;
376
+ if (typeof record.body === 'string') return record.body;
377
+ }
378
+ if (payload.current !== undefined && typeof payload.current !== 'boolean') {
379
+ const current = bodyTextOf(payload.current);
380
+ if (current) return current;
381
+ }
382
+ return typeof payload.body === 'string' ? payload.body : '';
383
+ };
384
+
385
+ const historyOfPayload = (payload) => {
386
+ const record = recordOfPayload(payload);
387
+ const current = payload && payload.current && typeof payload.current === 'object' ? payload.current : null;
388
+ const lists = [
389
+ payload && payload.versions,
390
+ payload && payload.history,
391
+ record && record.versions,
392
+ record && record.history,
393
+ current && current.versions,
394
+ ];
395
+ for (const list of lists) {
396
+ if (Array.isArray(list)) return list.filter((row) => row !== null && row !== undefined);
397
+ }
398
+ return EMPTY;
399
+ };
400
+
401
+ /** 历史行统一成 old → new,缺前后值用相邻版本补。 */
402
+ const historyDiffsOf = (payload) => {
403
+ const rows = historyOfPayload(payload);
404
+ if (rows.length === 0) return EMPTY;
405
+ const currentBody = currentBodyOf(payload);
406
+ const normalized = rows.map((row, index) => {
407
+ const value = row && typeof row === 'object' && !Array.isArray(row) ? row : { body: row };
408
+ const explicitOld = [value.old, value.from, value.before, value.previousBody]
409
+ .find((item) => typeof item === 'string');
410
+ const explicitNew = [value.next, value.to, value.after, value.newBody]
411
+ .find((item) => typeof item === 'string');
412
+ return {
413
+ key: `${Number.isInteger(value.revision) ? value.revision : 'v'}-${index}`,
414
+ at: typeof value.at === 'string' ? value.at : (typeof value.when === 'string' ? value.when : ''),
415
+ revision: Number.isInteger(value.revision) ? value.revision : null,
416
+ status: typeof value.status === 'string' ? value.status : '',
417
+ body: bodyTextOf(value), // ui-copy-ignore
418
+ old: explicitOld || '', // ui-copy-ignore
419
+ next: explicitNew || '', // ui-copy-ignore
420
+ };
421
+ });
422
+ const byRevision = normalized.every((row) => row.revision !== null);
423
+ if (byRevision) normalized.sort((a, b) => a.revision - b.revision);
424
+ else if (normalized.every((row) => Boolean(row.at))) normalized.sort((a, b) => a.at.localeCompare(b.at));
425
+ return normalized.map((row, index) => ({
426
+ ...row,
427
+ old: row.old || row.body || '',
428
+ next: row.next || (index + 1 < normalized.length ? normalized[index + 1].body : currentBody),
429
+ }));
430
+ };
431
+
432
+ const payloadFieldOf = (payload, field) => {
433
+ const record = recordOfPayload(payload);
434
+ const current = payload && payload.current && typeof payload.current === 'object' ? payload.current : null;
435
+ for (const owner of [payload, record, current]) {
436
+ if (owner && owner[field] !== undefined) return owner[field];
437
+ }
438
+ return undefined;
439
+ };
440
+
441
+ const sourceExcerptOf = (payload) => bodyTextOf(payloadFieldOf(payload, 'sourceExcerpt'));
442
+ const refsOfPayload = (payload) => {
443
+ const value = payloadFieldOf(payload, 'sourceRefs');
444
+ if (typeof value === 'string' && value) return [value];
445
+ if (Array.isArray(value)) return value;
446
+ if (value && typeof value === 'object') return [value];
447
+ return EMPTY;
448
+ };
449
+ const evidenceOfPayload = (payload) => {
450
+ const value = payloadFieldOf(payload, 'evidence');
451
+ if (typeof value === 'string' && value) return [value];
452
+ if (Array.isArray(value)) return value;
453
+ if (value && typeof value === 'object') return [value];
454
+ return EMPTY;
455
+ };
456
+
457
+ const refLabelOf = (ref, index) => {
458
+ if (typeof ref === 'string') return ref;
459
+ if (!ref || typeof ref !== 'object') return '';
460
+ if (typeof ref.label === 'string' && ref.label) return ref.label;
461
+ if (typeof ref.title === 'string' && ref.title) return ref.title;
462
+ if (typeof ref.text === 'string' && ref.text) return ref.text;
463
+ if (ref.message !== undefined) return `消息 ${ref.message}`;
464
+ if (ref.messageId !== undefined) return `消息 ${ref.messageId}`;
465
+ if (ref.turn !== undefined) return `第 ${ref.turn} 轮`;
466
+ if (ref.turnIndex !== undefined) return `第 ${ref.turnIndex} 轮`;
467
+ return ref.id !== undefined ? String(ref.id) : `引用 ${index + 1}`;
468
+ };
469
+ const refHrefOf = (ref) => (ref && typeof ref === 'object'
470
+ ? [ref.href, ref.url, ref.link].find((value) => typeof value === 'string' && value)
471
+ : '');
472
+
473
+ const linkStyle = { color: 'inherit', textDecoration: 'underline', textUnderlineOffset: '2px', wordBreak: 'break-all' };
474
+
475
+ const HistoryVersions = (props) => {
476
+ const rows = Array.isArray(props.rows) ? props.rows : EMPTY;
477
+ const [open, setOpen] = React.useState(false);
478
+ if (rows.length === 0) return null;
479
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
480
+ h('button', {
481
+ type: 'button',
482
+ onClick: () => setOpen((value) => !value),
483
+ 'aria-expanded': open,
484
+ 'aria-label': `${open ? '收起' : '展开'}历史版本`,
485
+ style: CHIP,
486
+ }, `历史 ${rows.length}${open ? ' · 收起' : ''}`),
487
+ open ? h('div', { style: BODY }, rows.map((row) => h('div', {
488
+ key: row.key, style: { margin: '0 0 4px', minWidth: 0 },
489
+ },
490
+ h('div', { style: { opacity: 0.65, fontSize: '11px' } },
491
+ [row.at, row.revision === null ? '' : `v${row.revision}`, row.status].filter(Boolean).join(' · ')),
492
+ h('div', null,
493
+ h('span', { style: { opacity: 0.6 } }, SHORT(row.old, 120) || '—'),
494
+ ' → ',
495
+ h('span', null, SHORT(row.next, 120) || '—'))))) : null,
496
+ );
497
+ };
498
+
499
+ const DetailExtras = (props) => {
500
+ const payload = props.payload;
501
+ const excerpt = sourceExcerptOf(payload);
502
+ const refs = refsOfPayload(payload);
503
+ const evidence = evidenceOfPayload(payload);
504
+ const history = historyDiffsOf(payload);
505
+ if (!excerpt && refs.length === 0 && evidence.length === 0 && history.length === 0) return null;
506
+ const links = (items, label) => (items.length === 0 ? null : h('div', { style: { display: 'flex', gap: '6px', minWidth: 0 } },
507
+ h('span', { style: { opacity: 0.6, flex: '0 0 auto' } }, label),
508
+ h('span', { style: { minWidth: 0, wordBreak: 'break-word' } }, items.map((item, index) => {
509
+ const href = refHrefOf(item);
510
+ const text = refLabelOf(item, index);
511
+ const key = `${index}-${text}`;
512
+ if (!href) return h('span', { key }, index > 0 ? ' · ' : '', text);
513
+ return h('span', { key }, index > 0 ? ' · ' : '', h('a', {
514
+ href, target: '_blank', rel: 'noreferrer', style: linkStyle,
515
+ }, text));
516
+ }))));
517
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
518
+ excerpt ? h('div', { style: BODY }, h('span', { style: { opacity: 0.65 } }, '来源'), ' ', excerpt) : null,
519
+ links(refs, '消息'),
520
+ links(evidence, '依据'),
521
+ h(HistoryVersions, { rows: history }));
522
+ };
523
+
319
524
  const versionLine = (shown) => {
320
525
  const scope = shown.scopeLabel || (shown.scope === 'global' ? '全局' : '项目');
321
526
  const archived = shown.status === 'archived' ? ' · 已归档' : '';
322
- return `${scope}${archived}`;
527
+ const processing = processingStatusOf(shown.processingStatus ? { processingStatus: shown.processingStatus } : shown);
528
+ return `${scope}${archived}${processing ? ` · ${PROCESSING_LABEL[processing]}` : ''}`;
529
+ };
530
+
531
+ /**
532
+ * 引用三层的色调:中性 / 强调 / 成功。
533
+ * 三档色阶:中性 / 强调 / 成功;正文用主文本色。
534
+ */
535
+ const REF_TONE = {
536
+ found: 'var(--dsw-alias-label-secondary)',
537
+ provided: 'var(--dsw-alias-brand-primary)',
538
+ update: 'var(--dsw-alias-state-success-primary)',
539
+ };
540
+ const REF_META = { display: 'flex', alignItems: 'center', gap: '6px', margin: '0 0 2px 2px', fontSize: '11px', lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' };
541
+ const REF_BODY = { margin: '2px 0 3px', padding: '3px 0 3px 9px', display: 'flex', flexDirection: 'column', gap: '3px', fontSize: '12px', lineHeight: '1.65', maxHeight: '180px', overflowY: 'auto' };
542
+
543
+ /** 出处一句话:项目/全局、来源、状态。 */
544
+ const refMetaOf = (item) => {
545
+ const parts = [item.scope === 'global' ? '全局' : '项目', item.origin === 'project_evolution' ? '自进化' : '你记的'];
546
+ if (item.status === 'archived') parts.push('已归档');
547
+ const processing = PROCESSING_LABEL[item.processingStatus] || '';
548
+ if (processing) parts.push(processing);
549
+ return parts.join(' · ');
550
+ };
551
+
552
+ const RefChevron = (props) => h('svg', {
553
+ width: 8, height: 8, viewBox: '0 0 8 8', 'aria-hidden': 'true', focusable: 'false',
554
+ style: { flex: '0 0 auto', opacity: 0.65, transform: props.open ? 'rotate(90deg)' : 'none' },
555
+ }, h('path', { d: 'M2.5 1 L5.5 4 L2.5 7', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }));
556
+
557
+ /** 一条引用:标题一行、原因与来源一行,窄栏也不抢宽度。 */
558
+ const RefRow = (props) => {
559
+ const [hover, setHover] = React.useState(false);
560
+ const item = props.item;
561
+ const reason = item.noTriggerReason || item.reason || '';
562
+ return h('button', {
563
+ type: 'button', // ui-copy-ignore
564
+ onMouseEnter: () => setHover(true),
565
+ onMouseLeave: () => setHover(false),
566
+ onClick: props.onShow, // ui-copy-ignore
567
+ 'aria-label': `查看记忆 ${item.title || item.id}${reason ? `,${reason}` : ''}`,
568
+ title: reason ? `${item.title || item.id},${reason}` : (item.title || item.id),
569
+ style: { // ui-copy-ignore
570
+ display: 'flex', alignItems: 'flex-start', gap: '7px', width: '100%', minWidth: 0, textAlign: 'left',
571
+ border: 'none', borderRadius: '8px', padding: '3px 7px', cursor: 'pointer', color: 'inherit',
572
+ background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
573
+ fontSize: '12px', lineHeight: '18px',
574
+ },
575
+ },
576
+ h('span', { 'aria-hidden': 'true', style: { flex: '0 0 auto', alignSelf: 'stretch', width: '2px', minHeight: '10px', borderRadius: '999px', background: props.tone, opacity: 0.85 } }),
577
+ h('span', { style: { display: 'flex', flexDirection: 'column', gap: '1px', flex: '1 1 auto', minWidth: 0 } },
578
+ h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.title || item.id),
579
+ h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary)' } },
580
+ reason ? h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, reason) : null,
581
+ h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, refMetaOf(item)),
582
+ ),
583
+ ));
323
584
  };
324
585
 
325
586
  /** 一段引用:点开是名字,再点是当前正文。 */
326
587
  const RefSection = (props) => {
327
588
  const items = Array.isArray(props.items) ? props.items : EMPTY;
328
589
  const sessionId = typeof props.sessionId === 'string' ? props.sessionId : '';
590
+ const tone = REF_TONE[props.tone] || REF_TONE.found;
329
591
  const [open, setOpen] = React.useState(false);
330
592
  const [shown, setShown] = React.useState(null);
331
593
  const [note, setNote] = React.useState('');
@@ -341,7 +603,10 @@ window.__ModuleLoader__.load({
341
603
  setShown(null);
342
604
  setNote('');
343
605
  }, [sessionId, props.turnKey]);
344
- if (items.length === 0) return null;
606
+ if (items.length === 0) {
607
+ const reason = typeof props.noTriggerReason === 'string' ? props.noTriggerReason : '';
608
+ return reason ? h('div', { style: { ...REF_META, margin: 0 } }, reason) : null;
609
+ }
345
610
  const close = () => {
346
611
  generation.current += 1;
347
612
  setShown(null);
@@ -378,30 +643,35 @@ window.__ModuleLoader__.load({
378
643
  scopeLabel: typeof payload.scopeLabel === 'string' ? payload.scopeLabel : '',
379
644
  revision: Number.isInteger(payload.revision) ? payload.revision : item.revision,
380
645
  status: typeof payload.status === 'string' ? payload.status : 'ok',
381
- body: typeof payload.body === 'string' ? payload.body : '',
646
+ body: currentBodyOf(payload),
647
+ processingStatus: processingStatusOf(payload) || item.processingStatus || '',
648
+ payload,
382
649
  current: payload.revision === item.revision,
383
650
  });
384
651
  });
385
652
  };
386
- const rows = open ? items.map((item) => h('button', {
653
+ const rows = open ? items.map((item) => h(RefRow, {
387
654
  key: `${item.scope}|${item.id}|${item.revision}`, // ui-copy-ignore
388
- type: 'button', // ui-copy-ignore
389
- onClick: () => show(item), // ui-copy-ignore
390
- 'aria-label': `查看记忆 ${item.title || item.id}`,
391
- title: item.title || item.id,
392
- style: ITEM, // ui-copy-ignore
393
- }, `${scopeTag(item.scope)} ${item.title || item.id}${item.origin === 'project_evolution' ? ' · 实验' : ''}`)) : [];
655
+ item, tone, active: Boolean(shown && shown.id === item.id),
656
+ onShow: () => show(item),
657
+ })) : [];
394
658
  const detail = shown && shown.ref && items.some((item) => item.id === shown.id && item.revision === shown.ref.revision)
395
- ? h('div', { style: BODY },
396
- shown.loading ? '读取中' : null,
397
- shown.gone ? '这条记忆已删除' : null,
398
- shown.invalid ? '读取失败,点名字重试' : null,
659
+ ? h('div', { style: { ...REF_BODY, borderLeft: `2px solid ${tone}` } },
660
+ shown.loading ? h('span', { style: REF_META }, '读取中…') : null,
661
+ shown.gone ? h('span', { style: REF_META }, '这条记忆已删除') : null,
662
+ shown.invalid ? h('span', { style: REF_META }, '读不到正文,点名字重试') : null,
399
663
  !shown.loading && !shown.gone && !shown.invalid ? h(React.Fragment, null,
400
- shown.title && shown.title !== shown.ref.title ? h('div', { style: { opacity: 0.9 } }, shown.title) : null,
401
- h('div', null, versionLine(shown)),
402
- shown.when ? h('div', null, shown.when) : null,
403
- shown.body ? h('div', null, shown.body) : null,
664
+ shown.title && shown.title !== shown.ref.title
665
+ ? h('div', { style: { fontSize: '12px', lineHeight: '1.55', fontWeight: 600 } }, shown.title)
666
+ : null,
667
+ h('div', { style: REF_META }, versionLine(shown)),
668
+ shown.when ? h('div', { style: REF_META }, shown.when) : null,
669
+ shown.body
670
+ ? h('div', { style: { color: 'var(--dsw-alias-label-primary)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, shown.body)
671
+ : null,
672
+ h(DetailExtras, { payload: { ...shown.ref, ...shown.payload } }),
404
673
  ) : null,
674
+ note ? h('span', { style: REF_META }, note) : null,
405
675
  )
406
676
  : null;
407
677
  return h('div', { style: DOCK, 'data-memory-section': props.label },
@@ -409,12 +679,20 @@ window.__ModuleLoader__.load({
409
679
  type: 'button',
410
680
  onClick: () => { if (open) close(); else setOpen(true); },
411
681
  'aria-expanded': open,
412
- 'aria-label': `${open ? '收起' : '展开'}${props.label}`,
413
- style: CHIP,
414
- }, `${props.label} ${items.length} 条${open ? ' · 收起' : ''}`),
682
+ 'aria-label': `${open ? '收起' : '展开'}${props.label},${items.length} 条`,
683
+ style: {
684
+ alignSelf: 'flex-start', display: 'inline-flex', alignItems: 'center', gap: '6px',
685
+ border: 'none', background: 'transparent', padding: '1px 0', cursor: 'pointer',
686
+ color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '18px',
687
+ },
688
+ },
689
+ h('span', { 'aria-hidden': 'true', style: { width: '2px', height: '10px', borderRadius: '999px', background: tone, opacity: 0.9 } }),
690
+ h('span', null, props.label),
691
+ h('span', { style: { fontVariantNumeric: 'tabular-nums', opacity: 0.8 } }, String(items.length)),
692
+ h(RefChevron, { open }),
693
+ ),
415
694
  ...rows,
416
695
  detail,
417
- note ? h('div', { style: BODY }, note) : null,
418
696
  );
419
697
  };
420
698
 
@@ -423,7 +701,7 @@ window.__ModuleLoader__.load({
423
701
  /** 大脑:两瓣轮廓加一条中线,状态只靠透明度和面板文字。 */
424
702
  const BrainIcon = () => h('svg', {
425
703
  width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none',
426
- stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round',
704
+ stroke: 'currentColor', 'stroke-width': 1.5, 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
427
705
  'aria-hidden': 'true', focusable: 'false',
428
706
  },
429
707
  h('path', { d: 'M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z' }),
@@ -432,7 +710,7 @@ window.__ModuleLoader__.load({
432
710
 
433
711
  const CloseIcon = () => h('svg', {
434
712
  width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none',
435
- stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round',
713
+ stroke: 'currentColor', 'stroke-width': 1.6, 'stroke-linecap': 'round',
436
714
  'aria-hidden': 'true', focusable: 'false',
437
715
  }, h('path', { d: 'M6 6l12 12M18 6L6 18' }));
438
716
 
@@ -450,6 +728,151 @@ window.__ModuleLoader__.load({
450
728
  const text = typeof value === 'string' ? value : '';
451
729
  return text.length > size ? `${text.slice(0, size)}…` : text;
452
730
  };
731
+
732
+ /** 显示设置只决定画不画,不改学习状态;按项目 key 存在本机。 */
733
+ const DISPLAY_DEFAULTS = Object.freeze({ graph: false, palace: false });
734
+ const displayKeyOf = (projectKey) => `lite-memory:display:${projectKey || 'default'}`;
735
+ const displayStoreOf = () => {
736
+ try {
737
+ const store = globalThis.localStorage;
738
+ return store && typeof store.getItem === 'function' ? store : null;
739
+ } catch {
740
+ return null;
741
+ }
742
+ };
743
+ const readDisplaySettings = (projectKey) => {
744
+ const store = displayStoreOf();
745
+ const next = { ...DISPLAY_DEFAULTS };
746
+ if (!store) return next;
747
+ try {
748
+ const raw = store.getItem(displayKeyOf(projectKey));
749
+ if (!raw) return next;
750
+ const saved = JSON.parse(raw);
751
+ if (saved && typeof saved === 'object') {
752
+ next.graph = saved.graph === true;
753
+ next.palace = saved.palace === true;
754
+ }
755
+ } catch {
756
+ return next;
757
+ }
758
+ return next;
759
+ };
760
+ const writeDisplaySettings = (projectKey, next) => {
761
+ const store = displayStoreOf();
762
+ if (!store) return false;
763
+ try {
764
+ store.setItem(displayKeyOf(projectKey), JSON.stringify({
765
+ graph: next && next.graph === true,
766
+ palace: next && next.palace === true,
767
+ }));
768
+ return true;
769
+ } catch {
770
+ return false;
771
+ }
772
+ };
773
+ /** 功能关闭时视图统一回列表,不保留一个画不出来的页签。 */
774
+ const graphViewOf = (enabled, view) => (enabled === true && view === 'graph' ? 'graph' : 'list');
775
+ const viewModeOf = (display, view) => {
776
+ if (view === 'graph' && display && display.graph === true) return 'graph';
777
+ if (view === 'palace' && display && display.palace === true) return 'palace';
778
+ return 'list';
779
+ };
780
+ const reduceMotionOf = () => {
781
+ try {
782
+ const query = globalThis.matchMedia && globalThis.matchMedia('(prefers-reduced-motion: reduce)');
783
+ return Boolean(query && query.matches === true);
784
+ } catch {
785
+ return false;
786
+ }
787
+ };
788
+ /** 跟随系统偏好切换,打开面板期间改设置也能立即生效。 */
789
+ const useReducedMotion = () => {
790
+ const [reduced, setReduced] = React.useState(reduceMotionOf);
791
+ React.useEffect(() => {
792
+ let query = null;
793
+ try {
794
+ query = globalThis.matchMedia && globalThis.matchMedia('(prefers-reduced-motion: reduce)');
795
+ } catch {
796
+ query = null;
797
+ }
798
+ if (!query || typeof query.addEventListener !== 'function') return undefined;
799
+ const update = () => setReduced(query.matches === true);
800
+ query.addEventListener('change', update);
801
+ return () => query.removeEventListener('change', update);
802
+ }, []);
803
+ return reduced;
804
+ };
805
+
806
+ /** 标签/话题按精确值筛选;只有自由搜索才用包含匹配。 */
807
+ const filterRecords = (records, query, hub) => {
808
+ const rows = Array.isArray(records) ? records : EMPTY;
809
+ const kind = hub && typeof hub.kind === 'string' ? hub.kind : '';
810
+ const value = hub && typeof hub.value === 'string' ? hub.value : '';
811
+ if (kind === 'tag' && value) {
812
+ return rows.filter((record) => Array.isArray(record.tags) && record.tags.includes(value));
813
+ }
814
+ if (kind === 'topic' && value) {
815
+ return rows.filter((record) => record.topicKey === value);
816
+ }
817
+ const needle = typeof query === 'string' ? query.trim().toLowerCase() : '';
818
+ if (!needle) return rows;
819
+ return rows.filter((record) => {
820
+ const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
821
+ return `${record.title || ''} ${record.summary || ''} ${tags} ${record.topicKey || ''} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
822
+ });
823
+ };
824
+
825
+ /** 记忆宫殿:房间固定,槽位固定,换项目只换陈列内容。 */
826
+ const PALACE_PAGE_SIZE = 24;
827
+ const PALACE_PREVIEW = 4;
828
+ const PALACE_ROOMS = Object.freeze([
829
+ Object.freeze({ id: 'habits', name: '习惯与规范', kinds: Object.freeze(['preference', 'convention']) }),
830
+ Object.freeze({ id: 'methods', name: '方法与经验', kinds: Object.freeze(['procedure', 'pitfall']) }),
831
+ Object.freeze({ id: 'entries', name: '资料与入口', kinds: Object.freeze(['pointer', 'credential_hint']) }),
832
+ Object.freeze({ id: 'notes', name: '其他记录', kinds: Object.freeze(['note']) }),
833
+ ]);
834
+ const PALACE_ROOM_OF_KIND = Object.freeze({
835
+ preference: 'habits', convention: 'habits',
836
+ procedure: 'methods', pitfall: 'methods',
837
+ pointer: 'entries', credential_hint: 'entries',
838
+ note: 'notes',
839
+ });
840
+ const palaceRoomIdOf = (kind) => PALACE_ROOM_OF_KIND[kind] || 'notes';
841
+ const palaceRoomsOf = (records) => {
842
+ const rows = Array.isArray(records) ? records : EMPTY;
843
+ return PALACE_ROOMS.map((room) => {
844
+ const items = rows.filter((record) => palaceRoomIdOf(record && record.kind) === room.id);
845
+ return { id: room.id, name: room.name, items, total: items.length, pages: Math.max(1, Math.ceil(items.length / PALACE_PAGE_SIZE)) };
846
+ });
847
+ };
848
+ const palaceRoomPageOf = (records, roomId, page) => {
849
+ const room = palaceRoomsOf(records).find((row) => row.id === roomId);
850
+ if (!room) return { items: EMPTY, total: 0, pages: 0, page: 1 };
851
+ const current = Math.min(room.pages, Math.max(1, Number.isFinite(page) ? Math.floor(page) : 1));
852
+ return {
853
+ items: room.items.slice((current - 1) * PALACE_PAGE_SIZE, current * PALACE_PAGE_SIZE),
854
+ total: room.total, pages: room.pages, page: current,
855
+ };
856
+ };
857
+ /** 页码最多铺 7 个,当前页始终在窗口里。 */
858
+ const palacePageWindow = (page, pages) => {
859
+ const total = Math.max(1, Math.floor(Number.isFinite(pages) ? pages : 1));
860
+ const current = Math.min(total, Math.max(1, Math.floor(Number.isFinite(page) ? page : 1)));
861
+ if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1);
862
+ const start = Math.max(1, Math.min(current - 3, total - 6));
863
+ return Array.from({ length: 7 }, (_, index) => start + index);
864
+ };
865
+ const palaceToggleOf = (expanded, roomId) => (expanded === roomId ? '' : roomId);
866
+ const palaceMotionOf = (reduced) => ({ duration: reduced === true ? 0 : 180, easing: 'cubic-bezier(0.2, 0.7, 0.3, 1)' });
867
+
868
+ /** 失败回执分「当前」和「过去」:当前失败要修,过去的只说明发生过。 */
869
+ const failureStateOf = (evolution) => {
870
+ const rows = evolution && evolution.economy && Array.isArray(evolution.economy.recent) ? evolution.economy.recent : EMPTY;
871
+ if (rows.length === 0) return '';
872
+ if (rows[0] && rows[0].status === 'failed') return 'current';
873
+ return rows.slice(1).some((row) => row && row.status === 'failed') ? 'past' : '';
874
+ };
875
+
453
876
  /** 面板开关。 */
454
877
  const panelUi = { open: false, sessionId: '' };
455
878
  const panelListeners = new Set();
@@ -534,74 +957,124 @@ window.__ModuleLoader__.load({
534
957
 
535
958
  /**
536
959
  * 图谱只画真实关系:显式关联,以及记录自己的标签 / 主题。
537
- * 位置由 id 决定,同一份记忆每次打开都在同一处。
960
+ * 标签 / 主题是组:枢纽画大点,成员围在它周围画小点。
961
+ * 位置由 id 与成员关系决定,同一份数据每次打开都在同一处。
538
962
  */
539
- /** 位置由 id 决定;同一份数据每次打开都在同一处。 */
540
963
  const layoutGraph = (nodes, edges, primaryKind) => {
964
+ const kindOf = new Map(nodes.map((node) => [node.id, node.kind]));
541
965
  const degree = new Map();
966
+ const linkedOf = new Map();
967
+ const membersOf = new Map();
968
+ const addLink = (primaryId, hubId) => {
969
+ if (!linkedOf.has(primaryId)) linkedOf.set(primaryId, new Set());
970
+ linkedOf.get(primaryId).add(hubId);
971
+ if (!membersOf.has(hubId)) membersOf.set(hubId, new Set());
972
+ membersOf.get(hubId).add(primaryId);
973
+ };
542
974
  for (const edge of edges) {
543
975
  degree.set(edge.from, (degree.get(edge.from) || 0) + 1);
544
976
  degree.set(edge.to, (degree.get(edge.to) || 0) + 1);
977
+ if (kindOf.get(edge.from) === primaryKind && kindOf.get(edge.to) !== primaryKind) addLink(edge.from, edge.to);
978
+ if (kindOf.get(edge.to) === primaryKind && kindOf.get(edge.from) !== primaryKind) addLink(edge.to, edge.from);
545
979
  }
546
- const placed = new Map();
547
980
  const primaries = nodes.filter((node) => node.kind === primaryKind);
548
- const count = primaries.length;
549
- const spread = 0.55 + Math.min(0.45, count * 0.06);
550
- primaries.forEach((node, index) => {
551
- const seed = hashOf(node.id);
552
- const ring = count <= 1 ? 0 : count <= 8 ? 128 : count <= 18 ? 168 : 206;
553
- const wobble = count <= 8 ? 1 : 0.64 + ((seed % 100) / 100) * 0.36;
554
- const angle = count <= 1 ? 0 : (index / count) * Math.PI * 2 - Math.PI / 2 + ((seed % 17) / 17) * 0.16;
555
- placed.set(node.id, {
556
- ...node,
557
- x: 320 + Math.cos(angle) * ring * 1.22 * wobble * spread,
558
- y: 236 + Math.sin(angle) * ring * 0.8 * wobble * spread,
559
- radius: 5.5 + Math.min(4, (degree.get(node.id) || 0) * 0.7),
560
- });
561
- });
562
981
  const hubs = nodes.filter((node) => node.kind !== primaryKind);
563
- const kindOf = new Map(nodes.map((node) => [node.id, node.kind]));
564
- const linkedOf = new Map(hubs.map((node) => [node.id, new Set()]));
565
- for (const edge of edges) {
566
- if (linkedOf.has(edge.from) && kindOf.get(edge.to) === primaryKind) linkedOf.get(edge.from).add(edge.to);
567
- if (linkedOf.has(edge.to) && kindOf.get(edge.from) === primaryKind) linkedOf.get(edge.to).add(edge.from);
568
- }
982
+ const weightOf = new Map(hubs.map((hub) => [hub.id, (membersOf.get(hub.id) || EMPTY).size]));
569
983
  const groups = new Map();
570
- for (const node of hubs) {
571
- const linked = [...linkedOf.get(node.id)].sort();
572
- const key = linked.length > 0 ? linked.join('|') : `solo:${node.id}`;
573
- if (!groups.has(key)) groups.set(key, { linked, hubs: [] });
574
- groups.get(key).hubs.push(node);
984
+ const assignedHub = new Map();
985
+ const loose = [];
986
+ for (const node of primaries) {
987
+ const options = [...(linkedOf.get(node.id) || EMPTY)]
988
+ .filter((id) => (weightOf.get(id) || 0) > 1)
989
+ .sort((a, b) => ((weightOf.get(b) || 0) - (weightOf.get(a) || 0)) || a.localeCompare(b));
990
+ const hubId = options[0];
991
+ if (!hubId) { loose.push(node); continue; }
992
+ if (!groups.has(hubId)) groups.set(hubId, []);
993
+ groups.get(hubId).push(node);
994
+ assignedHub.set(node.id, hubId);
575
995
  }
576
- for (const group of groups.values()) {
577
- const points = group.linked.map((id) => placed.get(id)).filter(Boolean);
578
- const cx = points.length > 0 ? points.reduce((sum, point) => sum + point.x, 0) / points.length : 320;
579
- const cy = points.length > 0 ? points.reduce((sum, point) => sum + point.y, 0) / points.length : 236;
580
- const outward = Math.hypot(cx - 320, cy - 236);
581
- group.hubs.forEach((node, index) => {
996
+ const placed = new Map();
997
+ if (primaryKind === 'memory') {
998
+ for (const [hubId, members] of [...groups]) {
999
+ if (members.length > 1) continue;
1000
+ for (const member of members) {
1001
+ loose.push(member);
1002
+ assignedHub.delete(member.id);
1003
+ }
1004
+ groups.delete(hubId);
1005
+ }
1006
+ }
1007
+ const clusterList = [...groups.entries()]
1008
+ .sort((a, b) => (b[1].length - a[1].length) || a[0].localeCompare(b[0]));
1009
+ const groupCount = clusterList.length;
1010
+ const ring = groupCount <= 1 ? 0 : groupCount <= 4 ? 150 : groupCount <= 8 ? 182 : 212;
1011
+ const hubRadius = (size) => 8 + Math.min(10, Math.sqrt(size) * 3);
1012
+ const placePrimary = (node, x, y) => {
1013
+ placed.set(node.id, {
1014
+ ...node, x, y,
1015
+ radius: 3.2 + Math.min(2.2, (degree.get(node.id) || 0) * 0.35),
1016
+ });
1017
+ };
1018
+ clusterList.forEach(([hubId, members], index) => {
1019
+ const seed = hashOf(hubId);
1020
+ const angle = groupCount <= 1
1021
+ ? 0
1022
+ : (index / groupCount) * Math.PI * 2 - Math.PI / 2 + ((seed % 13) / 13) * 0.12;
1023
+ const cx = 320 + Math.cos(angle) * ring * 1.22;
1024
+ const cy = 236 + Math.sin(angle) * ring * 0.8;
1025
+ const hub = hubs.find((node) => node.id === hubId);
1026
+ const radius = hubRadius(members.length);
1027
+ placed.set(hubId, { ...hub, x: cx, y: cy, radius });
1028
+ const orbit = radius + 20 + Math.sqrt(members.length) * 13;
1029
+ const start = ((seed % 360) / 180) * Math.PI;
1030
+ members.forEach((node, memberIndex) => {
1031
+ const step = (memberIndex / members.length) * Math.PI * 2;
1032
+ const wobble = 0.88 + ((hashOf(node.id) % 25) / 100);
1033
+ placePrimary(node, cx + Math.cos(start + step) * orbit * wobble, cy + Math.sin(start + step) * orbit * 0.82 * wobble);
1034
+ });
1035
+ });
1036
+ if (loose.length > 0) {
1037
+ const orbit = groupCount === 0
1038
+ ? (loose.length <= 1 ? 0 : loose.length <= 8 ? 128 : loose.length <= 18 ? 168 : 206)
1039
+ : ring + 96;
1040
+ loose.forEach((node, index) => {
582
1041
  const seed = hashOf(node.id);
583
- const base = outward > 12
584
- ? Math.atan2(cy - 236, cx - 320)
585
- : ((seed % 360) / 180) * Math.PI;
586
- const angle = base + (index - (group.hubs.length - 1) / 2) * 0.6;
587
- const distance = group.linked.length >= 3 && group.hubs.length === 1 ? 0 : 48 + group.hubs.length * 4;
588
- placed.set(node.id, {
589
- ...node,
590
- x: cx + Math.cos(angle) * distance,
591
- y: cy + Math.sin(angle) * distance * 0.82,
592
- radius: node.kind === 'topic' ? 5 : 4,
593
- });
1042
+ const angle = loose.length <= 1
1043
+ ? 0
1044
+ : (index / loose.length) * Math.PI * 2 - Math.PI / 2 + ((seed % 17) / 17) * 0.14;
1045
+ const wobble = loose.length <= 8 ? 1 : 0.64 + ((seed % 100) / 100) * 0.36;
1046
+ placePrimary(node, 320 + Math.cos(angle) * orbit * 1.22 * wobble, 236 + Math.sin(angle) * orbit * 0.8 * wobble);
594
1047
  });
595
1048
  }
596
1049
  return {
597
1050
  nodes: [...placed.values()],
598
- edges: edges.filter((edge) => placed.has(edge.from) && placed.has(edge.to)),
599
- people: count,
1051
+ edges: edges.filter((edge) => {
1052
+ if (!placed.has(edge.from) || !placed.has(edge.to)) return false;
1053
+ const fromPrimary = kindOf.get(edge.from) === primaryKind;
1054
+ const toPrimary = kindOf.get(edge.to) === primaryKind;
1055
+ if (fromPrimary && toPrimary) return true;
1056
+ if (primaryKind !== 'memory') return true;
1057
+ if (fromPrimary) return assignedHub.get(edge.from) === edge.to;
1058
+ if (toPrimary) return assignedHub.get(edge.to) === edge.from;
1059
+ return false;
1060
+ }),
1061
+ people: primaries.length,
600
1062
  };
601
1063
  };
602
1064
 
603
- const graphOf = (records) => {
604
- const rows = (Array.isArray(records) ? records : EMPTY).slice(0, 60);
1065
+ /** 60 条投影之外也要保留当前选中项,点开的节点不能从图里消失。 */
1066
+ const projectionOf = (all, selectedId) => {
1067
+ const rows = all.slice(0, 60);
1068
+ if (selectedId && !rows.some((row) => row.id === selectedId)) {
1069
+ const selected = all.find((row) => row.id === selectedId);
1070
+ if (selected) rows[rows.length - 1] = selected;
1071
+ }
1072
+ return rows;
1073
+ };
1074
+
1075
+ const graphOf = (records, selectedId) => {
1076
+ const all = Array.isArray(records) ? records : EMPTY;
1077
+ const rows = projectionOf(all, selectedId);
605
1078
  const byId = new Map(rows.map((record) => [record.id, record]));
606
1079
  const tagCount = new Map();
607
1080
  const topicCount = new Map();
@@ -609,7 +1082,7 @@ window.__ModuleLoader__.load({
609
1082
  for (const tag of Array.isArray(record.tags) ? record.tags : EMPTY) tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
610
1083
  if (record.topicKey) topicCount.set(record.topicKey, (topicCount.get(record.topicKey) || 0) + 1);
611
1084
  }
612
- const tags = [...tagCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
1085
+ const tags = [...tagCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
613
1086
  const topics = [...topicCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1]).slice(0, 4);
614
1087
  const nodes = [];
615
1088
  const edges = [];
@@ -637,12 +1110,17 @@ window.__ModuleLoader__.load({
637
1110
  if (byId.has(link.id)) addEdge(record.id, link.id, 'link');
638
1111
  }
639
1112
  }
640
- return layoutGraph(nodes, edges, 'memory');
1113
+ return {
1114
+ ...layoutGraph(nodes, edges, 'memory'),
1115
+ shown: rows.length,
1116
+ total: all.length,
1117
+ };
641
1118
  };
642
1119
 
643
1120
  /** 经验图谱只连共享条件。 */
644
- const ruleGraphOf = (rules) => {
645
- const rows = (Array.isArray(rules) ? rules : EMPTY).slice(0, 60);
1121
+ const ruleGraphOf = (rules, selectedId) => {
1122
+ const all = Array.isArray(rules) ? rules : EMPTY;
1123
+ const rows = projectionOf(all, selectedId);
646
1124
  const conditionCount = new Map();
647
1125
  for (const rule of rows) {
648
1126
  const seen = new Set();
@@ -679,34 +1157,134 @@ window.__ModuleLoader__.load({
679
1157
  }
680
1158
  }
681
1159
  }
682
- return layoutGraph(nodes, edges, 'rule');
1160
+ return {
1161
+ ...layoutGraph(nodes, edges, 'rule'),
1162
+ shown: rows.length,
1163
+ total: all.length,
1164
+ };
683
1165
  };
684
1166
 
685
1167
  /** 关系图只画真实关系;节点沿用原始 id,位置由 id 决定。 */
1168
+ /** 图谱缩放:28px 命中区,悬停/聚焦才给底色。 */
1169
+ const ZoomButton = (props) => {
1170
+ const [hover, setHover] = React.useState(false);
1171
+ return h('button', {
1172
+ type: 'button', onClick: props.onClick, disabled: props.disabled,
1173
+ 'aria-label': props.label, title: props.label,
1174
+ onMouseEnter: () => setHover(true),
1175
+ onMouseLeave: () => setHover(false),
1176
+ onFocus: () => setHover(true),
1177
+ onBlur: () => setHover(false),
1178
+ style: {
1179
+ width: '28px', height: '28px', padding: 0, display: 'inline-flex',
1180
+ alignItems: 'center', justifyContent: 'center', border: 'none',
1181
+ borderRadius: '8px', background: hover && !props.disabled ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1182
+ color: 'inherit', opacity: props.disabled ? 0.35 : hover ? 0.95 : 0.72,
1183
+ cursor: props.disabled ? 'default' : 'pointer',
1184
+ },
1185
+ }, props.children);
1186
+ };
1187
+
686
1188
  const MemoryGraph = (props) => {
687
1189
  const primaryKind = props.primaryKind || 'memory';
688
- const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records), [props.records, props.graphOf]);
1190
+ const selectedId = props.selectedId;
1191
+ const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records, selectedId), [props.records, props.graphOf, selectedId]);
689
1192
  const [zoom, setZoom] = React.useState(1);
1193
+ const [pan, setPan] = React.useState({ x: 0, y: 0 });
1194
+ const [dragging, setDragging] = React.useState(false);
690
1195
  const [hoveredId, setHoveredId] = React.useState('');
691
- const selectedId = props.selectedId;
1196
+ const svgRef = React.useRef(null);
1197
+ const dragRef = React.useRef(null);
692
1198
  const labelSet = graph.people <= 40;
1199
+ const hubCount = graph.nodes.length - graph.people;
1200
+ const shownCount = Number.isInteger(graph.shown) ? graph.shown : graph.people;
1201
+ const totalCount = Number.isInteger(props.total) && props.total >= shownCount
1202
+ ? props.total
1203
+ : (Number.isInteger(graph.total) ? graph.total : shownCount);
1204
+ const truncated = totalCount > shownCount;
1205
+ const nearIds = React.useMemo(() => {
1206
+ const set = new Set();
1207
+ if (!selectedId) return set;
1208
+ set.add(selectedId);
1209
+ for (const edge of graph.edges) {
1210
+ if (edge.from === selectedId) set.add(edge.to);
1211
+ if (edge.to === selectedId) set.add(edge.from);
1212
+ }
1213
+ return set;
1214
+ }, [graph.edges, selectedId]);
1215
+ const orderedEdges = selectedId
1216
+ ? [...graph.edges].sort((a, b) => Number(a.from === selectedId || a.to === selectedId) - Number(b.from === selectedId || b.to === selectedId))
1217
+ : graph.edges;
1218
+ const orderedNodes = selectedId
1219
+ ? [...graph.nodes].sort((a, b) => (Number(nearIds.has(a.id)) - Number(nearIds.has(b.id))) || (Number(a.id === selectedId) - Number(b.id === selectedId)))
1220
+ : graph.nodes;
693
1221
  const pickHub = props.onPickHub;
694
- const stepZoom = (delta) => setZoom((value) => Math.min(2.5, Math.max(0.75, Math.round((value + delta) * 100) / 100)));
1222
+ const MIN_ZOOM = 0.2;
1223
+ const MAX_ZOOM = 2.5;
1224
+ const stepZoom = (delta) => setZoom((value) => {
1225
+ const next = delta > 0 ? value * 1.25 : value / 1.25;
1226
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.round(next * 100) / 100));
1227
+ });
1228
+ const pointAt = (event, matrix) => {
1229
+ const svg = svgRef.current;
1230
+ if (!svg || !matrix) return null;
1231
+ const point = svg.createSVGPoint();
1232
+ point.x = event.clientX;
1233
+ point.y = event.clientY;
1234
+ return point.matrixTransform(matrix);
1235
+ };
1236
+ const startPan = (event) => {
1237
+ if (event.button !== 0) return;
1238
+ if (event.target && event.target.closest && event.target.closest('[data-node]')) return;
1239
+ const svg = svgRef.current;
1240
+ if (!svg) return;
1241
+ const ctm = svg.getScreenCTM();
1242
+ if (!ctm) return;
1243
+ const inverse = ctm.inverse();
1244
+ const point = pointAt(event, inverse);
1245
+ if (!point) return;
1246
+ dragRef.current = [event.pointerId, inverse, point.x, point.y, pan.x, pan.y];
1247
+ setDragging(true);
1248
+ if (svg.setPointerCapture) svg.setPointerCapture(event.pointerId);
1249
+ event.preventDefault();
1250
+ };
1251
+ const movePan = (event) => {
1252
+ const drag = dragRef.current;
1253
+ if (!drag || drag[0] !== event.pointerId) return;
1254
+ const point = pointAt(event, drag[1]);
1255
+ if (!point) return;
1256
+ setPan({ x: drag[4] - (point.x - drag[2]), y: drag[5] - (point.y - drag[3]) });
1257
+ event.preventDefault();
1258
+ };
1259
+ const endPan = (event) => {
1260
+ const drag = dragRef.current;
1261
+ if (!drag || (event && drag[0] !== event.pointerId)) return;
1262
+ dragRef.current = null;
1263
+ setDragging(false);
1264
+ const svg = svgRef.current;
1265
+ if (event && svg && svg.releasePointerCapture && svg.hasPointerCapture && svg.hasPointerCapture(event.pointerId)) {
1266
+ svg.releasePointerCapture(event.pointerId);
1267
+ }
1268
+ };
1269
+ React.useEffect(() => {
1270
+ setPan({ x: 0, y: 0 });
1271
+ }, [props.records, primaryKind]);
695
1272
  const frame = React.useMemo(() => {
696
1273
  const list = graph.nodes;
697
1274
  if (list.length === 0) return { x: 0, y: 0, width: 640, height: 472 };
698
1275
  let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity;
699
1276
  for (const node of list) {
700
- const half = node.kind === primaryKind ? 62 : 38;
1277
+ const wide = node.kind === primaryKind && (graph.people <= 12 || hubCount === 0 || node.id === selectedId);
1278
+ const half = node.kind === primaryKind ? (wide ? 62 : 54) : 38;
701
1279
  minX = Math.min(minX, node.x - half);
702
1280
  maxX = Math.max(maxX, node.x + half);
703
1281
  minY = Math.min(minY, node.y - 16);
704
- maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? 28 : 24));
1282
+ maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? (wide ? 30 : 28) : 26));
705
1283
  }
706
1284
  const width = Math.max(380, maxX - minX + 36);
707
1285
  const height = Math.max(250, maxY - minY + 36);
708
1286
  return { x: (minX + maxX) / 2 - width / 2, y: (minY + maxY) / 2 - height / 2, width, height };
709
- }, [graph, primaryKind]);
1287
+ }, [graph, primaryKind, selectedId]);
710
1288
  if (!props.records || props.records.length === 0) {
711
1289
  return h('div', {
712
1290
  style: { flex: 1, minHeight: '220px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '12px' },
@@ -714,102 +1292,184 @@ window.__ModuleLoader__.load({
714
1292
  }
715
1293
  const viewWidth = frame.width / zoom;
716
1294
  const viewHeight = frame.height / zoom;
717
- const zoomButton = (label, delta, icon) => h('button', {
718
- type: 'button', onClick: () => stepZoom(delta),
719
- disabled: delta > 0 ? zoom >= 2.5 : zoom <= 0.75,
720
- 'aria-label': label, title: label,
721
- style: {
722
- width: '26px', height: '26px', padding: 0, display: 'inline-flex',
723
- alignItems: 'center', justifyContent: 'center', border: 'none',
724
- borderRadius: '6px', background: 'transparent', color: 'inherit',
725
- opacity: delta > 0 ? (zoom >= 2.5 ? 0.28 : 0.68) : (zoom <= 0.75 ? 0.28 : 0.68),
726
- cursor: 'pointer',
727
- },
1295
+ const zoomButton = (label, delta, icon) => h(ZoomButton, {
1296
+ key: label, label, disabled: delta > 0 ? zoom >= MAX_ZOOM : zoom <= MIN_ZOOM,
1297
+ onClick: () => stepZoom(delta),
728
1298
  }, icon);
729
1299
  const magnifier = (sign) => h('svg', {
730
1300
  width: '15', height: '15', viewBox: '0 0 16 16', fill: 'none',
731
- stroke: 'currentColor', strokeWidth: '1.4', strokeLinecap: 'round',
1301
+ stroke: 'currentColor', 'stroke-width': '1.4', 'stroke-linecap': 'round',
732
1302
  'aria-hidden': 'true',
733
1303
  },
734
1304
  h('circle', { cx: '6.8', cy: '6.8', r: '4.5' }),
735
1305
  h('line', { x1: '10.1', y1: '10.1', x2: '14', y2: '14' }),
736
1306
  h('line', { x1: '4.8', y1: '6.8', x2: '8.8', y2: '6.8' }),
737
1307
  sign > 0 ? h('line', { x1: '6.8', y1: '4.8', x2: '6.8', y2: '8.8' }) : null);
1308
+ /** 标签避让:hub 常显、选中/悬停优先,记忆标签按度数贪心排布,重叠就省掉(悬停仍出标题)。 */
1309
+ const labelLayout = React.useMemo(() => {
1310
+ const nodes = graph.nodes;
1311
+ const fsOf = (primary) => (primary ? 10.5 : 10);
1312
+ const widthOf = (text, fs) => {
1313
+ let width = 0;
1314
+ for (const ch of text) width += ch.charCodeAt(0) > 255 ? fs : fs * 0.56;
1315
+ return width;
1316
+ };
1317
+ const rank = (node) => (node.id === selectedId ? 0 : node.id === hoveredId ? 1
1318
+ : nearIds.has(node.id) ? 2 : node.kind === primaryKind ? 4 : 3);
1319
+ const boxes = [];
1320
+ const placed = new Map();
1321
+ const hits = (box) => boxes.some((other) => !(box.x2 < other.x1 || box.x1 > other.x2 || box.y2 < other.y1 || box.y1 > other.y2));
1322
+ const list = nodes.slice().sort((a, b) => rank(a) - rank(b) || (b.degree || 0) - (a.degree || 0));
1323
+ /** 条目多时不全标,用度数最高的十条记忆给方向感。 */
1324
+ const topIds = new Set(list.filter((node) => node.kind === primaryKind).slice(0, 10).map((node) => node.id));
1325
+ for (const node of list) {
1326
+ const isPrimary = node.kind === primaryKind;
1327
+ if (isPrimary && !labelSet && rank(node) > 2 && !topIds.has(node.id)) continue;
1328
+ const chars = rank(node) <= 1 ? 16 : isPrimary ? (labelSet ? 10 : 8) : 13;
1329
+ const text = SHORT(node.label, chars);
1330
+ const fs = fsOf(isPrimary);
1331
+ const width = widthOf(text, fs);
1332
+ const gap = isPrimary ? 12 : 13;
1333
+ let spot = null;
1334
+ for (const below of [true, false]) {
1335
+ const baseline = below ? node.y + node.radius + gap : node.y - node.radius - gap + fs;
1336
+ const box = { x1: node.x - width / 2 - 3, x2: node.x + width / 2 + 3, y1: baseline - fs - 2, y2: baseline + 3 };
1337
+ if (box.y1 < 3 || box.y2 > graph.height - 3) continue;
1338
+ if (hits(box)) continue;
1339
+ spot = { box, baseline, below };
1340
+ break;
1341
+ }
1342
+ if (!spot) continue;
1343
+ boxes.push(spot.box);
1344
+ placed.set(node.id, { text, isPrimary, baseline: spot.baseline, fs, width });
1345
+ }
1346
+ return placed;
1347
+ }, [graph, primaryKind, selectedId, hoveredId, labelSet, nearIds]);
738
1348
  return h('div', { style: { position: 'relative', flex: 1, minHeight: 0, display: 'flex' } },
739
1349
  h('div', {
740
1350
  style: {
741
- position: 'absolute', top: '6px', right: '6px', zIndex: 2,
742
- display: 'flex', alignItems: 'center', gap: '1px', padding: '2px',
1351
+ position: 'absolute', top: '8px', right: '8px', zIndex: 2,
1352
+ display: 'flex', alignItems: 'center', gap: '2px', padding: '2px',
743
1353
  border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
744
1354
  background: 'var(--dsw-alias-bg-layer-1)',
745
1355
  },
746
1356
  },
747
1357
  zoomButton('缩小图谱', -0.25, magnifier(-1)),
748
- zoomButton('放大图谱', 0.25, magnifier(1))),
1358
+ zoomButton('放大图谱', 0.25, magnifier(1)),
1359
+ h('button', {
1360
+ type: 'button', onClick: () => setZoom(1),
1361
+ 'aria-label': '重置缩放', title: '重置缩放',
1362
+ style: {
1363
+ minWidth: '44px', height: '28px', padding: '0 6px', border: 'none', borderRadius: '8px',
1364
+ background: 'transparent', color: 'var(--dsw-alias-label-secondary)',
1365
+ fontSize: '11px', lineHeight: '28px', fontVariantNumeric: 'tabular-nums', cursor: 'pointer',
1366
+ },
1367
+ }, `${Math.round(zoom * 100)}%`)),
749
1368
  h('svg', {
750
- viewBox: `${frame.x + frame.width / 2 - viewWidth / 2} ${frame.y + frame.height / 2 - viewHeight / 2} ${viewWidth} ${viewHeight}`,
1369
+ ref: svgRef,
1370
+ viewBox: `${frame.x + frame.width / 2 - viewWidth / 2 + pan.x} ${frame.y + frame.height / 2 - viewHeight / 2 + pan.y} ${viewWidth} ${viewHeight}`,
751
1371
  preserveAspectRatio: 'xMidYMid meet',
752
- style: { width: '100%', height: '100%', flex: 1, minHeight: 0, display: 'block' },
1372
+ style: {
1373
+ width: '100%', height: '100%', flex: 1, minHeight: 0, display: 'block',
1374
+ cursor: dragging ? 'grabbing' : 'grab', touchAction: 'none',
1375
+ userSelect: 'none', WebkitUserSelect: 'none',
1376
+ },
1377
+ onPointerDown: startPan,
1378
+ onPointerMove: movePan,
1379
+ onPointerUp: endPan,
1380
+ onPointerCancel: endPan,
1381
+ onLostPointerCapture: endPan,
1382
+ onDragStart: (event) => event.preventDefault(),
753
1383
  role: 'img', 'aria-label': props.ariaLabel || '关系图',
754
1384
  },
755
- h('g', null, graph.edges.map((edge, index) => {
1385
+ h('g', null, orderedEdges.map((edge, index) => {
756
1386
  const from = graph.nodes.find((node) => node.id === edge.from);
757
1387
  const to = graph.nodes.find((node) => node.id === edge.to);
758
1388
  if (!from || !to) return null;
759
1389
  const linked = selectedId && (edge.from === selectedId || edge.to === selectedId);
1390
+ const dimmed = selectedId && !nearIds.has(edge.from) && !nearIds.has(edge.to);
760
1391
  return h('line', {
761
1392
  key: `e${index}`, x1: from.x, y1: from.y, x2: to.x, y2: to.y,
762
1393
  stroke: linked ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)',
763
- strokeWidth: linked ? 1.4 : 1,
764
- opacity: linked ? 0.65 : edge.type === 'link' ? 0.38 : 0.16,
765
- strokeDasharray: edge.type === 'link' ? undefined : '2 4',
1394
+ 'stroke-width': linked ? 1.4 : 1,
1395
+ opacity: linked ? 0.72 : dimmed ? 0.08 : edge.type === 'link' ? 0.38 : 0.16,
1396
+ 'stroke-dasharray': edge.type === 'link' ? undefined : '2 4',
766
1397
  });
767
1398
  })),
768
- h('g', null, graph.nodes.map((node) => {
1399
+ h('g', null, orderedNodes.map((node) => {
769
1400
  const isPrimary = node.kind === primaryKind;
770
1401
  const active = node.id === selectedId;
771
1402
  const hovered = node.id === hoveredId;
1403
+ const near = nearIds.has(node.id);
1404
+ const dimmed = Boolean(selectedId) && !near;
772
1405
  const pick = isPrimary ? props.onPick : (pickHub || undefined);
773
- const fill = !isPrimary ? 'var(--dsw-alias-label-secondary)'
774
- : node.state ? (STAGE_TONE[node.state] || 'var(--dsw-alias-brand-primary)')
775
- : node.experimental ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-brand-primary)';
776
- const showLabel = active || hovered || (isPrimary && labelSet) || !isPrimary;
777
- const nameChars = active || hovered ? 14 : isPrimary ? 7 : 10;
1406
+ /** 记忆是主体:实心、可读;主题/标签 hub 是背景:空心、只留轮廓。 */
1407
+ const fill = active ? 'var(--dsw-alias-brand-primary)'
1408
+ : isPrimary ? 'var(--dsw-alias-label-secondary)'
1409
+ : 'none';
1410
+ const stroke = active ? 'none'
1411
+ : hovered || near ? 'var(--dsw-alias-brand-primary)'
1412
+ : node.experimental ? 'var(--dsw-alias-state-warn-primary)'
1413
+ : isPrimary ? 'none' : 'var(--dsw-alias-label-secondary)';
1414
+ const strokeWidth = !active && (hovered || near) ? 1.3 : node.experimental ? 1 : isPrimary ? 0 : 1;
1415
+ const label = labelLayout.get(node.id) || null;
778
1416
  return h('g', {
779
1417
  key: node.id,
1418
+ 'data-node': 'true',
780
1419
  transform: `translate(${node.x} ${node.y})`,
781
1420
  onClick: pick ? () => pick(node) : undefined,
1421
+ onKeyDown: pick ? (event) => {
1422
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
1423
+ event.preventDefault();
1424
+ pick(node);
1425
+ }
1426
+ } : undefined,
1427
+ onFocus: () => setHoveredId(node.id),
1428
+ onBlur: () => setHoveredId((value) => (value === node.id ? '' : value)),
782
1429
  onMouseEnter: () => setHoveredId(node.id),
783
1430
  onMouseLeave: () => setHoveredId((value) => (value === node.id ? '' : value)),
784
- style: { cursor: pick ? 'pointer' : 'default' },
1431
+ tabIndex: pick ? 0 : undefined,
1432
+ role: pick ? 'button' : undefined,
1433
+ focusable: pick ? 'true' : 'false',
1434
+ 'aria-label': pick ? (node.title || node.label) : undefined,
1435
+ style: { cursor: pick ? 'pointer' : 'default', outline: 'none' },
785
1436
  },
786
- h('circle', { r: Math.max(12, node.radius + 6), fill: 'transparent' }),
787
- active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', strokeWidth: 1.4, opacity: 0.5 }) : null,
1437
+ h('circle', { r: Math.max(isPrimary ? 14 : 18, node.radius + 6), fill: 'transparent' }),
1438
+ active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', 'stroke-width': 1.4, opacity: 0.45 }) : null,
788
1439
  h('circle', {
789
1440
  r: node.radius, fill,
790
- opacity: active ? 1 : isPrimary ? 0.9 : 0.5,
791
- stroke: hovered ? 'var(--dsw-alias-brand-primary)' : 'none',
792
- strokeWidth: hovered ? 1.2 : 0,
1441
+ opacity: active ? 1 : dimmed ? (isPrimary ? 0.32 : 0.14) : isPrimary ? 0.92 : 0.34,
1442
+ stroke, 'stroke-width': strokeWidth,
793
1443
  }),
794
- showLabel ? h('text', {
795
- y: node.radius + 12, textAnchor: 'middle',
796
- fill: active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
797
- fontSize: isPrimary ? 9.5 : 8.5, opacity: active ? 0.95 : 0.72,
798
- paintOrder: 'stroke', stroke: 'var(--dsw-alias-bg-layer-1)', strokeWidth: 3,
799
- strokeLinejoin: 'round',
800
- }, SHORT(node.label, nameChars)) : null,
1444
+ label ? h('text', {
1445
+ y: label.baseline, 'text-anchor': 'middle',
1446
+ fill: label.isPrimary ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
1447
+ 'font-size': label.fs,
1448
+ 'font-weight': 500,
1449
+ opacity: active ? 1 : dimmed ? 0.2 : label.isPrimary ? 0.9 : 0.72,
1450
+ stroke: 'var(--dsw-alias-bg-layer-1)', 'stroke-width': 2.6, 'stroke-linejoin': 'round', 'paint-order': 'stroke',
1451
+ }, label.text) : null,
801
1452
  h('title', null, node.title || node.label),
802
1453
  );
803
1454
  }))),
804
1455
  h('div', {
805
1456
  style: {
806
1457
  position: 'absolute', left: '10px', bottom: '8px', zIndex: 1,
807
- maxWidth: 'calc(100% - 60px)', padding: '2px 6px', borderRadius: '6px',
1458
+ maxWidth: truncated ? 'calc(100% - 178px)' : 'calc(100% - 60px)', padding: '2px 6px', borderRadius: '6px',
808
1459
  background: 'var(--dsw-alias-bg-layer-1)',
809
1460
  color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
810
1461
  pointerEvents: 'none',
811
1462
  },
812
- }, props.legend || '圆点 记忆 · 小点 标签/主题 · 虚线 关联'),
1463
+ }, props.legend || '实心 记忆 · 空心 标签/主题 · 虚线 归类'),
1464
+ truncated ? h('div', {
1465
+ style: {
1466
+ position: 'absolute', right: '10px', bottom: '8px', zIndex: 1,
1467
+ padding: '2px 6px', borderRadius: '6px',
1468
+ background: 'var(--dsw-alias-bg-layer-1)',
1469
+ color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
1470
+ pointerEvents: 'none', whiteSpace: 'nowrap',
1471
+ },
1472
+ }, `显示 ${shownCount} / 共 ${totalCount} 条`) : null,
813
1473
  );
814
1474
  };
815
1475
 
@@ -826,7 +1486,7 @@ window.__ModuleLoader__.load({
826
1486
  style: {
827
1487
  position: 'absolute', top: '2px', left: props.on ? '18px' : '2px', width: '16px', height: '16px',
828
1488
  borderRadius: '999px', background: props.on ? 'var(--dsw-alias-bg-base)' : 'var(--dsw-alias-label-secondary)',
829
- transition: 'left 120ms ease',
1489
+ transition: reduceMotionOf() ? 'none' : 'left 120ms ease',
830
1490
  },
831
1491
  }));
832
1492
 
@@ -896,22 +1556,41 @@ window.__ModuleLoader__.load({
896
1556
  const evidenceTextOf = (row) => {
897
1557
  if (typeof row === 'string') return row;
898
1558
  if (!row || typeof row !== 'object') return '';
899
- return typeof row.text === 'string' ? row.text : (typeof row.note === 'string' ? row.note : '');
1559
+ for (const key of ['sourceExcerpt', 'excerpt', 'text', 'note']) {
1560
+ if (typeof row[key] === 'string' && row[key].trim()) return row[key];
1561
+ }
1562
+ return '';
1563
+ };
1564
+ /** 样例和证据都带版本/核对信息,不拿「样例」二字占位。 */
1565
+ const evidenceMetaOf = (row) => {
1566
+ if (!row || typeof row !== 'object') return '';
1567
+ const bits = [];
1568
+ if (Number.isInteger(row.artifactVersion)) bits.push(`第 ${row.artifactVersion} 版`);
1569
+ else if (typeof row.artifactVersion === 'string' && row.artifactVersion) bits.push(`第 ${row.artifactVersion} 版`);
1570
+ if (typeof row.dimension === 'string' && row.dimension) bits.push(row.dimension);
1571
+ if (row.verified === true) bits.push('已核对');
1572
+ else if (row.verified === false) bits.push('未核对');
1573
+ if (row.at) bits.push(EVO_DATE(row.at));
1574
+ return bits.join(' · ');
900
1575
  };
901
1576
 
902
1577
  const MemoryRow = (props) => {
903
1578
  const record = props.record;
1579
+ const [hover, setHover] = React.useState(false);
904
1580
  return h('button', {
905
1581
  type: 'button', onClick: props.onClick,
1582
+ onMouseEnter: () => setHover(true),
1583
+ onMouseLeave: () => setHover(false),
906
1584
  style: {
907
- display: 'flex', flexDirection: 'column', gap: '2px', width: '100%', textAlign: 'left',
908
- border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)',
909
- background: props.active ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
910
- color: 'inherit', padding: '8px 7px', cursor: 'pointer', minWidth: 0,
1585
+ display: 'flex', flexDirection: 'column', gap: 0, width: '100%', textAlign: 'left',
1586
+ border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)', borderRadius: '6px',
1587
+ borderLeft: `2px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'transparent'}`,
1588
+ background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1589
+ color: 'inherit', padding: '4px 8px', cursor: 'pointer', minWidth: 0,
911
1590
  },
912
1591
  },
913
- h('span', { style: { fontSize: '12px', lineHeight: '1.45', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.92 } }, record.title || record.id),
914
- h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0 } },
1592
+ h('span', { style: { fontSize: '12px', lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, record.title || record.id),
1593
+ h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0, lineHeight: '14px' } },
915
1594
  h('span', null, KIND_LABEL[record.kind] || record.kind),
916
1595
  record.experimental ? h('span', { style: { color: 'var(--dsw-alias-state-warn-primary)' } }, '实验') : null,
917
1596
  h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(record.updated)),
@@ -919,6 +1598,29 @@ window.__ModuleLoader__.load({
919
1598
  );
920
1599
  };
921
1600
 
1601
+ /** 一条经验:状态用小标签,标题独占一行。 */
1602
+ const EvolutionRow = (props) => {
1603
+ const rule = props.rule;
1604
+ const [hover, setHover] = React.useState(false);
1605
+ return h('button', {
1606
+ type: 'button', onClick: props.onClick,
1607
+ onMouseEnter: () => setHover(true),
1608
+ onMouseLeave: () => setHover(false),
1609
+ style: {
1610
+ display: 'flex', flexDirection: 'column', gap: '1px', width: '100%', textAlign: 'left',
1611
+ border: 'none', borderRadius: '8px', minWidth: 0,
1612
+ borderLeft: `2px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'transparent'}`,
1613
+ background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1614
+ color: 'inherit', padding: '6px 9px', cursor: 'pointer',
1615
+ },
1616
+ },
1617
+ h('span', { style: { fontSize: '12px', lineHeight: '18px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, rule.title),
1618
+ h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, lineHeight: '16px' } },
1619
+ h(StagePill, { state: rule.state }),
1620
+ h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
1621
+ ));
1622
+ };
1623
+
922
1624
  const EvolutionDetail = (props) => {
923
1625
  const rule = props.rule;
924
1626
  const ruleId = rule ? rule.id : '';
@@ -941,8 +1643,12 @@ window.__ModuleLoader__.load({
941
1643
  const versions = Array.isArray(rule.learning?.versions) ? rule.learning.versions : EMPTY;
942
1644
  const history = [
943
1645
  ...checkpoints.map((row) => ({ at: row.at || row.updated || '', text: typeof row.text === 'string' ? row.text : '' })),
944
- ...versions.map((row) => ({ at: row.at || row.updated || '', text: typeof row.title === 'string' ? row.title : '修改' })),
1646
+ ...versions.map((row, index) => ({
1647
+ at: row.at || row.updated || '',
1648
+ text: `第 ${versions.length - index} 版${row.body ? ` ${SHORT(row.body, 42)}` : row.digest ? ` ${SHORT(String(row.digest), 16)}` : ''}`,
1649
+ })),
945
1650
  ].filter((row) => row.text).sort((a, b) => String(b.at).localeCompare(String(a.at))).slice(0, 6);
1651
+ const failureState = failureStateOf(props.evolution);
946
1652
  const canManage = props.canManage && !props.busy;
947
1653
  const act = (name, extra) => { if (canManage) props.onManage(name, extra || {}); };
948
1654
  const actions = props.canManage
@@ -994,8 +1700,24 @@ window.__ModuleLoader__.load({
994
1700
  evidence.length === 0 && samples.length === 0
995
1701
  ? '暂无学习依据'
996
1702
  : [
997
- ...evidence.slice(0, 3).map((row, index) => h('div', { key: `e${index}` }, evidenceTextOf(row))),
998
- ...samples.slice(0, 2).map((row, index) => h('div', { key: `s${index}` }, evidenceTextOf(row) || '样例')),
1703
+ ...evidence.slice(0, 3).map((row, index) => {
1704
+ const text = evidenceTextOf(row);
1705
+ const meta = evidenceMetaOf(row);
1706
+ return text
1707
+ ? h('div', { key: `e${index}`, style: { display: 'flex', flexDirection: 'column', gap: '1px' } },
1708
+ h('span', { style: { color: 'var(--dsw-alias-label-primary)', opacity: 0.8, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, text),
1709
+ meta ? h('span', null, meta) : null)
1710
+ : null;
1711
+ }),
1712
+ ...samples.slice(0, 2).map((row, index) => {
1713
+ const text = evidenceTextOf(row);
1714
+ const meta = evidenceMetaOf(row);
1715
+ return text || meta
1716
+ ? h('div', { key: `s${index}`, style: { display: 'flex', flexDirection: 'column', gap: '1px' } },
1717
+ text ? h('span', { style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, text) : null,
1718
+ meta ? h('span', null, meta) : null)
1719
+ : null;
1720
+ }),
999
1721
  ].filter(Boolean))
1000
1722
  : null;
1001
1723
  const historyPane = open === 'history'
@@ -1023,6 +1745,10 @@ window.__ModuleLoader__.load({
1023
1745
  h(StagePill, { state: rule.state }),
1024
1746
  ),
1025
1747
  h('div', { style: { fontSize: '12px', lineHeight: '1.55', opacity: 0.82 } }, usabilityOf(rule, props.evolution, props.memoryOn)),
1748
+ failureState
1749
+ ? h('div', { style: { ...PANEL_MUTED, color: failureState === 'current' ? 'var(--dsw-alias-state-warn-primary)' : undefined } },
1750
+ failureState === 'current' ? '最近一次整理失败,现有经验未更新' : '以前有整理失败,最近一次已完成')
1751
+ : null,
1026
1752
  actions,
1027
1753
  ),
1028
1754
  editor,
@@ -1035,6 +1761,178 @@ window.__ModuleLoader__.load({
1035
1761
  );
1036
1762
  };
1037
1763
 
1764
+
1765
+ const PalaceSlot = (props) => {
1766
+ const record = props.record;
1767
+ if (!record) {
1768
+ return h('div', {
1769
+ 'aria-hidden': 'true',
1770
+ style: { height: '46px', border: '1px dashed var(--dsw-alias-border-l1)', borderRadius: '8px', opacity: 0.28 },
1771
+ });
1772
+ }
1773
+ const title = String(record.title || record.id || '记忆');
1774
+ return h('button', {
1775
+ type: 'button', title, 'aria-label': title,
1776
+ onClick: () => props.onPick(record.id),
1777
+ style: {
1778
+ height: '46px', minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center',
1779
+ padding: '4px 7px', textAlign: 'left', cursor: 'pointer',
1780
+ border: `1px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-border-l1)'}`,
1781
+ borderRadius: '8px', color: 'inherit',
1782
+ background: props.active ? 'var(--dsw-alias-bg-layer-2)' : 'var(--dsw-alias-bg-base)',
1783
+ },
1784
+ },
1785
+ h('span', {
1786
+ style: {
1787
+ fontSize: '11px', lineHeight: '15px', height: '30px', overflow: 'hidden',
1788
+ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', wordBreak: 'break-word',
1789
+ },
1790
+ }, title),
1791
+ );
1792
+ };
1793
+
1794
+ const PalaceRoomCard = (props) => {
1795
+ const room = props.room;
1796
+ const empty = room.total === 0;
1797
+ const preview = room.items.slice(0, PALACE_PREVIEW);
1798
+ const slots = Array.from({ length: PALACE_PREVIEW }, (_, index) => preview[index] || null);
1799
+ return h('div', {
1800
+ role: 'listitem',
1801
+ style: { ...PANEL_CARD, padding: 0, position: 'relative', overflow: 'hidden', display: 'flex', width: '100%', flexDirection: 'column' },
1802
+ },
1803
+ h('div', { style: { position: 'relative', display: 'flex', flexDirection: 'column', gap: '8px', padding: '9px 10px 10px', boxSizing: 'border-box' } },
1804
+ h('button', {
1805
+ type: 'button', 'aria-expanded': empty ? undefined : props.expanded, 'aria-controls': empty ? undefined : `lite-palace-room-${room.id}`,
1806
+ disabled: empty,
1807
+ onClick: () => { if (!empty) props.onToggle(room.id); },
1808
+ style: { ...PANEL_LINK, width: '100%', display: 'flex', alignItems: 'center', gap: '7px', textAlign: 'left', opacity: 1 },
1809
+ },
1810
+ h('span', { style: { fontSize: '12px', lineHeight: '18px', fontWeight: 600, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, room.name),
1811
+ h('span', { style: { ...PANEL_MUTED, flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, `${room.total} 条`),
1812
+ empty ? null : h('span', { style: { ...PANEL_MUTED, flex: '0 0 auto', marginLeft: 'auto', opacity: 0.6 } }, props.expanded ? '收起' : '展开'),
1813
+ ),
1814
+ empty
1815
+ ? h('span', { style: { ...PANEL_MUTED, padding: '2px 0 4px' } }, '还没有记录')
1816
+ : h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))', gap: '6px' } },
1817
+ slots.map((record, index) => h(PalaceSlot, {
1818
+ key: record ? record.id : `slot-${room.id}-${index}`,
1819
+ record, active: Boolean(record && record.id === props.selectedId), onPick: props.onPick,
1820
+ }))),
1821
+ ),
1822
+ );
1823
+ };
1824
+
1825
+ const MemoryPalace = (props) => {
1826
+ const rows = Array.isArray(props.records) ? props.records : EMPTY;
1827
+ const [expanded, setExpanded] = React.useState('');
1828
+ const [page, setPage] = React.useState(1);
1829
+ const panelRef = React.useRef(null);
1830
+ const reduced = useReducedMotion();
1831
+ const motion = palaceMotionOf(reduced);
1832
+ const rooms = React.useMemo(() => palaceRoomsOf(rows), [rows]);
1833
+ const activeRoom = rooms.find((room) => room.id === expanded) || null;
1834
+ const pageInfo = activeRoom ? palaceRoomPageOf(rows, activeRoom.id, page) : null;
1835
+
1836
+ React.useEffect(() => {
1837
+ if (expanded && !rooms.some((room) => room.id === expanded)) setExpanded('');
1838
+ }, [rooms, expanded]);
1839
+ React.useEffect(() => {
1840
+ const node = panelRef.current;
1841
+ if (!expanded || !node || motion.duration === 0 || typeof node.animate !== 'function') return;
1842
+ node.animate([
1843
+ { opacity: 0, transform: 'translateY(-6px)' },
1844
+ { opacity: 1, transform: 'translateY(0)' },
1845
+ ], { duration: motion.duration, easing: motion.easing });
1846
+ }, [expanded, page, motion.duration, motion.easing]);
1847
+
1848
+ if (rows.length === 0) {
1849
+ return h('div', { style: { ...PANEL_CARD, padding: '14px', minHeight: 0 } },
1850
+ h('div', { style: PANEL_MUTED }, props.emptyText || '这个项目还没有记忆'));
1851
+ }
1852
+ const toggle = (roomId) => {
1853
+ setExpanded((current) => palaceToggleOf(current, roomId));
1854
+ setPage(1);
1855
+ };
1856
+ const goPage = (target) => {
1857
+ if (!pageInfo) return;
1858
+ setPage(Math.min(pageInfo.pages, Math.max(1, target)));
1859
+ };
1860
+ const pageButton = (label, target, disabled, current) => h('button', {
1861
+ key: `page-${target}-${label}`, type: 'button', disabled,
1862
+ 'aria-current': current ? 'page' : undefined, 'aria-label': `第 ${target} 页`,
1863
+ onClick: () => goPage(target),
1864
+ style: {
1865
+ ...PANEL_LINK, minWidth: '22px', height: '22px', padding: '0 5px', borderRadius: '6px',
1866
+ border: 'none', opacity: disabled ? 0.28 : current ? 0.85 : 0.5,
1867
+ background: current ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1868
+ fontVariantNumeric: 'tabular-nums', cursor: disabled ? 'default' : 'pointer',
1869
+ },
1870
+ }, label);
1871
+ const pageSlots = pageInfo
1872
+ ? Array.from({ length: pageInfo.items.length ? Math.ceil(pageInfo.items.length / 6) * 6 : 0 }, (_, index) => pageInfo.items[index] || null)
1873
+ : EMPTY;
1874
+ return h('div', {
1875
+ role: 'group', 'aria-label': '记忆宫殿',
1876
+ onKeyDown: (event) => {
1877
+ if (!pageInfo || pageInfo.pages <= 1) return;
1878
+ if (event.key === 'ArrowRight' && pageInfo.page < pageInfo.pages) {
1879
+ event.preventDefault();
1880
+ goPage(pageInfo.page + 1);
1881
+ }
1882
+ if (event.key === 'ArrowLeft' && pageInfo.page > 1) {
1883
+ event.preventDefault();
1884
+ goPage(pageInfo.page - 1);
1885
+ }
1886
+ if (event.key === 'Home' && pageInfo.page > 1) {
1887
+ event.preventDefault();
1888
+ goPage(1);
1889
+ }
1890
+ if (event.key === 'End' && pageInfo.page < pageInfo.pages) {
1891
+ event.preventDefault();
1892
+ goPage(pageInfo.pages);
1893
+ }
1894
+ },
1895
+ style: { ...PANEL_CARD, padding: '10px', minHeight: 0, flex: '0 0 auto', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '10px' },
1896
+ },
1897
+ h('div', {
1898
+ role: 'list',
1899
+ 'aria-label': '记忆房间',
1900
+ style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(232px, 1fr))', gap: '9px' },
1901
+ },
1902
+ rooms.map((room) => h(PalaceRoomCard, {
1903
+ key: room.id, room, expanded: room.id === expanded, selectedId: props.selectedId,
1904
+ onPick: props.onPick, onToggle: toggle,
1905
+ }))),
1906
+ h('div', { 'aria-live': 'polite', style: { ...PANEL_MUTED, marginTop: '-2px' } }, `${rooms.length} 个房间 · ${rows.length} 条`),
1907
+ activeRoom && activeRoom.total > 0 && pageInfo
1908
+ ? h('section', {
1909
+ id: `lite-palace-room-${activeRoom.id}`, 'aria-label': activeRoom.name, ref: panelRef,
1910
+ style: { borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: '9px', display: 'flex', flexDirection: 'column', gap: '8px' },
1911
+ },
1912
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1913
+ h('span', { style: { fontSize: '12px', lineHeight: '1.4', fontWeight: 600 } }, activeRoom.name),
1914
+ h('span', { style: PANEL_MUTED, 'aria-live': 'polite', 'aria-atomic': 'true' }, `${pageInfo.total} 条${pageInfo.pages > 1 ? ` · 第 ${pageInfo.page} / ${pageInfo.pages} 页` : ''}`),
1915
+ h('button', {
1916
+ type: 'button', onClick: () => toggle(activeRoom.id),
1917
+ style: { ...PANEL_LINK, marginLeft: 'auto', height: '24px', padding: '0 7px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l1)' },
1918
+ }, '收起'),
1919
+ ),
1920
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))', gap: '6px' } },
1921
+ pageSlots.map((record, index) => h(PalaceSlot, {
1922
+ key: record ? record.id : `page-${pageInfo.page}-slot-${index}`,
1923
+ record, active: Boolean(record && record.id === props.selectedId), onPick: props.onPick,
1924
+ }))),
1925
+ pageInfo.pages > 1
1926
+ ? h('div', { role: 'navigation', 'aria-label': '分页', style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' } },
1927
+ pageButton('上一页', pageInfo.page - 1, pageInfo.page <= 1, false),
1928
+ palacePageWindow(pageInfo.page, pageInfo.pages).map((number) => pageButton(String(number), number, false, number === pageInfo.page)),
1929
+ pageButton('下一页', pageInfo.page + 1, pageInfo.page >= pageInfo.pages, false))
1930
+ : null,
1931
+ )
1932
+ : null,
1933
+ );
1934
+ };
1935
+
1038
1936
  const MemoryPanel = (props) => {
1039
1937
  const sessionId = props.sessionId;
1040
1938
  const [data, setData] = React.useState(null);
@@ -1052,9 +1950,14 @@ window.__ModuleLoader__.load({
1052
1950
  const [detailNote, setDetailNote] = React.useState('');
1053
1951
  const [busy, setBusy] = React.useState(false);
1054
1952
  const [showReason, setShowReason] = React.useState(false);
1953
+ const [showSettings, setShowSettings] = React.useState(false);
1954
+ const [display, setDisplay] = React.useState({ ...DISPLAY_DEFAULTS });
1955
+ const [hubFilter, setHubFilter] = React.useState(null);
1055
1956
  const alive = React.useRef(true);
1056
1957
  const generation = React.useRef(0);
1057
1958
  const scopeRef = React.useRef('');
1959
+ /** 先算出当前范围键,显示设置的读取/落盘都按它来,不能等后面的派生变量。 */
1960
+ const selectedKey = data && data.selected && data.selected.key ? data.selected.key : '';
1058
1961
  React.useEffect(() => {
1059
1962
  alive.current = true;
1060
1963
  return () => { alive.current = false; };
@@ -1091,6 +1994,22 @@ window.__ModuleLoader__.load({
1091
1994
 
1092
1995
  React.useEffect(() => { load(); }, [sessionId]);
1093
1996
 
1997
+ /** 显示设置跟着当前项目走;换项目或换范围时重新读。 */
1998
+ React.useEffect(() => {
1999
+ setDisplay(readDisplaySettings(selectedKey));
2000
+ setShowSettings(false);
2001
+ setHubFilter(null);
2002
+ }, [selectedKey]);
2003
+ React.useEffect(() => {
2004
+ setMemoryView((value) => viewModeOf(display, value));
2005
+ setEvolutionView((value) => viewModeOf(display, value));
2006
+ }, [display.graph, display.palace]);
2007
+ const setDisplayFlag = (name, value) => {
2008
+ const next = { ...display, [name]: value === true };
2009
+ setDisplay(next);
2010
+ writeDisplaySettings(selectedKey, next);
2011
+ };
2012
+
1094
2013
  React.useEffect(() => {
1095
2014
  if (typeof window === 'undefined') return undefined;
1096
2015
  const onKey = (event) => { if (event.key === 'Escape') { event.stopPropagation(); props.onClose(); } };
@@ -1123,7 +2042,6 @@ window.__ModuleLoader__.load({
1123
2042
  const records = selected && Array.isArray(selected.records) ? selected.records : EMPTY;
1124
2043
  const total = selected && selected.stats && Number.isInteger(selected.stats.total) ? selected.stats.total : records.length;
1125
2044
  const currentKey = current ? current.key : '';
1126
- const selectedKey = selected ? selected.key : '';
1127
2045
  const control = selected && selected.control
1128
2046
  ? selected.control
1129
2047
  : (current && selected && selected.key === current.key && current.control ? current.control : { enabled: true });
@@ -1185,14 +2103,7 @@ window.__ModuleLoader__.load({
1185
2103
  else if (rules.length === 0) activityText = countsUnknown ? '等待可学习的反馈' : '还没有整理出经验';
1186
2104
  else activityText = '暂无整理记录';
1187
2105
  }
1188
- const filtered = React.useMemo(() => {
1189
- const needle = query.trim().toLowerCase();
1190
- if (!needle) return records;
1191
- return records.filter((record) => {
1192
- const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
1193
- return `${record.title} ${record.summary} ${tags} ${record.topicKey || ''} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
1194
- });
1195
- }, [records, query]);
2106
+ const filtered = React.useMemo(() => filterRecords(records, query, hubFilter), [records, query, hubFilter]);
1196
2107
  const filteredRules = React.useMemo(() => {
1197
2108
  if (stage === 'all') return rules;
1198
2109
  return rules.filter((rule) => rule.state === stage);
@@ -1205,6 +2116,19 @@ window.__ModuleLoader__.load({
1205
2116
  setSelectedMemory(node.id);
1206
2117
  setSelectedRule('');
1207
2118
  };
2119
+ /** 点 hub 用精确值筛选,标签不发散成子串匹配。 */
2120
+ const onPickHub = (node) => {
2121
+ if (!node || typeof node.id !== 'string') return;
2122
+ const cut = node.id.indexOf(':');
2123
+ const kind = cut > 0 ? node.id.slice(0, cut) : '';
2124
+ const value = cut > 0 ? node.id.slice(cut + 1) : '';
2125
+ if ((kind !== 'tag' && kind !== 'topic') || !value) return;
2126
+ setHubFilter({ kind, value, display: kind === 'tag' ? `#${value}` : value });
2127
+ setQuery('');
2128
+ setSelectedMemory('');
2129
+ setSelectedRule('');
2130
+ setMemoryView('list');
2131
+ };
1208
2132
  const onPickRuleNode = (node) => {
1209
2133
  if (node.kind !== 'rule') return;
1210
2134
  setSelectedRule(node.id);
@@ -1276,31 +2200,47 @@ window.__ModuleLoader__.load({
1276
2200
  style: { height: '30px', maxWidth: '190px', padding: '0 8px', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-2)', color: 'inherit', fontSize: '12px' },
1277
2201
  },
1278
2202
  data && Array.isArray(data.scopes) ? data.scopes.map((scope) => h('option', { key: scope.key, value: scope.key }, scope.kind === 'global' ? '全局' : scope.label)) : null) : null,
2203
+ h('button', {
2204
+ type: 'button', 'aria-expanded': showSettings, 'aria-label': '显示设置',
2205
+ onClick: () => setShowSettings((value) => !value),
2206
+ style: {
2207
+ ...PANEL_LINK, height: '30px', padding: '0 9px', borderRadius: '8px',
2208
+ border: '1px solid var(--dsw-alias-border-l1)',
2209
+ background: showSettings ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
2210
+ opacity: showSettings ? 0.92 : 0.68,
2211
+ },
2212
+ }, '显示设置'),
1279
2213
  closeButton,
1280
2214
  ),
1281
2215
  );
1282
2216
 
1283
- const memorySwitch = h('div', { style: { ...PANEL_CARD, flex: '1 1 300px', display: 'flex', flexDirection: 'column', gap: '4px', padding: '9px 11px' } },
1284
- h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' } },
1285
- h('span', { style: { fontSize: '12px', lineHeight: '1.4', fontWeight: 600 } }, '启用记忆'),
1286
- h(Toggle, { ariaLabel: '启用记忆', on: memoryOn, busy: busy || !canToggle, onClick: canToggle ? toggleControl : undefined }),
1287
- ),
1288
- h('div', { style: PANEL_MUTED }, '读取项目和全局记忆'), // ui-copy-ignore 用户指定文案
2217
+ const displayToggle = (label, key) => switchItem(
2218
+ label, '', display[key] === true, !selectedKey,
2219
+ selectedKey ? () => setDisplayFlag(key, display[key] !== true) : undefined,
1289
2220
  );
1290
- const evolutionSwitch = h('div', { style: { ...PANEL_CARD, flex: '1 1 300px', display: 'flex', flexDirection: 'column', gap: '4px', padding: '9px 11px', opacity: memoryOn ? 1 : 0.55 } },
1291
- h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' } },
1292
- h('span', { style: { fontSize: '12px', lineHeight: '1.4', fontWeight: 600 } }, '自进化'),
1293
- h(Toggle, { ariaLabel: '自进化', on: Boolean(memoryOn && evolution && evolution.enabled === true), busy: busy || !evolution || !canToggle || !memoryOn, onClick: canToggle && memoryOn ? toggleEvolution : undefined }),
2221
+
2222
+ const switchItem = (label, hint, on, disabled, onClick) => h('div', {
2223
+ style: { display: 'flex', flexDirection: 'column', gap: '1px', minWidth: 0, opacity: disabled && !on ? 0.55 : 1 },
2224
+ },
2225
+ h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 } },
2226
+ h('span', { style: { fontSize: '12px', lineHeight: '20px', fontWeight: 600, whiteSpace: 'nowrap' } }, label),
2227
+ h(Toggle, { ariaLabel: label, on, busy: busy || disabled, onClick: disabled ? undefined : onClick }),
2228
+ ),
2229
+ hint ? h('span', { style: { ...PANEL_MUTED, lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, hint) : null);
2230
+ const evolutionOn = Boolean(memoryOn && evolution && evolution.enabled === true);
2231
+ const switchBar = h('div', {
2232
+ style: { display: 'flex', flexDirection: 'column', gap: '3px', padding: '7px 18px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
2233
+ },
2234
+ globalView
2235
+ ? h('div', { style: { ...PANEL_MUTED, lineHeight: '20px' } }, '全局记忆只读')
2236
+ : h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: '4px 18px' } },
2237
+ switchItem('启用记忆', memoryOn ? '读取项目和全局记忆' : '记忆已关闭,内容只读', memoryOn, busy || !canToggle, toggleControl),
2238
+ switchItem('自进化', evolutionOn ? '实验功能,会增加 token 消耗' : '从反馈里整理经验', evolutionOn, busy || !evolution || !canToggle || !memoryOn, toggleEvolution), // ui-copy-ignore 用户指定文案
2239
+ showSettings ? displayToggle('启用图谱', 'graph') : null,
2240
+ showSettings ? displayToggle('启用记忆宫殿', 'palace') : null,
1294
2241
  ),
1295
- h('div', { style: PANEL_MUTED }, '从反馈中整理经验,按需使用'), // ui-copy-ignore 用户指定文案
1296
- h('div', { style: PANEL_MUTED }, '实验功能,会增加 token 消耗'), // ui-copy-ignore 用户指定文案
1297
- );
1298
- const switchBar = h('div', { style: { display: 'flex', alignItems: 'stretch', gap: '10px', flexWrap: 'wrap', padding: '12px 18px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' } },
1299
- globalView ? h('div', { style: { ...PANEL_MUTED, padding: '2px 0' } }, '全局记忆只读') : memorySwitch,
1300
- globalView ? null : evolutionSwitch,
1301
- !memoryOn && !globalView ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%' } }, '记忆已关闭,内容只读') : null,
1302
- !globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%' } }, '只读,切换回当前项目后可修改') : null,
1303
- note ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
2242
+ !globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, '只读,切换回当前项目后可修改') : null,
2243
+ note ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
1304
2244
  );
1305
2245
 
1306
2246
  const switchTab = (key) => {
@@ -1324,15 +2264,20 @@ window.__ModuleLoader__.load({
1324
2264
  globalView ? null : tabButton('evolution', '自进化'),
1325
2265
  );
1326
2266
 
2267
+ const filteredNow = Boolean(query || hubFilter);
1327
2268
  const memoryList = loading
1328
- ? h('div', { style: { ...PANEL_MUTED, padding: '12px 4px' } }, '读取中…')
2269
+ ? h('div', { style: { ...PANEL_MUTED, padding: '14px 9px' } }, '读取中…')
1329
2270
  : filtered.length === 0
1330
- ? h('div', { style: { ...PANEL_MUTED, padding: '12px 4px' } }, query ? '没有匹配的记忆' : '还没有记忆')
2271
+ ? h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 9px', minWidth: 0 } },
2272
+ h('span', { style: { ...PANEL_MUTED, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, filteredNow ? '没有匹配的记忆' : '还没有记忆'),
2273
+ filteredNow ? h(Action, { onClick: () => { setHubFilter(null); setQuery(''); } }, '清除搜索') : null,
2274
+ )
1331
2275
  : filtered.map((record) => h(MemoryRow, {
1332
2276
  key: record.id, record, active: record.id === selectedMemory,
1333
2277
  onClick: () => { setSelectedMemory(record.id); setSelectedRule(''); },
1334
2278
  }));
1335
2279
 
2280
+ const detailProcessing = detail ? processingStatusOf(detail) : '';
1336
2281
  const memoryDetail = selectedMemory
1337
2282
  ? h('div', { style: { ...PANEL_CARD, minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '6px' } },
1338
2283
  detail
@@ -1341,17 +2286,18 @@ window.__ModuleLoader__.load({
1341
2286
  h('div', { style: { flex: 1, minWidth: 0, fontSize: '13px', lineHeight: '1.45', fontWeight: 600, wordBreak: 'break-word' } }, detail.title),
1342
2287
  h(Action, { onClick: () => setSelectedMemory('') }, '关闭'),
1343
2288
  ),
1344
- h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}`),
2289
+ h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}${detailProcessing ? ` · ${PROCESSING_LABEL[detailProcessing]}` : ''}`),
1345
2290
  Array.isArray(detailRecord && detailRecord.tags) && detailRecord.tags.length
1346
2291
  ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
1347
2292
  detailRecord.tags.slice(0, 6).map((tag) => h('span', { key: tag, style: { ...PANEL_MUTED, padding: '0 6px', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '999px' } }, `#${tag}`))) : null,
1348
- h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, detail.body || '没有正文'),
2293
+ h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, currentBodyOf(detail) || '没有正文'),
2294
+ h(DetailExtras, { payload: detail }),
1349
2295
  )
1350
2296
  : h('div', { style: PANEL_MUTED }, detailNote || '读取中…'),
1351
2297
  )
1352
2298
  : null;
1353
2299
 
1354
- const viewToggleOf = (current, setValue, ariaLabel) => {
2300
+ const viewToggleOf = (current, setValue, ariaLabel, modes) => {
1355
2301
  const button = (key, label) => h('button', {
1356
2302
  key, type: 'button', onClick: () => setValue(key),
1357
2303
  'aria-pressed': current === key,
@@ -1361,6 +2307,7 @@ window.__ModuleLoader__.load({
1361
2307
  opacity: current === key ? 0.9 : 0.5,
1362
2308
  },
1363
2309
  }, label);
2310
+ const options = Array.isArray(modes) ? modes : [['list', '列表'], ['graph', '图谱']];
1364
2311
  return h('div', {
1365
2312
  role: 'group', 'aria-label': ariaLabel,
1366
2313
  style: {
@@ -1369,31 +2316,41 @@ window.__ModuleLoader__.load({
1369
2316
  border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
1370
2317
  background: 'var(--dsw-alias-bg-base)',
1371
2318
  },
1372
- }, button('list', '列表'), button('graph', '图谱'));
2319
+ }, options.map(([key, label]) => button(key, label)));
1373
2320
  };
1374
- const memoryToggle = viewToggleOf(memoryView, setMemoryView, '记忆视图');
2321
+ const memoryModes = [['list', '列表']];
2322
+ if (display.graph) memoryModes.push(['graph', '图谱']);
2323
+ if (display.palace) memoryModes.push(['palace', '宫殿']);
2324
+ const memoryToggle = memoryModes.length > 1 ? viewToggleOf(memoryView, setMemoryView, '记忆视图', memoryModes) : null;
2325
+ const memoryViewActive = viewModeOf(display, memoryView);
2326
+ const evolutionViewActive = graphViewOf(display.graph, evolutionView);
1375
2327
 
1376
2328
  const memoryListPane = h('div', {
1377
2329
  style: {
1378
2330
  ...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
2331
+ display: 'flex', flexDirection: 'column', gap: 0,
1379
2332
  },
1380
2333
  }, memoryList);
1381
2334
  const memoryGraphPane = h('div', {
1382
2335
  style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
1383
2336
  },
1384
2337
  h(MemoryGraph, {
1385
- records: filtered, selectedId: selectedMemory, onPick: onPickNode,
1386
- onPickHub: (node) => {
1387
- setQuery(node.kind === 'tag' ? node.label.replace(/^#/, '') : node.label);
1388
- setMemoryView('list');
1389
- },
1390
- emptyText: query ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图',
1391
- legend: '圆点 记忆 · 小点 标签/主题 · 虚线 关联 · 点任意点筛选或查看',
2338
+ records: filtered, total: hubFilter ? filtered.length : total,
2339
+ selectedId: selectedMemory, onPick: onPickNode,
2340
+ onPickHub,
2341
+ emptyText: query || hubFilter ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图',
2342
+ legend: '实心 记忆 · 空心 标签/主题 · 虚线 归类 · 点标签筛选,点记忆看正文',
1392
2343
  }));
2344
+ const memoryPalacePane = h(MemoryPalace, {
2345
+ key: selectedKey, records: filtered, selectedId: selectedMemory,
2346
+ onPick: (id) => { setSelectedMemory(id); setSelectedRule(''); },
2347
+ emptyText: query || hubFilter ? '没有匹配的记忆' : '这个项目还没有记忆',
2348
+ });
1393
2349
  const memoryTab = h('div', { role: 'tabpanel', 'aria-label': '记忆', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
1394
2350
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
1395
2351
  h('input', {
1396
- value: query, onChange: (event) => setQuery(event.target.value),
2352
+ value: hubFilter ? hubFilter.display : query,
2353
+ onChange: (event) => { setHubFilter(null); setQuery(event.target.value); },
1397
2354
  placeholder: '搜索标题或标签', 'aria-label': '搜索记忆',
1398
2355
  style: { ...PANEL_INPUT, width: 'auto', flex: '1 1 0', minWidth: 0 },
1399
2356
  }),
@@ -1405,7 +2362,7 @@ window.__ModuleLoader__.load({
1405
2362
  gridTemplateColumns: selectedMemory ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
1406
2363
  },
1407
2364
  },
1408
- memoryView === 'graph' ? memoryGraphPane : memoryListPane,
2365
+ memoryViewActive === 'graph' ? memoryGraphPane : memoryViewActive === 'palace' ? memoryPalacePane : memoryListPane,
1409
2366
  memoryDetail),
1410
2367
  );
1411
2368
 
@@ -1431,7 +2388,7 @@ window.__ModuleLoader__.load({
1431
2388
  fontVariantNumeric: 'tabular-nums',
1432
2389
  },
1433
2390
  }, `${label} ${countText(key)}`)),
1434
- h('span', { style: { marginLeft: 'auto' } }, viewToggleOf(evolutionView, setEvolutionView, '自进化视图')),
2391
+ h('span', { style: { marginLeft: 'auto' } }, display.graph ? viewToggleOf(evolutionView, setEvolutionView, '自进化视图') : null),
1435
2392
  );
1436
2393
 
1437
2394
  const stageLabel = (stageFilters.find(([key]) => key === stage) || ['all', '全部'])[1];
@@ -1441,21 +2398,14 @@ window.__ModuleLoader__.load({
1441
2398
  : rules.length === 0 ? (countsUnknown ? '数量未知' : '还没有整理出经验')
1442
2399
  : `没有${stageLabel}的经验`;
1443
2400
  const evolutionList = filteredRules.length === 0
1444
- ? h('div', { style: { ...PANEL_MUTED, padding: '12px 4px' } }, evolutionEmptyText)
1445
- : filteredRules.map((rule) => h('button', {
1446
- key: rule.id, type: 'button', onClick: () => { setSelectedRule(rule.id); setSelectedMemory(''); setShowReason(false); },
1447
- style: {
1448
- display: 'flex', flexDirection: 'column', gap: '3px', width: '100%', textAlign: 'left',
1449
- border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)',
1450
- background: rule.id === selectedRule ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1451
- color: 'inherit', padding: '8px 7px', cursor: 'pointer', minWidth: 0,
1452
- },
1453
- },
1454
- h('span', { style: { fontSize: '12px', lineHeight: '1.45', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.92 } }, rule.title),
1455
- h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 } },
1456
- h(StagePill, { state: rule.state }),
1457
- h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
1458
- )));
2401
+ ? h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 9px', minWidth: 0 } },
2402
+ h('span', { style: { ...PANEL_MUTED, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, evolutionEmptyText),
2403
+ stage !== 'all' && (memoryOn && evolution && evolution.enabled === true) ? h(Action, { onClick: () => setStage('all') }, '看全部') : null,
2404
+ )
2405
+ : filteredRules.map((rule) => h(EvolutionRow, {
2406
+ key: rule.id, rule, active: rule.id === selectedRule,
2407
+ onClick: () => { setSelectedRule(rule.id); setSelectedMemory(''); setShowReason(false); },
2408
+ }));
1459
2409
 
1460
2410
  const evolutionReady = selectedKey === currentKey;
1461
2411
  const evolutionFailed = !evolutionReady || failed
@@ -1476,17 +2426,18 @@ window.__ModuleLoader__.load({
1476
2426
  : null;
1477
2427
  const evolutionListPane = h('div', {
1478
2428
  style: {
1479
- ...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
2429
+ ...PANEL_CARD, padding: '4px', minHeight: 0, overflowY: 'auto',
2430
+ display: 'flex', flexDirection: 'column', gap: '2px',
1480
2431
  },
1481
2432
  }, evolutionList);
1482
2433
  const evolutionGraphPane = h('div', {
1483
2434
  style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
1484
2435
  },
1485
2436
  h(MemoryGraph, {
1486
- records: filteredRules, graphOf: ruleGraphOf, primaryKind: 'rule',
2437
+ records: filteredRules, total: filteredRules.length, graphOf: ruleGraphOf, primaryKind: 'rule',
1487
2438
  selectedId: selectedRule, onPick: onPickRuleNode,
1488
2439
  emptyText: evolutionEmptyText, ariaLabel: '经验关系图',
1489
- legend: '圆点 经验 · 小点 条件 · 虚线 共用条件 · 点圆点查看',
2440
+ legend: '实心 经验 · 空心 条件 · 虚线 共用条件 · 点经验查看',
1490
2441
  }));
1491
2442
  const evolutionTab = evolutionFailed ? evolutionFailed : h('div', { role: 'tabpanel', 'aria-label': '自进化', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
1492
2443
  h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'flex-start', gap: '8px', flex: '0 0 auto' } },
@@ -1508,11 +2459,17 @@ window.__ModuleLoader__.load({
1508
2459
  gridTemplateColumns: selectedRuleRow ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
1509
2460
  },
1510
2461
  },
1511
- evolutionView === 'graph' ? evolutionGraphPane : evolutionListPane,
2462
+ evolutionViewActive === 'graph' ? evolutionGraphPane : evolutionListPane,
1512
2463
  evolutionDetailPane),
1513
2464
  );
1514
2465
 
1515
- const content = h('section', { style: { flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0, padding: '14px', gap: '10px' } },
2466
+ const palaceLoose = activeTab === 'memory' && viewModeOf(display, memoryView) === 'palace' && !selectedMemory;
2467
+ const content = h('section', {
2468
+ style: {
2469
+ flex: palaceLoose ? '0 1 auto' : 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0,
2470
+ padding: '12px 18px 14px', gap: '10px', overflowY: palaceLoose ? 'auto' : 'visible',
2471
+ },
2472
+ },
1516
2473
  tabBar,
1517
2474
  activeTab === 'memory' ? memoryTab : evolutionTab,
1518
2475
  );
@@ -1523,7 +2480,10 @@ window.__ModuleLoader__.load({
1523
2480
  },
1524
2481
  h('div', {
1525
2482
  role: 'dialog', 'aria-modal': 'true', 'aria-label': '记忆面板',
1526
- style: { ...PANEL_DIALOG, width: 'min(1120px, 100%)', height: 'min(760px, 100%)', boxSizing: 'border-box' },
2483
+ style: {
2484
+ ...PANEL_DIALOG, width: 'min(1120px, 100%)', boxSizing: 'border-box',
2485
+ height: palaceLoose ? 'auto' : 'min(760px, 100%)', maxHeight: 'min(760px, 100%)',
2486
+ },
1527
2487
  }, ...children));
1528
2488
 
1529
2489
  if (!sessionId) return null;
@@ -1568,12 +2528,22 @@ window.__ModuleLoader__.load({
1568
2528
  update: list.filter((item) => item.exposure === 'update'),
1569
2529
  };
1570
2530
  }, [refs]);
2531
+ const noTriggerReason = typeof props.noTriggerReason === 'string' && props.noTriggerReason
2532
+ ? props.noTriggerReason
2533
+ : (typeof refs.noTriggerReason === 'string' ? refs.noTriggerReason : '');
1571
2534
  if (!sessionId && refs.length === 0) return null;
1572
- if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) return null;
2535
+ if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) {
2536
+ return noTriggerReason
2537
+ ? h('div', { style: TAIL }, h(RefSection, { label: '搜索命中', tone: 'found', items: EMPTY, sessionId, turnKey: turn, noTriggerReason }))
2538
+ : null;
2539
+ }
2540
+ const section = (label, items, tone) => (items.length === 0
2541
+ ? null
2542
+ : h(RefSection, { key: label, label, tone, items, sessionId, turnKey: turn }));
1573
2543
  return h('div', { style: TAIL },
1574
- h(RefSection, { label: '搜索命中', items: groups.found, sessionId, turnKey: turn }),
1575
- h(RefSection, { label: '读过正文', items: groups.provided, sessionId, turnKey: turn }),
1576
- h(RefSection, { label: '本轮更新', items: groups.update, sessionId, turnKey: turn }),
2544
+ section('搜索命中', groups.found, 'found'),
2545
+ section('读过正文', groups.provided, 'provided'),
2546
+ section('本轮更新', groups.update, 'update'),
1577
2547
  );
1578
2548
  };
1579
2549
 
@@ -1581,7 +2551,7 @@ window.__ModuleLoader__.load({
1581
2551
 
1582
2552
  // 测试缝:Node 里验引用投影,浏览器里没有任何副作用。
1583
2553
  if (typeof globalThis.__LITE_MEMORY_TEST__ === 'function') {
1584
- globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, MemoryPanel });
2554
+ globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, MemoryPalace, TurnMemory, RefSection, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf });
1585
2555
  }
1586
2556
 
1587
2557
  return {