@cueloop/client 0.1.0-alpha.4 → 0.1.0-alpha.40

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 (131) hide show
  1. package/package.json +32 -24
  2. package/src/App.autoclose.test.tsx +137 -0
  3. package/src/App.diff.test.tsx +158 -22
  4. package/src/App.export.test.tsx +44 -37
  5. package/src/App.plan-review.test.tsx +632 -0
  6. package/src/App.readonly.test.tsx +60 -36
  7. package/src/App.rendering.test.tsx +132 -0
  8. package/src/App.review-panel.test.tsx +136 -0
  9. package/src/App.submit-confirm.test.tsx +220 -0
  10. package/src/App.test.tsx +128 -69
  11. package/src/App.toast.test.tsx +75 -0
  12. package/src/App.tsx +0 -0
  13. package/src/App.walk.test.tsx +288 -0
  14. package/src/attribution.test.ts +31 -0
  15. package/src/attribution.ts +38 -0
  16. package/src/clipboard.test.ts +23 -0
  17. package/src/clipboard.ts +30 -0
  18. package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
  19. package/src/components/AnnotationCard.stories.tsx +94 -0
  20. package/src/components/AnnotationCard.tsx +228 -0
  21. package/src/components/Breadcrumb.stories.tsx +19 -0
  22. package/src/components/Breadcrumb.tsx +44 -0
  23. package/src/components/CodeBlock.stories.tsx +42 -0
  24. package/src/components/CodeBlock.tsx +83 -0
  25. package/src/components/CompletionOverlay.stories.tsx +23 -0
  26. package/src/components/CompletionOverlay.tsx +59 -0
  27. package/src/components/ConfirmCard.stories.tsx +56 -0
  28. package/src/components/ConfirmCard.tsx +135 -0
  29. package/src/components/ConfirmDialog.stories.tsx +21 -0
  30. package/src/components/ConfirmDialog.tsx +64 -0
  31. package/src/components/DiffSheet.scroll.test.tsx +123 -0
  32. package/src/components/DiffSheet.stories.tsx +78 -0
  33. package/src/components/DiffSheet.syntax.test.tsx +62 -0
  34. package/src/components/DiffSheet.tsx +355 -0
  35. package/src/components/InboxList.stories.tsx +31 -0
  36. package/src/components/InboxList.tsx +62 -0
  37. package/src/components/KeybindsDialog.stories.tsx +31 -0
  38. package/src/components/KeybindsDialog.tsx +58 -0
  39. package/src/components/MarkerPopover.stories.tsx +58 -0
  40. package/src/components/MarkerPopover.tsx +102 -0
  41. package/src/components/MenuBar.stories.tsx +37 -0
  42. package/src/components/MenuBar.tsx +74 -0
  43. package/src/components/PlanSheet.stories.tsx +113 -0
  44. package/src/components/PlanSheet.tsx +389 -0
  45. package/src/components/PromptDialog.stories.tsx +36 -0
  46. package/src/components/PromptDialog.tsx +78 -0
  47. package/src/components/ReviewPanel.stories.tsx +86 -0
  48. package/src/components/ReviewPanel.tsx +101 -0
  49. package/src/components/ReviewRail.stories.tsx +220 -0
  50. package/src/components/ReviewRail.tsx +316 -0
  51. package/src/components/SettingsDialog.stories.tsx +111 -0
  52. package/src/components/SettingsDialog.tsx +141 -0
  53. package/src/components/SettingsRows.stories.tsx +18 -0
  54. package/src/components/SettingsRows.tsx +108 -0
  55. package/src/components/Toast.stories.tsx +12 -0
  56. package/src/components/Toast.tsx +44 -0
  57. package/src/components/WalkWizard.stories.tsx +100 -0
  58. package/src/components/WalkWizard.tsx +152 -0
  59. package/src/components/__snapshots__/stories.test.tsx.snap +1829 -0
  60. package/src/components/agent-launcher.stories.tsx +26 -0
  61. package/src/components/agent-launcher.test.tsx +21 -0
  62. package/src/components/agent-launcher.tsx +123 -0
  63. package/src/components/diff-sheet-layout.test.ts +127 -0
  64. package/src/components/diff-sheet-layout.ts +167 -0
  65. package/src/components/primitives/Button.stories.tsx +32 -0
  66. package/src/components/primitives/Button.tsx +48 -0
  67. package/src/components/primitives/Card.stories.tsx +25 -0
  68. package/src/components/primitives/Card.tsx +59 -0
  69. package/src/components/primitives/Dialog.stories.tsx +24 -0
  70. package/src/components/primitives/Dialog.tsx +64 -0
  71. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  72. package/src/components/primitives/StatusBar.tsx +22 -0
  73. package/src/components/primitives/Tabs.stories.tsx +29 -0
  74. package/src/components/primitives/Tabs.tsx +98 -0
  75. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  76. package/src/components/primitives/Toolbar.tsx +14 -0
  77. package/src/components/primitives/frame.test.ts +12 -0
  78. package/src/components/primitives/frame.ts +14 -0
  79. package/src/components/quick-actions-editor.stories.tsx +54 -0
  80. package/src/components/quick-actions-editor.tsx +136 -0
  81. package/src/components/stories-app.tsx +123 -0
  82. package/src/components/stories.test.tsx +62 -0
  83. package/src/components/story-fixtures.ts +98 -0
  84. package/src/components/story.ts +69 -0
  85. package/src/components/syntax-highlight.ts +92 -0
  86. package/src/components/theme-context.stories.tsx +46 -0
  87. package/src/components/theme-context.tsx +36 -0
  88. package/src/components/truncate-text.ts +6 -0
  89. package/src/config.test.ts +430 -31
  90. package/src/config.ts +304 -26
  91. package/src/diff-hunk-curate.test.ts +315 -0
  92. package/src/diff-hunk-curate.ts +233 -0
  93. package/src/diff-intraline.test.ts +164 -0
  94. package/src/diff-intraline.ts +132 -0
  95. package/src/diff-syntax.test.ts +96 -0
  96. package/src/diff-syntax.ts +154 -0
  97. package/src/editor.test.ts +143 -0
  98. package/src/editor.ts +123 -9
  99. package/src/herdr.test.ts +41 -0
  100. package/src/herdr.ts +27 -0
  101. package/src/index.ts +10 -0
  102. package/src/integrations.test.ts +41 -0
  103. package/src/integrations.ts +30 -0
  104. package/src/intent-dispatch.test.ts +492 -0
  105. package/src/intent-dispatch.ts +428 -0
  106. package/src/key-bindings.test.ts +152 -0
  107. package/src/key-bindings.ts +424 -0
  108. package/src/keymap.test.ts +622 -0
  109. package/src/keymap.ts +339 -0
  110. package/src/review-panel.test.ts +113 -0
  111. package/src/review-panel.ts +83 -0
  112. package/src/run.ts +9 -5
  113. package/src/serve.test.ts +24 -9
  114. package/src/serve.ts +7 -7
  115. package/src/session-controller.curate.test.ts +253 -0
  116. package/src/session-controller.share.test.ts +309 -0
  117. package/src/session-controller.ts +900 -0
  118. package/src/share.ts +113 -0
  119. package/src/test-support.test.ts +55 -0
  120. package/src/test-support.ts +187 -0
  121. package/src/theme-presets.ts +168 -0
  122. package/src/theme.ts +36 -15
  123. package/src/version.ts +5 -0
  124. package/src/view-diff.test.ts +19 -8
  125. package/src/view-diff.ts +34 -23
  126. package/src/view-plan.test.ts +294 -0
  127. package/src/view-plan.ts +408 -0
  128. package/src/walk.test.ts +121 -0
  129. package/src/walk.ts +70 -0
  130. package/src/view.test.ts +0 -157
  131. package/src/view.ts +0 -0
