dsh-compact-relay 0.3.3 → 0.3.5

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.en.md +6 -3
  3. package/README.md +6 -3
  4. package/ROADMAP.md +30 -3
  5. package/TUTORIAL.md +1 -1
  6. package/lib/actions/better-compact.js +24 -72
  7. package/lib/actions/compact-run.js +76 -0
  8. package/lib/actions/compress-hint.js +16 -7
  9. package/lib/actions/handoff-docs.js +9 -3
  10. package/lib/actions/handoff.js +5 -22
  11. package/lib/actions/settings.js +1 -4
  12. package/lib/actions/state.js +15 -4
  13. package/lib/actions/support.js +76 -0
  14. package/lib/actions/test-compact.js +12 -41
  15. package/lib/adapters/http.js +2 -1
  16. package/lib/adapters/prompt.js +72 -0
  17. package/lib/adapters/tool.js +12 -8
  18. package/lib/client.js +440 -100
  19. package/lib/config.js +1 -2
  20. package/lib/core/compress-cue.js +96 -0
  21. package/lib/core/compress-hint.js +1 -1
  22. package/lib/core/errors.js +0 -1
  23. package/lib/core/handoff-spec.js +1 -1
  24. package/lib/core/markdown-lite.js +178 -0
  25. package/lib/core/object.js +19 -0
  26. package/lib/core/ranges.js +1 -2
  27. package/lib/core/relay.js +2 -2
  28. package/lib/core/tokens.js +34 -0
  29. package/lib/dsh/compaction-log.js +3 -19
  30. package/lib/dsh/compaction-pair.js +48 -0
  31. package/lib/dsh/engine.js +2 -2
  32. package/lib/dsh/handoff-files.js +28 -0
  33. package/lib/dsh/handoff-writer.js +16 -21
  34. package/lib/dsh/model-task.js +112 -0
  35. package/lib/dsh/relay.js +1 -1
  36. package/lib/dsh/session-view.js +5 -17
  37. package/lib/dsh/settings-file.js +1 -4
  38. package/lib/dsh/state-hub.js +95 -19
  39. package/lib/dsh/summarizer.js +16 -25
  40. package/lib/index.js +28 -6
  41. package/package.json +2 -2
  42. package/lib/actions/stage.js +0 -36
  43. package/lib/core/stage.js +0 -96
