@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90

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 (316) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +77 -51
  10. package/src/App.diff.test.tsx +259 -45
  11. package/src/App.export.test.tsx +58 -41
  12. package/src/App.focus.test.ts +89 -0
  13. package/src/App.identity.test.tsx +78 -0
  14. package/src/App.plan-render.test.tsx +209 -0
  15. package/src/App.plan-review.test.tsx +330 -161
  16. package/src/App.prototype-markdown.test.tsx +128 -0
  17. package/src/App.prototype.test.tsx +176 -0
  18. package/src/App.readonly.test.tsx +129 -67
  19. package/src/App.regression.test.tsx +94 -0
  20. package/src/App.rendering.test.tsx +61 -35
  21. package/src/App.share-dialog.test.tsx +172 -0
  22. package/src/App.submit-confirm.test.tsx +264 -0
  23. package/src/App.test.tsx +402 -126
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1402 -1060
  26. package/src/App.visual.test.tsx +57 -0
  27. package/src/App.walk.test.tsx +299 -0
  28. package/src/App.workbench.test.tsx +774 -0
  29. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  30. package/src/annotation-palette.ts +62 -0
  31. package/src/ansi-screen.test.ts +28 -0
  32. package/src/ansi-screen.ts +72 -0
  33. package/src/app-key-state.ts +61 -0
  34. package/src/app-screens.tsx +577 -0
  35. package/src/app-view-model.ts +168 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/client-extension-registry.test.tsx +61 -0
  39. package/src/client-extension-registry.ts +159 -0
  40. package/src/clipboard.test.ts +23 -0
  41. package/src/clipboard.ts +33 -0
  42. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  43. package/src/components/AnnotatableFileView.test.tsx +102 -0
  44. package/src/components/AnnotatableFileView.tsx +134 -0
  45. package/src/components/AnnotationCards.stories.tsx +86 -0
  46. package/src/components/AnnotationCards.tsx +341 -0
  47. package/src/components/AnsiScreen.stories.tsx +12 -0
  48. package/src/components/AnsiScreen.tsx +28 -0
  49. package/src/components/AppHeader.stories.tsx +50 -0
  50. package/src/components/AppHeader.tsx +83 -0
  51. package/src/components/AppShell.brand.test.tsx +58 -0
  52. package/src/components/AppShell.extensions.test.tsx +191 -0
  53. package/src/components/AppShell.stories.tsx +89 -0
  54. package/src/components/AppShell.tsx +362 -0
  55. package/src/components/AppShell.zoom.test.tsx +135 -0
  56. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  57. package/src/components/BareWorkbenchFileView.tsx +64 -0
  58. package/src/components/Breadcrumb.stories.tsx +19 -0
  59. package/src/components/Breadcrumb.tsx +45 -0
  60. package/src/components/ChangesColumn.stories.tsx +44 -0
  61. package/src/components/ChangesColumn.tsx +199 -0
  62. package/src/components/ChangesFileTree.test.tsx +61 -0
  63. package/src/components/CodeBlock.stories.tsx +42 -0
  64. package/src/components/CodeBlock.tsx +84 -0
  65. package/src/components/CompletionOverlay.stories.tsx +37 -0
  66. package/src/components/CompletionOverlay.tsx +77 -0
  67. package/src/components/ConfirmCard.stories.tsx +37 -0
  68. package/src/components/ConfirmCard.tsx +150 -0
  69. package/src/components/ConfirmDialog.stories.tsx +21 -0
  70. package/src/components/ConfirmDialog.tsx +72 -0
  71. package/src/components/DiffContentView.scroll.test.tsx +149 -0
  72. package/src/components/DiffContentView.split.test.tsx +140 -0
  73. package/src/components/DiffContentView.stories.tsx +80 -0
  74. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  75. package/src/components/DiffContentView.tsx +906 -0
  76. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  77. package/src/components/DiscussionMarkerRail.tsx +186 -0
  78. package/src/components/EditorGrid.stories.tsx +59 -0
  79. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  80. package/src/components/EditorGrid.tsx +426 -0
  81. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  82. package/src/components/ExtensionZone.stories.tsx +28 -0
  83. package/src/components/ExtensionZone.tsx +145 -0
  84. package/src/components/FileContentsView.stories.tsx +16 -0
  85. package/src/components/FileContentsView.test.tsx +29 -0
  86. package/src/components/FileContentsView.tsx +98 -0
  87. package/src/components/GridTabContent.stories.tsx +59 -0
  88. package/src/components/GridTabContent.tsx +182 -0
  89. package/src/components/HandleChipsInput.stories.tsx +24 -0
  90. package/src/components/HandleChipsInput.test.tsx +35 -0
  91. package/src/components/HandleChipsInput.tsx +145 -0
  92. package/src/components/KeybindsDialog.stories.tsx +31 -0
  93. package/src/components/KeybindsDialog.tsx +103 -0
  94. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  95. package/src/components/MarkdownGridBlock.tsx +72 -0
  96. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  97. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  98. package/src/components/MarkdownThreadEditor.tsx +162 -0
  99. package/src/components/MenuBar.stories.tsx +28 -0
  100. package/src/components/MenuBar.tsx +44 -0
  101. package/src/components/NavModeHint.stories.tsx +40 -0
  102. package/src/components/NavModeHint.tsx +31 -0
  103. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  104. package/src/components/OverlayScrollbar.test.tsx +139 -0
  105. package/src/components/OverlayScrollbar.tsx +129 -0
  106. package/src/components/PaneGrid.stories.tsx +84 -0
  107. package/src/components/PaneGrid.tsx +128 -0
  108. package/src/components/PanelColumn.stories.tsx +25 -0
  109. package/src/components/PanelColumn.tsx +98 -0
  110. package/src/components/ProjectTreeView.stories.tsx +25 -0
  111. package/src/components/ProjectTreeView.test.tsx +75 -0
  112. package/src/components/ProjectTreeView.tsx +125 -0
  113. package/src/components/PromptDialog.stories.tsx +36 -0
  114. package/src/components/PromptDialog.test.tsx +61 -0
  115. package/src/components/PromptDialog.tsx +99 -0
  116. package/src/components/PrototypeContentView.stories.tsx +19 -0
  117. package/src/components/PrototypeContentView.tsx +446 -0
  118. package/src/components/RootOverlay.stories.tsx +45 -0
  119. package/src/components/RootOverlay.tsx +48 -0
  120. package/src/components/ScrollArea.stories.tsx +20 -0
  121. package/src/components/ScrollArea.tsx +44 -0
  122. package/src/components/SettingsDialog.stories.tsx +117 -0
  123. package/src/components/SettingsDialog.tsx +186 -0
  124. package/src/components/SettingsRows.stories.tsx +18 -0
  125. package/src/components/SettingsRows.tsx +112 -0
  126. package/src/components/ShareDialog.stories.tsx +121 -0
  127. package/src/components/ShareDialog.tsx +552 -0
  128. package/src/components/ShellHeader.stories.tsx +50 -0
  129. package/src/components/ShellHeader.tsx +137 -0
  130. package/src/components/SlashComposer.stories.tsx +37 -0
  131. package/src/components/SlashComposer.tsx +87 -0
  132. package/src/components/ThreadFooter.stories.tsx +30 -0
  133. package/src/components/ThreadFooter.test.tsx +47 -0
  134. package/src/components/ThreadFooter.tsx +76 -0
  135. package/src/components/ThreadTree.stories.tsx +46 -0
  136. package/src/components/ThreadTree.tsx +255 -0
  137. package/src/components/ThreadView.selection.test.tsx +951 -0
  138. package/src/components/ThreadView.slash.test.tsx +146 -0
  139. package/src/components/ThreadView.stories.tsx +26 -0
  140. package/src/components/ThreadView.tsx +525 -0
  141. package/src/components/Toast.stories.tsx +19 -0
  142. package/src/components/Toast.tsx +52 -0
  143. package/src/components/Tooltip.stories.tsx +26 -0
  144. package/src/components/Tooltip.tsx +72 -0
  145. package/src/components/TreePane.stories.tsx +106 -0
  146. package/src/components/TreePane.tsx +157 -0
  147. package/src/components/WalkWizard.stories.tsx +100 -0
  148. package/src/components/WalkWizard.tsx +150 -0
  149. package/src/components/WelcomePlayground.stories.tsx +12 -0
  150. package/src/components/WelcomePlayground.test.tsx +49 -0
  151. package/src/components/WelcomePlayground.tsx +139 -0
  152. package/src/components/WorkspacePanels.stories.tsx +36 -0
  153. package/src/components/WorkspacePanels.tsx +116 -0
  154. package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
  155. package/src/components/diff-content-view-layout.test.ts +198 -0
  156. package/src/components/diff-content-view-layout.ts +84 -0
  157. package/src/components/editor-grid.test.ts +86 -0
  158. package/src/components/editor-grid.ts +138 -0
  159. package/src/components/file-tree.test.ts +60 -0
  160. package/src/components/file-tree.ts +112 -0
  161. package/src/components/menu-control.stories.tsx +35 -0
  162. package/src/components/menu-control.tsx +43 -0
  163. package/src/components/pages/Workspace.stories.tsx +181 -0
  164. package/src/components/primitives/Button.stories.tsx +32 -0
  165. package/src/components/primitives/Button.tsx +59 -0
  166. package/src/components/primitives/Card.stories.tsx +25 -0
  167. package/src/components/primitives/Card.tsx +63 -0
  168. package/src/components/primitives/Dialog.stories.tsx +24 -0
  169. package/src/components/primitives/Dialog.test.tsx +37 -0
  170. package/src/components/primitives/Dialog.tsx +71 -0
  171. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  172. package/src/components/primitives/DialogActions.tsx +39 -0
  173. package/src/components/primitives/IconButton.stories.tsx +36 -0
  174. package/src/components/primitives/IconButton.tsx +62 -0
  175. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  176. package/src/components/primitives/StatusBar.tsx +23 -0
  177. package/src/components/primitives/Switch.stories.tsx +17 -0
  178. package/src/components/primitives/Switch.tsx +32 -0
  179. package/src/components/primitives/Tabs.stories.tsx +29 -0
  180. package/src/components/primitives/Tabs.tsx +102 -0
  181. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  182. package/src/components/primitives/Toolbar.tsx +14 -0
  183. package/src/components/primitives/Tree.stories.tsx +124 -0
  184. package/src/components/primitives/Tree.tsx +100 -0
  185. package/src/components/primitives/frame.test.ts +12 -0
  186. package/src/components/primitives/frame.ts +17 -0
  187. package/src/components/primitives/icons.ts +61 -0
  188. package/src/components/primitives/tree-model.test.ts +99 -0
  189. package/src/components/primitives/tree-model.ts +137 -0
  190. package/src/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/components/quick-actions-editor.test.tsx +117 -0
  192. package/src/components/quick-actions-editor.tsx +226 -0
  193. package/src/components/share-dialog-store.ts +58 -0
  194. package/src/components/stories-app.tsx +193 -0
  195. package/src/components/stories.test.tsx +74 -0
  196. package/src/components/story-fixtures.ts +102 -0
  197. package/src/components/story-tree.test.ts +108 -0
  198. package/src/components/story.ts +159 -0
  199. package/src/components/syntax-highlight.ts +133 -0
  200. package/src/components/terminal-pane.test.tsx +40 -0
  201. package/src/components/terminal-pane.ts +199 -0
  202. package/src/components/theme-context.stories.tsx +46 -0
  203. package/src/components/theme-context.tsx +37 -0
  204. package/src/components/thread-tree.test.ts +134 -0
  205. package/src/components/thread-tree.ts +109 -0
  206. package/src/components/truncate-text.ts +7 -0
  207. package/src/components/truncate-title.test.ts +31 -0
  208. package/src/components/truncate-title.ts +15 -0
  209. package/src/config-identity.test.ts +78 -0
  210. package/src/config.test.ts +579 -34
  211. package/src/config.ts +533 -57
  212. package/src/diff-fold.test.ts +78 -0
  213. package/src/diff-fold.ts +126 -0
  214. package/src/diff-hunk-curate.test.ts +315 -0
  215. package/src/diff-hunk-curate.ts +153 -0
  216. package/src/diff-intraline.test.ts +238 -0
  217. package/src/diff-intraline.ts +236 -0
  218. package/src/diff-syntax.test.ts +112 -0
  219. package/src/diff-syntax.ts +179 -0
  220. package/src/discussions.ts +79 -0
  221. package/src/editor.test.ts +149 -0
  222. package/src/editor.ts +139 -9
  223. package/src/ghostty-terminal.test.ts +85 -0
  224. package/src/ghostty-terminal.ts +199 -0
  225. package/src/github-identity.test.ts +58 -0
  226. package/src/github-identity.ts +41 -0
  227. package/src/herdr.test.ts +9 -34
  228. package/src/herdr.ts +25 -21
  229. package/src/index.ts +28 -0
  230. package/src/integrations.test.ts +41 -0
  231. package/src/integrations.ts +32 -0
  232. package/src/intent-dispatch.test.ts +724 -0
  233. package/src/intent-dispatch.ts +690 -0
  234. package/src/key-bindings.test.ts +156 -0
  235. package/src/key-bindings.ts +459 -0
  236. package/src/keymap.test.ts +456 -115
  237. package/src/keymap.ts +315 -81
  238. package/src/kitty-image.test.ts +66 -0
  239. package/src/kitty-image.ts +138 -0
  240. package/src/launch-layout.test.ts +67 -0
  241. package/src/launch-layout.ts +47 -0
  242. package/src/mark-runs.ts +121 -0
  243. package/src/markdown-editor-syntax-style.ts +50 -0
  244. package/src/markdown-grid.test.ts +101 -0
  245. package/src/markdown-grid.ts +203 -0
  246. package/src/markdown-highlight.test.ts +61 -0
  247. package/src/markdown-highlight.ts +122 -0
  248. package/src/pasted-image.test.ts +67 -0
  249. package/src/pasted-image.ts +60 -0
  250. package/src/perf/otlp-trace.test.ts +116 -0
  251. package/src/perf/otlp-trace.ts +130 -0
  252. package/src/perf/perf-report.ts +17 -0
  253. package/src/perf/perf-timings.test.ts +18 -0
  254. package/src/perf/perf-timings.ts +38 -0
  255. package/src/prototype-browser.test.ts +81 -0
  256. package/src/prototype-browser.ts +329 -0
  257. package/src/prototype-pixels.tsx +35 -0
  258. package/src/pty.test.ts +81 -0
  259. package/src/pty.ts +245 -0
  260. package/src/ready-signal.test.ts +85 -0
  261. package/src/ready-signal.ts +52 -0
  262. package/src/run.ts +85 -12
  263. package/src/serve.test.ts +24 -12
  264. package/src/serve.ts +52 -10
  265. package/src/share.ts +237 -0
  266. package/src/skills.test.ts +74 -0
  267. package/src/skills.ts +75 -0
  268. package/src/slash-palette.test.ts +103 -0
  269. package/src/slash-palette.ts +129 -0
  270. package/src/split-diff.test.ts +110 -0
  271. package/src/split-diff.ts +149 -0
  272. package/src/test-support.test.ts +264 -0
  273. package/src/test-support.ts +369 -0
  274. package/src/theme-presets.test.ts +51 -0
  275. package/src/theme-presets.ts +219 -0
  276. package/src/theme.ts +88 -19
  277. package/src/thread-chords.test.ts +79 -0
  278. package/src/thread-chords.ts +163 -0
  279. package/src/thread-code-syntax.ts +45 -0
  280. package/src/thread-controller.curate.test.ts +476 -0
  281. package/src/thread-controller.derivation.test.ts +62 -0
  282. package/src/thread-controller.live-diff.test.ts +322 -0
  283. package/src/thread-controller.optimistic.test.ts +69 -0
  284. package/src/thread-controller.share.test.ts +561 -0
  285. package/src/thread-controller.switch-cache.test.ts +176 -0
  286. package/src/thread-controller.tree.test.ts +261 -0
  287. package/src/thread-controller.ts +2050 -0
  288. package/src/thread-selection.test.ts +210 -0
  289. package/src/thread-selection.ts +243 -0
  290. package/src/tree-view.test.ts +128 -0
  291. package/src/tree-view.ts +153 -0
  292. package/src/use-annotation-surface.tsx +975 -0
  293. package/src/use-changes-workbench.ts +190 -0
  294. package/src/use-frame-measure.ts +41 -0
  295. package/src/use-remember-layout.ts +31 -0
  296. package/src/use-repo-changes.ts +87 -0
  297. package/src/use-settings-dialog.tsx +364 -0
  298. package/src/use-terminal-virtualizer.ts +259 -0
  299. package/src/use-thread-body-editing.tsx +58 -0
  300. package/src/version.ts +5 -0
  301. package/src/view-diff.test.ts +279 -9
  302. package/src/view-diff.ts +460 -24
  303. package/src/view-plan.regression.test.ts +60 -0
  304. package/src/view-plan.test.ts +545 -0
  305. package/src/view-plan.ts +1019 -0
  306. package/src/visual/contrast.test.ts +35 -0
  307. package/src/visual/contrast.ts +33 -0
  308. package/src/walk.test.ts +121 -0
  309. package/src/walk.ts +74 -0
  310. package/src/workbench-snapshot.test.ts +99 -0
  311. package/src/workbench-snapshot.ts +59 -0
  312. package/src/session-controller.ts +0 -398
  313. package/src/syntax.test.ts +0 -41
  314. package/src/syntax.ts +0 -88
  315. package/src/view.test.ts +0 -157
  316. package/src/view.ts +0 -364
