@cueloop/client 0.1.0-alpha.3 → 0.1.0-alpha.31

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 (105) hide show
  1. package/package.json +17 -11
  2. package/src/App.autoclose.test.tsx +133 -0
  3. package/src/App.diff.test.tsx +27 -20
  4. package/src/App.export.test.tsx +19 -28
  5. package/src/App.plan-review.test.tsx +507 -0
  6. package/src/App.readonly.test.tsx +26 -27
  7. package/src/App.rendering.test.tsx +119 -0
  8. package/src/App.review-panel.test.tsx +123 -0
  9. package/src/App.submit-confirm.test.tsx +216 -0
  10. package/src/App.test.tsx +83 -64
  11. package/src/App.tsx +0 -0
  12. package/src/App.walk.test.tsx +265 -0
  13. package/src/attribution.test.ts +31 -0
  14. package/src/attribution.ts +32 -0
  15. package/src/clipboard.test.ts +23 -0
  16. package/src/clipboard.ts +26 -0
  17. package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
  18. package/src/components/AnnotationCard.stories.tsx +75 -0
  19. package/src/components/AnnotationCard.tsx +208 -0
  20. package/src/components/Breadcrumb.stories.tsx +19 -0
  21. package/src/components/Breadcrumb.tsx +38 -0
  22. package/src/components/CodeBlock.stories.tsx +25 -0
  23. package/src/components/CodeBlock.tsx +68 -0
  24. package/src/components/CompletionOverlay.stories.tsx +23 -0
  25. package/src/components/CompletionOverlay.tsx +49 -0
  26. package/src/components/ComposeBar.stories.tsx +18 -0
  27. package/src/components/ComposeBar.tsx +54 -0
  28. package/src/components/ConfirmCard.stories.tsx +37 -0
  29. package/src/components/ConfirmCard.tsx +117 -0
  30. package/src/components/ConfirmDialog.stories.tsx +21 -0
  31. package/src/components/ConfirmDialog.tsx +56 -0
  32. package/src/components/DiffSheet.stories.tsx +44 -0
  33. package/src/components/DiffSheet.tsx +243 -0
  34. package/src/components/InboxList.stories.tsx +18 -0
  35. package/src/components/InboxList.tsx +55 -0
  36. package/src/components/PlanSheet.stories.tsx +81 -0
  37. package/src/components/PlanSheet.tsx +329 -0
  38. package/src/components/PromptDialog.stories.tsx +36 -0
  39. package/src/components/PromptDialog.tsx +62 -0
  40. package/src/components/ReviewPanel.stories.tsx +87 -0
  41. package/src/components/ReviewPanel.tsx +116 -0
  42. package/src/components/ReviewRail.stories.tsx +107 -0
  43. package/src/components/ReviewRail.tsx +180 -0
  44. package/src/components/SettingsDialog.stories.tsx +63 -0
  45. package/src/components/SettingsDialog.tsx +132 -0
  46. package/src/components/SettingsRows.stories.tsx +18 -0
  47. package/src/components/SettingsRows.tsx +92 -0
  48. package/src/components/WalkWizard.stories.tsx +89 -0
  49. package/src/components/WalkWizard.tsx +146 -0
  50. package/src/components/__snapshots__/stories.test.tsx.snap +1519 -0
  51. package/src/components/primitives/Button.stories.tsx +32 -0
  52. package/src/components/primitives/Button.tsx +44 -0
  53. package/src/components/primitives/Card.stories.tsx +25 -0
  54. package/src/components/primitives/Card.tsx +59 -0
  55. package/src/components/primitives/Dialog.stories.tsx +24 -0
  56. package/src/components/primitives/Dialog.tsx +54 -0
  57. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  58. package/src/components/primitives/StatusBar.tsx +22 -0
  59. package/src/components/primitives/Tabs.stories.tsx +29 -0
  60. package/src/components/primitives/Tabs.tsx +97 -0
  61. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  62. package/src/components/primitives/Toolbar.tsx +14 -0
  63. package/src/components/primitives/frame.test.ts +12 -0
  64. package/src/components/primitives/frame.ts +14 -0
  65. package/src/components/stories-app.tsx +95 -0
  66. package/src/components/stories.test.tsx +60 -0
  67. package/src/components/story-fixtures.ts +90 -0
  68. package/src/components/story.ts +66 -0
  69. package/src/components/syntax-highlight.ts +57 -0
  70. package/src/components/theme-context.stories.tsx +41 -0
  71. package/src/components/theme-context.tsx +30 -0
  72. package/src/components/truncate-text.ts +6 -0
  73. package/src/config.test.ts +196 -30
  74. package/src/config.ts +128 -16
  75. package/src/editor.test.ts +134 -0
  76. package/src/editor.ts +101 -13
  77. package/src/herdr.test.ts +41 -0
  78. package/src/herdr.ts +21 -0
  79. package/src/index.ts +2 -0
  80. package/src/integrations.test.ts +41 -0
  81. package/src/integrations.ts +28 -0
  82. package/src/intent-dispatch.test.ts +308 -0
  83. package/src/intent-dispatch.ts +295 -0
  84. package/src/key-bindings.test.ts +142 -0
  85. package/src/key-bindings.ts +363 -0
  86. package/src/keymap.test.ts +477 -0
  87. package/src/keymap.ts +272 -0
  88. package/src/review-panel.test.ts +111 -0
  89. package/src/review-panel.ts +76 -0
  90. package/src/run.ts +9 -5
  91. package/src/serve.test.ts +13 -6
  92. package/src/serve.ts +7 -7
  93. package/src/session-controller.share.test.ts +248 -0
  94. package/src/session-controller.ts +607 -0
  95. package/src/share.ts +80 -0
  96. package/src/test-support.ts +87 -0
  97. package/src/theme.ts +26 -3
  98. package/src/view-diff.test.ts +19 -8
  99. package/src/view-diff.ts +22 -22
  100. package/src/view-plan.test.ts +260 -0
  101. package/src/view-plan.ts +365 -0
  102. package/src/walk.test.ts +116 -0
  103. package/src/walk.ts +65 -0
  104. package/src/view.test.ts +0 -157
  105. package/src/view.ts +0 -0
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.31",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@cueloop/daemon": "0.1.0-alpha.3",
11
- "@cueloop/extension-api": "0.1.0-alpha.3",
12
- "@cueloop/integration-obsidian": "0.1.0-alpha.3",
13
- "@cueloop/schema": "0.1.0-alpha.3",
14
- "@opentui/core": "^0.4.3",
15
- "@opentui/react": "^0.4.3",
10
+ "@cueloop/daemon": "0.1.0-alpha.31",
11
+ "@cueloop/extension-api": "0.1.0-alpha.31",
12
+ "@cueloop/integration-obsidian": "0.1.0-alpha.31",
13
+ "@cueloop/schema": "0.1.0-alpha.31",
14
+ "@opentui/core": "^0.5.2",
15
+ "@opentui/keymap": "0.5.2",
16
+ "@opentui/react": "^0.5.2",
17
+ "@opentui/ssh": "0.5.2",
16
18
  "@pierre/diffs": "^1.3.5",
