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
package/src/compat.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * dsh 版本校验器(纯函数,零依赖:宿主侧 / 单测均可用)。
3
+ *
4
+ * 语义(对齐官方 deepseek-harness tag 线,如 dsh-v0.1.1-rc.2):
5
+ * - 比较按语义化版本:主/次/修订号数值比较优先;
6
+ * - 同基线预发布排序:alpha < rc(标识符字典序),数字标识按数值比较;
7
+ * - 正式版(无预发布)高于同基线任意预发布(0.1.1 > 0.1.1-rc.2);
8
+ * - 支持操作符:> < =(另附 >= <=);
9
+ * - 通配:仅 '=' 支持,如 '0.1.1-*' 匹配该基线的任意预发布(0.1.1-rc.1 / 0.1.1-rc.2 …);
10
+ * '*' 作为标识段时吞掉其后的全部剩余标识。
11
+ */
12
+ export type DshCmpOp = '>' | '>=' | '<' | '<=' | '='
13
+
14
+ /** 兼容策略:当前 dsh 版本与目标的比较约束。 */
15
+ export interface DshCompatPolicy {
16
+ op: DshCmpOp
17
+ /**
18
+ * 目标版本(可含通配,如 0.1.1-*,仅 op='=' 生效)。
19
+ * 支持数组:命中其中任一版本即通过,如 ['0.1.1-*', '0.1.2-rc.1']。
20
+ */
21
+ target: string | readonly string[]
22
+ }
23
+
24
+ export interface DshVersionParts {
25
+ /** [major, minor, patch] */
26
+ base: [number, number, number]
27
+ /** 预发布标识数组(rc、alpha、数字…);无预发布 = null */
28
+ pre: string[] | null
29
+ }
30
+
31
+ const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
32
+
33
+ /** 解析 dsh 版本串;非法返回 null(不支持 x.y / 纯数字 etc.)。 */
34
+ export function parseDshVersion(v: string): DshVersionParts | null {
35
+ const m = VERSION_RE.exec(v.trim())
36
+ if (!m) return null
37
+ const pre = m[4] !== undefined ? m[4].split('.') : null
38
+ if (pre !== null && pre.some((s) => s === '')) return null
39
+ return { base: [Number(m[1]), Number(m[2]), Number(m[3])], pre }
40
+ }
41
+
42
+ function compareIdent(a: string, b: string): number {
43
+ const na = /^\d+$/.test(a)
44
+ const nb = /^\d+$/.test(b)
45
+ if (na && nb) {
46
+ const x = Number(a)
47
+ const y = Number(b)
48
+ return x === y ? 0 : x < y ? -1 : 1
49
+ }
50
+ if (na) return -1 // 数字标识 < 字母标识
51
+ if (nb) return 1
52
+ return a === b ? 0 : a < b ? -1 : 1
53
+ }
54
+
55
+ function comparePre(a: string[], b: string[]): number {
56
+ const n = Math.min(a.length, b.length)
57
+ for (let i = 0; i < n; i++) {
58
+ const c = compareIdent(a[i], b[i])
59
+ if (c !== 0) return c
60
+ }
61
+ return a.length === b.length ? 0 : a.length < b.length ? -1 : 1
62
+ }
63
+
64
+ /**
65
+ * 语义化比较:a < b → -1;相等 → 0;a > b → 1;任一无法解析 → NaN。
66
+ */
67
+ export function compareDshVersions(a: string, b: string): number {
68
+ const pa = parseDshVersion(a)
69
+ const pb = parseDshVersion(b)
70
+ if (pa === null || pb === null) return NaN
71
+ for (let i = 0; i < 3; i++) {
72
+ if (pa.base[i] !== pb.base[i]) return pa.base[i] < pb.base[i] ? -1 : 1
73
+ }
74
+ if (pa.pre === null && pb.pre === null) return 0
75
+ if (pa.pre === null) return 1 // 正式版高于同基线任意预发布
76
+ if (pb.pre === null) return -1
77
+ return comparePre(pa.pre, pb.pre)
78
+ }
79
+
80
+ /** 操作符比较(非法版本一律 false)。 */
81
+ export function checkDshVersion(current: string, op: DshCmpOp, target: string): boolean {
82
+ const c = compareDshVersions(current, target)
83
+ if (Number.isNaN(c)) return false
84
+ switch (op) {
85
+ case '>':
86
+ return c > 0
87
+ case '>=':
88
+ return c >= 0
89
+ case '<':
90
+ return c < 0
91
+ case '<=':
92
+ return c <= 0
93
+ default:
94
+ return c === 0
95
+ }
96
+ }
97
+
98
+ /** 标识段匹配:'*' 吞掉 ≥1 个剩余标识;静态段全等(全数字按数值比较)。 */
99
+ function matchIdents(cur: string[], pat: string[]): boolean {
100
+ const f = (i: number, j: number): boolean => {
101
+ if (i === cur.length && j === pat.length) return true
102
+ if (j === pat.length) return false
103
+ if (pat[j] === '*') {
104
+ if (i >= cur.length) return false // * 至少匹配 1 个
105
+ for (let k = i + 1; k <= cur.length; k++) {
106
+ if (f(k, j + 1)) return true
107
+ }
108
+ return false
109
+ }
110
+ if (i >= cur.length) return false
111
+ if (compareIdent(pat[j], cur[i]) !== 0) return false
112
+ return f(i + 1, j + 1)
113
+ }
114
+ return f(0, 0)
115
+ }
116
+
117
+ /**
118
+ * 通配相等(仅配合 op='='):pattern 形如 0.1.1-*(任意预发布)、0.1.1-rc.*、
119
+ * 0.1.*(任意小版本)等;'*' 匹配 ≥1 个标识。例:0.1.1-* 命中 0.1.1-rc.1 / 0.1.1-rc.2。
120
+ */
121
+ export function wildcardEqual(version: string, pattern: string): boolean {
122
+ const pv = parseDshVersion(version)
123
+ if (pv === null) return false
124
+ const dash = pattern.indexOf('-')
125
+ const basePat = (dash >= 0 ? pattern.slice(0, dash) : pattern).split('.')
126
+ const prePat = dash >= 0 ? pattern.slice(dash + 1).split('.') : null
127
+ if (basePat.length !== 3) return false
128
+ const baseHasStar = basePat.some((t) => t === '*')
129
+ for (let i = 0; i < 3; i++) {
130
+ const t = basePat[i]
131
+ if (t === '*') continue
132
+ if (!/^\d+$/.test(t)) return false
133
+ if (Number(t) !== pv.base[i]) return false
134
+ }
135
+ if (prePat === null) {
136
+ // 无预发布子句:基线精确时要求同为正式版;基线含通配(0.1.*)则任意预发布也算命中
137
+ return baseHasStar || pv.pre === null
138
+ }
139
+ if (pv.pre === null) return false // 模式含预发布通配:当前必须有预发布
140
+ return matchIdents(pv.pre, prePat)
141
+ }
142
+
143
+ /** 单项命中:target 含 '*' 时仅接受 op='=' 的通配比较,否则按操作符数值比较。 */
144
+ function entryPasses(current: string, op: DshCmpOp, target: string): boolean {
145
+ if (target.includes('*')) {
146
+ if (op !== '=') return false
147
+ return wildcardEqual(current, target)
148
+ }
149
+ return checkDshVersion(current, op, target)
150
+ }
151
+
152
+ /** 策略校验:target 为数组时命中任一即通过;通配(如 0.1.1-*)仅在 op='=' 时参与。 */
153
+ export function checkDshPolicy(current: string, policy: DshCompatPolicy): boolean {
154
+ const targets = Array.isArray(policy.target) ? policy.target : [policy.target]
155
+ return targets.some((t) => entryPasses(current, policy.op, t))
156
+ }
157
+
158
+ /** 不兼容时的控制台提示文案(xxx = 检测到的当前 dsh 版本)。 */
159
+ export function dshCompatMessage(pluginName: string, currentDshVersion: string): string {
160
+ return pluginName + ' 可能不适配 dsh ' + currentDshVersion + ' 版本,请慎重使用'
161
+ }
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
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 type { Context } from '@deepseek-ai/cordis'
13
+ import { msgNavProjectionDefinition } from './projection.ts'
14
+ import { registerSearchRoute } from './search.ts'
15
+ import { megaChatNavSettingsNamespace, NavSettingsSchema } from './settings.ts'
16
+ import { checkDshPolicy, dshCompatMessage, type DshCompatPolicy } from './compat.ts'
17
+
18
+ /** Cordis plugin name. */
19
+ export const name = 'dsh-mega-chat-nav'
20
+
21
+ /**
22
+ * 兼容策略(按需修改):
23
+ * - '= 0.1.1-*' = 支持 dsh-v0.1.1 整条预发布线(0.1.1-rc.1 / rc.2);
24
+ * - target 支持数组:命中任一即通过,如 ['0.1.1', '0.1.2-rc.1'];
25
+ * - 关系约束示例:{ op: '>=', target: '0.1.1-rc.2' }。
26
+ */
27
+ const DSCH_COMPAT_POLICY: DshCompatPolicy = { op: '=', target: '0.1.1-*' }
28
+
29
+ const dshRequire = createRequire(import.meta.url)
30
+
31
+ /** 探测当前 dsh 版本:@deepseek-ai/dsh 本体 → dsh → dsh-client-runtime(与 tag 线 lockstep) */
32
+ function detectDshVersion(): string | null {
33
+ for (const pkg of ['@deepseek-ai/dsh', 'dsh', '@deepseek-ai/dsh-client-runtime']) {
34
+ try {
35
+ const pkgJson = dshRequire.resolve(pkg + '/package.json')
36
+ const parsed = JSON.parse(readFileSync(pkgJson, 'utf8')) as { version?: string }
37
+ if (typeof parsed.version === 'string' && parsed.version.length > 0) return parsed.version
38
+ } catch {
39
+ /* 尝试下一候选 */
40
+ }
41
+ }
42
+ return null
43
+ }
44
+
45
+ export function apply(ctx: Context): void {
46
+ // dsh 版本兼容校验:检测失败或不合规只打印警示,不阻断插件加载
47
+ try {
48
+ const dshVer = detectDshVersion()
49
+ if (dshVer !== null && !checkDshPolicy(dshVer, DSCH_COMPAT_POLICY)) {
50
+ console.warn(dshCompatMessage(name, dshVer))
51
+ }
52
+ } catch {
53
+ /* 校验器自身异常不应影响插件 */
54
+ }
55
+ ctx.inject(['sessionProjections'], (inner) => {
56
+ inner.sessionProjections.register(msgNavProjectionDefinition)
57
+ })
58
+ ctx.inject(['settings'], (settingsCtx) => {
59
+ settingsCtx.settings.register(megaChatNavSettingsNamespace, NavSettingsSchema)
60
+ })
61
+ try {
62
+ registerSearchRoute(ctx)
63
+ } catch (e) {
64
+ console.error('[dsh-mega-chat-nav] search route failed:', e)
65
+ }
66
+ }
@@ -0,0 +1,174 @@
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
+
19
+ import { z } from 'zod'
20
+ import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
21
+
22
+ /** 注册本投影单元到框架的合并扩展表(接口声明于 /types 出口,须在声明处合并) */
23
+ declare module '@deepseek-ai/dsh-session-projection/types' {
24
+ interface SessionProjectionStateMap {
25
+ msgNavMessages: QuestionIndexState
26
+ }
27
+ interface SessionProjectionMap {
28
+ msgNavMessages: NavItem[]
29
+ }
30
+ }
31
+
32
+ /** 投影条目:一条 user/message 提问的轻量索引(正文零驻留) */
33
+ export interface NavItem {
34
+ /** 归属回合号(turn/start 之后的第一个提问) */
35
+ turn: number
36
+ /** 消息稳定 id(跳转锚点推导源) */
37
+ id: string
38
+ /** 事件 seq(排序与锚点序号) */
39
+ seq: number
40
+ /** Unix 毫秒时间戳 */
41
+ time: number
42
+ /** 首文本块(悬停/级联卡正文) */
43
+ text: string
44
+ /** 该轮性能指标(view 附加;缺数据时省略) */
45
+ metrics?: { durationMs: number; firstTokenMs: number; tokensPerSec: number }
46
+ }
47
+
48
+ /** Fold state: the last opened turn plus every question recorded so far. */
49
+ /** 一轮的性能指标(由 turn/start、assistant/chunk、assistant/message 折叠) */
50
+ interface TurnMetric {
51
+ /** 轮开始时间(turn/start) */
52
+ startedAt: number
53
+ /** 首个内容增量块时间(assistant/chunk 首 text/reasoning delta);未流式时为 null */
54
+ firstTokenAt: number | null
55
+ /** 轮结束时间(assistant/message);未完成时为 null */
56
+ finishedAt: number | null
57
+ /** 累计输出 token(assistant/message 的 usage.outputTokens 求和) */
58
+ outputTokens: number
59
+ }
60
+
61
+ interface QuestionIndexState {
62
+ /** Turn of the last `turn/start` (0 before any; turns are 1-based). */
63
+ turn: number
64
+ /** Every recorded question, in event order. */
65
+ questions: NavItem[]
66
+ /** 每轮性能指标(turn 号 → 指标) */
67
+ metrics: Record<number, TurnMetric>
68
+ }
69
+
70
+ const navEntrySchema = z.object({
71
+ turn: z.number(),
72
+ id: z.string(),
73
+ seq: z.number(),
74
+ time: z.number(),
75
+ text: z.string(),
76
+ metrics: z.object({
77
+ durationMs: z.number(),
78
+ firstTokenMs: z.number(),
79
+ tokensPerSec: z.number(),
80
+ }).optional(),
81
+ })
82
+
83
+ const turnMetricSchema = z.object({
84
+ startedAt: z.number(),
85
+ firstTokenAt: z.number().nullable(),
86
+ finishedAt: z.number().nullable(),
87
+ outputTokens: z.number(),
88
+ })
89
+
90
+ const msgNavMessagesStateSchema = z.object({
91
+ turn: z.number(),
92
+ questions: z.array(navEntrySchema),
93
+ metrics: z.record(z.number(), turnMetricSchema),
94
+ }).strict()
95
+
96
+ /** Validates the wire payload before it leaves the host. */
97
+ const msgNavMessagesViewSchema = z.array(navEntrySchema)
98
+
99
+ /** First text block of a user message; empty string when absent. */
100
+ function messageText(content: readonly { type?: string; text?: string }[] | undefined): string {
101
+ const block = content?.find((part) => typeof part?.text === 'string')
102
+ return block?.text ?? ''
103
+ }
104
+
105
+ /** The `msgNavMessages` unit registered on `ctx.sessionProjections`. */
106
+ export const msgNavProjectionDefinition: Omit<ProjectionDefinition<'msgNavMessages', QuestionIndexState>, 'wire'> & {
107
+ wire: NonNullable<ProjectionDefinition<'msgNavMessages', QuestionIndexState>['wire']>
108
+ } = {
109
+ key: 'msgNavMessages',
110
+ stateVersion: 2,
111
+ stateSchema: msgNavMessagesStateSchema,
112
+ init: () => ({ turn: 0, questions: [], metrics: {} }),
113
+ apply: (state: QuestionIndexState, event) => {
114
+ // Every uninteresting event returns the same reference (Object.is gates
115
+ // the change feed and the persisted-cache dirty check).
116
+ switch (event.type) {
117
+ case 'turn/start': {
118
+ if (event.data.turn === state.turn) return state
119
+ const metrics = { ...state.metrics }
120
+ metrics[event.data.turn] = { startedAt: event.time, firstTokenAt: null, finishedAt: null, outputTokens: 0 }
121
+ return { ...state, turn: event.data.turn, metrics }
122
+ }
123
+ case 'user/message': {
124
+ if (event.surfaceOp !== 'append') return state
125
+ if (event.data.source?.kind !== 'user') return state
126
+ const entry: NavItem = {
127
+ turn: state.turn,
128
+ id: String(event.data.id),
129
+ seq: event.seq,
130
+ time: event.time,
131
+ text: messageText(event.data.content),
132
+ }
133
+ return { ...state, questions: [...state.questions, entry] }
134
+ }
135
+ case 'assistant/chunk': {
136
+ // 只记录每轮首个内容增量块(首 token 时间);后续增量块不产生新 state
137
+ const turn = event.data.turn
138
+ const chunk = event.data.chunk
139
+ const current = state.metrics[turn]
140
+ if (current === undefined) return state
141
+ if (current.firstTokenAt !== null) return state
142
+ if (chunk?.type !== 'text-delta' && chunk?.type !== 'reasoning-delta') return state
143
+ const metrics = { ...state.metrics, [turn]: { ...current, firstTokenAt: event.time } }
144
+ return { ...state, metrics }
145
+ }
146
+ case 'assistant/message': {
147
+ const turn = event.data.turn
148
+ const current = state.metrics[turn]
149
+ if (current === undefined) return state
150
+ const output = typeof event.data.usage?.outputTokens === 'number' ? event.data.usage.outputTokens : 0
151
+ if (output === 0 && current.finishedAt !== null) return state
152
+ const metrics = {
153
+ ...state.metrics,
154
+ [turn]: { ...current, finishedAt: event.time, outputTokens: current.outputTokens + output },
155
+ }
156
+ return { ...state, metrics }
157
+ }
158
+ default:
159
+ return state
160
+ }
161
+ },
162
+ wire: {
163
+ viewSchema: msgNavMessagesViewSchema,
164
+ view: (state: QuestionIndexState) => state.questions.map((q) => {
165
+ const m = state.metrics[q.turn]
166
+ if (m === undefined || m.finishedAt === null || m.startedAt <= 0) return q
167
+ const durationMs = m.finishedAt - m.startedAt
168
+ if (durationMs <= 0) return q
169
+ const firstTokenMs = m.firstTokenAt !== null ? m.firstTokenAt - m.startedAt : 0
170
+ const tokensPerSec = durationMs > 0 ? Math.round((m.outputTokens * 1000) / durationMs) : 0
171
+ return { ...q, metrics: { durationMs, firstTokenMs, tokensPerSec } }
172
+ }),
173
+ },
174
+ }