@cueloop/client 0.1.0-alpha.75 → 0.1.0-alpha.77

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 (82) hide show
  1. package/package.json +5 -5
  2. package/src/App.focus.test.ts +147 -0
  3. package/src/App.identity.test.tsx +78 -0
  4. package/src/App.regression.test.tsx +94 -0
  5. package/src/App.share-choice.test.tsx +105 -0
  6. package/src/App.test.tsx +11 -8
  7. package/src/App.toast.test.tsx +3 -1
  8. package/src/App.tsx +648 -221
  9. package/src/App.visual.test.tsx +57 -0
  10. package/src/App.workbench.test.tsx +41 -22
  11. package/src/app-screens.tsx +181 -123
  12. package/src/app-view-model.ts +1 -0
  13. package/src/components/AnnotatableFileView.tsx +9 -1
  14. package/src/components/AnnotationCards.tsx +22 -4
  15. package/src/components/AppShell.brand.test.tsx +1 -1
  16. package/src/components/AppShell.tsx +14 -1
  17. package/src/components/ChangesColumn.tsx +40 -12
  18. package/src/components/ChangesFileTree.test.tsx +61 -0
  19. package/src/components/ConfirmDialog.tsx +8 -1
  20. package/src/components/DiffContentView.tsx +12 -2
  21. package/src/components/DiscussionMarkerRail.tsx +2 -2
  22. package/src/components/EditorGrid.tsx +84 -26
  23. package/src/components/GridTabContent.tsx +12 -1
  24. package/src/components/ManageAccessDialog.stories.tsx +36 -0
  25. package/src/components/ManageAccessDialog.test.tsx +94 -0
  26. package/src/components/ManageAccessDialog.tsx +120 -0
  27. package/src/components/PanelColumn.tsx +3 -0
  28. package/src/components/ProjectTreeView.test.tsx +39 -12
  29. package/src/components/ProjectTreeView.tsx +38 -10
  30. package/src/components/PromptDialog.test.tsx +61 -0
  31. package/src/components/PromptDialog.tsx +16 -3
  32. package/src/components/RootOverlay.stories.tsx +1 -0
  33. package/src/components/RootOverlay.tsx +19 -6
  34. package/src/components/SettingsDialog.stories.tsx +3 -3
  35. package/src/components/SettingsDialog.tsx +3 -4
  36. package/src/components/ShareChoiceDialog.stories.tsx +34 -0
  37. package/src/components/ShareChoiceDialog.test.tsx +103 -0
  38. package/src/components/ShareChoiceDialog.tsx +82 -0
  39. package/src/components/ThreadFooter.test.tsx +12 -7
  40. package/src/components/ThreadFooter.tsx +18 -4
  41. package/src/components/ThreadTree.tsx +79 -36
  42. package/src/components/ThreadView.slash.test.tsx +36 -0
  43. package/src/components/ThreadView.tsx +12 -2
  44. package/src/components/WelcomePlayground.tsx +2 -0
  45. package/src/components/__snapshots__/stories.test.tsx.snap +147 -52
  46. package/src/components/menu-control.stories.tsx +35 -0
  47. package/src/components/menu-control.tsx +43 -0
  48. package/src/components/primitives/Dialog.test.tsx +37 -0
  49. package/src/components/primitives/Dialog.tsx +5 -0
  50. package/src/components/primitives/icons.ts +1 -1
  51. package/src/components/quick-actions-editor.stories.tsx +2 -0
  52. package/src/components/quick-actions-editor.test.tsx +35 -1
  53. package/src/components/quick-actions-editor.tsx +23 -22
  54. package/src/components/session-tree.test.ts +3 -3
  55. package/src/components/session-tree.ts +1 -1
  56. package/src/config-identity.test.ts +78 -0
  57. package/src/config.ts +45 -1
  58. package/src/diff-intraline.ts +26 -5
  59. package/src/github-identity.test.ts +58 -0
  60. package/src/github-identity.ts +41 -0
  61. package/src/intent-dispatch.test.ts +17 -2
  62. package/src/intent-dispatch.ts +7 -1
  63. package/src/keymap.ts +1 -0
  64. package/src/perf/otlp-trace.test.ts +116 -0
  65. package/src/perf/otlp-trace.ts +130 -0
  66. package/src/perf/perf-report.ts +17 -0
  67. package/src/perf/perf-timings.test.ts +18 -0
  68. package/src/perf/perf-timings.ts +38 -0
  69. package/src/run.ts +22 -2
  70. package/src/share.ts +10 -2
  71. package/src/thread-chords.test.ts +33 -1
  72. package/src/thread-chords.ts +114 -29
  73. package/src/thread-controller.curate.test.ts +1 -0
  74. package/src/thread-controller.live-diff.test.ts +1 -0
  75. package/src/thread-controller.share.test.ts +36 -2
  76. package/src/thread-controller.tree.test.ts +1 -0
  77. package/src/thread-controller.ts +18 -3
  78. package/src/use-annotation-surface.tsx +32 -2
  79. package/src/use-frame-measure.ts +4 -1
  80. package/src/use-settings-dialog.tsx +33 -7
  81. package/src/visual/contrast.test.ts +35 -0
  82. package/src/visual/contrast.ts +33 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.75",
