@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71

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 (248) 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 +39 -32
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +76 -50
  10. package/src/App.diff.test.tsx +156 -42
  11. package/src/App.export.test.tsx +58 -40
  12. package/src/App.plan-review.test.tsx +363 -0
  13. package/src/App.prototype-markdown.test.tsx +92 -0
  14. package/src/App.prototype.test.tsx +176 -0
  15. package/src/App.readonly.test.tsx +128 -67
  16. package/src/App.rendering.test.tsx +61 -35
  17. package/src/App.submit-confirm.test.tsx +191 -0
  18. package/src/App.test.tsx +310 -114
  19. package/src/App.toast.test.tsx +89 -0
  20. package/src/App.tsx +1043 -798
  21. package/src/App.walk.test.tsx +298 -0
  22. package/src/App.workbench.test.tsx +495 -0
  23. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  24. package/src/annotation-palette.ts +61 -0
  25. package/src/app-key-state.ts +61 -0
  26. package/src/app-screens.tsx +432 -0
  27. package/src/app-view-model.ts +159 -0
  28. package/src/attribution.test.ts +31 -0
  29. package/src/attribution.ts +42 -0
  30. package/src/clipboard.test.ts +23 -0
  31. package/src/clipboard.ts +33 -0
  32. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  33. package/src/components/AnnotatableFileView.test.tsx +102 -0
  34. package/src/components/AnnotatableFileView.tsx +126 -0
  35. package/src/components/AnnotationCards.stories.tsx +87 -0
  36. package/src/components/AnnotationCards.tsx +303 -0
  37. package/src/components/AppHeader.stories.tsx +50 -0
  38. package/src/components/AppHeader.tsx +83 -0
  39. package/src/components/AppShell.stories.tsx +89 -0
  40. package/src/components/AppShell.tsx +290 -0
  41. package/src/components/AppShell.zoom.test.tsx +135 -0
  42. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  43. package/src/components/BareWorkbenchFileView.tsx +64 -0
  44. package/src/components/Breadcrumb.stories.tsx +19 -0
  45. package/src/components/Breadcrumb.tsx +45 -0
  46. package/src/components/ChangesColumn.stories.tsx +39 -0
  47. package/src/components/ChangesColumn.tsx +171 -0
  48. package/src/components/CodeBlock.stories.tsx +42 -0
  49. package/src/components/CodeBlock.tsx +84 -0
  50. package/src/components/CompletionOverlay.stories.tsx +23 -0
  51. package/src/components/CompletionOverlay.tsx +60 -0
  52. package/src/components/ConfirmCard.stories.tsx +35 -0
  53. package/src/components/ConfirmCard.tsx +128 -0
  54. package/src/components/ConfirmDialog.stories.tsx +21 -0
  55. package/src/components/ConfirmDialog.tsx +66 -0
  56. package/src/components/DiffContentView.scroll.test.tsx +121 -0
  57. package/src/components/DiffContentView.split.test.tsx +140 -0
  58. package/src/components/DiffContentView.stories.tsx +80 -0
  59. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  60. package/src/components/DiffContentView.tsx +859 -0
  61. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  62. package/src/components/DiscussionMarkerRail.tsx +181 -0
  63. package/src/components/EditorGrid.stories.tsx +59 -0
  64. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  65. package/src/components/EditorGrid.tsx +368 -0
  66. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  67. package/src/components/FileContentsView.stories.tsx +16 -0
  68. package/src/components/FileContentsView.test.tsx +29 -0
  69. package/src/components/FileContentsView.tsx +98 -0
  70. package/src/components/KeybindsDialog.stories.tsx +31 -0
  71. package/src/components/KeybindsDialog.tsx +103 -0
  72. package/src/components/MenuBar.stories.tsx +28 -0
  73. package/src/components/MenuBar.tsx +44 -0
  74. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  75. package/src/components/OverlayScrollbar.test.tsx +139 -0
  76. package/src/components/OverlayScrollbar.tsx +129 -0
  77. package/src/components/PaneGrid.stories.tsx +84 -0
  78. package/src/components/PaneGrid.tsx +128 -0
  79. package/src/components/PanelColumn.stories.tsx +25 -0
  80. package/src/components/PanelColumn.tsx +95 -0
  81. package/src/components/ProjectTreeView.stories.tsx +25 -0
  82. package/src/components/ProjectTreeView.test.tsx +32 -0
  83. package/src/components/ProjectTreeView.tsx +83 -0
  84. package/src/components/PromptDialog.stories.tsx +36 -0
  85. package/src/components/PromptDialog.tsx +87 -0
  86. package/src/components/PrototypeContentView.stories.tsx +19 -0
  87. package/src/components/PrototypeContentView.tsx +446 -0
  88. package/src/components/RootOverlay.stories.tsx +44 -0
  89. package/src/components/RootOverlay.tsx +35 -0
  90. package/src/components/ScrollArea.stories.tsx +20 -0
  91. package/src/components/ScrollArea.tsx +44 -0
  92. package/src/components/SettingsDialog.stories.tsx +117 -0
  93. package/src/components/SettingsDialog.tsx +195 -0
  94. package/src/components/SettingsRows.stories.tsx +18 -0
  95. package/src/components/SettingsRows.tsx +112 -0
  96. package/src/components/ShellHeader.stories.tsx +50 -0
  97. package/src/components/ShellHeader.tsx +137 -0
  98. package/src/components/ThreadFooter.stories.tsx +30 -0
  99. package/src/components/ThreadFooter.test.tsx +31 -0
  100. package/src/components/ThreadFooter.tsx +54 -0
  101. package/src/components/ThreadTree.stories.tsx +46 -0
  102. package/src/components/ThreadTree.tsx +219 -0
  103. package/src/components/ThreadView.selection.test.tsx +812 -0
  104. package/src/components/ThreadView.slash.test.tsx +158 -0
  105. package/src/components/ThreadView.stories.tsx +26 -0
  106. package/src/components/ThreadView.tsx +330 -0
  107. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  108. package/src/components/ThreadsSidebar.tsx +71 -0
  109. package/src/components/Toast.stories.tsx +12 -0
  110. package/src/components/Toast.tsx +45 -0
  111. package/src/components/Tooltip.stories.tsx +26 -0
  112. package/src/components/Tooltip.tsx +72 -0
  113. package/src/components/TreePane.stories.tsx +106 -0
  114. package/src/components/TreePane.tsx +157 -0
  115. package/src/components/WalkWizard.stories.tsx +100 -0
  116. package/src/components/WalkWizard.tsx +152 -0
  117. package/src/components/WelcomePlayground.stories.tsx +12 -0
  118. package/src/components/WelcomePlayground.test.tsx +47 -0
  119. package/src/components/WelcomePlayground.tsx +140 -0
  120. package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
  121. package/src/components/agent-launcher.stories.tsx +26 -0
  122. package/src/components/agent-launcher.test.tsx +50 -0
  123. package/src/components/agent-launcher.tsx +149 -0
  124. package/src/components/diff-content-view-layout.test.ts +198 -0
  125. package/src/components/diff-content-view-layout.ts +84 -0
  126. package/src/components/editor-grid.test.ts +86 -0
  127. package/src/components/editor-grid.ts +138 -0
  128. package/src/components/fade-title.test.ts +49 -0
  129. package/src/components/fade-title.ts +61 -0
  130. package/src/components/file-tree.test.ts +60 -0
  131. package/src/components/file-tree.ts +112 -0
  132. package/src/components/pages/Workspace.stories.tsx +181 -0
  133. package/src/components/primitives/Button.stories.tsx +32 -0
  134. package/src/components/primitives/Button.tsx +52 -0
  135. package/src/components/primitives/Card.stories.tsx +25 -0
  136. package/src/components/primitives/Card.tsx +60 -0
  137. package/src/components/primitives/Dialog.stories.tsx +24 -0
  138. package/src/components/primitives/Dialog.tsx +66 -0
  139. package/src/components/primitives/IconButton.stories.tsx +36 -0
  140. package/src/components/primitives/IconButton.tsx +62 -0
  141. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  142. package/src/components/primitives/StatusBar.tsx +23 -0
  143. package/src/components/primitives/Tabs.stories.tsx +29 -0
  144. package/src/components/primitives/Tabs.tsx +102 -0
  145. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  146. package/src/components/primitives/Toolbar.tsx +14 -0
  147. package/src/components/primitives/Tree.stories.tsx +124 -0
  148. package/src/components/primitives/Tree.tsx +96 -0
  149. package/src/components/primitives/frame.test.ts +12 -0
  150. package/src/components/primitives/frame.ts +14 -0
  151. package/src/components/primitives/icons.ts +61 -0
  152. package/src/components/primitives/tree-model.test.ts +99 -0
  153. package/src/components/primitives/tree-model.ts +137 -0
  154. package/src/components/quick-actions-editor.stories.tsx +54 -0
  155. package/src/components/quick-actions-editor.test.tsx +49 -0
  156. package/src/components/quick-actions-editor.tsx +150 -0
  157. package/src/components/session-tree.test.ts +134 -0
  158. package/src/components/session-tree.ts +108 -0
  159. package/src/components/stories-app.tsx +184 -0
  160. package/src/components/stories.test.tsx +66 -0
  161. package/src/components/story-fixtures.ts +102 -0
  162. package/src/components/story-tree.test.ts +100 -0
  163. package/src/components/story.ts +142 -0
  164. package/src/components/syntax-highlight.ts +110 -0
  165. package/src/components/terminal-pane.test.tsx +40 -0
  166. package/src/components/terminal-pane.ts +199 -0
  167. package/src/components/theme-context.stories.tsx +46 -0
  168. package/src/components/theme-context.tsx +37 -0
  169. package/src/components/truncate-text.ts +7 -0
  170. package/src/config.test.ts +461 -31
  171. package/src/config.ts +389 -57
  172. package/src/diff-fold.test.ts +78 -0
  173. package/src/diff-fold.ts +126 -0
  174. package/src/diff-hunk-curate.test.ts +315 -0
  175. package/src/diff-hunk-curate.ts +153 -0
  176. package/src/diff-intraline.test.ts +165 -0
  177. package/src/diff-intraline.ts +146 -0
  178. package/src/diff-syntax.test.ts +112 -0
  179. package/src/diff-syntax.ts +179 -0
  180. package/src/discussions.ts +79 -0
  181. package/src/editor.test.ts +149 -0
  182. package/src/editor.ts +139 -9
  183. package/src/ghostty-terminal.test.ts +85 -0
  184. package/src/ghostty-terminal.ts +199 -0
  185. package/src/herdr.test.ts +9 -34
  186. package/src/herdr.ts +25 -21
  187. package/src/index.ts +11 -0
  188. package/src/integrations.test.ts +41 -0
  189. package/src/integrations.ts +32 -0
  190. package/src/intent-dispatch.test.ts +698 -0
  191. package/src/intent-dispatch.ts +700 -0
  192. package/src/key-bindings.test.ts +156 -0
  193. package/src/key-bindings.ts +465 -0
  194. package/src/keymap.test.ts +611 -0
  195. package/src/keymap.ts +410 -0
  196. package/src/kitty-image.test.ts +66 -0
  197. package/src/kitty-image.ts +138 -0
  198. package/src/mark-runs.ts +120 -0
  199. package/src/prototype-browser.test.ts +56 -0
  200. package/src/prototype-browser.ts +307 -0
  201. package/src/prototype-pixels.tsx +35 -0
  202. package/src/pty.test.ts +81 -0
  203. package/src/pty.ts +245 -0
  204. package/src/ready-signal.test.ts +85 -0
  205. package/src/ready-signal.ts +52 -0
  206. package/src/run.ts +30 -9
  207. package/src/serve.test.ts +24 -12
  208. package/src/serve.ts +20 -9
  209. package/src/share.ts +215 -0
  210. package/src/skills.test.ts +74 -0
  211. package/src/skills.ts +68 -0
  212. package/src/slash-palette.test.ts +54 -0
  213. package/src/slash-palette.ts +104 -0
  214. package/src/split-diff.test.ts +110 -0
  215. package/src/split-diff.ts +149 -0
  216. package/src/test-support.test.ts +264 -0
  217. package/src/test-support.ts +355 -0
  218. package/src/theme-presets.test.ts +51 -0
  219. package/src/theme-presets.ts +214 -0
  220. package/src/theme.ts +84 -19
  221. package/src/thread-chords.test.ts +170 -0
  222. package/src/thread-chords.ts +169 -0
  223. package/src/thread-controller.curate.test.ts +438 -0
  224. package/src/thread-controller.live-diff.test.ts +199 -0
  225. package/src/thread-controller.share.test.ts +472 -0
  226. package/src/thread-controller.tree.test.ts +255 -0
  227. package/src/thread-controller.ts +1509 -0
  228. package/src/thread-selection.test.ts +210 -0
  229. package/src/thread-selection.ts +243 -0
  230. package/src/tree-view.test.ts +128 -0
  231. package/src/tree-view.ts +148 -0
  232. package/src/use-annotation-surface.tsx +851 -0
  233. package/src/use-changes-workbench.ts +153 -0
  234. package/src/use-frame-measure.ts +38 -0
  235. package/src/use-settings-dialog.tsx +274 -0
  236. package/src/use-terminal-virtualizer.ts +233 -0
  237. package/src/version.ts +5 -0
  238. package/src/view-diff.test.ts +66 -9
  239. package/src/view-diff.ts +231 -23
  240. package/src/view-plan.regression.test.ts +60 -0
  241. package/src/view-plan.test.ts +406 -0
  242. package/src/view-plan.ts +570 -0
  243. package/src/walk.test.ts +121 -0
  244. package/src/walk.ts +74 -0
  245. package/src/syntax.test.ts +0 -41
  246. package/src/syntax.ts +0 -88
  247. package/src/view.test.ts +0 -157
  248. package/src/view.ts +0 -312