17
- "react": "^19.2.0",
18
- "@opentui/ssh": "0.4.5"
19
+ "react": "^19.2.0"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@types/react": "^19.2.0",
@@ -36,5 +37,10 @@
36
37
  "type": "git",
37
38
  "url": "git+https://github.com/mmurakaru/cueloop.git",
38
39
  "directory": "packages/client"
39
- }
40
- }
40
+ },
41
+ "homepage": "https://github.com/mmurakaru/cueloop#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/mmurakaru/cueloop/issues"
44
+ },
45
+ "description": "The cueloop review UI: projection renderer, keyboard grammar, review rail"
46
+ }
@@ -0,0 +1,133 @@
1
+ /** Post-submit completion overlay: prompt, opt-in persistence, countdown, dismissal. Countdown seconds tick on an injected ManualClock, so tests advance time instead of waiting it out. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } 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 { ManualClock } from "@opentui/core/testing";
10
+ import { DaemonServer } from "@cueloop/daemon";
11
+ import type { ReviewSession } from "@cueloop/schema";
12
+ import { App } from "./App";
13
+ import { press, settle, waitForText } from "./test-support";
14
+
15
+ const PLAN = "# Plan\n\nShip the thing.\n";
16
+
17
+ let home: string;
18
+ let server: DaemonServer;
19
+ let session: ReviewSession;
20
+ let configPath: string;
21
+ let clock: ManualClock;
22
+
23
+ beforeEach(() => {
24
+ home = mkdtempSync(join(tmpdir(), "cueloop-close-"));
25
+ configPath = join(home, "config.toml");
26
+ process.env.CUELOOP_CONFIG = configPath;
27
+ server = new DaemonServer({ home, idleExitMs: 0 });
28
+ server.start();
29
+ session = server.core.sessionCreate({
30
+ workspace: { repoRoot: "/repo", branch: "main" },
31
+ artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
32
+ });
33
+ clock = new ManualClock();
34
+ });
35
+ afterEach(() => {
36
+ delete process.env.CUELOOP_CONFIG;
37
+ server.stop();
38
+ rmSync(home, { recursive: true, force: true });
39
+ });
40
+
41
+ async function renderApp(onExit?: (code: number) => void) {
42
+ const setup = await testRender(<App home={home} sessionId={session.id} onExit={onExit} clock={clock} />, {
43
+ width: 120,
44
+ height: 30,
45
+ });
46
+ await waitForText(setup, "cueloop");
47
+ return setup;
48
+ }
49
+
50
+ type Setup = Awaited<ReturnType<typeof renderApp>>;
51
+
52
+ async function submitApprove(setup: Setup): Promise<void> {
53
+ await press(setup, "enter"); // open submit (approve default: no pending items)
54
+ await press(setup, "enter"); // confirm
55
+ await waitForText(setup, "review approved");
56
+ }
57
+
58
+ describe("completion overlay", () => {
59
+ test("submit with auto-close off counts down from 5; ⏎ exits now", async () => {
60
+ // Arrange
61
+ let exited = -1;
62
+ const setup = await renderApp((code) => (exited = code));
63
+
64
+ // Act
65
+ await submitApprove(setup);
66
+
67
+ // Assert
68
+ const frame = await waitForText(setup, "closing in 5s");
69
+ expect(frame).toContain("review approved");
70
+ expect(frame).toContain("close [return]");
71
+ expect(frame).toContain("return to plan [esc]");
72
+
73
+ // Act
74
+ await press(setup, "enter");
75
+
76
+ // Assert
77
+ expect(exited).toBe(0);
78
+ });
79
+
80
+ test("a remembers the countdown as the persisted default", async () => {
81
+ // Arrange
82
+ const setup = await renderApp();
83
+ await submitApprove(setup);
84
+ await waitForText(setup, "closing in 5s");
85
+
86
+ // Act
87
+ await press(setup, "a");
88
+
89
+ // Assert
90
+ expect(existsSync(configPath)).toBe(true);
91
+ expect(readFileSync(configPath, "utf8")).toContain("auto_close = 5");
92
+
93
+ // Act
94
+ // the countdown keeps running; each second is one manual-clock tick
95
+ clock.advance(1000);
96
+
97
+ // Assert
98
+ await waitForText(setup, "closing in 4s");
99
+ });
100
+
101
+ test("esc dismisses to the resolved read-only view", async () => {
102
+ // Arrange
103
+ const setup = await renderApp();
104
+ await submitApprove(setup);
105
+ await waitForText(setup, "closing in 5s");
106
+
107
+ // Act
108
+ // a bare ESC sits in the input parser's escape-sequence disambiguation
109
+ // window before it is delivered - the frame wait absorbs it
110
+ await press(setup, "escape");
111
+
112
+ // Assert
113
+ const frame = await waitForText(setup, "resolved: approve");
114
+ expect(frame).not.toContain("closing in");
115
+ });
116
+
117
+ test("configured auto_close counts down and exits without interaction", async () => {
118
+ // Arrange
119
+ writeFileSync(configPath, "[ui]\nauto_close = 1\n");
120
+ let exited = -1;
121
+ const setup = await renderApp((code) => (exited = code));
122
+ await submitApprove(setup);
123
+ await waitForText(setup, "closing in 1s");
124
+ expect(exited).toBe(-1);
125
+
126
+ // Act
127
+ clock.advance(1000);
128
+ await settle(setup);
129
+
130
+ // Assert
131
+ expect(exited).toBe(0);
132
+ });
133
+ });
@@ -9,6 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
+ import { press, waitForText } from "./test-support";
12
13
 
13
14
  const PATCH = `diff --git a/src/store.ts b/src/store.ts
14
15
  index 111..222 100644
@@ -41,25 +42,16 @@ afterEach(() => {
41
42
 
42
43
  async function renderApp() {
43
44
  const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 30 });
44
- for (let i = 0; i < 40 && !setup.captureCharFrame().includes("cueloop"); i++) {
45
- await Bun.sleep(25);
46
- await setup.renderOnce();
47
- }
48
- await setup.renderOnce();
45
+ await waitForText(setup, "cueloop");
49
46
  return setup;
50
47
  }
51
48
 
52
- type Setup = Awaited<ReturnType<typeof renderApp>>;
53
- async function press(setup: Setup, k: string): Promise<void> {
54
- if (k === "enter") setup.mockInput.pressKey("RETURN");
55
- else await setup.mockInput.typeText(k);
56
- await Bun.sleep(15);
57
- await setup.renderOnce();
58
- }
59
-
60
49
  describe("diff review", () => {
61
50
  test("renders file header, hunks, and signed lines", async () => {
51
+ // Arrange
62
52
  const setup = await renderApp();
53
+
54
+ // Assert
63
55
  const frame = setup.captureCharFrame();
64
56
  expect(frame).toContain("■ src/store.ts");
65
57
  expect(frame).toContain("@@ -1,4 +1,4 @@");
@@ -68,32 +60,47 @@ describe("diff review", () => {
68
60
  });
69
61
 
70
62
  test("line comment: quote-anchored, lands in feedback with the code line", async () => {
63
+ // Arrange
71
64
  const setup = await renderApp();
65
+
66
+ // Act
72
67
  // rows: file(0), hunk(1), ctx(2), del(3), add(4)
73
68
  for (let i = 0; i < 4; i++) await press(setup, "j");
74
69
  await press(setup, "c");
75
- expect(setup.captureCharFrame()).toContain("COMMENT ON");
70
+
71
+ // Assert
72
+ await waitForText(setup, "COMMENT ON");
73
+
74
+ // Act
76
75
  await setup.mockInput.typeText("Map needs an eviction story.");
77
76
  await press(setup, "enter");
78
- await Bun.sleep(80);
79
- await setup.renderOnce();
77
+
78
+ // Assert
79
+ // inline annotation card rendered under the line
80
+ await waitForText(setup, "◆ Map needs an eviction story.");
80
81
  const stored = server.core.sessionGet(session.id);
81
82
  expect(stored.annotations.length).toBe(1);
82
83
  expect(stored.annotations[0]!.anchor.quote).toContain("new Map()");
83
- // inline annotation card rendered under the line
84
- expect(setup.captureCharFrame()).toContain("◆ Map needs an eviction story.");
85
84
 
85
+ // Act
86
86
  await press(setup, "enter"); // submit
87
87
  await press(setup, "enter"); // confirm request_changes
88
- await Bun.sleep(100);
88
+
89
+ // Assert
90
+ await waitForText(setup, "feedback sent");
89
91
  const resolved = server.core.sessionGet(session.id);
90
92
  expect(resolved.verdict!.feedback).toContain("new Map()");
91
93
  expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
92
94
  });
93
95
 
94
96
  test("plan verbs are guarded in diff sessions", async () => {
97
+ // Arrange
95
98
  const setup = await renderApp();
99
+
100
+ // Act
96
101
  await press(setup, "x");
97
- expect(setup.captureCharFrame()).toContain("plan-only verb");
102
+
103
+ // Assert
104
+ await waitForText(setup, "plan-only verb");
98
105
  });
99
106
  });
@@ -1,8 +1,4 @@
1
- /**
2
- * Tier-2 wiring test for the notes-vault export: with [integrations.obsidian]
3
- * configured to export on resolve, submitting a review writes the note into
4
- * the vault and surfaces the path in the status line.
5
- */
1
+ /** Tier-2 wiring for the notes-vault export: with [integrations.obsidian] set to export on resolve, submitting a review writes the note into the vault and surfaces the path in the status line. */
6
2
 