package/lib/client.js CHANGED
@@ -376,6 +376,355 @@ window.__ModuleLoader__.load({
376
376
  return theme;
377
377
  }
378
378
 
379
+ /**
380
+ * 跨片段共享的浮层 / 指示灯环小部件(30-indicator 与 50-settings 共用)。
381
+ *
382
+ * 为什么单独一个片段:这些几何/材质在「指示灯」和「设置页教学卡片」里各画过一遍,
383
+ * 以前是两份点坐标/弧线 path/原生线刻度/闪烁 keyframes。放在 15 号片段(按文件名
384
+ * 排序在 10-runtime 之后、20-format/30-indicator/50-settings 之前),函数声明会
385
+ * 提升,后面的片段可直接调用;常量在使用点(组件渲染时)早已初始化。
386
+ */
387
+
388
+ /**
389
+ * 环上比例 → SVG 坐标(viewBox 0 0 20 20,圆心 10,10,半径用 10-runtime 的 RADIUS)。
390
+ * @param {number} value 0–1 的比例
391
+ * @returns {[number, number]}
392
+ */
393
+ function ringPoint(value) {
394
+ const angle = value * Math.PI * 2 - Math.PI / 2;
395
+ return [10 + RADIUS * Math.cos(angle), 10 + RADIUS * Math.sin(angle)];
396
+ }
397
+
398
+ /**
399
+ * 环上两比例之间的弧线 path(0.5 以上用大弧标志)。
400
+ * @param {number} from
401
+ * @param {number} to
402
+ * @returns {string}
403
+ */
404
+ function ringArcPath(from, to) {
405
+ const [x1, y1] = ringPoint(from);
406
+ const [x2, y2] = ringPoint(to);
407
+ const large = to - from > 0.5 ? 1 : 0;
408
+ return `M ${x1.toFixed(3)} ${y1.toFixed(3)} A ${RADIUS} ${RADIUS} 0 ${large} 1 ${x2.toFixed(3)} ${y2.toFixed(3)}`;
409
+ }
410
+
411
+ /**
412
+ * DSH 原生自动压缩线的「径向刻度」:穿过环带的一条短线(内 5.6 → 外 9.6)。
413
+ * 指示灯与设置页教学图用同一套几何,只是描边/属性不同。
414
+ * @param {number} value 合法的原生阈值比例(0.02–1 之间,调用方先校验)
415
+ * @returns {{x1:number, y1:number, x2:number, y2:number}}
416
+ */
417
+ function nativeTickAt(value) {
418
+ const angle = value * Math.PI * 2 - Math.PI / 2;
419
+ const cos = Math.cos(angle);
420
+ const sin = Math.sin(angle);
421
+ return { x1: 10 + 5.6 * cos, y1: 10 + 5.6 * sin, x2: 10 + 9.6 * cos, y2: 10 + 9.6 * sin };
422
+ }
423
+
424
+ /** 越过原生自动压缩线后「整环红色闪烁」的 keyframes(指示灯/教学图共用一份)。 */
425
+ const NATIVE_ALERT_CSS = '@keyframes cr-native-alert { 0%,100% { opacity: 1 } 50% { opacity: 0.15 } } @media (prefers-reduced-motion: reduce) { [data-cr-native-alert] { animation: none !important; opacity: 0.85 !important; } }';
426
+
427
+ /**
428
+ * 浮层外观(菜单 / 悬停卡 / 确认卡 / 交接单面板 / 历史面板 / toast 共用)。
429
+ * 主题色由 10-runtime 的 detectTheme() 给出(BUG-001:优先 DSH 主题 token)。
430
+ * @param {object} theme detectTheme() 的返回值
431
+ * @returns {{background:string, backdropFilter:string, WebkitBackdropFilter:string, border:string, boxShadow:string}}
432
+ */
433
+ function panelChrome(theme) {
434
+ return {
435
+ background: theme.panelBg,
436
+ backdropFilter: theme.panelBackdrop,
437
+ WebkitBackdropFilter: theme.panelBackdrop,
438
+ border: `1px solid ${theme.panelBorder}`,
439
+ boxShadow: theme.panelShadow,
440
+ };
441
+ }
442
+
443
+ /**
444
+ * 轻量 Markdown 解析(0.3.4 BUG-005)—— 纯函数,零依赖。
445
+ *
446
+ * 为什么自己写:交接单详情面板以前把 markdown 原样塞进 `<pre>`,用户在面板里看到的是
447
+ * `## 目标`/`- xxx`/`**粗体**` 源码。引第三方 markdown 渲染器会增加运行时依赖
448
+ * (插件只有 peerDependencies、零运行时依赖是硬约束),而 `dangerouslySetInnerHTML`
449
+ * 又引入 XSS 面。所以这里只解析交接单实际用到的子集,产出**结构化数据**,由客户端
450
+ * 用 React 文本节点渲染 —— 从不生成 HTML,任何 `<script>` 都只会显示成文字。
451
+ *
452
+ * 支持(够用即可,不做完整 CommonMark):
453
+ * - 标题 `#`–`######`(段落里的行首)
454
+ * - 段落(连续非空行;保留原始换行)
455
+ * - 无序列表 `-` / `*` / `+`
456
+ * - 有序列表 `1.` / `1)`(保留起始编号)
457
+ * - 围栏代码块 ```(保留语言标记;块内不做行内解析)
458
+ * - 分隔线 `---` / `***` / `___`
459
+ * - 行内:`**粗体**`、`` `行内代码` ``、反斜杠转义
460
+ *
461
+ * 返回值约定(客户端渲染器按此映射 React 节点):
462
+ * blocks: Array<
463
+ * | {type:'heading', level:number, segments:Segment[]}
464
+ * | {type:'paragraph', segments:Segment[]}
465
+ * | {type:'list', ordered:boolean, start:number, items:Segment[][]}
466
+ * | {type:'code', lang:string, text:string}
467
+ * | {type:'hr'}
468
+ * >
469
+ * Segment = {type:'text'|'strong'|'code', text:string}
470
+ */
471
+
472
+ const FENCE_RE = /^\s*```\s*([^\s`]*)\s*$/;
473
+ const HEADING_RE = /^(#{1,6})\s+(.*)$/;
474
+ const HR_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
475
+ const UL_RE = /^\s*[-*+]\s+(.*)$/;
476
+ const OL_RE = /^\s*(\d+)[.)]\s+(.*)$/;
477
+
478
+ /**
479
+ * 解析行内标记:`**粗体**`、`` `代码` ``、反斜杠转义。
480
+ * 未闭合的标记按普通文本处理(不吞字符)。
481
+ * @param {string} text
482
+ * @returns {{type:'text'|'strong'|'code', text:string}[]}
483
+ */
484
+ function parseInlineLite(text) {
485
+ const source = typeof text === 'string' ? text : String(text ?? '');
486
+ const segments = [];
487
+ let buffer = '';
488
+ const flush = () => {
489
+ if (buffer.length > 0) {
490
+ segments.push({ type: 'text', text: buffer });
491
+ buffer = '';
492
+ }
493
+ };
494
+ let index = 0;
495
+ while (index < source.length) {
496
+ const char = source[index];
497
+ // 反斜杠转义:下一个字符按字面量处理(\* 不开启粗体)。
498
+ if (char === '\\' && index + 1 < source.length) {
499
+ buffer += source[index + 1];
500
+ index += 2;
501
+ continue;
502
+ }
503
+ if (char === '`') {
504
+ const end = source.indexOf('`', index + 1);
505
+ if (end === -1) {
506
+ buffer += char;
507
+ index += 1;
508
+ continue;
509
+ }
510
+ flush();
511
+ segments.push({ type: 'code', text: source.slice(index + 1, end) });
512
+ index = end + 1;
513
+ continue;
514
+ }
515
+ if (char === '*' && source[index + 1] === '*') {
516
+ const end = source.indexOf('**', index + 2);
517
+ if (end === -1) {
518
+ buffer += '**';
519
+ index += 2;
520
+ continue;
521
+ }
522
+ flush();
523
+ segments.push({ type: 'strong', text: source.slice(index + 2, end) });
524
+ index = end + 2;
525
+ continue;
526
+ }
527
+ buffer += char;
528
+ index += 1;
529
+ }
530
+ flush();
531
+ return segments;
532
+ }
533
+
534
+ /**
535
+ * 把 markdown 文本解析成块数组;空文本返回 []。
536
+ * @param {string} text
537
+ * @returns {object[]}
538
+ */
539
+ function parseMarkdownLite(text) {
540
+ const source = typeof text === 'string' ? text : String(text ?? '');
541
+ const lines = source.replace(/\r\n?/g, '\n').split('\n');
542
+ const blocks = [];
543
+ let index = 0;
544
+
545
+ // 连续非空行攒成段落;遇到块级语法先 flush。
546
+ let paragraph = [];
547
+ const flushParagraph = () => {
548
+ if (paragraph.length === 0) return;
549
+ blocks.push({ type: 'paragraph', segments: parseInlineLite(paragraph.join('\n')) });
550
+ paragraph = [];
551
+ };
552
+
553
+ while (index < lines.length) {
554
+ const line = lines[index];
555
+
556
+ // 围栏代码块
557
+ const fence = FENCE_RE.exec(line);
558
+ if (fence !== null) {
559
+ flushParagraph();
560
+ const codeLines = [];
561
+ index += 1;
562
+ while (index < lines.length && FENCE_RE.exec(lines[index]) === null) {
563
+ codeLines.push(lines[index]);
564
+ index += 1;
565
+ }
566
+ // 吃掉收尾围栏(未闭合时到文件尾)
567
+ if (index < lines.length) index += 1;
568
+ blocks.push({ type: 'code', lang: fence[1] ?? '', text: codeLines.join('\n') });
569
+ continue;
570
+ }
571
+
572
+ // 空行:结束段落
573
+ if (line.trim().length === 0) {
574
+ flushParagraph();
575
+ index += 1;
576
+ continue;
577
+ }
578
+
579
+ // 标题
580
+ const heading = HEADING_RE.exec(line);
581
+ if (heading !== null) {
582
+ flushParagraph();
583
+ blocks.push({ type: 'heading', level: heading[1].length, segments: parseInlineLite(heading[2]) });
584
+ index += 1;
585
+ continue;
586
+ }
587
+
588
+ // 分隔线(注意:`---` 也匹配无序列表语法,必须先判 hr)
589
+ if (HR_RE.test(line)) {
590
+ flushParagraph();
591
+ blocks.push({ type: 'hr' });
592
+ index += 1;
593
+ continue;
594
+ }
595
+
596
+ const unordered = UL_RE.exec(line);
597
+ const ordered = unordered === null ? OL_RE.exec(line) : null;
598
+ if (unordered !== null || ordered !== null) {
599
+ flushParagraph();
600
+ const isOrdered = ordered !== null;
601
+ const start = isOrdered ? Number(ordered[1]) : 1;
602
+ const items = [];
603
+ const itemRe = isOrdered ? OL_RE : UL_RE;
604
+ while (index < lines.length) {
605
+ const matched = itemRe.exec(lines[index]);
606
+ if (matched === null) break;
607
+ items.push(parseInlineLite(isOrdered ? matched[2] : matched[1]));
608
+ index += 1;
609
+ }
610
+ blocks.push({ type: 'list', ordered: isOrdered, start, items });
611
+ continue;
612
+ }
613
+
614
+ // 普通段落行(保留换行)
615
+ paragraph.push(line);
616
+ index += 1;
617
+ }
618
+ flushParagraph();
619
+ return blocks;
620
+ }
621
+
622
+ /**
623
+ * 交接单详情面板的轻量 Markdown 渲染(0.3.4 BUG-005)。
624
+ *
625
+ * 解析在 core/markdown-lite.js(构建时内联,同一份源码有 node 单测),这里只把
626
+ * 结构化块映射成 React 节点。全部是 React 文本节点,**从不生成 HTML**,
627
+ * 因此交接单内容里的 `<script>` 只会显示成文字,没有 XSS 面。
628
+ */
629
+ const MARKDOWN_MONO_STYLE = {
630
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
631
+ };
632
+ const MARKDOWN_INLINE_CODE_STYLE = {
633
+ ...MARKDOWN_MONO_STYLE,
634
+ fontSize: 10.5,
635
+ background: 'rgba(127,127,127,0.16)',
636
+ borderRadius: 4,
637
+ padding: '0 3px',
638
+ };
639
+ const MARKDOWN_CODE_BLOCK_STYLE = {
640
+ ...MARKDOWN_MONO_STYLE,
641
+ margin: '6px 0',
642
+ padding: '6px 8px',
643
+ borderRadius: 6,
644
+ background: 'rgba(127,127,127,0.16)',
645
+ overflowX: 'auto',
646
+ whiteSpace: 'pre-wrap',
647
+ wordBreak: 'break-word',
648
+ fontSize: 10.5,
649
+ lineHeight: '15px',
650
+ };
651
+
652
+ /** 行内片段 → React 子节点(文本/粗体/行内代码)。 */
653
+ function renderInlineLite(segments, keyPrefix) {
654
+ return segments.map((segment, index) => {
655
+ const key = `${keyPrefix}-${index}`;
656
+ if (segment.type === 'strong') return h('strong', { key }, segment.text);
657
+ if (segment.type === 'code') return h('code', { key, style: MARKDOWN_INLINE_CODE_STYLE }, segment.text);
658
+ return segment.text;
659
+ });
660
+ }
661
+
662
+ /**
663
+ * 把 markdown 文本渲染成 React 节点数组(标题/段落/列表/代码块/分隔线)。
664
+ * @param {string} text
665
+ * @returns {object[]}
666
+ */
667
+ function renderMarkdownLite(text) {
668
+ const blocks = parseMarkdownLite(typeof text === 'string' ? text : '');
669
+ return blocks
670
+ .map((block, index) => {
671
+ const key = `md-${index}`;
672
+ if (block.type === 'heading') {
673
+ return h(
674
+ 'div',
675
+ {
676
+ key,
677
+ style: {
678
+ fontWeight: 600,
679
+ fontSize: block.level <= 2 ? 13 : 12,
680
+ lineHeight: '17px',
681
+ marginTop: index === 0 ? 0 : 10,
682
+ marginBottom: 4,
683
+ },
684
+ },
685
+ renderInlineLite(block.segments, `${key}-h`),
686
+ );
687
+ }
688
+ if (block.type === 'paragraph') {
689
+ return h(
690
+ 'div',
691
+ { key, style: { margin: '4px 0', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } },
692
+ renderInlineLite(block.segments, `${key}-p`),
693
+ );
694
+ }
695
+ if (block.type === 'list') {
696
+ return h(
697
+ 'div',
698
+ { key, style: { margin: '2px 0 6px' } },
699
+ block.items.map((item, itemIndex) =>
700
+ h(
701
+ 'div',
702
+ { key: `${key}-i${itemIndex}`, style: { display: 'flex', gap: 6, alignItems: 'flex-start' } },
703
+ h(
704
+ 'span',
705
+ { style: { flex: 'none', minWidth: block.ordered ? 18 : 8, opacity: 0.7 } },
706
+ block.ordered ? `${block.start + itemIndex}.` : '•',
707
+ ),
708
+ h(
709
+ 'span',
710
+ { style: { flex: 1, minWidth: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } },
711
+ renderInlineLite(item, `${key}-i${itemIndex}-s`),
712
+ ),
713
+ ),
714
+ ),
715
+ );
716
+ }
717
+ if (block.type === 'code') {
718
+ return h('pre', { key, style: MARKDOWN_CODE_BLOCK_STYLE }, block.text);
719
+ }
720
+ if (block.type === 'hr') {
721
+ return h('div', { key, style: { borderTop: '1px solid rgba(127,127,127,0.35)', margin: '8px 0' } });
722
+ }
723
+ return null;
724
+ })
725
+ .filter((node) => node !== null);
726
+ }
727
+
379
728
  const fmt = (n) => {
380
729
  if (n == null) return '—';
381
730
  if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
@@ -665,6 +1014,9 @@ window.__ModuleLoader__.load({
665
1014
  startSeq: data.compactRange.startSeq ?? null,
666
1015
  endSeq: data.compactRange.endSeq ?? null,
667
1016
  },
1017
+ // 0.3.5:范围存在但会话没有活 agent —— 菜单据此显示「会话未激活…」而不是
1018
+ // 「暂无可压缩内容」(applyState 只保留白名单字段,新字段必须显式透传)。
1019
+ compactRangeReason: data.compactRangeReason === 'no_agent' ? 'no_agent' : null,
668
1020
  });