@@ -0,0 +1,255 @@
1
+ /**
2
+ * The controller's tree primitives over a fake client: a move shows its path
3
+ * before the daemon answers, refusals surface as status, a fork opens the fork,
4
+ * and fork-and-share shares the fork while the current session stays put.
5
+ */
6
+
7
+ import { describe, expect, mock, test } from "bun:test";
8
+ import {
9
+ appendEntry,
10
+ createBranch,
11
+ historyFromLinear,
12
+ SCHEMA_VERSION,
13
+ switchBranch,
14
+ type Annotation,
15
+ type Thread,
16
+ } from "@cueloop/schema";
17
+ import type { SessionClient } from "@cueloop/daemon/client";
18
+ import { createReviewController, type ShareTransport } from "./thread-controller";
19
+ import { mergeFromShare } from "./share";
20
+
21
+ const AT = "2026-01-01T00:00:00.000Z";
22
+
23
+ function annotation(id: string, body: string): Annotation {
24
+ return {
25
+ id,
26
+ kind: "comment",
27
+ anchor: { quote: "Plan", prefix: "", suffix: "" },
28
+ body,
29
+ createdAt: AT,
30
+ };
31
+ }
32
+
33
+ /** A plan with one comment on main and its history. */
34
+ function sessionFixture(): Thread {
35
+ const session: Thread = {
36
+ schemaVersion: SCHEMA_VERSION,
37
+ id: "ses_1",
38
+ workspace: { repoRoot: "/repo", branch: "main" },
39
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
40
+ revisions: [{ revision: 1, content: "# Plan\n", submittedAt: AT }],
41
+ annotations: [annotation("a1", "first")],
42
+ verdict: null,
43
+ status: "pending",
44
+ createdAt: AT,
45
+ };
46
+
47
+ session.history = historyFromLinear(session);
48
+
49
+ return session;
50
+ }
51
+
52
+ const publish = mock(async (session: Thread) => ({
53
+ line: `ssh p_${session.id}@cueloop.dev`,
54
+ copied: true,
55
+ }));
56
+ const shareTransport: ShareTransport = {
57
+ publish,
58
+ pull: mock(async () => sessionFixture()),
59
+ push: mock(async () => {}),
60
+ watch: () => () => {},
61
+ parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
62
+ collaboratorAnnotations: (session) => session.annotations.filter((entry) => entry.author),
63
+ mergeFromShare,
64
+ };
65
+
66
+ const unimplemented = (member: string) => () =>
67
+ Promise.reject(new Error(`fakeClient does not implement ${member}`));
68
+
69
+ /** Tree requests never resolve here: what the controller shows is its own guess. */
70
+ const pending = () => new Promise<Thread>(() => {});
71
+
72
+ function fakeClient(session: Thread) {
73
+ return {
74
+ onEvent: () => () => {},
75
+ subscribe: async () => {},
76
+ sessionGet: async () => session,
77
+ sessionList: async () => [session],
78
+ sessionComment: unimplemented("sessionComment"),
79
+ sessionAnnotate: unimplemented("sessionAnnotate"),
80
+ sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
81
+ sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
82
+ sessionCutBlock: unimplemented("sessionCutBlock"),
83
+ sessionRestoreBlock: unimplemented("sessionRestoreBlock"),
84
+ sessionCurate: unimplemented("sessionCurate"),
85
+ sessionNavigate: mock(pending),
86
+ sessionBranch: mock(pending),
87
+ sessionSwitch: mock(pending),
88
+ sessionLabel: mock(pending),
89
+ sessionFork: mock(async (id: string) => ({
90
+ ...sessionFixture(),
91
+ id: `${id}_fork`,
92
+ parentSessionId: id,
93
+ })),
94
+ sessionSetViewed: unimplemented("sessionSetViewed"),
95
+ sessionSetTitle: unimplemented("sessionSetTitle"),
96
+ sessionSetShareId: mock(async (_id: string, _shareId: string) => session),
97
+ sessionMergeShared: unimplemented("sessionMergeShared"),
98
+ sessionDelete: unimplemented("sessionDelete"),
99
+ sessionSetSelfName: unimplemented("sessionSetSelfName"),
100
+ sessionResolve: unimplemented("sessionResolve"),
101
+ close: () => {},
102
+ } satisfies SessionClient;
103
+ }
104
+
105
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
106
+
107
+ async function connected(session = sessionFixture()) {
108
+ const client = fakeClient(session);
109
+ const controller = createReviewController({
110
+ sessionId: session.id,
111
+ openClient: async () => client,
112
+ shareTransport,
113
+ });
114
+
115
+ controller.connect();
116
+ await tick();
117
+
118
+ return { controller, client, session };
119
+ }
120
+
121
+ describe("read-only guards", () => {
122
+ test("an observer's rename and delete never reach the daemon", async () => {
123
+ // Arrange - a read-only controller over a client whose mutations would throw
124
+ const session = sessionFixture();
125
+ const client = fakeClient(session);
126
+ const controller = createReviewController({
127
+ sessionId: session.id,
128
+ openClient: async () => client,
129
+ readOnly: true,
130
+ shareTransport,
131
+ });
132
+
133
+ controller.connect();
134
+ await tick();
135
+
136
+ // Act - the sidebar's rename and delete, invoked directly
137
+ controller.renameSession(session.id, "hijacked");
138
+ controller.deleteSession(session.id);
139
+ await tick();
140
+
141
+ // Assert - both answer read-only and the throwing client stubs were never called
142
+ expect(controller.getSnapshot().status).toBe("observer - read-only");
143
+ });
144
+ });
145
+
146
+ describe("tree primitives", () => {
147
+ test("a move back shows the earlier path at once and asks the daemon for the same move", async () => {
148
+ // Arrange
149
+ const { controller, client, session } = await connected();
150
+ const revision = session.history!.entries[0]!.id;
151
+
152
+ // Act
153
+ controller.goToEntry(revision, "too soon");
154
+
155
+ // Assert: the comment is off the shown path before any answer; the summary travels
156
+ expect(controller.getSnapshot().session!.annotations).toEqual([]);
157
+ expect(controller.getSnapshot().session!.shelvedAnnotations?.map((entry) => entry.id)).toEqual([
158
+ "a1",
159
+ ]);
160
+ expect(controller.treeRows().find((row) => row.glyph === "↩")?.isCurrentTip).toBe(true);
161
+ expect(client.sessionNavigate).toHaveBeenCalledWith("ses_1", revision, "too soon", undefined);
162
+ });
163
+
164
+ test("a move on another branch is one request that names the branch", async () => {
165
+ // Arrange: alt with its own comment entry; the owner stands on main
166
+ const session = sessionFixture();
167
+ let history = createBranch(session.history!, "alt");
168
+
169
+ history = appendEntry(history, { type: "comment", annotationId: "b1", createdAt: AT }).history;
170
+ history = appendEntry(history, { type: "comment", annotationId: "b2", createdAt: AT }).history;
171
+ session.history = switchBranch(history, "main");
172
+ session.shelvedAnnotations = [annotation("b1", "alt one"), annotation("b2", "alt two")];
173
+ const { controller, client } = await connected(session);
174
+ const altFirst = history.entries.find(
175
+ (entry) => entry.type === "comment" && entry.annotationId === "b1",
176
+ )!;
177
+
178
+ // Act
179
+ controller.goToEntry(altFirst.id);
180
+
181
+ // Assert
182
+ expect(client.sessionSwitch).not.toHaveBeenCalled();
183
+ expect(client.sessionNavigate).toHaveBeenCalledWith("ses_1", altFirst.id, undefined, "alt");
184
+ expect(controller.getSnapshot().session!.annotations.map((entry) => entry.id)).toEqual([
185
+ "a1",
186
+ "b1",
187
+ ]);
188
+ });
189
+
190
+ test("a refused move gives way to the daemon's record", async () => {
191
+ // Arrange: the daemon refuses; a re-read hands back the untouched record
192
+ const session = sessionFixture();
193
+ const { controller, client } = await connected(session);
194
+
195
+ client.sessionNavigate.mockImplementationOnce(() => Promise.reject(new Error("resolved")));
196
+
197
+ // Act
198
+ controller.goToEntry(session.history!.entries[0]!.id);
199
+ const guessed = controller.getSnapshot().session!.annotations.length;
200
+
201
+ await tick();
202
+
203
+ // Assert: the guess showed, then the record came back
204
+ expect(guessed).toBe(0);
205
+ expect(controller.getSnapshot().session!.annotations.map((entry) => entry.id)).toEqual(["a1"]);
206
+ expect(controller.getSnapshot().status).toBe("resolved");
207
+ });
208
+
209
+ test("branch and label refuse an empty name and a taken branch name", async () => {
210
+ // Arrange
211
+ const { controller, client } = await connected();
212
+
213
+ // Act
214
+ controller.branch(" ");
215
+ controller.branch("main");
216
+ controller.labelTip("");
217
+
218
+ // Assert
219
+ expect(client.sessionBranch).not.toHaveBeenCalled();
220
+ expect(client.sessionLabel).not.toHaveBeenCalled();
221
+ expect(controller.getSnapshot().status).toBe("a checkpoint needs a name");
222
+ });
223
+
224
+ test("a branch shows up on the tip and becomes the current branch", async () => {
225
+ // Arrange
226
+ const { controller, client } = await connected();
227
+
228
+ // Act
229
+ controller.branch("alt");
230
+
231
+ // Assert
232
+ expect(controller.getSnapshot().session!.history!.branch).toBe("alt");
233
+ expect(controller.treeRows().find((row) => row.isCurrentTip)?.tips).toEqual(["main", "alt"]);
234
+ expect(client.sessionBranch).toHaveBeenCalledWith("ses_1", "alt");
235
+ });
236
+
237
+ test("fork opens the fork; fork-and-share shares the fork and stays on the original", async () => {
238
+ // Arrange
239
+ const forked = await connected();
240
+ const shared = await connected();
241
+
242
+ // Act
243
+ forked.controller.fork();
244
+ shared.controller.forkAndShare();
245
+ await tick();
246
+ await tick();
247
+
248
+ // Assert
249
+ expect(forked.controller.getSnapshot().session!.id).toBe("ses_1_fork");
250
+ expect(shared.controller.getSnapshot().session!.id).toBe("ses_1");
251
+ expect(publish).toHaveBeenCalledWith(expect.objectContaining({ id: "ses_1_fork" }));
252
+ expect(shared.client.sessionSetShareId).toHaveBeenCalledWith("ses_1_fork", "p_ses_1_fork");
253
+ expect(shared.controller.getSnapshot().toast?.title).toBe("fork shared - link copied");
254
+ });
255
+ });