@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.66

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 (78) hide show
  1. package/package.json +5 -5
  2. package/src/App.diff.test.tsx +8 -73
  3. package/src/App.plan-review.test.tsx +4 -185
  4. package/src/App.prototype.test.tsx +1 -8
  5. package/src/App.readonly.test.tsx +6 -28
  6. package/src/App.submit-confirm.test.tsx +19 -72
  7. package/src/App.test.tsx +70 -24
  8. package/src/App.tsx +177 -254
  9. package/src/App.walk.test.tsx +1 -1
  10. package/src/app-screens.tsx +40 -51
  11. package/src/app-view-model.ts +2 -4
  12. package/src/components/AnnotationCard.stories.tsx +1 -1
  13. package/src/components/AppShell.stories.tsx +84 -0
  14. package/src/components/AppShell.tsx +127 -0
  15. package/src/components/Breadcrumb.stories.tsx +1 -1
  16. package/src/components/ChangesColumn.stories.tsx +39 -0
  17. package/src/components/ChangesColumn.tsx +147 -0
  18. package/src/components/CodeBlock.stories.tsx +1 -1
  19. package/src/components/CompletionOverlay.stories.tsx +1 -1
  20. package/src/components/ConfirmCard.stories.tsx +1 -1
  21. package/src/components/ConfirmCard.tsx +3 -4
  22. package/src/components/ConfirmDialog.stories.tsx +1 -1
  23. package/src/components/DiffSheet.stories.tsx +1 -1
  24. package/src/components/InboxList.stories.tsx +23 -8
  25. package/src/components/InboxList.tsx +45 -19
  26. package/src/components/KeybindsDialog.stories.tsx +1 -1
  27. package/src/components/MarkerPopover.stories.tsx +1 -1
  28. package/src/components/MenuBar.stories.tsx +3 -12
  29. package/src/components/MenuBar.tsx +25 -56
  30. package/src/components/PromptDialog.stories.tsx +1 -1
  31. package/src/components/PrototypeSheet.stories.tsx +1 -1
  32. package/src/components/ReviewPanel.stories.tsx +1 -1
  33. package/src/components/ReviewRail.stories.tsx +1 -1
  34. package/src/components/SettingsDialog.stories.tsx +7 -1
  35. package/src/components/SettingsDialog.tsx +112 -66
  36. package/src/components/SettingsRows.stories.tsx +1 -1
  37. package/src/components/ShellHeader.stories.tsx +50 -0
  38. package/src/components/ShellHeader.tsx +137 -0
  39. package/src/components/ThreadFooter.stories.tsx +30 -0
  40. package/src/components/ThreadFooter.tsx +49 -0
  41. package/src/components/ThreadView.slash.test.tsx +113 -0
  42. package/src/components/ThreadView.stories.tsx +1 -1
  43. package/src/components/ThreadView.tsx +99 -11
  44. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  45. package/src/components/ThreadsSidebar.tsx +57 -0
  46. package/src/components/Toast.stories.tsx +1 -1
  47. package/src/components/TreePane.stories.tsx +1 -1
  48. package/src/components/WalkWizard.stories.tsx +1 -1
  49. package/src/components/__snapshots__/stories.test.tsx.snap +348 -138
  50. package/src/components/agent-launcher.stories.tsx +1 -1
  51. package/src/components/file-tree.test.ts +45 -0
  52. package/src/components/file-tree.ts +57 -0
  53. package/src/components/pages/Workspace.stories.tsx +181 -0
  54. package/src/components/primitives/Button.stories.tsx +1 -1
  55. package/src/components/primitives/Card.stories.tsx +1 -1
  56. package/src/components/primitives/Dialog.stories.tsx +1 -1
  57. package/src/components/primitives/IconButton.stories.tsx +36 -0
  58. package/src/components/primitives/IconButton.tsx +43 -0
  59. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  60. package/src/components/primitives/Tabs.stories.tsx +1 -1
  61. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  62. package/src/components/primitives/Tree.stories.tsx +124 -0
  63. package/src/components/primitives/Tree.tsx +87 -0
  64. package/src/components/primitives/icons.ts +38 -0
  65. package/src/components/primitives/tree-model.test.ts +99 -0
  66. package/src/components/primitives/tree-model.ts +137 -0
  67. package/src/components/quick-actions-editor.stories.tsx +1 -1
  68. package/src/components/session-tree.test.ts +114 -0
  69. package/src/components/session-tree.ts +94 -0
  70. package/src/components/stories-app.tsx +142 -82
  71. package/src/components/story-tree.test.ts +100 -0
  72. package/src/components/story.ts +55 -0
  73. package/src/components/theme-context.stories.tsx +1 -1
  74. package/src/session-controller.ts +9 -4
  75. package/src/theme.ts +4 -4
  76. package/src/use-settings-dialog.tsx +9 -0
  77. package/src/App.review-panel.test.tsx +0 -140
  78. package/src/App.tree.test.tsx +0 -224