669
1021
  }, []);
670
1022
 
@@ -841,16 +1193,7 @@ window.__ModuleLoader__.load({
841
1193
  const nativeCrossed = nativeValid && ratio > nativeAuto;
842
1194
  // 断点缺口:0.9 SVG 单位(≈6.4°),折算成比例;段太短时缺口会吃掉整段,就跳过该段。
843
1195
  const ringGapRatio = 0.9 / CIRC;
844
- const ringPoint = (value) => {
845
- const angle = value * Math.PI * 2 - Math.PI / 2;
846
- return [10 + RADIUS * Math.cos(angle), 10 + RADIUS * Math.sin(angle)];
847
- };
848
- const ringArcPath = (from, to) => {
849
- const [x1, y1] = ringPoint(from);
850
- const [x2, y2] = ringPoint(to);
851
- const largeArc = to - from > 0.5 ? 1 : 0;
852
- return `M ${x1.toFixed(3)} ${y1.toFixed(3)} A ${RADIUS} ${RADIUS} 0 ${largeArc} 1 ${x2.toFixed(3)} ${y2.toFixed(3)}`;
853
- };
1196
+ // 点坐标/弧线 path 是共享几何(15-shared.js 的 ringPoint / ringArcPath)。
854
1197
  const ringNodes = ring.segments
855
1198
  .map((segment, index) => {
856
1199
  const trimmed = trimSegment(segment, ringGapRatio, ring.ratio);
@@ -871,14 +1214,11 @@ window.__ModuleLoader__.load({
871
1214
  // 刻度用一条「穿过环带」的短线(内 5.6 → 外 9.6)+ 一圈主题底色描边,避免和红色分段糊在一起。
872
1215
  const nativeNodes = [];
873
1216
  if (nativeValid) {
874
- const nativeAngle = nativeAuto * Math.PI * 2 - Math.PI / 2;
875
- const nativeCos = Math.cos(nativeAngle);
876
- const nativeSin = Math.sin(nativeAngle);
877
- const tick = { x1: 10 + 5.6 * nativeCos, y1: 10 + 5.6 * nativeSin, x2: 10 + 9.6 * nativeCos, y2: 10 + 9.6 * nativeSin };
1217
+ const tick = nativeTickAt(nativeAuto);
878
1218
  // 越过原生压缩线:整环红色闪烁(1s 呼吸);prefers-reduced-motion 下改为常亮红色(不闪)。
879
1219
  if (nativeCrossed) {
880
1220
  nativeNodes.push(
881
- h('style', { key: 'native-alert-style' }, '@keyframes cr-native-alert { 0%,100% { opacity: 1 } 50% { opacity: 0.15 } } @media (prefers-reduced-motion: reduce) { [data-cr-native-alert] { animation: none !important; opacity: 0.85 !important; } }'),
1221
+ h('style', { key: 'native-alert-style' }, NATIVE_ALERT_CSS),
882
1222
  h('circle', {
883
1223
  key: 'native-alert',
884
1224
  cx: 10,
@@ -1125,7 +1465,9 @@ window.__ModuleLoader__.load({
1125
1465
 
1126
1466
  // M6:交接单列表(菜单入口)——只读接口,点开可看全文/复制路径。
1127
1467
  // P2-C3/C4:请求带序号 + AbortController;只接受「仍然是当前选中文件」的响应。
1128
- const loadDocs = useCallback(async () => {
1468
+ // 0.3.4(BUG-006):列表先取最近 20 份,「加载更多」再把 limit 提到 50;
1469
+ // 端点返回 total,界面据此显示「共 N 份(显示最近 M)」并在无更多时隐藏按钮。
1470
+ const loadDocs = useCallback(async (limit = 20) => {
1129
1471
  setOpen(false);
1130
1472
  setPreview(null);
1131
1473
  closeDiff();
@@ -1141,7 +1483,7 @@ window.__ModuleLoader__.load({
1141
1483
  const reqId = ++docsRequestRef.current;
1142
1484
  setDocs({ loading: true });
1143
1485
  try {
1144
- const res = await fetch('/api/compact-relay-handoffs?limit=20', {
1486
+ const res = await fetch(`/api/compact-relay-handoffs?limit=${limit}`, {
1145
1487
  cache: 'no-store',
1146
1488
  ...(controller === null ? {} : { signal: controller.signal }),
1147
1489
  });
@@ -1151,7 +1493,9 @@ window.__ModuleLoader__.load({
1151
1493
  setDocs({ error: data && data.message ? String(data.message) : '读取交接单失败' });
1152
1494
  return;
1153
1495
  }
1154
- setDocs({ items: Array.isArray(data.items) ? data.items : [], selected: null });
1496
+ const items = Array.isArray(data.items) ? data.items : [];
1497
+ const total = typeof data.total === 'number' && Number.isFinite(data.total) ? data.total : items.length;
1498
+ setDocs({ items, total, limit, selected: null });
1155
1499
  } catch (error) {
1156
1500
  if (reqId !== docsRequestRef.current || (error && error.name === 'AbortError')) return;
1157
1501
  setDocs({ error: '读取交接单失败:' + (error && error.message ? error.message : '未知错误') });
@@ -1399,11 +1743,7 @@ window.__ModuleLoader__.load({
1399
1743
  wordBreak: 'break-word',
1400
1744
  padding: '6px 9px',
1401
1745
  borderRadius: 8,
1402
- background: theme.panelBg,
1403
- backdropFilter: theme.panelBackdrop,
1404
- WebkitBackdropFilter: theme.panelBackdrop,
1405
- border: `1px solid ${theme.panelBorder}`,
1406
- boxShadow: theme.panelShadow,
1746
+ ...panelChrome(theme),
1407
1747
  color: theme.panelFg,
1408
1748
  fontSize: 11,
1409
1749
  lineHeight: '16px',
@@ -1518,7 +1858,8 @@ window.__ModuleLoader__.load({
1518
1858
  role: 'menuitem',
1519
1859
  'data-cr-item': action,
1520
1860
  disabled,
1521
- title: oneLine ? (meta.note ?? sub) : undefined,
1861
+ // 禁用态的两行条目也把说明挂到 title:鼠标悬停能读到完整原因(含数字)。
1862
+ title: meta.title ?? (oneLine || disabled ? (meta.note ?? sub) : undefined),
1522
1863
  onClick: () => {
1523
1864
  if (disabled) return;
1524
1865
  // M6:压缩项先走预览(meta.onClick),其余动作直接执行。
@@ -1575,6 +1916,12 @@ window.__ModuleLoader__.load({
1575
1916
 
1576
1917
  // undefined=宿主没上报(老版本/未知);null=明确没有可压范围;object=可压多少。
1577
1918
  const rangeInfo = state.compactRange;
1919
+ // 0.3.5:有可压范围、但会话还没有活 agent(passivized live session)——preview 需要
1920
+ // 活 agent,所以按钮仍禁用,但文案要说清「发一条消息唤醒 agent」,不能再报「暂无可压缩内容」。
1921
+ const passiveRangeNote =
1922
+ rangeInfo !== null && rangeInfo !== undefined && state.compactRangeReason === 'no_agent'
1923
+ ? `会话未激活:发一条消息唤醒 agent 后可压 ${rangeInfo.nodes} 条 · 约 ${fmt(rangeInfo.tokens)} tokens`
1924
+ : null;
1578
1925
  const panel = open
1579
1926
  ? h(
1580
1927
  'div',
@@ -1591,11 +1938,7 @@ window.__ModuleLoader__.load({
1591
1938
  overflowY: 'auto',
1592
1939
  padding: 6,
1593
1940
  borderRadius: 12,
1594
- background: theme.panelBg,
1595
- backdropFilter: theme.panelBackdrop,
1596
- WebkitBackdropFilter: theme.panelBackdrop,
1597
- border: `1px solid ${theme.panelBorder}`,
1598
- boxShadow: theme.panelShadow,
1941
+ ...panelChrome(theme),
1599
1942
  zIndex: 40,
1600
1943
  },
1601
1944
  },
@@ -1620,9 +1963,11 @@ window.__ModuleLoader__.load({
1620
1963
  ? `暂无可压内容:有 ${pins.count} 条消息被锁定保护(先点「解锁消息」)`
1621
1964
  : '暂无可压缩内容(会话太短,或近期会话需保留原文)',
1622
1965
  }
1623
- : rangeInfo !== undefined
1624
- ? { note: `可压 ${rangeInfo.nodes} 条 · 约 ${fmt(rangeInfo.tokens)} tokens · 点击先看压缩预估` }
1625
- : { note: '点击先看压缩预估(范围/容量)' }),
1966
+ : passiveRangeNote !== null
1967
+ ? { disabled: true, title: passiveRangeNote, note: passiveRangeNote }
1968
+ : rangeInfo !== undefined
1969
+ ? { note: `可压 ${rangeInfo.nodes} 条 · 约 ${fmt(rangeInfo.tokens)} tokens · 点击先看压缩预估` }
1970
+ : { note: '点击先看压缩预估(范围/容量)' }),
1626
1971
  onClick: () => {
1627
1972
  void startPreview();
1628
1973
  },
@@ -1702,11 +2047,7 @@ window.__ModuleLoader__.load({
1702
2047
  maxWidth: 'calc(100vw - 16px)',
1703
2048
  padding: '10px 12px',
1704
2049
  borderRadius: 12,
1705
- background: theme.panelBg,
1706
- backdropFilter: theme.panelBackdrop,
1707
- WebkitBackdropFilter: theme.panelBackdrop,
1708
- border: `1px solid ${theme.panelBorder}`,
1709
- boxShadow: theme.panelShadow,
2050
+ ...panelChrome(theme),
1710
2051
  color: theme.panelFg,
1711
2052
  zIndex: 42,
1712
2053
  },
@@ -1821,12 +2162,8 @@ window.__ModuleLoader__.load({
1821
2162
  gap: 8,
1822
2163
  padding: '10px 12px',
1823
2164
  borderRadius: 12,
1824
- background: theme.panelBg,
1825
- backdropFilter: theme.panelBackdrop,
1826
- WebkitBackdropFilter: theme.panelBackdrop,
2165
+ ...panelChrome(theme),
1827
2166
  color: theme.panelFg,
1828
- border: `1px solid ${theme.panelBorder}`,
1829
- boxShadow: theme.panelShadow,
1830
2167
  zIndex: 44,
1831
2168
  },
1832
2169
  },
@@ -1917,6 +2254,12 @@ window.__ModuleLoader__.load({
1917
2254
  ),
1918
2255
  );
1919
2256
 
2257
+ // 0.3.4(BUG-006):total 与当前窗口(20/50),用于列表底部与「加载更多」。
2258
+ const docsItems = docs !== null && Array.isArray(docs.items) ? docs.items : [];
2259
+ const docsTotal =
2260
+ docs !== null && typeof docs.total === 'number' && Number.isFinite(docs.total)
2261
+ ? docs.total
2262
+ : docsItems.length;
1920
2263
  const docsPanel =
1921
2264
  docs === null
1922
2265
  ? null
@@ -1937,12 +2280,8 @@ window.__ModuleLoader__.load({
1937
2280
  flexDirection: 'column',
1938
2281
  overflow: 'hidden',
1939
2282
  borderRadius: 12,
1940
- background: theme.panelBg,
1941
- backdropFilter: theme.panelBackdrop,
1942
- WebkitBackdropFilter: theme.panelBackdrop,
2283
+ ...panelChrome(theme),
1943
2284
  color: theme.panelFg,
1944
- border: `1px solid ${theme.panelBorder}`,
1945
- boxShadow: theme.panelShadow,
1946
2285
  zIndex: 43,
1947
2286
  },
1948
2287
  },
@@ -1964,7 +2303,7 @@ window.__ModuleLoader__.load({
1964
2303
  { style: { flex: 1, minWidth: 0 } },
1965
2304
  docs.selected !== undefined && docs.selected !== null
1966
2305
  ? '交接单详情'
1967
- : `历史交接单(${Array.isArray(docs.items) ? docs.items.length : 0})`,
2306
+ : `历史交接单(${docsTotal})`,
1968
2307
  ),
1969
2308
  h('button', { type: 'button', 'data-cr-docs-close': '1', onClick: closeDocs, style: smallBtnStyle }, '关闭'),
1970
2309
  ),
@@ -1981,7 +2320,7 @@ window.__ModuleLoader__.load({
1981
2320
  'button',
1982
2321
  {
1983
2322
  type: 'button',
1984
- onClick: () => setDocs({ items: docs.items, selected: null }),
2323
+ onClick: () => setDocs((prev) => (prev === null ? null : { ...prev, selected: null })),
1985
2324
  style: smallBtnStyle,
1986
2325
  },
1987
2326
  '← 列表',
@@ -2004,21 +2343,28 @@ window.__ModuleLoader__.load({
2004
2343
  ),
2005
2344
  ),
2006
2345
  h(
2007
- 'pre',
2346
+ 'div',
2008
2347
  {
2348
+ // 0.3.4(BUG-005):交接单详情从 <pre> 纯文本改成轻量 markdown 渲染。
2349
+ // 解析器是 core/markdown-lite.js(构建时内联),渲染只产生 React
2350
+ // 文本节点,不生成 HTML、不引第三方依赖。
2009
2351
  'data-cr-doc-content': '1',
2010
2352
  style: {
2011
2353
  margin: 0,
2012
2354
  padding: '8px 12px',
2013
2355
  overflowY: 'auto',
2014
- whiteSpace: 'pre-wrap',
2356
+ overflowX: 'hidden',
2015
2357
  wordBreak: 'break-word',
2016
2358
  fontSize: 11,
2017
2359
  lineHeight: '16px',
2018
2360
  maxHeight: 'min(50vh, 320px)',
2019
2361
  },
2020
2362
  },
2021
- docs.contentLoading === true ? '读取中…' : (docs.contentError ?? docs.content ?? ''),
2363
+ docs.contentLoading === true
2364
+ ? '读取中…'
2365
+ : docs.contentError !== undefined && docs.contentError !== null
2366
+ ? h('div', { style: { color: COLORS.error } }, String(docs.contentError))
2367
+ : renderMarkdownLite(docs.content ?? ''),
2022
2368
  ),
2023
2369
  )
2024
2370
  : h(
@@ -2026,8 +2372,8 @@ window.__ModuleLoader__.load({
2026
2372
  { style: { overflowY: 'auto', padding: 4 } },
2027
2373
  docs.error !== undefined && docs.error !== null
2028
2374
  ? h('div', { style: { padding: '8px 10px', fontSize: 11, color: COLORS.error } }, String(docs.error))
2029
- : Array.isArray(docs.items) && docs.items.length > 0
2030
- ? docs.items.map((entry) =>
2375
+ : docsItems.length > 0
2376
+ ? [...docsItems.map((entry) =>
2031
2377
  h(
2032
2378
  'button',
2033
2379
  {
@@ -2070,7 +2416,38 @@ window.__ModuleLoader__.load({
2070
2416
  `${fmtTime(entry.mtime)} · ${Math.max(1, Math.round((Number(entry.size) || 0) / 1024))}KB · ${entry.name}`,
2071
2417
  ),
2072
2418
  ),
2073
- )
2419
+ ),
2420
+ h(
2421
+ 'div',
2422
+ {
2423
+ key: 'docs-footer',
2424
+ style: {
2425
+ display: 'flex',
2426
+ alignItems: 'center',
2427
+ gap: 8,
2428
+ padding: '6px 10px',
2429
+ borderTop: `1px solid ${theme.panelBorder}`,
2430
+ fontSize: 10,
2431
+ color: theme.subText,
2432
+ },
2433
+ },
2434
+ h('span', { style: { flex: 1, minWidth: 0 } }, `共 ${docsTotal} 份(显示最近 ${docsItems.length})`),
2435
+ docsItems.length < docsTotal && docsItems.length < 50
2436
+ ? h(
2437
+ 'button',
2438
+ {
2439
+ type: 'button',
2440
+ 'data-cr-docs-more': '1',
2441
+ onClick: () => {
2442
+ void loadDocs(50);
2443
+ },
2444
+ style: smallBtnStyle,
2445
+ },
2446
+ '加载更多',
2447
+ )
2448
+ : null,
2449
+ ),
2450
+ ]
2074
2451
  : h(
2075
2452
  'div',
2076
2453
  { style: { padding: '10px 12px', fontSize: 11, color: theme.subText } },
@@ -2106,12 +2483,8 @@ window.__ModuleLoader__.load({
2106
2483
  flexDirection: 'column',
2107
2484
  overflow: 'hidden',
2108
2485
  borderRadius: 12,
2109
- background: theme.panelBg,
2110
- backdropFilter: theme.panelBackdrop,
2111
- WebkitBackdropFilter: theme.panelBackdrop,
2486
+ ...panelChrome(theme),
2112
2487
  color: theme.panelFg,
2113
- border: `1px solid ${theme.panelBorder}`,
2114
- boxShadow: theme.panelShadow,
2115
2488
  zIndex: 43,
2116
2489
  },
2117
2490
  },
@@ -2208,16 +2581,12 @@ window.__ModuleLoader__.load({
2208
2581
  textAlign: 'left',
2209
2582
  padding: '8px 12px',
2210
2583
  borderRadius: 10,
2211
- background: theme.panelBg,
2212
- backdropFilter: theme.panelBackdrop,
2213
- WebkitBackdropFilter: theme.panelBackdrop,
2584
+ ...panelChrome(theme),
2214
2585
  color: theme.panelFg,
2215
- border: `1px solid ${theme.panelBorder}`,
2216
2586
  borderLeft: `3px solid ${/失败|错误|无法|没有可|不满足|拒绝|未知/.test(toastText) ? COLORS.ringAdviseHandoff : '#2fb344'}`,
2217
2587
  fontSize: 12,
2218
2588
  lineHeight: '18px',
2219
2589
  zIndex: 41,
2220
- boxShadow: theme.panelShadow,
2221
2590
  display: 'flex',
2222
2591
  alignItems: 'center',
2223
2592
  gap: 8,
@@ -2232,16 +2601,7 @@ window.__ModuleLoader__.load({
2232
2601
  type: 'button',
2233
2602
  'data-cr-toast-cancel': '1',
2234
2603
  onClick: cancelRun,
2235
- style: {
2236
- flex: 'none',
2237
- border: `1px solid ${theme.panelBorder}`,
2238
- borderRadius: 6,
2239
- padding: '2px 8px',
2240
- fontSize: 11,
2241
- cursor: 'pointer',
2242
- background: 'transparent',
2243
- color: theme.panelFg,
2244
- },
2604
+ style: { ...smallBtnStyle, flex: 'none' },
2245
2605
  },
2246
2606
  '取消',
2247
2607
  )
@@ -2958,16 +3318,7 @@ window.__ModuleLoader__.load({
2958
3318
  // BUG-009(2026-10-20):每个示例环单独构造参数——比例按各自语义取、hint 用 null;
2959
3319
  // 不再共用 minRatio + 0.12 的假断点(那会让 5 个环的断点全挤在同一处、颜色也对不上)。
2960
3320
  const demoRatio = (ratio, hintRatio) => ringSegments(ratio, form.thresholds.compactRatio, form.thresholds.handoffRatio, hintRatio ?? null);
2961
- const point = (value) => {
2962
- const angle = value * Math.PI * 2 - Math.PI / 2;
2963
- return [10 + 8 * Math.cos(angle), 10 + 8 * Math.sin(angle)];
2964
- };
2965
- const arcPath = (from, to) => {
2966
- const [x1, y1] = point(from);
2967
- const [x2, y2] = point(to);
2968
- const large = to - from > 0.5 ? 1 : 0;
2969
- return `M ${x1.toFixed(3)} ${y1.toFixed(3)} A 8 8 0 ${large} 1 ${x2.toFixed(3)} ${y2.toFixed(3)}`;
2970
- };
3321
+ // 点坐标/弧线 path 是共享几何(15-shared.js 的 ringPoint / ringArcPath)。
2971
3322
  const colorOf = (key) =>
2972
3323
  key === 'stage' ? previewLight.stage : key === 'compact' ? previewLight.compact : key === 'handoff' ? previewLight.handoff : previewLight.idle;
2973
3324
  const demoRing = (ring, options = {}) => {
@@ -2977,18 +3328,7 @@ window.__ModuleLoader__.load({
2977
3328
  // 原生自动压缩线:和指示灯一样用「径向紫色刻度 + 底色描边」;越过线后整环红色闪烁。
2978
3329
  const nativeAt =
2979
3330
  typeof options.nativeAt === 'number' && options.nativeAt > 0.02 && options.nativeAt < 1 ? options.nativeAt : null;
2980
- const pointAt = (value, radius) => {
2981
- const angle = value * Math.PI * 2 - Math.PI / 2;
2982
- return [10 + radius * Math.cos(angle), 10 + radius * Math.sin(angle)];
2983
- };
2984
- const nativeTick =
2985
- nativeAt === null
2986
- ? null
2987
- : (() => {
2988
- const [x1, y1] = pointAt(nativeAt, 5.6);
2989
- const [x2, y2] = pointAt(nativeAt, 9.6);
2990
- return { x1, y1, x2, y2 };
2991
- })();
3331
+ const nativeTick = nativeAt === null ? null : nativeTickAt(nativeAt);
2992
3332
  const nativeCrossed = nativeAt !== null && ring.ratio > nativeAt + 1e-9;
2993
3333
  return h(
2994
3334
  'svg',
@@ -3001,17 +3341,17 @@ window.__ModuleLoader__.load({
3001
3341
  const from = segment.from <= 1e-9 ? segment.from : segment.from + gap;
3002
3342
  const to = Math.abs(segment.to - ring.ratio) < 1e-9 ? segment.to : segment.to - gap;
3003
3343
  if (to - from <= 0.001) return null;
3004
- return h('path', { key: index, d: arcPath(from, to), fill: 'none', stroke: colorOf(segment.color), strokeWidth: 2, strokeLinecap: 'round', 'data-cr-demo': segment.color });
3344
+ return h('path', { key: index, d: ringArcPath(from, to), fill: 'none', stroke: colorOf(segment.color), strokeWidth: 2, strokeLinecap: 'round', 'data-cr-demo': segment.color });
3005
3345
  })
3006
3346
  .filter((node) => node !== null),
3007
3347
  // 断点缺口用「背景色小段」强调一下(教学图里看得清)
3008
3348
  ...gaps.map((at, index) =>
3009
- h('path', { key: `b${index}`, d: arcPath(at - 0.01, at + 0.01), fill: 'none', stroke: 'var(--dsw-alias-bg-base, #fff)', strokeWidth: 3, 'data-cr-demo-gap': String(at) }),
3349
+ h('path', { key: `b${index}`, d: ringArcPath(at - 0.01, at + 0.01), fill: 'none', stroke: 'var(--dsw-alias-bg-base, #fff)', strokeWidth: 3, 'data-cr-demo-gap': String(at) }),
3010
3350
  ),
3011
3351
  // 越过原生压缩线:整环红色闪烁(教学图里也演示出来)。
3012
3352
  ...(nativeCrossed
3013
3353
  ? [
3014
- h('style', { key: 'native-alert-style' }, '@keyframes cr-native-alert { 0%,100% { opacity: 1 } 50% { opacity: 0.15 } } @media (prefers-reduced-motion: reduce) { [data-cr-native-alert] { animation: none !important; opacity: 0.85 !important; } }'),
3354
+ h('style', { key: 'native-alert-style' }, NATIVE_ALERT_CSS),
3015
3355
  h('circle', {
3016
3356
  key: 'native-alert',
3017
3357
  cx: 10,