dsh-mega-chat-nav 0.1.1

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 (192) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +149 -0
  3. package/README.md +149 -0
  4. package/cordis.patch.yml +4 -0
  5. package/lib/client/NavOverlay.js +35 -0
  6. package/lib/client/NavRail.js +12 -0
  7. package/lib/client/QuestionNavSettingsTab.js +26 -0
  8. package/lib/client/QuestionNavStrip.js +555 -0
  9. package/lib/client/components/ConfigPanel.js +14 -0
  10. package/lib/client/components/DotRail.js +62 -0
  11. package/lib/client/components/NavOverlay.js +121 -0
  12. package/lib/client/components/NavRail.js +97 -0
  13. package/lib/client/components/RailDispatch.js +22 -0
  14. package/lib/client/components/RailHost.js +79 -0
  15. package/lib/client/components/SearchBox.js +66 -0
  16. package/lib/client/components/SettingsPage.js +46 -0
  17. package/lib/client/components/StandaloneSettings.js +16 -0
  18. package/lib/client/components/codex/CodexRail.js +139 -0
  19. package/lib/client/components/deepseek/DeepseekRail.js +264 -0
  20. package/lib/client/components/minimal/MinimalRail.js +135 -0
  21. package/lib/client/components/minimal/cards.js +32 -0
  22. package/lib/client/components/minimal/dot.js +11 -0
  23. package/lib/client/components/minimal/hint.js +30 -0
  24. package/lib/client/components/minimal/paging.js +5 -0
  25. package/lib/client/components/minimal/quick-settings.js +28 -0
  26. package/lib/client/components/minimal/search.js +67 -0
  27. package/lib/client/components/minimal/useRail.js +716 -0
  28. package/lib/client/components/rail/cards.js +32 -0
  29. package/lib/client/components/rail/hint.js +30 -0
  30. package/lib/client/components/rail/paging.js +5 -0
  31. package/lib/client/components/rail/quick-settings.js +28 -0
  32. package/lib/client/components/rail/search.js +76 -0
  33. package/lib/client/components/rail/useRail.js +845 -0
  34. package/lib/client/components/settings/SettingsPage.js +46 -0
  35. package/lib/client/components/settings/SettingsTab.js +17 -0
  36. package/lib/client/components/settings/StandaloneSettings.js +16 -0
  37. package/lib/client/components/shared/Cascade.js +26 -0
  38. package/lib/client/components/shared/ConfigPanel.js +27 -0
  39. package/lib/client/components/shared/Dot.js +8 -0
  40. package/lib/client/components/shared/Hint.js +5 -0
  41. package/lib/client/components/shared/PagingButton.js +5 -0
  42. package/lib/client/components/shared/RailShell.js +78 -0
  43. package/lib/client/components/shared/SearchBox.js +63 -0
  44. package/lib/client/components/shared/hooks.js +427 -0
  45. package/lib/client/components/shared/types.js +1 -0
  46. package/lib/client/config.js +1 -0
  47. package/lib/client/engines/jump.js +34 -0
  48. package/lib/client/engines/useAtBottom.js +25 -0
  49. package/lib/client/engines/useJump.js +40 -0
  50. package/lib/client/engines/useScrollSpy.js +88 -0
  51. package/lib/client/hooks.js +11 -0
  52. package/lib/client/index.js +312 -0
  53. package/lib/client/jump.js +255 -0
  54. package/lib/client/locales.js +142 -0
  55. package/lib/client/pure.js +61 -0
  56. package/lib/client/settings.js +369 -0
  57. package/lib/client/virtual/VirtualList.js +21 -0
  58. package/lib/client.js +3213 -0
  59. package/lib/compat.js +145 -0
  60. package/lib/config.js +37 -0
  61. package/lib/core/active-dot.js +17 -0
  62. package/lib/core/align.js +9 -0
  63. package/lib/core/config.js +37 -0
  64. package/lib/core/focus.js +38 -0
  65. package/lib/core/jump.js +98 -0
  66. package/lib/core/logic/active-dot.js +16 -0
  67. package/lib/core/logic/focus.js +34 -0
  68. package/lib/core/logic/jump.js +188 -0
  69. package/lib/core/logic/nodes.js +59 -0
  70. package/lib/core/model/nav-item.js +1 -0
  71. package/lib/core/model/turns.js +73 -0
  72. package/lib/core/nav-item.js +1 -0
  73. package/lib/core/nodes.js +57 -0
  74. package/lib/core/page-size.js +7 -0
  75. package/lib/core/question-entry.js +1 -0
  76. package/lib/core/time.js +23 -0
  77. package/lib/core/turn-dots.js +85 -0
  78. package/lib/core/utils/measure.js +24 -0
  79. package/lib/core/utils/time.js +26 -0
  80. package/lib/index.js +64 -0
  81. package/lib/nav-item.js +1 -0
  82. package/lib/nodes.js +59 -0
  83. package/lib/projection.js +126 -0
  84. package/lib/schema.js +26 -0
  85. package/lib/search.js +285 -0
  86. package/lib/settings.js +89 -0
  87. package/lib/types/client/NavOverlay.d.ts +18 -0
  88. package/lib/types/client/NavRail.d.ts +7 -0
  89. package/lib/types/client/QuestionNavSettingsTab.d.ts +16 -0
  90. package/lib/types/client/QuestionNavStrip.d.ts +48 -0
  91. package/lib/types/client/components/ConfigPanel.d.ts +14 -0
  92. package/lib/types/client/components/DotRail.d.ts +15 -0
  93. package/lib/types/client/components/NavOverlay.d.ts +30 -0
  94. package/lib/types/client/components/NavRail.d.ts +16 -0
  95. package/lib/types/client/components/RailDispatch.d.ts +8 -0
  96. package/lib/types/client/components/RailHost.d.ts +27 -0
  97. package/lib/types/client/components/SearchBox.d.ts +9 -0
  98. package/lib/types/client/components/SettingsPage.d.ts +9 -0
  99. package/lib/types/client/components/StandaloneSettings.d.ts +13 -0
  100. package/lib/types/client/components/codex/CodexRail.d.ts +3 -0
  101. package/lib/types/client/components/deepseek/DeepseekRail.d.ts +3 -0
  102. package/lib/types/client/components/minimal/MinimalRail.d.ts +8 -0
  103. package/lib/types/client/components/minimal/cards.d.ts +19 -0
  104. package/lib/types/client/components/minimal/dot.d.ts +14 -0
  105. package/lib/types/client/components/minimal/hint.d.ts +11 -0
  106. package/lib/types/client/components/minimal/paging.d.ts +10 -0
  107. package/lib/types/client/components/minimal/quick-settings.d.ts +8 -0
  108. package/lib/types/client/components/minimal/search.d.ts +27 -0
  109. package/lib/types/client/components/minimal/useRail.d.ts +90 -0
  110. package/lib/types/client/components/rail/cards.d.ts +19 -0
  111. package/lib/types/client/components/rail/hint.d.ts +11 -0
  112. package/lib/types/client/components/rail/paging.d.ts +10 -0
  113. package/lib/types/client/components/rail/quick-settings.d.ts +8 -0
  114. package/lib/types/client/components/rail/search.d.ts +34 -0
  115. package/lib/types/client/components/rail/useRail.d.ts +132 -0
  116. package/lib/types/client/components/settings/SettingsPage.d.ts +9 -0
  117. package/lib/types/client/components/settings/SettingsTab.d.ts +6 -0
  118. package/lib/types/client/components/settings/StandaloneSettings.d.ts +13 -0
  119. package/lib/types/client/components/shared/Cascade.d.ts +14 -0
  120. package/lib/types/client/components/shared/ConfigPanel.d.ts +8 -0
  121. package/lib/types/client/components/shared/Dot.d.ts +17 -0
  122. package/lib/types/client/components/shared/Hint.d.ts +6 -0
  123. package/lib/types/client/components/shared/PagingButton.d.ts +10 -0
  124. package/lib/types/client/components/shared/RailShell.d.ts +43 -0
  125. package/lib/types/client/components/shared/SearchBox.d.ts +18 -0
  126. package/lib/types/client/components/shared/hooks.d.ts +64 -0
  127. package/lib/types/client/components/shared/types.d.ts +101 -0
  128. package/lib/types/client/config.d.ts +9 -0
  129. package/lib/types/client/engines/jump.d.ts +17 -0
  130. package/lib/types/client/engines/useAtBottom.d.ts +1 -0
  131. package/lib/types/client/engines/useJump.d.ts +16 -0
  132. package/lib/types/client/engines/useScrollSpy.d.ts +12 -0
  133. package/lib/types/client/hooks.d.ts +7 -0
  134. package/lib/types/client/index.d.ts +31 -0
  135. package/lib/types/client/jump.d.ts +47 -0
  136. package/lib/types/client/locales.d.ts +142 -0
  137. package/lib/types/client/pure.d.ts +21 -0
  138. package/lib/types/client/settings.d.ts +173 -0
  139. package/lib/types/client/virtual/VirtualList.d.ts +15 -0
  140. package/lib/types/compat.d.ts +44 -0
  141. package/lib/types/config.d.ts +44 -0
  142. package/lib/types/core/active-dot.d.ts +15 -0
  143. package/lib/types/core/align.d.ts +10 -0
  144. package/lib/types/core/config.d.ts +44 -0
  145. package/lib/types/core/focus.d.ts +24 -0
  146. package/lib/types/core/jump.d.ts +39 -0
  147. package/lib/types/core/logic/active-dot.d.ts +15 -0
  148. package/lib/types/core/logic/focus.d.ts +22 -0
  149. package/lib/types/core/logic/jump.d.ts +39 -0
  150. package/lib/types/core/logic/nodes.d.ts +48 -0
  151. package/lib/types/core/model/nav-item.d.ts +12 -0
  152. package/lib/types/core/model/turns.d.ts +30 -0
  153. package/lib/types/core/nav-item.d.ts +12 -0
  154. package/lib/types/core/nodes.d.ts +43 -0
  155. package/lib/types/core/page-size.d.ts +8 -0
  156. package/lib/types/core/question-entry.d.ts +12 -0
  157. package/lib/types/core/time.d.ts +4 -0
  158. package/lib/types/core/turn-dots.d.ts +29 -0
  159. package/lib/types/core/utils/measure.d.ts +12 -0
  160. package/lib/types/core/utils/time.d.ts +4 -0
  161. package/lib/types/index.d.ts +4 -0
  162. package/lib/types/nav-item.d.ts +12 -0
  163. package/lib/types/nodes.d.ts +48 -0
  164. package/lib/types/projection.d.ts +71 -0
  165. package/lib/types/schema.d.ts +37 -0
  166. package/lib/types/search.d.ts +42 -0
  167. package/lib/types/settings.d.ts +99 -0
  168. package/package.json +71 -0
  169. package/src/client/components/RailDispatch.tsx +27 -0
  170. package/src/client/components/codex/CodexRail.tsx +348 -0
  171. package/src/client/components/deepseek/DeepseekRail.tsx +522 -0
  172. package/src/client/components/minimal/MinimalRail.tsx +324 -0
  173. package/src/client/components/minimal/dot.tsx +32 -0
  174. package/src/client/components/rail/cards.tsx +102 -0
  175. package/src/client/components/rail/hint.tsx +50 -0
  176. package/src/client/components/rail/paging.tsx +26 -0
  177. package/src/client/components/rail/quick-settings.tsx +130 -0
  178. package/src/client/components/rail/search.tsx +147 -0
  179. package/src/client/components/rail/useRail.ts +906 -0
  180. package/src/client/components/settings/SettingsPage.tsx +198 -0
  181. package/src/client/components/settings/StandaloneSettings.tsx +28 -0
  182. package/src/client/components/shared/types.ts +99 -0
  183. package/src/client/index.ts +388 -0
  184. package/src/client/jump.ts +293 -0
  185. package/src/client/locales.ts +143 -0
  186. package/src/client/settings.ts +482 -0
  187. package/src/client/styles.css +1580 -0
  188. package/src/compat.ts +161 -0
  189. package/src/index.ts +66 -0
  190. package/src/projection.ts +174 -0
  191. package/src/search.ts +291 -0
  192. package/src/settings.ts +169 -0