@@ -1,224 +0,0 @@
1
- /**
2
- * The rail's Tree tab over the real App and an in-process daemon: the option
3
- * chords and the mouse reach the same primitives, the pane paints the path,
4
- * the tips, and the checkpoints, and the thread view shows a moved path in the
5
- * frame after the key.
6
- */
7
-
8
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
- import { mkdtempSync, rmSync } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- import { join } from "node:path";
12
- import React from "react";
13
- import { testRender } from "@opentui/react/test-utils";
14
- import { DaemonServer } from "@cueloop/daemon";
15
- import type { ReviewSession } from "@cueloop/schema";
16
- import { App } from "./App";
17
- import {
18
- clickText,
19
- isolateUserConfig,
20
- press,
21
- pressKey,
22
- typeText as type,
23
- waitForState,
24
- waitForText,
25
- waitForTextGone,
26
- } from "./test-support";
27
-
28
- const PLAN = `# Migration Plan
29
-
30
- ## Context
31
-
32
- The daemon persists sessions to disk atomically.
33
-
34
- ## Steps
35
-
36
- - move the store
37
- - add recovery
38
- `;
39
-
40
- let home: string;
41
- let server: DaemonServer;
42
- let session: ReviewSession;
43
- let restoreUserConfig: () => void;
44
-
45
- beforeEach(() => {
46
- home = mkdtempSync(join(tmpdir(), "cueloop-tree-"));
47
- restoreUserConfig = isolateUserConfig(home);
48
- server = new DaemonServer({ home, idleExitMs: 0 });
49
- server.start();
50
- session = server.core.sessionCreate({
51
- workspace: { repoRoot: "/repo", branch: "main" },
52
- artifact: {
53
- type: "plan",
54
- content: PLAN,
55
- meta: { title: "Migration Plan", planPath: "plan.md", agent: "agent/worker-3" },
56
- },
57
- });
58
- });
59
- afterEach(() => {
60
- restoreUserConfig();
61
- server.stop();
62
- rmSync(home, { recursive: true, force: true });
63
- });
64
-
65
- async function renderApp(role: "owner" | "collaborator" = "owner") {
66
- const setup = await testRender(<App home={home} sessionId={session.id} role={role} />, {
67
- width: 120,
68
- height: 32,
69
- });
70
-
71
- await waitForText(setup, "cueloop");
72
-
73
- return setup;
74
- }
75
-
76
- type Setup = Awaited<ReturnType<typeof renderApp>>;
77
-
78
- /** Leave a comment on the word, sent with the terminal-safe chord. */
79
- async function comment(setup: Setup, word: string, body: string): Promise<void> {
80
- await clickText(setup, word);
81
- await type(setup, body);
82
- await pressKey(setup, "RETURN", { meta: true });
83
- await waitForText(setup, body);
84
- }
85
-
86
- /** Frames painted until the needle leaves the screen, starting right after a key with no settle. */
87
- async function framesUntilGone(setup: Setup, needle: string, limit = 4): Promise<number> {
88
- for (let frame = 1; frame <= limit; frame++) {
89
- await new Promise((resolve) => setTimeout(resolve, 0));
90
- await setup.renderOnce();
91
- if (!setup.captureCharFrame().includes(needle)) return frame;
92
- }
93
- throw new Error(`"${needle}" never left the frame`);
94
- }
95
-
96
- describe("the Tree tab", () => {
97
- test("option+t shows the tree: the revision, the comments, the current tip, a checkpoint", async () => {
98
- // Arrange
99
- const setup = await renderApp();
100
-
101
- await comment(setup, "daemon", "Which daemon?");
102
-
103
- // Act
104
- await pressKey(setup, "t", { meta: true });
105
- await waitForText(setup, "● comment");
106
- await pressKey(setup, "l", { meta: true });
107
- await waitForText(setup, "Checkpoint");
108
- await type(setup, "first pass");
109
- await press(setup, "enter");
110
-
111
- // Assert: the tip row carries the branch and the new label
112
- await waitForText(setup, "⚑ first pass");
113
- expect(setup.captureCharFrame()).toContain("◉ revision 1");
114
- expect(setup.captureCharFrame()).toContain("← main");
115
- await waitForState(
116
- setup,
117
- () => Object.values(server.core.sessionGet(session.id).history!.labels)[0] === "first pass",
118
- );
119
- });
120
-
121
- test("option+b branches; option+p / option+g move back and the path repaints in one frame", async () => {
122
- // Arrange: a comment on main, a branch, a second comment on it
123
- const setup = await renderApp();
124
-
125
- await comment(setup, "daemon", "Which daemon?");
126
- await pressKey(setup, "t", { meta: true });
127
- await waitForText(setup, "● comment");
128
- await pressKey(setup, "b", { meta: true });
129
- await waitForText(setup, "Branch");
130
- await type(setup, "alt");
131
- await press(setup, "enter");
132
- await waitForText(setup, "← main, alt");
133
- await comment(setup, "recovery", "Only on alt.");
134
- await waitForText(setup, "● comment ← alt");
135
-
136
- // Act: stand on the revision (two rows up from alt's tip) and move back to it
137
- await pressKey(setup, "p", { meta: true });
138
- await pressKey(setup, "p", { meta: true });
139
- await pressKey(setup, "g", { meta: true });
140
- await waitForText(setup, "Move back");
141
- setup.mockInput.pressKey("RETURN");
142
-
143
- // Assert: both comments leave the document in the frame after the key; the record follows
144
- expect(await framesUntilGone(setup, "Only on alt.")).toBeLessThanOrEqual(1);
145
- expect(setup.captureCharFrame()).not.toContain("Which daemon?");
146
- await waitForState(setup, () => {
147
- const record = server.core.sessionGet(session.id);
148
-
149
- return (
150
- record.history!.branch === "alt" &&
151
- record.annotations.length === 0 &&
152
- record.shelvedAnnotations?.length === 2
153
- );
154
- });
155
-
156
- // Act: main's tip is the first comment - going there is a switch, no prompt
157
- await pressKey(setup, "n", { meta: true });
158
- await pressKey(setup, "g", { meta: true });
159
-
160
- // Assert
161
- await waitForText(setup, "on branch main");
162
- await waitForText(setup, "Which daemon?");
163
- expect(setup.captureCharFrame()).not.toContain("Only on alt.");
164
- await waitForState(setup, () => server.core.sessionGet(session.id).history!.branch === "main");
165
- });
166
-
167
- test("the mouse reaches the same primitives: the tab, a row, and the buttons", async () => {
168
- // Arrange
169
- const setup = await renderApp();
170
-
171
- await comment(setup, "daemon", "Which daemon?");
172
-
173
- // Act: open the tab, select the revision row, open the label prompt from its button
174
- await clickText(setup, "Tree");
175
- await waitForText(setup, "◉ revision 1");
176
- await clickText(setup, "◉ revision 1");
177
- await clickText(setup, "Label");
178
-
179
- // Assert
180
- await waitForText(setup, "Checkpoint");
181
- await press(setup, "escape");
182
- await waitForTextGone(setup, "Checkpoint");
183
-
184
- // Act: a second click on the selected row goes there - a move back asks for a summary
185
- await clickText(setup, "◉ revision 1");
186
- await waitForText(setup, "Move back");
187
- });
188
-
189
- test("a collaborator reads the tree and cannot move it", async () => {
190
- // Arrange
191
- const setup = await renderApp("collaborator");
192
-
193
- // Act
194
- await pressKey(setup, "t", { meta: true });
195
- await waitForText(setup, "● revision 1");
196
- await pressKey(setup, "b", { meta: true });
197
-
198
- // Assert: no prompt, the read-only answer, no buttons
199
- await waitForText(setup, "observer - read-only");
200
- expect(setup.captureCharFrame()).not.toContain("Fork+share");
201
-
202
- // Act: two clicks on the revision row select it and nothing more
203
- await clickText(setup, "● revision 1");
204
- await clickText(setup, "● revision 1");
205
-
206
- // Assert: no prompt opened, the record stands
207
- expect(setup.captureCharFrame()).not.toContain("Move back");
208
- expect(server.core.sessionGet(session.id).history!.entries).toHaveLength(1);
209
- });
210
-
211
- test("a resolved review keeps the fork buttons and drops the moving ones", async () => {
212
- // Arrange
213
- server.core.sessionResolve(session.id, "approve", "ship it");
214
- const setup = await renderApp();
215
-
216
- // Act
217
- await pressKey(setup, "t", { meta: true });
218
- await waitForText(setup, "Fork+share");
219
-
220
- // Assert
221
- expect(setup.captureCharFrame()).not.toContain("Branch");
222
- expect(setup.captureCharFrame()).not.toContain("Label");
223
- });
224
- });