3
+ "version": "0.1.0-alpha.77",
4
4
  "description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -27,10 +27,10 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@cueloop/daemon": "0.1.0-alpha.75",
31
- "@cueloop/extension-api": "0.1.0-alpha.75",
32
- "@cueloop/integration-obsidian": "0.1.0-alpha.75",
33
- "@cueloop/schema": "0.1.0-alpha.75",
30
+ "@cueloop/daemon": "0.1.0-alpha.77",
31
+ "@cueloop/extension-api": "0.1.0-alpha.77",
32
+ "@cueloop/integration-obsidian": "0.1.0-alpha.77",
33
+ "@cueloop/schema": "0.1.0-alpha.77",
34
34
  "@opentui/core": "^0.5.8",
35
35
  "@opentui/keymap": "0.5.8",
36
36
  "@opentui/react": "^0.5.8",
@@ -0,0 +1,147 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ appLeaderHandled,
4
+ nextFocusPane,
5
+ reconciledFocus,
6
+ threadsNavHandled,
7
+ visiblePanes,
8
+ } from "./App";
9
+
10
+ describe("threads keyboard nav", () => {
11
+ const base = () => {
12
+ let cursor = 1;
13
+ let opened = 0;
14
+
15
+ return {
16
+ setInboxCursor: (update: (cursor: number) => number) => (cursor = update(cursor)),
17
+ openSession: () => (opened += 1),
18
+ read: () => ({
19
+ get cursor() {
20
+ return cursor;
21
+ },
22
+ get opened() {
23
+ return opened;
24
+ },
25
+ }),
26
+ };
27
+ };
28
+
29
+ test("moves the cursor and opens only when the Threads pane is quiet and focused", () => {
30
+ const spy = base();
31
+ const call = (name: string) =>
32
+ threadsNavHandled({
33
+ focusedPane: "threads",
34
+ quiet: true,
35
+ key: { name },
36
+ count: 3,
37
+ setInboxCursor: spy.setInboxCursor,
38
+ openSession: spy.openSession,
39
+ });
40
+
41
+ expect(call("down")).toBe(true);
42
+ expect(spy.read().cursor).toBe(2);
43
+ expect(call("down")).toBe(true); // clamps at the last row
44
+ expect(spy.read().cursor).toBe(2);
45
+ expect(call("k")).toBe(true);
46
+ expect(spy.read().cursor).toBe(1);
47
+ expect(call("return")).toBe(true);
48
+ expect(spy.read().opened).toBe(1);
49
+ });
50
+
51
+ test("defers when another pane is focused, keys are owned elsewhere, or the inbox is empty", () => {
52
+ const spy = base();
53
+ const params = {
54
+ quiet: true,
55
+ key: { name: "down" },
56
+ count: 3,
57
+ setInboxCursor: spy.setInboxCursor,
58
+ openSession: spy.openSession,
59
+ };
60
+
61
+ expect(threadsNavHandled({ ...params, focusedPane: "changes" })).toBe(false);
62
+ expect(threadsNavHandled({ ...params, focusedPane: "threads", quiet: false })).toBe(false);
63
+ expect(threadsNavHandled({ ...params, focusedPane: "threads", count: 0 })).toBe(false);
64
+ expect(spy.read().cursor).toBe(1);
65
+ });
66
+ });
67
+
68
+ describe("pane focus cycle", () => {
69
+ test("visiblePanes lists only the open panes, in reading order", () => {
70
+ expect(visiblePanes(true, true, true, true)).toEqual([
71
+ "threads",
72
+ "thread",
73
+ "changes",
74
+ "project",
75
+ ]);
76
+ expect(visiblePanes(true, false, true, true)).toEqual(["threads", "changes", "project"]);
77
+ expect(visiblePanes(false, false, true, false)).toEqual(["changes"]);
78
+ });
79
+
80
+ test("nextFocusPane wraps forward and back over the visible panes", () => {
81
+ const panes = visiblePanes(true, false, true, true);
82
+
83
+ expect(nextFocusPane("threads", panes, false)).toBe("changes");
84
+ expect(nextFocusPane("project", panes, false)).toBe("threads");
85
+ expect(nextFocusPane("threads", panes, true)).toBe("project");
86
+ expect(nextFocusPane("changes", [], false)).toBe("changes");
87
+ });
88
+
89
+ test("reconciledFocus falls back only when the focused pane is not navigable", () => {
90
+ expect(reconciledFocus(["threads", "changes"], "threads")).toBeNull();
91
+ expect(reconciledFocus(["threads", "changes"], "thread")).toBe("threads");
92
+ expect(reconciledFocus(["changes", "project"], "thread")).toBe("changes");
93
+ expect(reconciledFocus([], "thread")).toBeNull();
94
+ });
95
+
96
+ test("appLeaderHandled captures the leader for the sidebar panes, then the next key", () => {
97
+ const leaderCombos = ["ctrl+g"];
98
+ const pending = { current: false };
99
+ const ran: string[] = [];
100
+ const call = (
101
+ focusedPane: "threads" | "project" | "thread" | "changes",
102
+ key: { name: string; ctrl?: boolean },
103
+ ) =>
104
+ appLeaderHandled({
105
+ focusedPane,
106
+ key,
107
+ leaderCombos,
108
+ pending,
109
+ runLeaderCommand: (pressed) => ran.push(pressed.name),
110
+ });
111
+
112
+ expect(call("threads", { name: "g", ctrl: true })).toBe(true);
113
+ expect(pending.current).toBe(true);
114
+ expect(call("threads", { name: "tab" })).toBe(true);
115
+ expect(pending.current).toBe(false);
116
+ expect(ran).toEqual(["tab"]);
117
+ });
118
+
119
+ test("appLeaderHandled defers on the content panes and swallows escape without a command", () => {
120
+ const leaderCombos = ["ctrl+g"];
121
+ let ran = false;
122
+ const runLeaderCommand = () => (ran = true);
123
+
124
+ expect(
125
+ appLeaderHandled({
126
+ focusedPane: "thread",
127
+ key: { name: "ctrl+g" },
128
+ leaderCombos,
129
+ pending: { current: false },
130
+ runLeaderCommand,
131
+ }),
132
+ ).toBe(false);
133
+
134
+ const pending = { current: true };
135
+ expect(
136
+ appLeaderHandled({
137
+ focusedPane: "project",
138
+ key: { name: "escape" },
139
+ leaderCombos,
140
+ pending,
141
+ runLeaderCommand,
142
+ }),
143
+ ).toBe(true);
144
+ expect(pending.current).toBe(false);
145
+ expect(ran).toBe(false);
146
+ });
147
+ });
@@ -0,0 +1,78 @@
1
+ /** Reviewer identity surfaces: the Account settings tab and the author-name tooltip on a comment dot. */
2
+
3
+ import { afterEach, beforeEach, expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { DaemonServer } from "@cueloop/daemon";
9
+ import { App } from "./App";
10
+ import { isolateUserConfig, locateText, renderReadyApp, waitForText } from "./test-support";
11
+
12
+ const PLAN = "# Migration Plan\n\nThe daemon persists sessions to disk atomically.\n";
13
+
14
+ let home: string;
15
+ let restoreUserConfig: () => void;
16
+ let server: DaemonServer;
17
+ let sessionId: string;
18
+
19
+ beforeEach(() => {
20
+ home = mkdtempSync(join(tmpdir(), "cueloop-identity-"));
21
+ restoreUserConfig = isolateUserConfig(home);
22
+ server = new DaemonServer({ home, idleExitMs: 0 });
23
+ server.start();
24
+ const session = server.core.sessionCreate({
25
+ workspace: { repoRoot: "/repo", branch: "main" },
26
+ artifact: {
27
+ type: "plan",
28
+ content: PLAN,
29
+ meta: { title: "Migration Plan", planPath: "plan.md" },
30
+ },
31
+ });
32
+ sessionId = session.id;
33
+ });
34
+ afterEach(() => {
35
+ restoreUserConfig();
36
+ server.stop();
37
+ rmSync(home, { recursive: true, force: true });
38
+ });
39
+
40
+ async function renderApp() {
41
+ return renderReadyApp(<App home={home} sessionId={sessionId} />, { width: 120, height: 32 });
42
+ }
43
+
44
+ test("the Account settings tab shows the display name row and the GitHub sync action", async () => {
45
+ const setup = await renderApp();
46
+
47
+ await setup.mockMouse.click(1, 0);
48
+ await waitForText(setup, "Settings");
49
+ const menu = setup.captureCharFrame().split("\n");
50
+ const settingsRow = menu.findIndex((line) => line.includes("Settings"));
51
+
52
+ await setup.mockMouse.click(menu[settingsRow]!.indexOf("Settings") + 1, settingsRow);
53
+ await waitForText(setup, "Account");
54
+ const dialog = setup.captureCharFrame().split("\n");
55
+ const accountRow = dialog.findIndex((line) => line.includes("Account"));
56
+
57
+ await setup.mockMouse.click(dialog[accountRow]!.indexOf("Account") + 1, accountRow);
58
+ await waitForText(setup, "Display name");
59
+ expect(setup.captureCharFrame()).toContain("Sync from GitHub");
60
+ });
61
+
62
+ test("hovering an own comment's dot surfaces the author tooltip", async () => {
63
+ server.core.sessionAnnotate(sessionId, {
64
+ id: "a_own",
65
+ kind: "comment",
66
+ anchor: { quote: "atomically", prefix: "sessions to disk ", suffix: "." },
67
+ body: "My note.",
68
+ });
69
+ const setup = await renderApp();
70
+
71
+ await waitForText(setup, "● My note.");
72
+ // the tooltip is not painted until the pointer is over the dot
73
+ expect(setup.captureCharFrame()).not.toContain("you");
74
+ const dot = locateText(setup, "● My note.");
75
+
76
+ await setup.mockMouse.moveTo(dot.column, dot.row);
77
+ await waitForText(setup, "you");
78
+ });
@@ -0,0 +1,94 @@
1
+ /** Regressions from the focus/perf round: ctrl+q must quit from any keyboard state, and clicking a
2
+ * sidebar thread must give it the selected accent at once (the cursor follows the click). */
3
+
4
+ import { afterEach, beforeEach, expect, test } from "bun:test";
5
+ import { mkdtempSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import React from "react";
9
+ import { DaemonServer } from "@cueloop/daemon";
10
+ import { App } from "./App";
11
+ import {
12
+ clickText,
13
+ isolateUserConfig,
14
+ pressKey,
15
+ renderReadyApp,
16
+ waitForText,
17
+ } from "./test-support";
18
+
19
+ const ACCENT_DARK: [number, number, number] = [203, 166, 247];
20
+
21
+ let home: string;
22
+ let restoreUserConfig: () => void;
23
+ let server: DaemonServer;
24
+
25
+ function plan(title: string): void {
26
+ server.core.sessionCreate({
27
+ workspace: { repoRoot: "/repo", branch: "main" },
28
+ artifact: { type: "plan", content: `# ${title}\n`, meta: { title, planPath: "plan.md" } },
29
+ });
30
+ }
31
+
32
+ beforeEach(() => {
33
+ home = mkdtempSync(join(tmpdir(), "cueloop-regression-"));
34
+ restoreUserConfig = isolateUserConfig(home);
35
+ server = new DaemonServer({ home, idleExitMs: 0 });
36
+ server.start();
37
+ plan("First review");
38
+ plan("Second review");
39
+ });
40
+ afterEach(() => {
41
+ restoreUserConfig();
42
+ server.stop();
43
+ rmSync(home, { recursive: true, force: true });
44
+ });
45
+
46
+ /** The text of the sidebar title span currently painted in the accent colour, or undefined. */
47
+ function accentTitle(setup: Awaited<ReturnType<typeof renderReadyApp>>): string | undefined {
48
+ for (const line of setup.captureSpans().lines) {
49
+ for (const span of line.spans) {
50
+ const rgb = span.fg?.toInts();
51
+ if (
52
+ rgb &&
53
+ rgb[0] === ACCENT_DARK[0] &&
54
+ rgb[1] === ACCENT_DARK[1] &&
55
+ rgb[2] === ACCENT_DARK[2]
56
+ ) {
57
+ const text = span.text.trim();
58
+ if (text.includes("review")) return text;
59
+ }
60
+ }
61
+ }
62
+
63
+ return undefined;
64
+ }
65
+
66
+ test("ctrl+q quits even when a menu holds the keyboard", async () => {
67
+ let exitCode: number | undefined;
68
+ const setup = await renderReadyApp(<App home={home} onExit={(code) => (exitCode = code)} />, {
69
+ width: 120,
70
+ height: 32,
71
+ });
72
+ await waitForText(setup, "review");
73
+
74
+ // open the settings dialog from the top-left gear - it takes the keyboard
75
+ await setup.mockMouse.click(1, 0);
76
+ await waitForText(setup, "Keybinds");
77
+
78
+ await pressKey(setup, "q", { ctrl: true });
79
+
80
+ expect(exitCode).toBe(0);
81
+ setup.renderer.destroy();
82
+ });
83
+
84
+ test("clicking a sidebar thread gives it the selected accent at once", async () => {
85
+ const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
86
+ await waitForText(setup, "Second review");
87
+
88
+ // click whichever thread is not already the selected (accent) one
89
+ const target = accentTitle(setup)?.includes("Second") ? "First review" : "Second review";
90
+ await clickText(setup, target);
91
+
92
+ expect(accentTitle(setup)).toContain(target.split(" ")[0]!);
93
+ setup.renderer.destroy();
94
+ });
@@ -0,0 +1,105 @@
1
+ /** The share shortcut opens an app-level public/private choice reachable from any view. */
2
+
3
+ import { afterEach, beforeEach, describe, mock, test } from "bun:test";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import { DaemonServer } from "@cueloop/daemon";
10
+ import type { Thread } from "@cueloop/schema";
11
+ import { App } from "./App";
12
+ import type { ShareTransport } from "./thread-controller";
13
+ import { isolateUserConfig, press, pressKey, waitForText, waitForTextGone } from "./test-support";
14
+
15
+ const shareTransport: ShareTransport = {
16
+ publish: mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true })),
17
+ pull: mock(async () => {
18
+ throw new Error("Unexpected share pull");
19
+ }),
20
+ push: mock(async () => {}),
21
+ watch: () => () => {},
22
+ parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
23
+ collaboratorAnnotations: () => [],
24
+ mergeFromShare: () => ({ annotations: [] }),
25
+ };
26
+
27
+ const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
28
+
29
+ let home: string;
30
+ let restoreUserConfig: () => void;
31
+ let server: DaemonServer;
32
+ let session: Thread;
33
+
34
+ beforeEach(() => {
35
+ home = mkdtempSync(join(tmpdir(), "cueloop-share-choice-"));
36
+ restoreUserConfig = isolateUserConfig(home);
37
+ server = new DaemonServer({ home, idleExitMs: 0 });
38
+ server.start();
39
+ session = server.core.sessionCreate({
40
+ workspace: { repoRoot: "/repo", branch: "main" },
41
+ artifact: {
42
+ type: "plan",
43
+ content: PLAN,
44
+ meta: { title: "Migration Plan", planPath: "plan.md" },
45
+ },
46
+ });
47
+ });
48
+ afterEach(() => {
49
+ restoreUserConfig();
50
+ server.stop();
51
+ rmSync(home, { recursive: true, force: true });
52
+ });
53
+
54
+ describe("share choice", () => {
55
+ test("the shortcut opens the public/private choice", async () => {
56
+ // Arrange
57
+ const setup = await testRender(
58
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
59
+ { width: 120, height: 32 },
60
+ );
61
+ await waitForText(setup, "cueloop");
62
+
63
+ // Act
64
+ await pressKey(setup, "s", { ctrl: true });
65
+
66
+ // Assert
67
+ await waitForText(setup, "public link");
68
+ await waitForText(setup, "private link");
69
+ });
70
+
71
+ test("the private choice opens the manage-access surface", async () => {
72
+ // Arrange
73
+ const setup = await testRender(
74
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
75
+ { width: 120, height: 32 },
76
+ );
77
+ await waitForText(setup, "cueloop");
78
+
79
+ // Act: open the choice, move to private, select it
80
+ await pressKey(setup, "s", { ctrl: true });
81
+ await waitForText(setup, "private link");
82
+ await press(setup, "down");
83
+ await press(setup, "enter");
84
+
85
+ // Assert
86
+ await waitForText(setup, "Manage access");
87
+ });
88
+
89
+ test("escape cancels the choice without sharing", async () => {
90
+ // Arrange
91
+ const setup = await testRender(
92
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
93
+ { width: 120, height: 32 },
94
+ );
95
+ await waitForText(setup, "cueloop");
96
+
97
+ // Act
98
+ await pressKey(setup, "s", { ctrl: true });
99
+ await waitForText(setup, "public link");
100
+ await press(setup, "escape");
101
+
102
+ // Assert
103
+ await waitForTextGone(setup, "public link");
104
+ });
105
+ });
package/src/App.test.tsx CHANGED
@@ -285,24 +285,27 @@ describe("no-thread shell", () => {
285
285
  expect(frame).toContain("Other Plan");
286
286
  });
