@tita-agent/dsh-data-table 0.0.0-stage → 0.2.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.
Files changed (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -2
  3. package/cordis.patch.yml +7 -0
  4. package/lib/client.js +16766 -0
  5. package/lib/contract-envelope.d.ts +91 -0
  6. package/lib/contract-envelope.js +106 -0
  7. package/lib/contract-layout.d.ts +130 -0
  8. package/lib/contract-layout.js +138 -0
  9. package/lib/contract-refs.d.ts +39 -0
  10. package/lib/contract-refs.js +9 -0
  11. package/lib/contract-version.d.ts +311 -0
  12. package/lib/contract-version.js +270 -0
  13. package/lib/contract-wire.d.ts +147 -0
  14. package/lib/contract-wire.js +99 -0
  15. package/lib/contracts.d.ts +12 -0
  16. package/lib/contracts.js +12 -0
  17. package/lib/types/audit.d.ts +42 -0
  18. package/lib/types/audit.js +39 -0
  19. package/lib/types/client/ProjectImportMenu.d.ts +30 -0
  20. package/lib/types/client/ProjectImportMenu.js +70 -0
  21. package/lib/types/client/SheetPanel.d.ts +33 -0
  22. package/lib/types/client/SheetPanel.js +1766 -0
  23. package/lib/types/client/SheetToggle.d.ts +18 -0
  24. package/lib/types/client/SheetToggle.js +26 -0
  25. package/lib/types/client/index.d.ts +15 -0
  26. package/lib/types/client/index.js +110 -0
  27. package/lib/types/client/persistence/changes.d.ts +15 -0
  28. package/lib/types/client/persistence/changes.js +51 -0
  29. package/lib/types/client/persistence/ids.d.ts +34 -0
  30. package/lib/types/client/persistence/ids.js +48 -0
  31. package/lib/types/client/persistence/merge.d.ts +35 -0
  32. package/lib/types/client/persistence/merge.js +362 -0
  33. package/lib/types/client/store.d.ts +61 -0
  34. package/lib/types/client/store.js +152 -0
  35. package/lib/types/client/surface-context.d.ts +27 -0
  36. package/lib/types/client/surface-context.js +104 -0
  37. package/lib/types/client/surface-control.d.ts +2 -0
  38. package/lib/types/client/surface-control.js +12 -0
  39. package/lib/types/client/transport/workbookApi.d.ts +159 -0
  40. package/lib/types/client/transport/workbookApi.js +246 -0
  41. package/lib/types/client/univer/StandaloneWorkbookPage.d.ts +5 -0
  42. package/lib/types/client/univer/StandaloneWorkbookPage.js +7 -0
  43. package/lib/types/client/univer/WorkbookCanvas.d.ts +40 -0
  44. package/lib/types/client/univer/WorkbookCanvas.js +87 -0
  45. package/lib/types/client/univer/WorkbookView.d.ts +3 -0
  46. package/lib/types/client/univer/WorkbookView.js +2 -0
  47. package/lib/types/client/univer/loader.d.ts +18 -0
  48. package/lib/types/client/univer/loader.js +81 -0
  49. package/lib/types/client/univer/standalone-entry.d.ts +1 -0
  50. package/lib/types/client/univer/standalone-entry.js +19 -0
  51. package/lib/types/client/versions/VersionChrome.d.ts +33 -0
  52. package/lib/types/client/versions/VersionChrome.js +21 -0
  53. package/lib/types/client/versions/inspect.d.ts +70 -0
  54. package/lib/types/client/versions/inspect.js +88 -0
  55. package/lib/types/client/versions/preview-decoration.d.ts +11 -0
  56. package/lib/types/client/versions/preview-decoration.js +54 -0
  57. package/lib/types/client/versions/records.d.ts +19 -0
  58. package/lib/types/client/versions/records.js +70 -0
  59. package/lib/types/client/workspace-host.d.ts +61 -0
  60. package/lib/types/client/workspace-host.js +124 -0
  61. package/lib/types/collaboration-hub.d.ts +31 -0
  62. package/lib/types/collaboration-hub.js +69 -0
  63. package/lib/types/contract/ids.d.ts +24 -0
  64. package/lib/types/contract/ids.js +46 -0
  65. package/lib/types/contract/mutations.d.ts +25 -0
  66. package/lib/types/contract/mutations.js +782 -0
  67. package/lib/types/contract/operations.d.ts +54 -0
  68. package/lib/types/contract/operations.js +203 -0
  69. package/lib/types/contract/snapshots.d.ts +176 -0
  70. package/lib/types/contract/snapshots.js +351 -0
  71. package/lib/types/contract/univer-api-catalog.d.ts +29 -0
  72. package/lib/types/contract/univer-api-catalog.js +264 -0
  73. package/lib/types/errors.d.ts +8 -0
  74. package/lib/types/errors.js +12 -0
  75. package/lib/types/http/assets-routes.d.ts +3 -0
  76. package/lib/types/http/assets-routes.js +169 -0
  77. package/lib/types/http/collaboration.d.ts +10 -0
  78. package/lib/types/http/collaboration.js +154 -0
  79. package/lib/types/http/history.d.ts +15 -0
  80. package/lib/types/http/history.js +134 -0
  81. package/lib/types/http/project-files.d.ts +79 -0
  82. package/lib/types/http/project-files.js +206 -0
  83. package/lib/types/http/project-scope.d.ts +50 -0
  84. package/lib/types/http/project-scope.js +52 -0
  85. package/lib/types/http/routes.d.ts +98 -0
  86. package/lib/types/http/routes.js +323 -0
  87. package/lib/types/http/standalone.d.ts +9 -0
  88. package/lib/types/http/standalone.js +31 -0
  89. package/lib/types/http/trunk.d.ts +10 -0
  90. package/lib/types/http/trunk.js +322 -0
  91. package/lib/types/http/worktree.d.ts +9 -0
  92. package/lib/types/http/worktree.js +145 -0
  93. package/lib/types/index.d.ts +18 -0
  94. package/lib/types/index.js +156 -0
  95. package/lib/types/invariant.d.ts +15 -0
  96. package/lib/types/invariant.js +15 -0
  97. package/lib/types/request-params.d.ts +21 -0
  98. package/lib/types/request-params.js +76 -0
  99. package/lib/types/store/paths.d.ts +68 -0
  100. package/lib/types/store/paths.js +148 -0
  101. package/lib/types/store/project.d.ts +88 -0
  102. package/lib/types/store/project.js +120 -0
  103. package/lib/types/store/read.d.ts +110 -0
  104. package/lib/types/store/read.js +404 -0
  105. package/lib/types/store/retention.d.ts +27 -0
  106. package/lib/types/store/retention.js +111 -0
  107. package/lib/types/store/write.d.ts +22 -0
  108. package/lib/types/store/write.js +210 -0
  109. package/lib/types/tools/arguments.d.ts +40 -0
  110. package/lib/types/tools/arguments.js +130 -0
  111. package/lib/types/tools/cells.d.ts +50 -0
  112. package/lib/types/tools/cells.js +298 -0
  113. package/lib/types/tools/index.d.ts +2 -0
  114. package/lib/types/tools/index.js +250 -0
  115. package/lib/types/tools/render.d.ts +47 -0
  116. package/lib/types/tools/render.js +290 -0
  117. package/lib/types/tools/univer-api.d.ts +18 -0
  118. package/lib/types/tools/univer-api.js +198 -0
  119. package/lib/types/tools/workbook-catalog.d.ts +3 -0
  120. package/lib/types/tools/workbook-catalog.js +83 -0
  121. package/lib/types/tools/workbook-edit.d.ts +192 -0
  122. package/lib/types/tools/workbook-edit.js +912 -0
  123. package/lib/types/tools/workbook-find.d.ts +11 -0
  124. package/lib/types/tools/workbook-find.js +89 -0
  125. package/lib/types/tools/workbook-history.d.ts +2 -0
  126. package/lib/types/tools/workbook-history.js +59 -0
  127. package/lib/types/tools/workbook-read.d.ts +21 -0
  128. package/lib/types/tools/workbook-read.js +204 -0
  129. package/lib/types/types.d.ts +377 -0
  130. package/lib/types/types.js +34 -0
  131. package/lib/types/univer/apply-operation.d.ts +14 -0
  132. package/lib/types/univer/apply-operation.js +398 -0
  133. package/lib/types/univer/chunk.d.ts +68 -0
  134. package/lib/types/univer/chunk.js +16 -0
  135. package/lib/types/univer/decoration.d.ts +34 -0
  136. package/lib/types/univer/decoration.js +88 -0
  137. package/lib/types/univer/diagnostics.d.ts +31 -0
  138. package/lib/types/univer/diagnostics.js +141 -0
  139. package/lib/types/univer/engine.d.ts +33 -0
  140. package/lib/types/univer/engine.js +338 -0
  141. package/lib/types/univer/entry.d.ts +8 -0
  142. package/lib/types/univer/entry.js +11 -0
  143. package/lib/types/univer/facade.d.ts +107 -0
  144. package/lib/types/univer/facade.js +1 -0
  145. package/lib/types/univer/fonts.d.ts +202 -0
  146. package/lib/types/univer/fonts.js +140 -0
  147. package/lib/types/univer/plugins/collaboration.d.ts +23 -0
  148. package/lib/types/univer/plugins/collaboration.js +47 -0
  149. package/lib/types/univer/plugins/export.d.ts +39 -0
  150. package/lib/types/univer/plugins/export.js +246 -0
  151. package/lib/types/univer/plugins/history.d.ts +24 -0
  152. package/lib/types/univer/plugins/history.js +50 -0
  153. package/lib/types/univer/plugins/version.d.ts +13 -0
  154. package/lib/types/univer/plugins/version.js +13 -0
  155. package/lib/types/version/diff.d.ts +36 -0
  156. package/lib/types/version/diff.js +156 -0
  157. package/lib/types/version/grouping.d.ts +7 -0
  158. package/lib/types/version/grouping.js +94 -0
  159. package/lib/types/version/records.d.ts +17 -0
  160. package/lib/types/version/records.js +102 -0
  161. package/lib/types/version/session.d.ts +36 -0
  162. package/lib/types/version/session.js +89 -0
  163. package/lib/types/version/summary.d.ts +24 -0
  164. package/lib/types/version/summary.js +111 -0
  165. package/lib/types/workbook-manager.d.ts +191 -0
  166. package/lib/types/workbook-manager.js +935 -0
  167. package/lib/types/xlsx-export-worker.d.ts +5 -0
  168. package/lib/types/xlsx-export-worker.js +24 -0
  169. package/lib/types/xlsx-export.d.ts +21 -0
  170. package/lib/types/xlsx-export.js +371 -0
  171. package/lib/types/xlsx-import-worker.d.ts +1 -0
  172. package/lib/types/xlsx-import-worker.js +19 -0
  173. package/lib/univer-client.js +690 -0
  174. package/lib/workbook-page.js +23826 -0
  175. package/lib/xlsx-export-worker.js +104 -0
  176. package/lib/xlsx-import-worker.js +46 -0
  177. package/package.json +106 -3
  178. package/src/audit.ts +58 -0
  179. package/src/client/ProjectImportMenu.tsx +100 -0
  180. package/src/client/SheetPanel.module.css +1578 -0
  181. package/src/client/SheetPanel.tsx +1934 -0
  182. package/src/client/SheetToggle.module.css +73 -0
  183. package/src/client/SheetToggle.tsx +65 -0
  184. package/src/client/contract.d.ts +19 -0
  185. package/src/client/index.ts +131 -0
  186. package/src/client/persistence/changes.ts +58 -0
  187. package/src/client/persistence/ids.ts +60 -0
  188. package/src/client/persistence/merge.ts +392 -0
  189. package/src/client/store.ts +188 -0
  190. package/src/client/surface-context.ts +123 -0
  191. package/src/client/surface-control.ts +13 -0
  192. package/src/client/transport/workbookApi.ts +355 -0
  193. package/src/client/univer/SheetView.module.css +110 -0
  194. package/src/client/univer/StandaloneWorkbookPage.module.css +8 -0
  195. package/src/client/univer/StandaloneWorkbookPage.tsx +15 -0
  196. package/src/client/univer/WorkbookCanvas.tsx +128 -0
  197. package/src/client/univer/WorkbookView.module.css +30 -0
  198. package/src/client/univer/WorkbookView.tsx +3 -0
  199. package/src/client/univer/loader.ts +90 -0
  200. package/src/client/univer/react-dom-client.d.ts +10 -0
  201. package/src/client/univer/standalone-entry.tsx +19 -0
  202. package/src/client/univer/standalone.css +12 -0
  203. package/src/client/univer/univer-locales.d.ts +89 -0
  204. package/src/client/univer/univer-modules.d.ts +26 -0
  205. package/src/client/versions/VersionChrome.tsx +114 -0
  206. package/src/client/versions/inspect.ts +138 -0
  207. package/src/client/versions/preview-decoration.ts +54 -0
  208. package/src/client/versions/records.ts +78 -0
  209. package/src/client/workspace-host.ts +147 -0
  210. package/src/collaboration-hub.ts +83 -0
  211. package/src/contract/ids.ts +50 -0
  212. package/src/contract/mutations.ts +791 -0
  213. package/src/contract/operations.ts +221 -0
  214. package/src/contract/snapshots.ts +383 -0
  215. package/src/contract/univer-api-catalog.ts +289 -0
  216. package/src/css-modules.d.ts +6 -0
  217. package/src/errors.ts +25 -0
  218. package/src/http/assets-routes.ts +177 -0
  219. package/src/http/collaboration.ts +144 -0
  220. package/src/http/history.ts +151 -0
  221. package/src/http/project-files.ts +227 -0
  222. package/src/http/project-scope.ts +77 -0
  223. package/src/http/routes.ts +364 -0
  224. package/src/http/standalone.ts +32 -0
  225. package/src/http/trunk.ts +322 -0
  226. package/src/http/worktree.ts +119 -0
  227. package/src/index.ts +165 -0
  228. package/src/invariant.ts +17 -0
  229. package/src/request-params.ts +92 -0
  230. package/src/store/paths.ts +165 -0
  231. package/src/store/project.ts +137 -0
  232. package/src/store/read.ts +454 -0
  233. package/src/store/retention.ts +106 -0
  234. package/src/store/write.ts +230 -0
  235. package/src/tools/arguments.ts +128 -0
  236. package/src/tools/cells.ts +298 -0
  237. package/src/tools/index.ts +274 -0
  238. package/src/tools/render.ts +301 -0
  239. package/src/tools/univer-api.ts +243 -0
  240. package/src/tools/workbook-catalog.ts +94 -0
  241. package/src/tools/workbook-edit.ts +987 -0
  242. package/src/tools/workbook-find.ts +80 -0
  243. package/src/tools/workbook-history.ts +63 -0
  244. package/src/tools/workbook-read.ts +210 -0
  245. package/src/types.ts +432 -0
  246. package/src/univer/apply-operation.ts +369 -0
  247. package/src/univer/chunk.ts +77 -0
  248. package/src/univer/decoration.ts +111 -0
  249. package/src/univer/diagnostics.ts +136 -0
  250. package/src/univer/engine.ts +370 -0
  251. package/src/univer/entry.ts +14 -0
  252. package/src/univer/facade.ts +91 -0
  253. package/src/univer/fonts.ts +148 -0
  254. package/src/univer/overrides.css +33 -0
  255. package/src/univer/plugins/collaboration.ts +47 -0
  256. package/src/univer/plugins/export.ts +236 -0
  257. package/src/univer/plugins/history.ts +49 -0
  258. package/src/univer/plugins/version.ts +13 -0
  259. package/src/version/diff.ts +169 -0
  260. package/src/version/grouping.ts +96 -0
  261. package/src/version/records.ts +113 -0
  262. package/src/version/session.ts +90 -0
  263. package/src/version/summary.ts +117 -0
  264. package/src/workbook-manager.ts +1111 -0
  265. package/src/xlsx-export-worker.ts +34 -0
  266. package/src/xlsx-export.ts +373 -0
  267. package/src/xlsx-import-worker.ts +30 -0
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Three-way merge for Univer workbook snapshots.
3
+ *
4
+ * The browser keeps the last server snapshot as the base. When a user save
5
+ * races an Agent/user write, this module compares base/local/remote at cell
6
+ * granularity, adopts one-sided changes automatically, and keeps local values
7
+ * for true conflicts while adding a visible conflict style.
8
+ */
9
+
10
+ import { cellAddress } from '#contracts'
11
+ import { cloneSnapshot, normalizeCellFormula, stableJson, toCellValue } from '../../contract/snapshots.ts'
12
+ import type { WorkbookLocalChange, WorkbookOperation } from '../../types.ts'
13
+
14
+ type JsonRecord = Record<string, any>
15
+ type CellData = Record<string, Record<string, JsonRecord>>
16
+
17
+ export interface MergeConflict {
18
+ kind: 'cell' | 'structure' | 'sheet'
19
+ sheetId?: string
20
+ sheetName?: string
21
+ row?: number
22
+ column?: number
23
+ address?: string
24
+ baseValue?: unknown
25
+ localValue?: unknown
26
+ remoteValue?: unknown
27
+ description: string
28
+ }
29
+
30
+ export interface WorkbookMergeResult {
31
+ snapshot: object
32
+ conflicts: MergeConflict[]
33
+ autoMergedCells: number
34
+ conflictStyleMap: Record<string, string | null>
35
+ }
36
+
37
+ interface MergeSheetResult {
38
+ sheet: JsonRecord
39
+ conflicts: MergeConflict[]
40
+ autoMergedCells: number
41
+ conflictStyleMap: Record<string, string | null>
42
+ }
43
+
44
+ const MERGEABLE_SHEET_KEYS = [
45
+ 'name',
46
+ 'tabColor',
47
+ 'hidden',
48
+ 'zoomRatio',
49
+ 'defaultColumnWidth',
50
+ 'defaultRowHeight',
51
+ 'freeze',
52
+ 'mergeData',
53
+ 'rowData',
54
+ 'columnData',
55
+ 'showGridlines',
56
+ 'rowHeader',
57
+ 'columnHeader',
58
+ 'rightToLeft',
59
+ 'defaultStyle',
60
+ 'dataSheetMeta',
61
+ ] as const
62
+
63
+ /** Merge local and remote snapshots against the last common base snapshot. */
64
+ export function mergeWorkbookSnapshots(baseSnapshot: object, localSnapshot: object, remoteSnapshot: object): WorkbookMergeResult {
65
+ const base = asRecord(baseSnapshot)
66
+ const local = asRecord(localSnapshot)
67
+ const remote = asRecord(remoteSnapshot)
68
+ const merged = cloneSnapshot(local)
69
+ const conflicts: MergeConflict[] = []
70
+ const conflictStyleMap: Record<string, string | null> = {}
71
+ const localStyles = asRecord(merged.styles)
72
+ const remoteStyles = asRecord(remote.styles)
73
+ const remoteStyleMap = new Map<string, string>()
74
+
75
+ const adoptRemoteCell = (cell: JsonRecord | undefined): JsonRecord | undefined => {
76
+ if (cell === undefined) return undefined
77
+ const next = cloneSnapshot(cell)
78
+ if (typeof next.s === 'string') next.s = remapRemoteStyle(next.s)
79
+ return next
80
+ }
81
+
82
+ const remapRemoteStyle = (styleId: string): string => {
83
+ const known = remoteStyleMap.get(styleId)
84
+ if (known !== undefined) return known
85
+ const remoteStyle = remoteStyles[styleId]
86
+ if (remoteStyle === undefined) {
87
+ remoteStyleMap.set(styleId, styleId)
88
+ return styleId
89
+ }
90
+ const localStyle = localStyles[styleId]
91
+ if (localStyle === undefined || deepEqual(localStyle, remoteStyle)) {
92
+ localStyles[styleId] = cloneSnapshot(remoteStyle)
93
+ remoteStyleMap.set(styleId, styleId)
94
+ return styleId
95
+ }
96
+ let candidate = `${styleId}-remote`
97
+ let suffix = 1
98
+ while (localStyles[candidate] !== undefined) candidate = `${styleId}-remote-${suffix++}`
99
+ localStyles[candidate] = cloneSnapshot(remoteStyle)
100
+ remoteStyleMap.set(styleId, candidate)
101
+ return candidate
102
+ }
103
+
104
+ const baseSheets = asRecord(base.sheets)
105
+ const localSheets = asRecord(local.sheets)
106
+ const remoteSheets = asRecord(remote.sheets)
107
+ const mergedSheets: JsonRecord = {}
108
+ const allSheetIds = new Set([...Object.keys(baseSheets), ...Object.keys(localSheets), ...Object.keys(remoteSheets)])
109
+
110
+ for (const sheetId of allSheetIds) {
111
+ const baseSheet = asOptionalRecord(baseSheets[sheetId])
112
+ const localSheet = asOptionalRecord(localSheets[sheetId])
113
+ const remoteSheet = asOptionalRecord(remoteSheets[sheetId])
114
+ if (localSheet === undefined && remoteSheet === undefined) continue
115
+ if (localSheet === undefined) {
116
+ if (baseSheet !== undefined && remoteSheet !== undefined && !deepEqual(remoteSheet, baseSheet)) {
117
+ conflicts.push(sheetConflict(sheetId, remoteSheet, '本地删除 sheet,但远端同时修改了它'))
118
+ }
119
+ // A sheet deleted locally remains deleted. A remote-only sheet is kept.
120
+ if (baseSheet === undefined && remoteSheet !== undefined) mergedSheets[sheetId] = cloneAndRemapSheet(remoteSheet, adoptRemoteCell)
121
+ continue
122
+ }
123
+ if (remoteSheet === undefined) {
124
+ if (baseSheet !== undefined && !deepEqual(localSheet, baseSheet)) {
125
+ conflicts.push(sheetConflict(sheetId, localSheet, '远端删除 sheet,但本地同时修改了它'))
126
+ }
127
+ // A sheet deleted remotely is deleted only when local had not changed it.
128
+ if (baseSheet !== undefined && deepEqual(localSheet, baseSheet)) continue
129
+ mergedSheets[sheetId] = cloneSnapshot(localSheet)
130
+ continue
131
+ }
132
+ const result = mergeSheet(sheetId, baseSheet, localSheet, remoteSheet, adoptRemoteCell, localStyles)
133
+ mergedSheets[sheetId] = result.sheet
134
+ conflicts.push(...result.conflicts)
135
+ Object.assign(conflictStyleMap, result.conflictStyleMap)
136
+ }
137
+
138
+ merged.sheets = mergedSheets
139
+ merged.styles = localStyles
140
+ const baseOrder = asStringArray(base.sheetOrder)
141
+ const localOrder = asStringArray(local.sheetOrder)
142
+ const remoteOrder = asStringArray(remote.sheetOrder)
143
+ if (deepEqual(localOrder, baseOrder) && !deepEqual(remoteOrder, baseOrder)) merged.sheetOrder = remoteOrder.filter(id => mergedSheets[id] !== undefined)
144
+ else if (!deepEqual(localOrder, remoteOrder) && !deepEqual(remoteOrder, baseOrder)) {
145
+ conflicts.push({ kind: 'structure', description: '本地和远端同时调整了 sheet 顺序,当前保留本地顺序' })
146
+ }
147
+ merged.sheetOrder = normalizeSheetOrder(asStringArray(merged.sheetOrder), mergedSheets)
148
+
149
+ return {
150
+ snapshot: merged,
151
+ conflicts,
152
+ autoMergedCells: countAutoMergedCells(base, local, remote, mergedSheets),
153
+ conflictStyleMap,
154
+ }
155
+ }
156
+
157
+ /** Remove temporary conflict styles before a merged snapshot is persisted. */
158
+ export function stripConflictMarkers(snapshot: object, conflictStyleMap: Record<string, string | null>): object {
159
+ if (Object.keys(conflictStyleMap).length === 0) return snapshot
160
+ const next = cloneSnapshot(snapshot) as JsonRecord
161
+ const sheets = asRecord(next.sheets)
162
+ for (const sheet of Object.values(sheets)) {
163
+ const cellData = asRecord(sheet).cellData as CellData | undefined
164
+ if (cellData === undefined) continue
165
+ for (const [rowKey, row] of Object.entries(cellData)) {
166
+ for (const [columnKey, cell] of Object.entries(row)) {
167
+ const marker = typeof cell.s === 'string' ? conflictStyleMap[cell.s] : undefined
168
+ if (marker === undefined && !Object.prototype.hasOwnProperty.call(conflictStyleMap, cell.s)) continue
169
+ if (marker === null) delete cell.s
170
+ else cell.s = marker
171
+ if (Object.keys(cell).length === 1 && cell.v === null) delete row[columnKey]
172
+ }
173
+ if (Object.keys(row).length === 0) delete cellData[rowKey]
174
+ }
175
+ }
176
+ const styles = asRecord(next.styles)
177
+ for (const markerId of Object.keys(conflictStyleMap)) delete styles[markerId]
178
+ next.styles = styles
179
+ return next
180
+ }
181
+
182
+ /** Apply ordinary cell edits to the merge base without cloning the workbook. */
183
+ export function applyIncrementalChangesToSnapshot(snapshot: object, changes: WorkbookLocalChange[]): void {
184
+ const root = asRecord(snapshot)
185
+ const sheets = asRecord(root.sheets)
186
+ for (const change of changes) {
187
+ const sheet = sheets[change.payload.sheetId] ?? Object.values(sheets).find(candidate => asRecord(candidate).name === change.payload.sheetId)
188
+ if (sheet === undefined) continue
189
+ const cellData = (asRecord(sheet).cellData ??= {}) as CellData
190
+ for (const update of change.payload.cells) {
191
+ const row = (cellData[String(update.row)] ??= {})
192
+ if (update.value === null && (update.formula === undefined || update.formula.trim().length === 0)) {
193
+ delete row[String(update.column)]
194
+ if (Object.keys(row).length === 0) delete cellData[String(update.row)]
195
+ continue
196
+ }
197
+ const current = row[String(update.column)] ?? {}
198
+ row[String(update.column)] = { ...current, v: update.value }
199
+ const formula = normalizeCellFormula(update.formula)
200
+ if (formula === undefined) delete row[String(update.column)].f
201
+ else row[String(update.column)].f = formula
202
+ }
203
+ }
204
+ }
205
+
206
+ /** Patch a merge base from a replayable operation log; false means fallback. */
207
+ export function applyIncrementalOperationsToSnapshot(snapshot: object, operations: WorkbookOperation[]): boolean {
208
+ const changes: WorkbookLocalChange[] = []
209
+ const collect = (operation: WorkbookOperation): boolean => {
210
+ if (operation.type === 'update_cells') {
211
+ const sheetId = operation.payload.sheetId ?? operation.payload.sheetName
212
+ if (sheetId === undefined || (operation.payload.cells ?? []).length === 0) return false
213
+ changes.push({ type: 'update_cells', payload: { sheetId, cells: operation.payload.cells ?? [] } })
214
+ return true
215
+ }
216
+ if (operation.type === 'batch') {
217
+ return (operation.payload.operations ?? []).every((child, index) => collect({
218
+ ...operation,
219
+ opId: `${operation.opId}:${index}`,
220
+ type: child.type,
221
+ payload: child.payload,
222
+ }))
223
+ }
224
+ return false
225
+ }
226
+ if (!operations.every(collect)) return false
227
+ applyIncrementalChangesToSnapshot(snapshot, changes)
228
+ return true
229
+ }
230
+
231
+ function mergeSheet(
232
+ sheetId: string,
233
+ baseSheet: JsonRecord | undefined,
234
+ localSheet: JsonRecord,
235
+ remoteSheet: JsonRecord,
236
+ adoptRemoteCell: (cell: JsonRecord | undefined) => JsonRecord | undefined,
237
+ styles: JsonRecord,
238
+ ): MergeSheetResult {
239
+ const sheet = cloneSnapshot(localSheet)
240
+ const conflicts: MergeConflict[] = []
241
+ const conflictStyleMap: Record<string, string | null> = {}
242
+ let autoMergedCells = 0
243
+ const localRowCount = typeof localSheet.rowCount === 'number' ? localSheet.rowCount : 0
244
+ const remoteRowCount = typeof remoteSheet.rowCount === 'number' ? remoteSheet.rowCount : 0
245
+ const localColumnCount = typeof localSheet.columnCount === 'number' ? localSheet.columnCount : 0
246
+ const remoteColumnCount = typeof remoteSheet.columnCount === 'number' ? remoteSheet.columnCount : 0
247
+ sheet.rowCount = Math.max(localRowCount, remoteRowCount)
248
+ sheet.columnCount = Math.max(localColumnCount, remoteColumnCount)
249
+
250
+ for (const key of MERGEABLE_SHEET_KEYS) {
251
+ const baseValue = baseSheet?.[key]
252
+ const localValue = localSheet[key]
253
+ const remoteValue = remoteSheet[key]
254
+ if (deepEqual(localValue, remoteValue)) continue
255
+ if (deepEqual(localValue, baseValue)) {
256
+ if (remoteValue === undefined) delete sheet[key]
257
+ else sheet[key] = cloneSnapshot(remoteValue)
258
+ } else if (deepEqual(remoteValue, baseValue)) {
259
+ continue
260
+ } else {
261
+ conflicts.push({ kind: 'structure', sheetId, sheetName: sheetName(localSheet, sheetId), description: `sheet 属性 ${key} 在本地和远端均有修改,当前保留本地值` })
262
+ }
263
+ }
264
+
265
+ const baseCells = asCellData(baseSheet?.cellData)
266
+ const localCells = asCellData(localSheet.cellData)
267
+ const remoteCells = asCellData(remoteSheet.cellData)
268
+ const mergedCells: CellData = {}
269
+ const rowKeys = new Set([...Object.keys(baseCells), ...Object.keys(localCells), ...Object.keys(remoteCells)])
270
+ for (const rowKey of rowKeys) {
271
+ const baseRow = baseCells[rowKey] ?? {}
272
+ const localRow = localCells[rowKey] ?? {}
273
+ const remoteRow = remoteCells[rowKey] ?? {}
274
+ const columnKeys = new Set([...Object.keys(baseRow), ...Object.keys(localRow), ...Object.keys(remoteRow)])
275
+ for (const columnKey of columnKeys) {
276
+ const baseCell = baseRow[columnKey]
277
+ const localCell = localRow[columnKey]
278
+ const remoteCell = remoteRow[columnKey]
279
+ let selected: JsonRecord | undefined
280
+ if (deepEqual(localCell, remoteCell)) selected = cloneOptional(localCell)
281
+ else if (deepEqual(localCell, baseCell)) {
282
+ selected = adoptRemoteCell(remoteCell)
283
+ autoMergedCells++
284
+ } else if (deepEqual(remoteCell, baseCell)) {
285
+ selected = cloneOptional(localCell)
286
+ autoMergedCells++
287
+ } else {
288
+ selected = cloneOptional(localCell) ?? { v: null }
289
+ const markerId = createConflictStyle(styles, selected, sheetId, Number(rowKey), Number(columnKey))
290
+ conflictStyleMap[markerId] = typeof localCell?.s === 'string' ? localCell.s : null
291
+ selected.s = markerId
292
+ conflicts.push({
293
+ kind: 'cell',
294
+ sheetId,
295
+ sheetName: sheetName(localSheet, sheetId),
296
+ row: Number(rowKey),
297
+ column: Number(columnKey),
298
+ address: `${sheetName(localSheet, sheetId)}!${cellAddress(Number(rowKey), Number(columnKey))}`,
299
+ baseValue: toCellValue(baseCell),
300
+ localValue: toCellValue(localCell),
301
+ remoteValue: toCellValue(remoteCell),
302
+ description: '本地和远端同时修改了同一单元格,当前保留本地值',
303
+ })
304
+ }
305
+ if (selected !== undefined) (mergedCells[rowKey] ??= {})[columnKey] = selected
306
+ }
307
+ }
308
+ sheet.cellData = mergedCells
309
+ return { sheet, conflicts, autoMergedCells, conflictStyleMap }
310
+ }
311
+
312
+ function createConflictStyle(styles: JsonRecord, cell: JsonRecord, sheetId: string, row: number, column: number): string {
313
+ const original = typeof cell.s === 'string' ? asOptionalRecord(styles[cell.s]) ?? {} : {}
314
+ const baseId = `data-sheet-conflict-${sheetId}-${row}-${column}`
315
+ let id = baseId
316
+ let suffix = 1
317
+ while (styles[id] !== undefined) id = `${baseId}-${suffix++}`
318
+ styles[id] = { ...cloneSnapshot(original), bg: '#FFF2CC', cl: '#9C0006' }
319
+ return id
320
+ }
321
+
322
+ function sheetConflict(sheetId: string, sheet: JsonRecord, description: string): MergeConflict {
323
+ return { kind: 'sheet', sheetId, sheetName: sheetName(sheet, sheetId), description }
324
+ }
325
+
326
+ function sheetName(sheet: JsonRecord, fallback: string): string {
327
+ return typeof sheet.name === 'string' && sheet.name.length > 0 ? sheet.name : fallback
328
+ }
329
+
330
+ function countAutoMergedCells(base: JsonRecord, local: JsonRecord, remote: JsonRecord, mergedSheets: JsonRecord): number {
331
+ let count = 0
332
+ const baseSheets = asRecord(base.sheets)
333
+ const localSheets = asRecord(local.sheets)
334
+ const remoteSheets = asRecord(remote.sheets)
335
+ for (const [sheetId, sheet] of Object.entries(mergedSheets)) {
336
+ const baseCells = asCellData(baseSheets[sheetId]?.cellData)
337
+ const localCells = asCellData(localSheets[sheetId]?.cellData)
338
+ const remoteCells = asCellData(remoteSheets[sheetId]?.cellData)
339
+ const mergedCells = asCellData(sheet.cellData)
340
+ for (const [rowKey, row] of Object.entries(mergedCells)) {
341
+ for (const columnKey of Object.keys(row)) {
342
+ const localCell = localCells[rowKey]?.[columnKey]
343
+ const remoteCell = remoteCells[rowKey]?.[columnKey]
344
+ const baseCell = baseCells[rowKey]?.[columnKey]
345
+ if (!deepEqual(localCell, remoteCell) && (deepEqual(localCell, baseCell) || deepEqual(remoteCell, baseCell))) count++
346
+ }
347
+ }
348
+ }
349
+ return count
350
+ }
351
+
352
+ function cloneAndRemapSheet(sheet: JsonRecord, adoptRemoteCell: (cell: JsonRecord | undefined) => JsonRecord | undefined): JsonRecord {
353
+ const next = cloneSnapshot(sheet)
354
+ const cellData = asCellData(next.cellData)
355
+ for (const [rowKey, row] of Object.entries(cellData)) {
356
+ for (const [columnKey, cell] of Object.entries(row)) row[columnKey] = adoptRemoteCell(cell) ?? cell
357
+ cellData[rowKey] = row
358
+ }
359
+ next.cellData = cellData
360
+ return next
361
+ }
362
+
363
+ function normalizeSheetOrder(order: string[], sheets: JsonRecord): string[] {
364
+ const result = order.filter(id => sheets[id] !== undefined)
365
+ for (const id of Object.keys(sheets)) if (!result.includes(id)) result.push(id)
366
+ return result
367
+ }
368
+
369
+ function asRecord(value: unknown): JsonRecord {
370
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : {}
371
+ }
372
+
373
+ function asOptionalRecord(value: unknown): JsonRecord | undefined {
374
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : undefined
375
+ }
376
+
377
+ function asCellData(value: unknown): CellData {
378
+ return asRecord(value) as CellData
379
+ }
380
+
381
+ function asStringArray(value: unknown): string[] {
382
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
383
+ }
384
+
385
+ function cloneOptional<T>(value: T | undefined): T | undefined {
386
+ return value === undefined ? undefined : cloneSnapshot(value)
387
+ }
388
+
389
+ function deepEqual(left: unknown, right: unknown): boolean {
390
+ return stableJson(left) === stableJson(right)
391
+ }
392
+
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Minimal sheet-panel store for dsh-data-table: the right-column
3
+ * open state. Workbook data lives server-side (node half, snapshot files);
4
+ * this store only tracks the panel toggle.
5
+ * @module dsh-data-table/client/store
6
+ */
7
+
8
+ import { useSyncExternalStore } from 'react'
9
+
10
+ let sheetOpen = false
11
+
12
+ /**
13
+ * Last workbook the user opened. DSH remounts the pane when the conversation
14
+ * tab changes, so the selection has to live outside React; localStorage keeps
15
+ * it across a reload too. Persistence is best-effort: a locked-down browser
16
+ * simply falls back to an in-memory value for this page life.
17
+ */
18
+ const LAST_WORKBOOK_KEY = 'dsh.data-sheet.lastWorkbookId'
19
+ let lastWorkbookId = ''
20
+ let lastWorkbookLoaded = false
21
+
22
+ function readLastWorkbook(): string {
23
+ if (lastWorkbookLoaded) return lastWorkbookId
24
+ lastWorkbookLoaded = true
25
+ try {
26
+ lastWorkbookId = globalThis.localStorage?.getItem(LAST_WORKBOOK_KEY) ?? ''
27
+ } catch {
28
+ lastWorkbookId = ''
29
+ }
30
+ return lastWorkbookId
31
+ }
32
+
33
+ /** The workbook to reopen when the pane mounts without an explicit target. */
34
+ export function lastSelectedWorkbookId(): string {
35
+ return readLastWorkbook()
36
+ }
37
+
38
+ /** Record the active workbook; call with an empty string when it is removed. */
39
+ export function rememberWorkbook(id: string): void {
40
+ readLastWorkbook()
41
+ if (lastWorkbookId === id) return
42
+ lastWorkbookId = id
43
+ try {
44
+ if (id === '') globalThis.localStorage?.removeItem(LAST_WORKBOOK_KEY)
45
+ else globalThis.localStorage?.setItem(LAST_WORKBOOK_KEY, id)
46
+ } catch {
47
+ // Ignore storage failures; the in-memory value still survives remounts.
48
+ }
49
+ }
50
+ /**
51
+ * The **unretained editing session** the next save joins, per workbook
52
+ * (PRD v3.3 D22).
53
+ *
54
+ * The session is not a saved Version: it keeps accumulating Steps and only
55
+ * becomes one when the idle window closes it or it reaches the Step cap. It has
56
+ * to outlive the React panel, because DSH remounts the pane whenever the
57
+ * conversation tab changes — losing the cursor there was what made "every visit
58
+ * is its own version", i.e. what users read as "leaving the artifact saved a
59
+ * version". localStorage is best-effort; the in-memory map covers a browser
60
+ * that refuses storage.
61
+ */
62
+ export interface VersionSession {
63
+ /** The Version id the next save joins; `null` when there is nothing open. */
64
+ versionId: string
65
+ /** Steps already committed into that Version. */
66
+ stepCount: number
67
+ }
68
+
69
+ const VERSION_SESSION_PREFIX = 'dsh.data-sheet.versionSession.'
70
+ const versionSessions = new Map<string, VersionSession>()
71
+
72
+ function versionSessionKey(workbookId: string): string {
73
+ return `${VERSION_SESSION_PREFIX}${workbookId}`
74
+ }
75
+
76
+ /** The open session for a workbook, or `null` when there is none. */
77
+ export function readVersionSession(workbookId: string): VersionSession | null {
78
+ if (workbookId === '') return null
79
+ const cached = versionSessions.get(workbookId)
80
+ if (cached !== undefined) return cached
81
+ try {
82
+ const raw = globalThis.localStorage?.getItem(versionSessionKey(workbookId))
83
+ if (raw === null || raw === undefined) return null
84
+ const parsed = JSON.parse(raw) as Partial<VersionSession>
85
+ if (typeof parsed.versionId !== 'string' || parsed.versionId === '') return null
86
+ const session: VersionSession = { versionId: parsed.versionId, stepCount: typeof parsed.stepCount === 'number' && parsed.stepCount > 0 ? parsed.stepCount : 0 }
87
+ versionSessions.set(workbookId, session)
88
+ return session
89
+ } catch {
90
+ return null
91
+ }
92
+ }
93
+
94
+ /** Record (or with `null`, forget) the open session for a workbook. */
95
+ export function rememberVersionSession(workbookId: string, session: VersionSession | null): void {
96
+ if (workbookId === '') return
97
+ if (session === null) versionSessions.delete(workbookId)
98
+ else versionSessions.set(workbookId, session)
99
+ try {
100
+ if (session === null) globalThis.localStorage?.removeItem(versionSessionKey(workbookId))
101
+ else globalThis.localStorage?.setItem(versionSessionKey(workbookId), JSON.stringify(session))
102
+ } catch {
103
+ // Ignore storage failures; the in-memory value still survives remounts.
104
+ }
105
+ }
106
+
107
+ const listeners = new Set<() => void>()
108
+
109
+ function emit(): void {
110
+ for (const listener of [...listeners]) listener()
111
+ }
112
+
113
+ function subscribe(listener: () => void): () => void {
114
+ listeners.add(listener)
115
+ return () => { listeners.delete(listener) }
116
+ }
117
+
118
+ /** Whether the right column is open. */
119
+ export function isSheetOpen(): boolean {
120
+ return sheetOpen
121
+ }
122
+
123
+ /** Open/close the right column. */
124
+ export function setSheetOpen(value: boolean): void {
125
+ sheetOpen = value
126
+ emit()
127
+ }
128
+
129
+ /** React hook: right-column open state. */
130
+ export function useSheetOpen(): boolean {
131
+ return useSyncExternalStore(subscribe, () => sheetOpen)
132
+ }
133
+
134
+ /**
135
+ * Whether the Table tab is currently on screen.
136
+ *
137
+ * The workspace is a resident tree that outlives the pane, so its own `open`
138
+ * flag stays true for the life of the page. Anything that must react only to a
139
+ * *visible* table — the window-level Ctrl/Cmd+S shortcut in particular — reads
140
+ * this instead.
141
+ */
142
+ let sheetVisible = false
143
+
144
+ /** Record whether the pane is showing the Table tab. */
145
+ export function setSheetVisible(value: boolean): void {
146
+ if (sheetVisible === value) return
147
+ sheetVisible = value
148
+ emit()
149
+ }
150
+
151
+ /** Whether the Table tab is on screen. */
152
+ export function isSheetVisible(): boolean {
153
+ return sheetVisible
154
+ }
155
+
156
+ /** React hook: whether the Table tab is on screen. */
157
+ export function useSheetVisible(): boolean {
158
+ return useSyncExternalStore(subscribe, () => sheetVisible)
159
+ }
160
+
161
+ /**
162
+ * Last workbook projection kept outside React. DSH unmounts the right pane when
163
+ * the conversation tab changes, so caching here lets a remount paint the
164
+ * previous content immediately instead of showing an empty panel while the
165
+ * request is in flight. The panel still revalidates against the Host, so edits
166
+ * made in another browser tab are not lost.
167
+ */
168
+ export interface WorkbookProjection {
169
+ id: string
170
+ name: string
171
+ snapshot: object
172
+ revision: number
173
+ }
174
+
175
+ let cachedProjection: WorkbookProjection | null = null
176
+
177
+ export function cachedWorkbookProjection(id: string): WorkbookProjection | null {
178
+ return cachedProjection !== null && cachedProjection.id === id ? cachedProjection : null
179
+ }
180
+
181
+ export function rememberWorkbookProjection(projection: WorkbookProjection): void {
182
+ cachedProjection = projection
183
+ }
184
+
185
+ /** Drop the cache when the workbook is deleted or fails to load. */
186
+ export function forgetWorkbookProjection(id?: string): void {
187
+ if (id === undefined || cachedProjection?.id === id) cachedProjection = null
188
+ }