package/src/view.ts DELETED
@@ -1,364 +0,0 @@
1
- /**
2
- * The projection pipeline (#22): parse → display blocks (base vs working
3
- * copy reconciled) → styled runs → wrapped lines. Pure functions - the TUI
4
- * renders the output, tests assert on it directly. One planning layer:
5
- * rendering, navigation, and selection all derive from this module.
6
- */
7
-
8
- import {
9
- lcsDiff,
10
- parseBlocks,
11
- resolveAnchor,
12
- wordDiff,
13
- type Annotation,
14
- type Block,
15
- } from "@cueloop/schema";
16
-
17
- // ── display model ─────────────────────────────
18
-
19
- export type DisplayType = "same" | "mod" | "del" | "add";
20
-
21
- export interface DisplayBlock {
22
- type: DisplayType;
23
- kind: Block["kind"];
24
- /** Block in the working copy (absent for del). */
25
- work?: Block;
26
- /** Block in the submitted revision (absent for add). */
27
- base?: Block;
28
- oliNum?: number;
29
- }
30
-
31
- export function displayText(d: DisplayBlock): string {
32
- return (d.work ?? d.base)!.text;
33
- }
34
-
35
- /** Word-overlap similarity gate: an unrelated cut + insert must not merge. */
36
- function similar(a: Block, b: Block): boolean {
37
- if (a.kind !== b.kind) return false;
38
- const words = (t: string) => new Set(t.toLowerCase().split(/\s+/).filter(Boolean));
39
- const wa = words(a.text);
40
- const wb = words(b.text);
41
- let inter = 0;
42
- for (const w of wa) if (wb.has(w)) inter++;
43
- const union = wa.size + wb.size - inter;
44
- return union === 0 ? true : inter / union >= 0.3;
45
- }
46
-
47
- /** Reconcile base blocks vs working blocks into the display list. */
48
- export function buildDisplay(baseContent: string, workingContent?: string): DisplayBlock[] {
49
- const baseBlocks = parseBlocks(baseContent);
50
- if (workingContent === undefined) {
51
- const out: DisplayBlock[] = baseBlocks.map((b) => ({ type: "same", kind: b.kind, work: b, base: b }));
52
- numberOlis(out);
53
- return out;
54
- }
55
- const workBlocks = parseBlocks(workingContent);
56
- const sig = (b: Block) => b.kind + "\0" + b.text;
57
- const ops = lcsDiff(baseBlocks, workBlocks, (x, y) => sig(x) === sig(y));
58
- const out: DisplayBlock[] = [];
59
- let k = 0;
60
- while (k < ops.length) {
61
- const op = ops[k]!;
62
- if (op.t === "ctx") {
63
- out.push({ type: "same", kind: op.b!.kind, work: op.b, base: op.a });
64
- k++;
65
- continue;
66
- }
67
- const dels: Block[] = [];
68
- const adds: Block[] = [];
69
- while (k < ops.length && ops[k]!.t !== "ctx") {
70
- const o = ops[k]!;
71
- if (o.t === "del") dels.push(o.a!);
72
- else adds.push(o.b!);
73
- k++;
74
- }
75
- let di = 0;
76
- let ai = 0;
77
- while (di < dels.length && ai < adds.length) {
78
- if (similar(dels[di]!, adds[ai]!)) {
79
- out.push({ type: "mod", kind: adds[ai]!.kind, base: dels[di]!, work: adds[ai]! });
80
- di++;
81
- ai++;
82
- } else if (dels.length - di >= adds.length - ai) {
83
- out.push({ type: "del", kind: dels[di]!.kind, base: dels[di]! });
84
- di++;
85
- } else {
86
- out.push({ type: "add", kind: adds[ai]!.kind, work: adds[ai]! });
87
- ai++;
88
- }
89
- }
90
- while (di < dels.length) out.push({ type: "del", kind: dels[di]!.kind, base: dels[di++]! });
91
- while (ai < adds.length) out.push({ type: "add", kind: adds[ai]!.kind, work: adds[ai++]! });
92
- }
93
- numberOlis(out);
94
- return out;
95
- }
96
-
97
- function numberOlis(disp: DisplayBlock[]): void {
98
- let run = 0;
99
- for (const d of disp) {
100
- if (d.kind === "oli") d.oliNum = ++run;
101
- else run = 0;
102
- }
103
- }
104
-
105
- // ── marks (annotation highlights + keyboard span) ──
106
-
107
- export type RunRole =
108
- | "plain"
109
- | "ins"
110
- | "del"
111
- | "mark-comment"
112
- | "mark-suggestion"
113
- | "kspan"
114
- | "mark-focus";
115
-
116
- export interface StyleRun {
117
- text: string;
118
- role: RunRole;
119
- /** Offset of this run in the block's working text; null for del runs. */
120
- start: number | null;
121
- annotationId?: string;
122
- }
123
-
124
- export interface Mark {
125
- start: number;
126
- end: number;
127
- role: RunRole;
128
- annotationId?: string;
129
- }
130
-
131
- /**
132
- * Resolve annotations against the working blocks and group marks per display
133
- * index. Working blocks are the blocks of (workingCopy ?? artifact) - the
134
- * same list buildDisplay derived work entries from.
135
- */
136
- export function marksByDisplay(
137
- annotations: Annotation[],
138
- display: DisplayBlock[],
139
- focusedId?: string,
140
- ): Map<number, Mark[]> {
141
- // block list in "work space": display entries that carry a work block, in order
142
- const workEntries = display
143
- .map((d, dispIdx) => ({ d, dispIdx }))
144
- .filter((e) => e.d.work !== undefined);
145
- const workBlocks = workEntries.map((e) => e.d.work!);
146
- const map = new Map<number, Mark[]>();
147
- for (const a of annotations) {
148
- const res = resolveAnchor(a.anchor, workBlocks);
149
- if (!res) continue;
150
- const entry = workEntries[res.blockIndex];
151
- if (!entry) continue;
152
- const marks = map.get(entry.dispIdx) ?? [];
153
- marks.push({
154
- start: res.start,
155
- end: res.end,
156
- role: a.id === focusedId ? "mark-focus" : a.kind === "suggestion" ? "mark-suggestion" : "mark-comment",
157
- annotationId: a.id,
158
- });
159
- map.set(entry.dispIdx, marks);
160
- }
161
- return map;
162
- }
163
-
164
- /** Base runs for a display block: plain text, or word-diff for mod blocks. */
165
- export function blockRuns(d: DisplayBlock, markup: boolean): StyleRun[] {
166
- if (d.type === "mod" && markup) {
167
- const ops = wordDiff(d.base!.text, d.work!.text);
168
- const runs: StyleRun[] = [];
169
- let off = 0;
170
- for (const op of ops) {
171
- if (op.t === "del") {
172
- runs.push({ text: op.a!, role: "del", start: null });
173
- } else {
174
- runs.push({ text: op.b!, role: op.t === "add" ? "ins" : "plain", start: off });
175
- off += op.b!.length;
176
- }
177
- }
178
- return runs;
179
- }
180
- return [{ text: displayText(d), role: "plain", start: 0 }];
181
- }
182
-
183
- /** Split runs at mark boundaries; marks only bind to runs with offsets. */
184
- export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
185
- if (!marks.length) return runs;
186
- const out: StyleRun[] = [];
187
- for (const run of runs) {
188
- if (run.start === null) {
189
- out.push(run);
190
- continue;
191
- }
192
- const bounds = new Set<number>([0, run.text.length]);
193
- for (const m of marks) {
194
- const s = Math.max(0, m.start - run.start);
195
- const e = Math.min(run.text.length, m.end - run.start);
196
- if (s < e) {
197
- bounds.add(s);
198
- bounds.add(e);
199
- }
200
- }
201
- const runStart = run.start;
202
- const pts = [...bounds].sort((a, b) => a - b);
203
- for (let i = 0; i < pts.length - 1; i++) {
204
- const s = pts[i]!;
205
- const e = pts[i + 1]!;
206
- if (s >= e) continue;
207
- const abs = runStart + s;
208
- const mark = marks.find((m) => m.start <= abs && runStart + e <= m.end);
209
- out.push({
210
- text: run.text.slice(s, e),
211
- // a mark never overrides ins/del emphasis; it rides on plain text
212
- role: mark && run.role === "plain" ? mark.role : run.role,
213
- start: abs,
214
- annotationId: mark?.annotationId ?? run.annotationId,
215
- });
216
- }
217
- }
218
- return out;
219
- }
220
-
221
- /**
222
- * Wrap styled runs into lines of at most `width` display cells. Split pieces
223
- * keep exact `start` offsets into the block text, so wrapped lines stay
224
- * addressable for the native selection primitive.
225
- */
226
- export function wrapRuns(runs: StyleRun[], width: number): StyleRun[][] {
227
- const lines: StyleRun[][] = [];
228
- let line: StyleRun[] = [];
229
- let used = 0;
230
- const pushLine = () => {
231
- lines.push(line);
232
- line = [];
233
- used = 0;
234
- };
235
- for (const run of runs) {
236
- // split run into word/space tokens; newlines force breaks
237
- const tokens = run.text.split(/(\n|\s+)/).filter((t) => t !== "");
238
- let consumed = 0;
239
- const startAt = (offsetInRun: number): number | null => (run.start === null ? null : run.start + offsetInRun);
240
- for (const tok of tokens) {
241
- if (tok === "\n") {
242
- consumed += 1;
243
- pushLine();
244
- continue;
245
- }
246
- if (used + tok.length > width && used > 0 && tok.trim() !== "") pushLine();
247
- if (tok.trim() === "" && used === 0 && lines.length > 0) {
248
- consumed += tok.length;
249
- continue; // no leading spaces after wrap
250
- }
251
- let slice = tok;
252
- while (slice.length > width) {
253
- const piece = slice.slice(0, width - used);
254
- line.push({ ...run, text: piece, start: startAt(consumed) });
255
- consumed += piece.length;
256
- slice = slice.slice(piece.length);
257
- pushLine();
258
- }
259
- line.push({ ...run, text: slice, start: startAt(consumed) });
260
- consumed += slice.length;
261
- used += slice.length;
262
- }
263
- }
264
- if (line.length || !lines.length) lines.push(line);
265
- return lines;
266
- }
267
-
268
- /**
269
- * Locate a block-text offset inside wrapped run lines: the wrapped line index
270
- * and the column within the line's rendered run text. Drives the renderer's
271
- * native selection from keyboard span offsets.
272
- */
273
- export function locateOffset(lines: StyleRun[][], offset: number): { lineIndex: number; column: number } | null {
274
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
275
- let column = 0;
276
- for (const run of lines[lineIndex]!) {
277
- if (run.start !== null && offset >= run.start && offset < run.start + run.text.length) {
278
- return { lineIndex, column: column + (offset - run.start) };
279
- }
280
- column += run.text.length;
281
- }
282
- }
283
- return null;
284
- }
285
-
286
- /**
287
- * Inline compose box height, shared by layout and render so the row math and
288
- * the mounted box never drift: top border (title), the single-row input, the
289
- * Save/Cancel button row, bottom border.
290
- */
291
- export function composeBoxHeight(): number {
292
- return 4;
293
- }
294
-
295
- /** Line delta between two revision contents, for the sheet-header summary. */
296
- export function revisionDelta(previousContent: string, nextContent: string): { added: number; removed: number } {
297
- const ops = lcsDiff(previousContent.split("\n"), nextContent.split("\n"), (a, b) => a === b);
298
- let added = 0;
299
- let removed = 0;
300
- for (const op of ops) {
301
- if (op.t === "add") added++;
302
- else if (op.t === "del") removed++;
303
- }
304
- return { added, removed };
305
- }
306
-
307
- // ── keyboard span mode ────────────────────────
308
-
309
- export interface SpanState {
310
- dispIdx: number;
311
- wordIdx: number;
312
- wordEnd: number;
313
- start: number;
314
- end: number;
315
- }
316
-
317
- export function wordRanges(text: string): [number, number][] {
318
- const out: [number, number][] = [];
319
- const re = /\S+/g;
320
- let m: RegExpExecArray | null;
321
- while ((m = re.exec(text))) out.push([m.index, m.index + m[0]!.length]);
322
- return out;
323
- }
324
-
325
- export function startSpan(dispIdx: number, text: string): SpanState | null {
326
- const words = wordRanges(text);
327
- if (!words.length) return null;
328
- return { dispIdx, wordIdx: 0, wordEnd: 0, start: words[0]![0], end: words[0]![1] };
329
- }
330
-
331
- export function spanKey(span: SpanState, key: string, text: string): SpanState {
332
- const words = wordRanges(text);
333
- const s = { ...span };
334
- const width = s.wordEnd - s.wordIdx;
335
- if (key === "l") s.wordEnd = Math.min(words.length - 1, s.wordEnd + 1);
336
- else if (key === "h") s.wordEnd = Math.max(s.wordIdx, s.wordEnd - 1);
337
- else if (key === "w") {
338
- const n = Math.min(words.length - 1 - width, s.wordIdx + 1);
339
- s.wordIdx = n;
340
- s.wordEnd = n + width;
341
- } else if (key === "b") {
342
- const n = Math.max(0, s.wordIdx - 1);
343
- s.wordIdx = n;
344
- s.wordEnd = n + width;
345
- } else if (key === "$") s.wordEnd = words.length - 1;
346
- else if (key === "0") {
347
- s.wordEnd = width;
348
- s.wordIdx = 0;
349
- } else return span;
350
- s.start = words[s.wordIdx]![0];
351
- s.end = words[s.wordEnd]![1];
352
- return s;
353
- }
354
-
355
- // ── working-copy block operations ─────────────
356
-
357
- /** The next work block after a display index (a del block re-enters before it). */
358
- export function nextWorkBlock(display: DisplayBlock[], dispIdx: number): Block | undefined {
359
- for (let i = dispIdx + 1; i < display.length; i++) {
360
- const w = display[i]!.work;
361
- if (w) return w;
362
- }
363
- return undefined;
364
- }