7
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8
4
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -13,6 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
13
9
  import { DaemonServer } from "@cueloop/daemon";
14
10
  import type { ReviewSession } from "@cueloop/schema";
15
11
  import { App } from "./App";
12
+ import { press, waitForState, waitForText } from "./test-support";
16
13
 
17
14
  const PLAN = `# Migration Plan
18
15
 
@@ -49,31 +46,25 @@ afterEach(() => {
49
46
 
50
47
  describe("obsidian export on resolve", () => {
51
48
  test("submitting a review writes the plan into the vault and shows the path", async () => {
49
+ // Arrange
52
50
  const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 32 });
53
- for (let i = 0; i < 40 && !setup.captureCharFrame().includes("cueloop"); i++) {
54
- await Bun.sleep(25);
55
- await setup.renderOnce();
56
- }
57
- // open the submit overlay, keep the default verdict (approve), submit
58
- setup.mockInput.pressKey("RETURN");
59
- await Bun.sleep(15);
60
- await setup.renderOnce();
61
- expect(setup.captureCharFrame()).toContain("verdict");
62
- setup.mockInput.pressKey("RETURN");
63
- // wall-clock deadlines: the export is an async round-trip after resolve,
64
- // and an iteration count times out on a loaded runner
51
+ await waitForText(setup, "cueloop");
52
+
53
+ // Act
54
+ await press(setup, "enter"); // open the submit overlay, keep the default verdict (approve)
55
+
56
+ // Assert
57
+ await waitForText(setup, "verdict");
58
+
59
+ // Act
60
+ await press(setup, "enter"); // submit
61
+
62
+ // Assert
63
+ // the export is an async round-trip after resolve; the status line lands
64
+ // on a later render than the file write
65
65
  const dir = join(vault, "cueloop");
66
- const fileDeadline = Date.now() + 30_000;
67
- while (Date.now() < fileDeadline && !existsSync(dir)) {
68
- await Bun.sleep(25);
69
- await setup.renderOnce();
70
- }
71
- // the status line lands on a later render than the file write
72
- const statusDeadline = Date.now() + 30_000;
73
- while (Date.now() < statusDeadline && !setup.captureCharFrame().includes("exported to")) {
74
- await Bun.sleep(25);
75
- await setup.renderOnce();
76
- }
66
+ await waitForState(setup, () => existsSync(dir));
67
+ await waitForText(setup, "exported to");
77
68
 
78
69
  const notes = readdirSync(dir);
79
70
  expect(notes.length).toBe(1);