@@ -0,0 +1,59 @@
1
+ function rowData(data) {
2
+ if (typeof data !== 'object' || data === null)
3
+ return undefined;
4
+ return data;
5
+ }
6
+ /** 会话行节点的导航提取 / 查找工具 */
7
+ export class NavNodes {
8
+ /** 计入「提问」的节点 kind */
9
+ static KINDS = ['user', 'steering'];
10
+ /** 判定节点 kind 是否计入提问 */
11
+ static isKind(kind) {
12
+ return NavNodes.KINDS.includes(kind);
13
+ }
14
+ /** 消息首文本块;缺省空串 */
15
+ static messageText(content) {
16
+ const block = content?.find((part) => typeof part?.text === 'string');
17
+ return block?.text ?? '';
18
+ }
19
+ /** 从节点窗口提取提问列表,按 anchorSeq 升序(筛选 → 投影 → 排序 管线) */
20
+ static extract(rows) {
21
+ return [...rows]
22
+ .filter((node) => NavNodes.isKind(node.kind))
23
+ .map((node) => {
24
+ const payload = rowData(node.data);
25
+ return {
26
+ key: node.key,
27
+ anchorSeq: node.anchorSeq,
28
+ seq: payload?.seq ?? -1,
29
+ time: payload?.time ?? 0,
30
+ text: NavNodes.messageText(payload?.content),
31
+ };
32
+ })
33
+ .sort((a, b) => a.anchorSeq - b.anchorSeq);
34
+ }
35
+ /** 节点是否实际渲染(隐藏行不可作滚动目标) */
36
+ static isRenderable(node) {
37
+ return node.visibility !== 'hidden';
38
+ }
39
+ /** 窗口内精确查找 key 对应行 */
40
+ static find(rows, key) {
41
+ for (const node of rows) {
42
+ if (node.key === key)
43
+ return node;
44
+ }
45
+ return null;
46
+ }
47
+ /** 缺失/隐藏 key 时回退到最近可渲染行(取最新 anchorSeq,贴近用户点击意图) */
48
+ static nearest(rows, excludeKey) {
49
+ let best = null;
50
+ for (const node of rows) {
51
+ if (node.visibility === 'hidden' || node.key === excludeKey)
52
+ continue;
53
+ if (best === null || node.anchorSeq > best.anchorSeq) {
54
+ best = { key: node.key, anchorSeq: node.anchorSeq };
55
+ }
56
+ }
57
+ return best;
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ /** 归约一步:把一条 input-message 投影条目并入圆点列(同回合追加,否则新开圆点) */
2
+ function appendEntry(acc, entry) {
3
+ const key = '13:input-message' + String(entry.id);
4
+ if (acc.tail !== null && acc.tail.turn === entry.turn) {
5
+ const merged = {
6
+ ...acc.tail,
7
+ texts: [...acc.tail.texts, entry.text],
8
+ memberKeys: [...acc.tail.memberKeys, key],
9
+ };
10
+ acc.dots[acc.dots.length - 1] = merged;
11
+ return { dots: acc.dots, tail: merged };
12
+ }
13
+ const dot = {
14
+ turn: entry.turn,
15
+ key,
16
+ anchorSeq: entry.seq,
17
+ time: entry.time,
18
+ texts: [entry.text],
19
+ memberKeys: [key],
20
+ };
21
+ acc.dots.push(dot);
22
+ return { dots: acc.dots, tail: dot };
23
+ }
24
+ /** 投影提问列表 → 每回合一个圆点;同回合连续条目合并(锚点取首问) */
25
+ export function groupByTurn(entries) {
26
+ const alreadyOrdered = entries.every((entry, index) => index === 0 || entry.seq >= entries[index - 1].seq);
27
+ const sorted = alreadyOrdered ? entries : [...entries].sort((a, b) => a.seq - b.seq);
28
+ return sorted.reduce((acc, entry) => appendEntry(acc, entry), { dots: [], tail: null }).dots;
29
+ }
30
+ /**
31
+ * 合并活窗口提问(投影推送帧到达前的短暂窗口):
32
+ * 已并入圆点的 key 丢弃(投影副本优先);其余作为单问圆点(turn:null)
33
+ * 按 anchorSeq 有序插入。无新增时返回原数组引用(调用方可 bail out)。
34
+ */
35
+ export function mergeLiveEntries(dots, live) {
36
+ if (live.length === 0)
37
+ return dots;
38
+ const known = new Set();
39
+ for (const dot of dots) {
40
+ for (const key of dot.memberKeys)
41
+ known.add(key);
42
+ }
43
+ const extras = [];
44
+ for (const node of live) {
45
+ if (known.has(node.key))
46
+ continue;
47
+ extras.push({
48
+ turn: null,
49
+ key: node.key,
50
+ anchorSeq: node.anchorSeq,
51
+ time: node.time,
52
+ texts: [node.text],
53
+ memberKeys: [node.key],
54
+ });
55
+ }
56
+ if (extras.length === 0)
57
+ return dots;
58
+ // 两路有序归并:单循环统一推进(同序时投影条目在前,保持稳定)
59
+ const merged = [];
60
+ let i = 0;
61
+ let j = 0;
62
+ while (i < dots.length || j < extras.length) {
63
+ if (j >= extras.length || (i < dots.length && dots[i].anchorSeq <= extras[j].anchorSeq)) {
64
+ merged.push(dots[i]);
65
+ i += 1;
66
+ }
67
+ else {
68
+ merged.push(extras[j]);
69
+ j += 1;
70
+ }
71
+ }
72
+ return merged;
73
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ /** 计入「提问」的节点 kind */
2
+ export const NAV_KINDS = ['user', 'steering'];
3
+ function rowData(data) {
4
+ if (typeof data !== 'object' || data === null)
5
+ return undefined;
6
+ return data;
7
+ }
8
+ /** 消息首文本块;缺省空串 */
9
+ export function messageText(content) {
10
+ if (content === undefined || content.length === 0)
11
+ return '';
12
+ const first = content[0];
13
+ return typeof first?.text === 'string' ? first.text : '';
14
+ }
15
+ /** 从节点窗口提取提问列表,按 anchorSeq 升序 */
16
+ export function extractNavNodes(nodes) {
17
+ const out = [];
18
+ for (const node of nodes) {
19
+ if (!NAV_KINDS.includes(node.kind ?? ''))
20
+ continue;
21
+ const payload = rowData(node.data);
22
+ out.push({
23
+ key: node.key,
24
+ anchorSeq: node.anchorSeq,
25
+ seq: payload?.seq ?? -1,
26
+ time: payload?.time ?? 0,
27
+ text: messageText(payload?.content),
28
+ });
29
+ }
30
+ out.sort((a, b) => a.anchorSeq - b.anchorSeq);
31
+ return out;
32
+ }
33
+ /** 节点是否实际渲染(隐藏行不可作滚动目标) */
34
+ export function isRenderable(node) {
35
+ return node.visibility !== 'hidden';
36
+ }
37
+ /** 窗口内精确查找 key 对应行 */
38
+ export function findNavRow(nodes, key) {
39
+ for (const node of nodes) {
40
+ if (node.key === key)
41
+ return node;
42
+ }
43
+ return null;
44
+ }
45
+ /** 缺失/隐藏 key 时回退到最近可渲染行(取最新 anchorSeq,贴近用户点击意图) */
46
+ export function nearestRenderable(nodes, excludeKey) {
47
+ let best = null;
48
+ for (const node of nodes) {
49
+ if (node.visibility === 'hidden')
50
+ continue;
51
+ if (node.key === excludeKey)
52
+ continue;
53
+ if (best === null || node.anchorSeq > best.anchorSeq)
54
+ best = { key: node.key, anchorSeq: node.anchorSeq };
55
+ }
56
+ return best;
57
+ }
@@ -0,0 +1,7 @@
1
+ // ▲/▼ 翻页步长常量:host 设置 schema 与浏览器设置面共用(纯数据)
2
+ /** 可选翻页数量 */
3
+ export const PageSizeOptions = [3, 5, 8, 10];
4
+ /** 默认翻页数量 */
5
+ export const DEFAULT_PAGE = 5;
6
+ /** 设置字段:翻页数量 */
7
+ export const PAGE_SIZE_KEY = 'pageSize';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ // 消息时间格式化(纯函数)
2
+ function pad2(n) {
3
+ return n < 10 ? '0' + n : String(n);
4
+ }
5
+ /** 是否同一自然日 */
6
+ export function sameDay(time, now) {
7
+ const a = new Date(time);
8
+ const b = new Date(now);
9
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
10
+ }
11
+ /** 智能时间:同日 HH:MM / 同年 MM-DD HH:MM / 其他 YYYY-MM-DD HH:MM;非法时间返回空串 */
12
+ export function formatNavTime(time, now) {
13
+ if (!Number.isFinite(time) || time <= 0)
14
+ return '';
15
+ const d = new Date(time);
16
+ const hm = pad2(d.getHours()) + ':' + pad2(d.getMinutes());
17
+ if (sameDay(time, now))
18
+ return hm;
19
+ const md = pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
20
+ if (d.getFullYear() === new Date(now).getFullYear())
21
+ return md + ' ' + hm;
22
+ return d.getFullYear() + '-' + md + ' ' + hm;
23
+ }
@@ -0,0 +1,85 @@
1
+ /** 用户提问节点的定义 kind(锚点 key 推导用) */
2
+ export const MESSAGE_KIND = 'input-message';
3
+ /** 消息稳定聊天 key:镜像 conversationContextKey(kind, id) 公式 */
4
+ export function navKeyOf(id) {
5
+ const kind = MESSAGE_KIND;
6
+ return kind.length + ':' + kind + String(id);
7
+ }
8
+ /** 输入已按 seq 非降序(投影按事件序追加,常见路径免排序) */
9
+ function isOrdered(entries) {
10
+ for (let i = 1; i < entries.length; i++) {
11
+ if (entries[i].seq < entries[i - 1].seq)
12
+ return false;
13
+ }
14
+ return true;
15
+ }
16
+ /** 投影提问列表 → 每回合一个圆点;同回合连续条目合并(锚点取首问) */
17
+ export function groupByTurn(entries) {
18
+ const ordered = isOrdered(entries) ? entries : [...entries].sort((a, b) => a.seq - b.seq);
19
+ const dots = [];
20
+ for (const entry of ordered) {
21
+ const key = navKeyOf(entry.id);
22
+ const last = dots[dots.length - 1];
23
+ if (last !== undefined && last.turn === entry.turn) {
24
+ dots[dots.length - 1] = {
25
+ ...last,
26
+ texts: [...last.texts, entry.text],
27
+ memberKeys: [...last.memberKeys, key],
28
+ };
29
+ continue;
30
+ }
31
+ dots.push({
32
+ turn: entry.turn,
33
+ key,
34
+ anchorSeq: entry.seq,
35
+ time: entry.time,
36
+ texts: [entry.text],
37
+ memberKeys: [key],
38
+ });
39
+ }
40
+ return dots;
41
+ }
42
+ /**
43
+ * 合并活窗口提问(投影推送帧到达前的短暂窗口):
44
+ * 已并入圆点的 key 丢弃(投影副本优先);其余作为单问圆点(turn:null)
45
+ * 按 anchorSeq 有序插入。无新增时返回原数组引用(调用方可 bail out)。
46
+ */
47
+ export function mergeLiveEntries(dots, live) {
48
+ if (live.length === 0)
49
+ return dots;
50
+ const known = new Set();
51
+ for (const dot of dots) {
52
+ for (const key of dot.memberKeys)
53
+ known.add(key);
54
+ }
55
+ const extras = [];
56
+ for (const node of live) {
57
+ if (known.has(node.key))
58
+ continue;
59
+ extras.push({
60
+ turn: null,
61
+ key: node.key,
62
+ anchorSeq: node.anchorSeq,
63
+ time: node.time,
64
+ texts: [node.text],
65
+ memberKeys: [node.key],
66
+ });
67
+ }
68
+ if (extras.length === 0)
69
+ return dots;
70
+ // 两侧均有序:线性归并(同序时投影条目在前,保持稳定)
71
+ const out = [];
72
+ let i = 0;
73
+ for (const extra of extras) {
74
+ while (i < dots.length && dots[i].anchorSeq <= extra.anchorSeq) {
75
+ out.push(dots[i]);
76
+ i += 1;
77
+ }
78
+ out.push(extra);
79
+ }
80
+ while (i < dots.length) {
81
+ out.push(dots[i]);
82
+ i += 1;
83
+ }
84
+ return out;
85
+ }
@@ -0,0 +1,24 @@
1
+ // 翻页几何工具(纯函数):圆点直径/间距与 ▲/▼ 步进
2
+ /** 圆点直径(px) */
3
+ export const DOT_SIZE = 8;
4
+ /** 相邻圆点间距(px) */
5
+ export const DOT_GAP = 6;
6
+ /** 一次翻页揭示的整行数 */
7
+ export const DOT_PAGE_ROWS = 5;
8
+ /** 单行占位(直径 + 间距) */
9
+ export function rowPitch(dot = DOT_SIZE, gap = DOT_GAP) {
10
+ return dot + gap;
11
+ }
12
+ /** 一次翻页的滚动步长(px):行数 × 单行占位 */
13
+ export function pageStep(dot = DOT_SIZE, gap = DOT_GAP, rows = DOT_PAGE_ROWS) {
14
+ return rows * rowPitch(dot, gap);
15
+ }
16
+ /** 将滚动位置钳制到有效区间 [0, maxScroll];非法上限按 0 处理 */
17
+ export function clampScrollTop(target, maxScroll) {
18
+ const ceiling = Math.max(0, maxScroll);
19
+ if (target < 0)
20
+ return 0;
21
+ if (target > ceiling)
22
+ return ceiling;
23
+ return target;
24
+ }
@@ -0,0 +1,26 @@
1
+ // 时间工具(纯函数):圆点/级联卡/搜索结果的相对时间显示
2
+ function twoDigits(n) {
3
+ return String(n).padStart(2, '0');
4
+ }
5
+ /** 自然日整数键(YYYYMMDD),同日比较用 */
6
+ function dayKey(time) {
7
+ const d = new Date(time);
8
+ return d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
9
+ }
10
+ /** 是否为同一自然日 */
11
+ export function sameDay(time, now) {
12
+ return dayKey(time) === dayKey(now);
13
+ }
14
+ /** 智能时间:同日 HH:MM / 同年 MM-DD HH:MM / 跨年 YYYY-MM-DD HH:MM;非法输入返回空串 */
15
+ export function formatNavTime(time, now) {
16
+ if (!(time > 0) || !Number.isFinite(time))
17
+ return '';
18
+ const stamp = new Date(time);
19
+ const clock = twoDigits(stamp.getHours()) + ':' + twoDigits(stamp.getMinutes());
20
+ if (dayKey(time) === dayKey(now))
21
+ return clock;
22
+ const calendar = twoDigits(stamp.getMonth() + 1) + '-' + twoDigits(stamp.getDate());
23
+ if (stamp.getFullYear() === new Date(now).getFullYear())
24
+ return calendar + ' ' + clock;
25
+ return stamp.getFullYear() + '-' + calendar + ' ' + clock;
26
+ }
package/lib/index.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Host half of the dsh-mega-chat-nav plugin — runs in the DSH host process.
3
+ * Registers the `msgNavMessages` session projection unit (the ordered list
4
+ * of user questions, each tagged with its turn) and the plugin's durable
5
+ * settings namespace. Both registries are optional capabilities, so each
6
+ * registration rides `ctx.inject`: without them the host half contributes
7
+ * nothing and the browser strip falls back to live-window questions and
8
+ * default rail alignment.
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import { createRequire } from 'node:module';
12
+ import { msgNavProjectionDefinition } from "./projection.js";
13
+ import { registerSearchRoute } from "./search.js";
14
+ import { megaChatNavSettingsNamespace, NavSettingsSchema } from "./settings.js";
15
+ import { checkDshPolicy, dshCompatMessage } from "./compat.js";
16
+ /** Cordis plugin name. */
17
+ export const name = 'dsh-mega-chat-nav';
18
+ /**
19
+ * 兼容策略(按需修改):
20
+ * - '= 0.1.1-*' = 支持 dsh-v0.1.1 整条预发布线(0.1.1-rc.1 / rc.2);
21
+ * - target 支持数组:命中任一即通过,如 ['0.1.1', '0.1.2-rc.1'];
22
+ * - 关系约束示例:{ op: '>=', target: '0.1.1-rc.2' }。
23
+ */
24
+ const DSCH_COMPAT_POLICY = { op: '=', target: '0.1.1-*' };
25
+ const dshRequire = createRequire(import.meta.url);
26
+ /** 探测当前 dsh 版本:@deepseek-ai/dsh 本体 → dsh → dsh-client-runtime(与 tag 线 lockstep) */
27
+ function detectDshVersion() {
28
+ for (const pkg of ['@deepseek-ai/dsh', 'dsh', '@deepseek-ai/dsh-client-runtime']) {
29
+ try {
30
+ const pkgJson = dshRequire.resolve(pkg + '/package.json');
31
+ const parsed = JSON.parse(readFileSync(pkgJson, 'utf8'));
32
+ if (typeof parsed.version === 'string' && parsed.version.length > 0)
33
+ return parsed.version;
34
+ }
35
+ catch {
36
+ /* 尝试下一候选 */
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ export function apply(ctx) {
42
+ // dsh 版本兼容校验:检测失败或不合规只打印警示,不阻断插件加载
43
+ try {
44
+ const dshVer = detectDshVersion();
45
+ if (dshVer !== null && !checkDshPolicy(dshVer, DSCH_COMPAT_POLICY)) {
46
+ console.warn(dshCompatMessage(name, dshVer));
47
+ }
48
+ }
49
+ catch {
50
+ /* 校验器自身异常不应影响插件 */
51
+ }
52
+ ctx.inject(['sessionProjections'], (inner) => {
53
+ inner.sessionProjections.register(msgNavProjectionDefinition);
54
+ });
55
+ ctx.inject(['settings'], (settingsCtx) => {
56
+ settingsCtx.settings.register(megaChatNavSettingsNamespace, NavSettingsSchema);
57
+ });
58
+ try {
59
+ registerSearchRoute(ctx);
60
+ }
61
+ catch (e) {
62
+ console.error('[dsh-mega-chat-nav] search route failed:', e);
63
+ }
64
+ }
@@ -0,0 +1 @@
1
+ export {};
package/lib/nodes.js ADDED
@@ -0,0 +1,59 @@
1
+ function rowData(data) {
2
+ if (typeof data !== 'object' || data === null)
3
+ return undefined;
4
+ return data;
5
+ }
6
+ /** 会话行节点的导航提取 / 查找工具 */
7
+ export class NavNodes {
8
+ /** 计入「提问」的节点 kind */
9
+ static KINDS = ['user', 'steering'];
10
+ /** 判定节点 kind 是否计入提问 */
11
+ static isKind(kind) {
12
+ return NavNodes.KINDS.includes(kind);
13
+ }
14
+ /** 消息首文本块;缺省空串 */
15
+ static messageText(content) {
16
+ const block = content?.find((part) => typeof part?.text === 'string');
17
+ return block?.text ?? '';
18
+ }
19
+ /** 从节点窗口提取提问列表,按 anchorSeq 升序(筛选 → 投影 → 排序 管线) */
20
+ static extract(rows) {
21
+ return [...rows]
22
+ .filter((node) => NavNodes.isKind(node.kind))
23
+ .map((node) => {
24
+ const payload = rowData(node.data);
25
+ return {
26
+ key: node.key,
27
+ anchorSeq: node.anchorSeq,
28
+ seq: payload?.seq ?? -1,
29
+ time: payload?.time ?? 0,
30
+ text: NavNodes.messageText(payload?.content),
31
+ };
32
+ })
33
+ .sort((a, b) => a.anchorSeq - b.anchorSeq);
34
+ }
35
+ /** 节点是否实际渲染(隐藏行不可作滚动目标) */
36
+ static isRenderable(node) {
37
+ return node.visibility !== 'hidden';
38
+ }
39
+ /** 窗口内精确查找 key 对应行 */
40
+ static find(rows, key) {
41
+ for (const node of rows) {
42
+ if (node.key === key)
43
+ return node;
44
+ }
45
+ return null;
46
+ }
47
+ /** 缺失/隐藏 key 时回退到最近可渲染行(取最新 anchorSeq,贴近用户点击意图) */
48
+ static nearest(rows, excludeKey) {
49
+ let best = null;
50
+ for (const node of rows) {
51
+ if (node.visibility === 'hidden' || node.key === excludeKey)
52
+ continue;
53
+ if (best === null || node.anchorSeq > best.anchorSeq) {
54
+ best = { key: node.key, anchorSeq: node.anchorSeq };
55
+ }
56
+ }
57
+ return best;
58
+ }
59
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The `msgNavMessages` session projection unit: a pure fold of the session
3
+ * event log into the ordered list of user questions, each tagged with the
4
+ * turn that claimed it. Registered on `ctx.sessionProjections` by the host
5
+ * half (src/index.ts); persistence, replay, and client delivery are the
6
+ * projection seam's (session-projection-cache checkpoints the state, the
7
+ * api-proxy carriers seed + push the wire view).
8
+ *
9
+ * Fold rules mirror the chat messageDefinition classification: an
10
+ * append-origin `user/message` with a human (`user`) source is a question;
11
+ * replacement copies (compaction checkpoints) and injected context are not.
12
+ * Turns come from `turn/start` boundaries, so retry/goal-continuation turns
13
+ * without a question simply produce no entry — dots may skip turn numbers,
14
+ * staying exactly aligned with the Trajectory view's turn labels.
15
+ *
16
+ * @module dsh-mega-chat-nav/projection
17
+ */
18
+ import { z } from 'zod';
19
+ const navEntrySchema = z.object({
20
+ turn: z.number(),
21
+ id: z.string(),
22
+ seq: z.number(),
23
+ time: z.number(),
24
+ text: z.string(),
25
+ metrics: z.object({
26
+ durationMs: z.number(),
27
+ firstTokenMs: z.number(),
28
+ tokensPerSec: z.number(),
29
+ }).optional(),
30
+ });
31
+ const turnMetricSchema = z.object({
32
+ startedAt: z.number(),
33
+ firstTokenAt: z.number().nullable(),
34
+ finishedAt: z.number().nullable(),
35
+ outputTokens: z.number(),
36
+ });
37
+ const msgNavMessagesStateSchema = z.object({
38
+ turn: z.number(),
39
+ questions: z.array(navEntrySchema),
40
+ metrics: z.record(z.number(), turnMetricSchema),
41
+ }).strict();
42
+ /** Validates the wire payload before it leaves the host. */
43
+ const msgNavMessagesViewSchema = z.array(navEntrySchema);
44
+ /** First text block of a user message; empty string when absent. */
45
+ function messageText(content) {
46
+ const block = content?.find((part) => typeof part?.text === 'string');
47
+ return block?.text ?? '';
48
+ }
49
+ /** The `msgNavMessages` unit registered on `ctx.sessionProjections`. */
50
+ export const msgNavProjectionDefinition = {
51
+ key: 'msgNavMessages',
52
+ stateVersion: 2,
53
+ stateSchema: msgNavMessagesStateSchema,
54
+ init: () => ({ turn: 0, questions: [], metrics: {} }),
55
+ apply: (state, event) => {
56
+ // Every uninteresting event returns the same reference (Object.is gates
57
+ // the change feed and the persisted-cache dirty check).
58
+ switch (event.type) {
59
+ case 'turn/start': {
60
+ if (event.data.turn === state.turn)
61
+ return state;
62
+ const metrics = { ...state.metrics };
63
+ metrics[event.data.turn] = { startedAt: event.time, firstTokenAt: null, finishedAt: null, outputTokens: 0 };
64
+ return { ...state, turn: event.data.turn, metrics };
65
+ }
66
+ case 'user/message': {
67
+ if (event.surfaceOp !== 'append')
68
+ return state;
69
+ if (event.data.source?.kind !== 'user')
70
+ return state;
71
+ const entry = {
72
+ turn: state.turn,
73
+ id: String(event.data.id),
74
+ seq: event.seq,
75
+ time: event.time,
76
+ text: messageText(event.data.content),
77
+ };
78
+ return { ...state, questions: [...state.questions, entry] };
79
+ }
80
+ case 'assistant/chunk': {
81
+ // 只记录每轮首个内容增量块(首 token 时间);后续增量块不产生新 state
82
+ const turn = event.data.turn;
83
+ const chunk = event.data.chunk;
84
+ const current = state.metrics[turn];
85
+ if (current === undefined)
86
+ return state;
87
+ if (current.firstTokenAt !== null)
88
+ return state;
89
+ if (chunk?.type !== 'text-delta' && chunk?.type !== 'reasoning-delta')
90
+ return state;
91
+ const metrics = { ...state.metrics, [turn]: { ...current, firstTokenAt: event.time } };
92
+ return { ...state, metrics };
93
+ }
94
+ case 'assistant/message': {
95
+ const turn = event.data.turn;
96
+ const current = state.metrics[turn];
97
+ if (current === undefined)
98
+ return state;
99
+ const output = typeof event.data.usage?.outputTokens === 'number' ? event.data.usage.outputTokens : 0;
100
+ if (output === 0 && current.finishedAt !== null)
101
+ return state;
102
+ const metrics = {
103
+ ...state.metrics,
104
+ [turn]: { ...current, finishedAt: event.time, outputTokens: current.outputTokens + output },
105
+ };
106
+ return { ...state, metrics };
107
+ }
108
+ default:
109
+ return state;
110
+ }
111
+ },
112
+ wire: {
113
+ viewSchema: msgNavMessagesViewSchema,
114
+ view: (state) => state.questions.map((q) => {
115
+ const m = state.metrics[q.turn];
116
+ if (m === undefined || m.finishedAt === null || m.startedAt <= 0)
117
+ return q;
118
+ const durationMs = m.finishedAt - m.startedAt;
119
+ if (durationMs <= 0)
120
+ return q;
121
+ const firstTokenMs = m.firstTokenAt !== null ? m.firstTokenAt - m.startedAt : 0;
122
+ const tokensPerSec = durationMs > 0 ? Math.round((m.outputTokens * 1000) / durationMs) : 0;
123
+ return { ...q, metrics: { durationMs, firstTokenMs, tokensPerSec } };
124
+ }),
125
+ },
126
+ };
package/lib/schema.js ADDED
@@ -0,0 +1,26 @@
1
+ // 配置模型(阶段一:简约导航条基础;风格专属配置后续风格定稿时再入)
2
+ import z from '@deepseek-ai/schemastery';
3
+ export const ConfigSchema = z.object({
4
+ enabled: z.boolean().default(true),
5
+ align: z.union(['left', 'right']).default('right'),
6
+ panelWidth: z.number().min(200).max(400).default(280),
7
+ collapsed: z.boolean().default(false),
8
+ anchorRule: z.union(['line40', 'top', 'center', 'lastInteraction']).default('line40'),
9
+ followNewMessage: z.boolean().default(true),
10
+ scrollBehavior: z.union(['smooth', 'instant']).default('smooth'),
11
+ pulseMs: z.number().min(200).max(5000).default(1200),
12
+ summaryLength: z.number().min(20).max(80).default(60),
13
+ a11yShortcuts: z.boolean().default(true),
14
+ });
15
+ export const defaultConfig = {
16
+ enabled: true,
17
+ align: 'right',
18
+ panelWidth: 280,
19
+ collapsed: false,
20
+ anchorRule: 'line40',
21
+ followNewMessage: true,
22
+ scrollBehavior: 'smooth',
23
+ pulseMs: 1200,
24
+ summaryLength: 60,
25
+ a11yShortcuts: true,
26
+ };