287
287
 
288
- test("the row kebab menu pins a thread into a Pinned section", async () => {
288
+ test("the row kebab menu stars a thread into a Starred section", async () => {
289
289
  // Arrange
290
290
  const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
291
291
 
292
292
  await waitForText(setup, "Migration Plan");
293
293
 
294
- // Act - hover the row to reveal its kebab, open the menu, and pick Pin
294
+ // Act - hover the row to reveal its kebab, open the menu, and pick Star
295
295
  const row = locateText(setup, "Migration Plan");
296
296
 
297
297
  await setup.mockMouse.moveTo(row.column, row.row);
298
298
  await waitForText(setup, "⋮");
299
299
  await clickText(setup, "⋮");
300
- await waitForText(setup, "Pin");
301
- await clickText(setup, "Pin");
302
-
303
- // Assert - a Pinned section now holds the thread
304
- await waitForText(setup, "Pinned");
305
- expect(frameRow(setup, "Migration Plan")).toBeGreaterThan(frameRow(setup, "Pinned"));
300
+ // "star" alone collides with "Getting started" in the welcome pane, so target the menu via its
301
+ // unique "rename" row - the star action sits directly above it
302
+ await waitForText(setup, "rename");
303
+ const rename = locateText(setup, "rename");
304
+ await setup.mockMouse.click(rename.column, rename.row - 1);
305
+
306
+ // Assert - a Starred section now holds the thread
307
+ await waitForText(setup, "Starred");
308
+ expect(frameRow(setup, "Migration Plan")).toBeGreaterThan(frameRow(setup, "Starred"));
306
309
  });
307
310
 
308
311
  test("a bare launch shows the Welcome surface with the right region collapsed", async () => {
@@ -73,8 +73,10 @@ describe("share toast", () => {
73
73
 
74
74
  await waitForText(setup, "cueloop");
75
75
 
76
- // Act: share raises the toast, then open a composer under it by typing
76
+ // Act: share opens the choice, enter picks public and raises the toast, then a composer under it
77
77
  await pressKey(setup, "s", { ctrl: true });
78
+ await waitForText(setup, "public link");
79
+ await press(setup, "enter");
78
80
  await waitForText(setup, "share link copied");
79
81
  await clickText(setup, "daemon");
80
82
  await typeText(setup, "x");