package/src/view-diff.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Diff artifact projection (#18 slice 3): flatten @pierre/diffs' parsed
2
+ * Diff artifact projection: flatten @pierre/diffs' parsed
3
3
  * patch model into render rows. Line-anchored annotations use the same
4
4
  * quote-primary anchors as plans: quote = the line content, prefix/suffix =
5
5
  * the neighbor lines - so the whole anchor/feedback pipeline is shared.
@@ -7,10 +7,10 @@
7
7
 
8
8
  import { parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs";
9
9
 
10
- export type DiffRowType = "file" | "hunk" | "ctx" | "add" | "del";
10
+ export type DiffRowKind = "file" | "hunk" | "ctx" | "add" | "del";
11
11
 
12
12
  export interface DiffRow {
13
- t: DiffRowType;
13
+ kind: DiffRowKind;
14
14
  text: string;
15
15
  file: string;
16
16
  oldLine?: number;
@@ -22,35 +22,35 @@ export function diffRows(patchText: string): DiffRow[] {
22
22
  const patches = parsePatchFiles(patchText);
23
23
  for (const patch of patches) {
24
24
  for (const file of patch.files) {
25
- rows.push({ t: "file", text: fileLabel(file), file: file.name });
25
+ rows.push({ kind: "file", text: fileLabel(file), file: file.name });
26
26
  for (const hunk of file.hunks) {
27
- rows.push({ t: "hunk", text: hunk.hunkSpecs ?? "@@", file: file.name });
27
+ rows.push({ kind: "hunk", text: hunk.hunkSpecs ?? "@@", file: file.name });
28
28
  let oldLine = hunk.deletionStart;
29
29
  let newLine = hunk.additionStart;
30
- for (const seg of hunk.hunkContent) {
31
- if (seg.type === "context") {
32
- for (let i = 0; i < seg.lines; i++) {
30
+ for (const segment of hunk.hunkContent) {
31
+ if (segment.type === "context") {
32
+ for (let lineOffset = 0; lineOffset < segment.lines; lineOffset++) {
33
33
  rows.push({
34
- t: "ctx",
35
- text: file.additionLines[seg.additionLineIndex + i] ?? "",
34
+ kind: "ctx",
35
+ text: file.additionLines[segment.additionLineIndex + lineOffset] ?? "",
36
36
  file: file.name,
37
37
  oldLine: oldLine++,
38
38
  newLine: newLine++,
39
39
  });
40
40
  }
41
41
  } else {
42
- for (let i = 0; i < seg.deletions; i++) {
42
+ for (let lineOffset = 0; lineOffset < segment.deletions; lineOffset++) {
43
43
  rows.push({
44
- t: "del",
45
- text: file.deletionLines[seg.deletionLineIndex + i] ?? "",
44
+ kind: "del",
45
+ text: file.deletionLines[segment.deletionLineIndex + lineOffset] ?? "",
46
46
  file: file.name,
47
47
  oldLine: oldLine++,
48
48
  });
49
49
  }
50
- for (let i = 0; i < seg.additions; i++) {
50
+ for (let lineOffset = 0; lineOffset < segment.additions; lineOffset++) {
51
51
  rows.push({
52
- t: "add",
53
- text: file.additionLines[seg.additionLineIndex + i] ?? "",
52
+ kind: "add",
53
+ text: file.additionLines[segment.additionLineIndex + lineOffset] ?? "",
54
54
  file: file.name,
55
55
  newLine: newLine++,
56
56
  });
@@ -64,18 +64,29 @@ export function diffRows(patchText: string): DiffRow[] {
64
64
  }
65
65
 
66
66
  function fileLabel(file: FileDiffMetadata): string {
67
- return file.prevName && file.prevName !== file.name ? `${file.prevName} → ${file.name}` : file.name;
67
+ return file.prevName && file.prevName !== file.name
68
+ ? `${file.prevName} → ${file.name}`
69
+ : file.name;
68
70
  }
69
71
 
70
72
  /** Quote-primary anchor for a diff row: neighbors as context selectors. */
71
- export function diffRowAnchor(rows: DiffRow[], idx: number): { quote: string; prefix: string; suffix: string } {
72
- const row = rows[idx]!;
73
- const prev = rows[idx - 1];
74
- const next = rows[idx + 1];
73
+ export function diffRowAnchor(
74
+ rows: DiffRow[],
75
+ rowIndex: number,
76
+ ): { quote: string; prefix: string; suffix: string } {
77
+ const row = rows[rowIndex]!;
78
+ const prev = rows[rowIndex - 1];
79
+ const next = rows[rowIndex + 1];
75
80
  return {
76
81
  quote: row.text,
77
- prefix: prev && (prev.t === "ctx" || prev.t === "add" || prev.t === "del") ? prev.text.slice(-24) : "",
78
- suffix: next && (next.t === "ctx" || next.t === "add" || next.t === "del") ? next.text.slice(0, 24) : "",
82
+ prefix:
83
+ prev && (prev.kind === "ctx" || prev.kind === "add" || prev.kind === "del")
84
+ ? prev.text.slice(-24)
85
+ : "",
86
+ suffix:
87
+ next && (next.kind === "ctx" || next.kind === "add" || next.kind === "del")
88
+ ? next.text.slice(0, 24)
89
+ : "",
79
90
  };
80
91
  }
81
92
 
@@ -0,0 +1,294 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ blockRuns,
4
+ buildDisplay,
5
+ displayText,
6
+ marksByDisplay,
7
+ nextWorkBlock,
8
+ overlayMarks,
9
+ renderedOffsetFor,
10
+ spanFromRange,
11
+ spanKey,
12
+ startSpan,
13
+ workRangeForRendered,
14
+ } from "./view-plan";
15
+ import { cutBlock, parseBlocks, restoreBlock, restoreLine, type Annotation } from "@cueloop/schema";
16
+
17
+ const BASE = `# Plan
18
+
19
+ ## Context
20
+
21
+ The daemon persists sessions to disk atomically.
22
+
23
+ - first item
24
+ - second item
25
+ `;
26
+
27
+ describe("buildDisplay reconciliation", () => {
28
+ test("no working copy: everything is same", () => {
29
+ // Act
30
+ const display = buildDisplay(BASE);
31
+
32
+ // Assert
33
+ expect(display.every((block) => block.type === "same")).toBe(true);
34
+ });
35
+
36
+ test("reworded block pairs as mod (similar)", () => {
37
+ // Arrange
38
+ const working = BASE.replace("persists sessions to disk", "writes sessions to disk");
39
+
40
+ // Act
41
+ const display = buildDisplay(BASE, working);
42
+
43
+ // Assert
44
+ const mod = display.find((block) => block.type === "mod")!;
45
+ expect(mod.base!.text).toContain("persists");
46
+ expect(mod.work!.text).toContain("writes");
47
+ });
48
+
49
+ test("unrelated cut + insert do NOT merge into a mod", () => {
50
+ // Arrange
51
+ const working = BASE.replace("- second item\n", "") + "\nA totally new closing note.\n";
52
+
53
+ // Act
54
+ const display = buildDisplay(BASE, working);
55
+
56
+ // Assert
57
+ expect(
58
+ display.some((block) => block.type === "del" && block.base!.text === "second item"),
59
+ ).toBe(true);
60
+ expect(
61
+ display.some((block) => block.type === "add" && block.work!.text.includes("closing note")),
62
+ ).toBe(true);
63
+ expect(display.some((block) => block.type === "mod")).toBe(false);
64
+ });
65
+
66
+ test("ordered list items get running numbers", () => {
67
+ // Act
68
+ const display = buildDisplay("1. one\n2. two\n3. three\n");
69
+
70
+ // Assert
71
+ expect(display.map((block) => block.orderedItemNumber)).toEqual([1, 2, 3]);
72
+ });
73
+ });
74
+
75
+ describe("blockRuns + overlayMarks", () => {
76
+ test("mod blocks emit word-diff runs that reconstruct both sides", () => {
77
+ // Arrange
78
+ const block = buildDisplay("plain old text here\n", "plain new text here\n")[0]!;
79
+
80
+ // Act
81
+ const runs = blockRuns(block, true);
82
+
83
+ // Assert
84
+ const oldSide = runs
85
+ .filter((run) => run.role !== "ins")
86
+ .map((run) => run.text)
87
+ .join("");
88
+ const newSide = runs
89
+ .filter((run) => run.role !== "del")
90
+ .map((run) => run.text)
91
+ .join("");
92
+ expect(oldSide).toBe("plain old text here");
93
+ expect(newSide).toBe("plain new text here");
94
+ });
95
+
96
+ test("marks bind to working-text offsets and skip del runs", () => {
97
+ // Arrange
98
+ const block = buildDisplay("old words linger\n", "new words linger\n")[0]!;
99
+ const runs = blockRuns(block, true);
100
+
101
+ // Act
102
+ const marked = overlayMarks(runs, [{ start: 4, end: 9, role: "mark-comment" }]); // "words"
103
+
104
+ // Assert
105
+ const markedText = marked
106
+ .filter((run) => run.role === "mark-comment")
107
+ .map((run) => run.text)
108
+ .join("");
109
+ expect(markedText).toBe("words");
110
+ // del run text untouched
111
+ expect(marked.some((run) => run.role === "del" && run.text === "old")).toBe(true);
112
+ });
113
+
114
+ test("marks never override ins emphasis", () => {
115
+ // Arrange
116
+ const block = buildDisplay("keep\n", "keep added\n")[0]!;
117
+
118
+ // Act
119
+ const runs = overlayMarks(blockRuns(block, true), [
120
+ { start: 0, end: 10, role: "mark-comment" },
121
+ ]);
122
+
123
+ // Assert
124
+ expect(runs.some((run) => run.role === "ins")).toBe(true);
125
+ });
126
+ });
127
+
128
+ describe("marksByDisplay", () => {
129
+ test("annotations resolve into display coordinates", () => {
130
+ // Arrange
131
+ const working = BASE.replace("first item", "first item edited beyond recognition of it");
132
+ const display = buildDisplay(BASE, working);
133
+ const ann: Annotation = {
134
+ id: "a1",
135
+ kind: "comment",
136
+ anchor: { quote: "atomically", prefix: "to disk ", suffix: "." },
137
+ body: "define atomically",
138
+ createdAt: "",
139
+ };
140
+
141
+ // Act
142
+ const map = marksByDisplay([ann], display);
143
+
144
+ // Assert
145
+ const [displayIndex, marks] = [...map.entries()][0]!;
146
+ expect(displayText(display[displayIndex]!)).toContain("atomically");
147
+ expect(marks[0]!.annotationId).toBe("a1");
148
+ });
149
+
150
+ test("orphaned annotations produce no marks", () => {
151
+ // Arrange
152
+ const display = buildDisplay(BASE);
153
+ const ann: Annotation = {
154
+ id: "a1",
155
+ kind: "comment",
156
+ anchor: { quote: "text that is gone", prefix: "", suffix: "" },
157
+ body: "",
158
+ createdAt: "",
159
+ };
160
+
161
+ // Assert
162
+ expect(marksByDisplay([ann], display).size).toBe(0);
163
+ });
164
+ });
165
+
166
+ describe("rendered/work offset mapping", () => {
167
+ test("plain blocks map 1:1 in both directions", () => {
168
+ // Arrange
169
+ const block = buildDisplay("alpha beta gamma\n")[0]!;
170
+ const runs = blockRuns(block, true);
171
+
172
+ // Assert
173
+ expect(renderedOffsetFor(runs, 6)).toBe(6);
174
+ expect(workRangeForRendered(runs, 6, 10)).toEqual({ start: 6, end: 10 });
175
+ });
176
+
177
+ test("inline del runs shift rendered offsets in word-diffed blocks", () => {
178
+ // Arrange
179
+ // rendered: "old new words linger" - "old " is a del run without offsets
180
+ const block = buildDisplay("old words linger\n", "new words linger\n")[0]!;
181
+ const runs = blockRuns(block, true);
182
+ const workText = displayText(block);
183
+ const wordsAt = workText.indexOf("words");
184
+
185
+ // Act
186
+ const rendered = renderedOffsetFor(runs, wordsAt)!;
187
+
188
+ // Assert
189
+ const renderedText = runs.map((run) => run.text).join("");
190
+ expect(renderedText.slice(rendered, rendered + "words".length)).toBe("words");
191
+ // reading a rendered selection over "words" recovers the work range
192
+ expect(workRangeForRendered(runs, rendered, rendered + "words".length)).toEqual({
193
+ start: wordsAt,
194
+ end: wordsAt + "words".length,
195
+ });
196
+ });
197
+
198
+ test("a rendered selection spanning a del run keeps only positioned text", () => {
199
+ // Arrange
200
+ const block = buildDisplay("old words\n", "new words\n")[0]!;
201
+ const runs = blockRuns(block, true);
202
+ const renderedText = runs.map((run) => run.text).join("");
203
+
204
+ // Act
205
+ // select everything rendered: the work range is the whole work text
206
+ const range = workRangeForRendered(runs, 0, renderedText.length)!;
207
+
208
+ // Assert
209
+ expect(displayText(block).slice(range.start, range.end)).toBe(displayText(block));
210
+ });
211
+
212
+ test("offsets outside any positioned run map to null", () => {
213
+ // Arrange
214
+ const runs = blockRuns(buildDisplay("short\n")[0]!, true);
215
+
216
+ // Assert
217
+ expect(renderedOffsetFor(runs, 99)).toBeNull();
218
+ expect(workRangeForRendered(runs, 90, 99)).toBeNull();
219
+ });
220
+ });
221
+
222
+ describe("span mode", () => {
223
+ const text = "one two three four";
224
+ test("v selects the first word; l grows; w slides; $ to end", () => {
225
+ // Act
226
+ let span = startSpan(0, text)!;
227
+
228
+ // Assert
229
+ expect(text.slice(span.start, span.end)).toBe("one");
230
+
231
+ // Act
232
+ span = spanKey(span, "l", text);
233
+
234
+ // Assert
235
+ expect(text.slice(span.start, span.end)).toBe("one two");
236
+
237
+ // Act
238
+ span = spanKey(span, "w", text);
239
+
240
+ // Assert
241
+ expect(text.slice(span.start, span.end)).toBe("two three");
242
+
243
+ // Act
244
+ span = spanKey(span, "$", text);
245
+
246
+ // Assert
247
+ expect(text.slice(span.start, span.end)).toBe("two three four");
248
+ });
249
+
250
+ test("spanFromRange snaps a mid-word mouse drag to whole words", () => {
251
+ // Arrange - a drag from inside "two" to inside "three"
252
+ const dragStart = text.indexOf("two") + 1;
253
+ const dragEnd = text.indexOf("three") + 2;
254
+
255
+ // Act
256
+ const span = spanFromRange(0, text, dragStart, dragEnd)!;
257
+
258
+ // Assert - snaps outward to cover both touched words, ready for w/b/l/h
259
+ expect(text.slice(span.start, span.end)).toBe("two three");
260
+ expect(spanKey(span, "l", text)).toMatchObject({ wordEnd: 3 });
261
+ });
262
+
263
+ test("spanFromRange returns null when the range covers no word", () => {
264
+ // Assert - a range past the last word has nothing to select
265
+ expect(spanFromRange(0, text, text.length, text.length)).toBeNull();
266
+ });
267
+ });
268
+
269
+ describe("cut / restore round-trip", () => {
270
+ test("cut removes the block lines; restore puts them back", () => {
271
+ // Arrange
272
+ const blocks = parseBlocks(BASE);
273
+ const listItem = blocks.find((candidate) => candidate.text === "second item")!;
274
+
275
+ // Act
276
+ const cut = cutBlock(BASE, listItem);
277
+
278
+ // Assert
279
+ expect(cut).not.toContain("second item");
280
+
281
+ // Arrange
282
+ const display = buildDisplay(BASE, cut);
283
+ const deletedIndex = display.findIndex((block) => block.type === "del");
284
+ const line = restoreLine(nextWorkBlock(display, deletedIndex), cut.split("\n").length);
285
+
286
+ // Act
287
+ // undefined = the working copy round-tripped back to the submitted revision
288
+ const restored = restoreBlock(BASE, cut, listItem, line);
289
+
290
+ // Assert
291
+ expect(restored).toBeUndefined();
292
+ expect(buildDisplay(BASE, restored).every((block) => block.type === "same")).toBe(true);
293
+ });
294
+ });