@cueloop/client 0.1.0-alpha.5 → 0.1.0-alpha.50

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 (152) 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 +36 -31
  8. package/src/App.autoclose.test.tsx +137 -0
  9. package/src/App.diff.test.tsx +158 -22
  10. package/src/App.export.test.tsx +44 -37
  11. package/src/App.plan-review.test.tsx +672 -0
  12. package/src/App.prototype.test.tsx +155 -0
  13. package/src/App.readonly.test.tsx +60 -36
  14. package/src/App.rendering.test.tsx +132 -0
  15. package/src/App.review-panel.test.tsx +136 -0
  16. package/src/App.submit-confirm.test.tsx +218 -0
  17. package/src/App.test.tsx +128 -69
  18. package/src/App.toast.test.tsx +75 -0
  19. package/src/App.tsx +0 -0
  20. package/src/App.walk.test.tsx +289 -0
  21. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  22. package/src/attribution.test.ts +31 -0
  23. package/src/attribution.ts +38 -0
  24. package/src/clipboard.test.ts +23 -0
  25. package/src/clipboard.ts +30 -0
  26. package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
  27. package/src/components/AnnotationCard.stories.tsx +94 -0
  28. package/src/components/AnnotationCard.tsx +241 -0
  29. package/src/components/Breadcrumb.stories.tsx +19 -0
  30. package/src/components/Breadcrumb.tsx +44 -0
  31. package/src/components/CodeBlock.stories.tsx +42 -0
  32. package/src/components/CodeBlock.tsx +83 -0
  33. package/src/components/CompletionOverlay.stories.tsx +23 -0
  34. package/src/components/CompletionOverlay.tsx +59 -0
  35. package/src/components/ConfirmCard.stories.tsx +35 -0
  36. package/src/components/ConfirmCard.tsx +127 -0
  37. package/src/components/ConfirmDialog.stories.tsx +21 -0
  38. package/src/components/ConfirmDialog.tsx +64 -0
  39. package/src/components/DiffSheet.scroll.test.tsx +123 -0
  40. package/src/components/DiffSheet.stories.tsx +78 -0
  41. package/src/components/DiffSheet.syntax.test.tsx +62 -0
  42. package/src/components/DiffSheet.tsx +355 -0
  43. package/src/components/InboxList.stories.tsx +31 -0
  44. package/src/components/InboxList.tsx +62 -0
  45. package/src/components/KeybindsDialog.stories.tsx +31 -0
  46. package/src/components/KeybindsDialog.tsx +58 -0
  47. package/src/components/MarkerPopover.stories.tsx +58 -0
  48. package/src/components/MarkerPopover.tsx +108 -0
  49. package/src/components/MenuBar.stories.tsx +37 -0
  50. package/src/components/MenuBar.tsx +74 -0
  51. package/src/components/PlanSheet.stories.tsx +113 -0
  52. package/src/components/PlanSheet.tsx +456 -0
  53. package/src/components/PromptDialog.stories.tsx +36 -0
  54. package/src/components/PromptDialog.tsx +78 -0
  55. package/src/components/PrototypeSheet.stories.tsx +20 -0
  56. package/src/components/PrototypeSheet.tsx +358 -0
  57. package/src/components/ReviewPanel.stories.tsx +86 -0
  58. package/src/components/ReviewPanel.tsx +101 -0
  59. package/src/components/ReviewRail.stories.tsx +218 -0
  60. package/src/components/ReviewRail.tsx +317 -0
  61. package/src/components/SettingsDialog.stories.tsx +111 -0
  62. package/src/components/SettingsDialog.tsx +141 -0
  63. package/src/components/SettingsRows.stories.tsx +18 -0
  64. package/src/components/SettingsRows.tsx +108 -0
  65. package/src/components/Toast.stories.tsx +12 -0
  66. package/src/components/Toast.tsx +44 -0
  67. package/src/components/WalkWizard.stories.tsx +100 -0
  68. package/src/components/WalkWizard.tsx +152 -0
  69. package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
  70. package/src/components/agent-launcher.stories.tsx +26 -0
  71. package/src/components/agent-launcher.test.tsx +49 -0
  72. package/src/components/agent-launcher.tsx +147 -0
  73. package/src/components/diff-sheet-layout.test.ts +127 -0
  74. package/src/components/diff-sheet-layout.ts +167 -0
  75. package/src/components/primitives/Button.stories.tsx +32 -0
  76. package/src/components/primitives/Button.tsx +51 -0
  77. package/src/components/primitives/Card.stories.tsx +25 -0
  78. package/src/components/primitives/Card.tsx +59 -0
  79. package/src/components/primitives/Dialog.stories.tsx +24 -0
  80. package/src/components/primitives/Dialog.tsx +64 -0
  81. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  82. package/src/components/primitives/StatusBar.tsx +22 -0
  83. package/src/components/primitives/Tabs.stories.tsx +29 -0
  84. package/src/components/primitives/Tabs.tsx +98 -0
  85. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  86. package/src/components/primitives/Toolbar.tsx +14 -0
  87. package/src/components/primitives/frame.test.ts +12 -0
  88. package/src/components/primitives/frame.ts +14 -0
  89. package/src/components/quick-actions-editor.stories.tsx +54 -0
  90. package/src/components/quick-actions-editor.tsx +136 -0
  91. package/src/components/stories-app.tsx +123 -0
  92. package/src/components/stories.test.tsx +62 -0
  93. package/src/components/story-fixtures.ts +98 -0
  94. package/src/components/story.ts +69 -0
  95. package/src/components/syntax-highlight.ts +92 -0
  96. package/src/components/terminal-pane.test.tsx +40 -0
  97. package/src/components/terminal-pane.ts +181 -0
  98. package/src/components/theme-context.stories.tsx +46 -0
  99. package/src/components/theme-context.tsx +36 -0
  100. package/src/components/truncate-text.ts +6 -0
  101. package/src/config.test.ts +430 -31
  102. package/src/config.ts +304 -26
  103. package/src/diff-hunk-curate.test.ts +315 -0
  104. package/src/diff-hunk-curate.ts +233 -0
  105. package/src/diff-intraline.test.ts +164 -0
  106. package/src/diff-intraline.ts +132 -0
  107. package/src/diff-syntax.test.ts +96 -0
  108. package/src/diff-syntax.ts +154 -0
  109. package/src/editor.test.ts +143 -0
  110. package/src/editor.ts +123 -9
  111. package/src/ghostty-terminal.test.ts +82 -0
  112. package/src/ghostty-terminal.ts +168 -0
  113. package/src/herdr.test.ts +41 -0
  114. package/src/herdr.ts +27 -0
  115. package/src/index.ts +10 -0
  116. package/src/integrations.test.ts +41 -0
  117. package/src/integrations.ts +30 -0
  118. package/src/intent-dispatch.test.ts +493 -0
  119. package/src/intent-dispatch.ts +428 -0
  120. package/src/key-bindings.test.ts +152 -0
  121. package/src/key-bindings.ts +430 -0
  122. package/src/keymap.test.ts +622 -0
  123. package/src/keymap.ts +339 -0
  124. package/src/kitty-image.test.ts +20 -0
  125. package/src/kitty-image.ts +75 -0
  126. package/src/prototype-browser.test.ts +55 -0
  127. package/src/prototype-browser.ts +243 -0
  128. package/src/pty.test.ts +78 -0
  129. package/src/pty.ts +222 -0
  130. package/src/review-panel.test.ts +113 -0
  131. package/src/review-panel.ts +83 -0
  132. package/src/run.ts +9 -5
  133. package/src/serve.test.ts +24 -9
  134. package/src/serve.ts +7 -7
  135. package/src/session-controller.curate.test.ts +253 -0
  136. package/src/session-controller.share.test.ts +309 -0
  137. package/src/session-controller.ts +914 -0
  138. package/src/share.ts +113 -0
  139. package/src/test-support.test.ts +55 -0
  140. package/src/test-support.ts +187 -0
  141. package/src/theme-presets.ts +168 -0
  142. package/src/theme.ts +38 -17
  143. package/src/version.ts +5 -0
  144. package/src/view-diff.test.ts +19 -8
  145. package/src/view-diff.ts +34 -23
  146. package/src/view-plan.regression.test.ts +60 -0
  147. package/src/view-plan.test.ts +399 -0
  148. package/src/view-plan.ts +495 -0
  149. package/src/walk.test.ts +121 -0
  150. package/src/walk.ts +70 -0
  151. package/src/view.test.ts +0 -157
  152. package/src/view.ts +0 -0
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { ConfirmDialog } from "./ConfirmDialog";
5
+
6
+ export const meta: StoryMeta = { title: "ConfirmDialog" };
7
+
8
+ const callbacks = { onConfirm: () => {}, onCancel: () => {} };
9
+
10
+ export const DeletePlan: Story = {
11
+ render: () => (
12
+ <ConfirmDialog
13
+ isOpen
14
+ title=" Delete plan "
15
+ message={'Delete "Auth rollout"? This removes the plan and its review.'}
16
+ {...callbacks}
17
+ />
18
+ ),
19
+ expectedColors: [DARK.accent],
20
+ size: { width: 64, height: 16 },
21
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A centered yes/no confirmation on the shared Dialog overlay. Presentational:
3
+ * the key grammar (⏎ confirm, esc cancel) is driven by the reducer; the buttons
4
+ * carry the same intents for the mouse. Used for destructive actions like the
5
+ * inbox delete.
6
+ */
7
+
8
+ import React from "react";
9
+ import { useTerminalDimensions } from "@opentui/react";
10
+ import type { Theme } from "../theme";
11
+ import { useComponentTheme } from "./theme-context";
12
+ import { Dialog } from "./primitives/Dialog";
13
+ import { Button } from "./primitives/Button";
14
+ import { Toolbar } from "./primitives/Toolbar";
15
+
16
+ export interface ConfirmDialogProps {
17
+ isOpen: boolean;
18
+ title?: string;
19
+ message: string;
20
+ confirmLabel?: string;
21
+ cancelLabel?: string;
22
+ onConfirm: () => void;
23
+ onCancel: () => void;
24
+ theme?: Theme;
25
+ }
26
+
27
+ export function ConfirmDialog({
28
+ isOpen,
29
+ title,
30
+ message,
31
+ confirmLabel = "Delete",
32
+ cancelLabel = "Cancel",
33
+ onConfirm,
34
+ onCancel,
35
+ theme,
36
+ }: ConfirmDialogProps): React.ReactNode {
37
+ const tokens = useComponentTheme(theme);
38
+ const { width: terminalWidth } = useTerminalDimensions();
39
+ if (!isOpen) return null;
40
+ return (
41
+ <Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
42
+ <box
43
+ style={{
44
+ flexDirection: "column",
45
+ flexGrow: 1,
46
+ paddingLeft: 1,
47
+ paddingRight: 1,
48
+ paddingTop: 1,
49
+ }}
50
+ >
51
+ <text fg={tokens.text}>{message}</text>
52
+ <box style={{ flexGrow: 1 }} />
53
+ <Toolbar>
54
+ <Button variant="solid" onPress={onConfirm} theme={theme}>
55
+ {` ${confirmLabel} `}
56
+ </Button>
57
+ <Button onPress={onCancel} theme={theme}>
58
+ {` ${cancelLabel} esc `}
59
+ </Button>
60
+ </Toolbar>
61
+ </box>
62
+ </Dialog>
63
+ );
64
+ }
@@ -0,0 +1,123 @@
1
+ /** Keyboard scroll is smooth: cursor-follow never drifts past wrapped cards. */
2
+
3
+ import { expect, test } from "bun:test";
4
+ import React, { useState } from "react";
5
+ import { testRender } from "@opentui/react/test-utils";
6
+ import { useKeyboard } from "@opentui/react";
7
+ import type { Renderable, ScrollBoxRenderable } from "@opentui/core";
8
+ import type { Annotation } from "@cueloop/schema";
9
+ import { DiffSheet } from "./DiffSheet";
10
+ import { diffRows, type DiffRow } from "../view-diff";
11
+ import { DARK } from "../theme";
12
+ import { press } from "../test-support";
13
+
14
+ /** Find a renderable by id anywhere in the tree (getRenderable is not recursive). */
15
+ function findById(node: Renderable, id: string): Renderable | undefined {
16
+ if (node.id === id) return node;
17
+ for (const child of node.getChildren()) {
18
+ const found = findById(child, id);
19
+ if (found) return found;
20
+ }
21
+ return undefined;
22
+ }
23
+
24
+ function hex(color: { toInts(): [number, number, number, number] }): string {
25
+ const [red, green, blue] = color.toInts();
26
+ return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
27
+ }
28
+
29
+ /** A single-hunk patch of `count` added lines, taller than any test viewport. */
30
+ function tallPatch(count: number): string {
31
+ const adds = Array.from(
32
+ { length: count },
33
+ (_, index) => `+line ${String(index).padStart(2, "0")}`,
34
+ );
35
+ return [
36
+ "diff --git a/f.ts b/f.ts",
37
+ "--- a/f.ts",
38
+ "+++ b/f.ts",
39
+ `@@ -0,0 +1,${count} @@`,
40
+ ...adds,
41
+ "",
42
+ ].join("\n");
43
+ }
44
+
45
+ /** DiffSheet with j-driven cursor, mirroring the App's down-navigation. */
46
+ function ScrollHarness({
47
+ rows,
48
+ annotations,
49
+ }: {
50
+ rows: DiffRow[];
51
+ annotations: Annotation[];
52
+ }): React.ReactNode {
53
+ const [cursor, setCursor] = useState(0);
54
+ useKeyboard((key) => {
55
+ if (key.name === "j") setCursor((current) => Math.min(rows.length - 1, current + 1));
56
+ });
57
+ return <DiffSheet rows={rows} cursor={cursor} annotations={annotations} />;
58
+ }
59
+
60
+ test("scrolling down past wrapped annotation cards keeps the cursor pinned and the scroll monotonic", async () => {
61
+ // Arrange - a tall diff whose annotation bodies would wrap at this width; the
62
+ // wrapped card is exactly the layout the cursor-follow model used to mis-measure
63
+ const rows = diffRows(tallPatch(40));
64
+ const longBody =
65
+ "this is a deliberately long annotation body that would wrap across the narrow test viewport";
66
+ const annotations: Annotation[] = [8, 16, 24].map((rowIndex) => ({
67
+ id: `a${rowIndex}`,
68
+ kind: "comment",
69
+ body: longBody,
70
+ anchor: { quote: rows[rowIndex]!.text, prefix: "", suffix: "" },
71
+ createdAt: "2026-01-01T00:00:00Z",
72
+ }));
73
+ const setup = await testRender(<ScrollHarness rows={rows} annotations={annotations} />, {
74
+ width: 60,
75
+ height: 12,
76
+ });
77
+ await setup.waitForVisualIdle();
78
+ const scrollbox = findById(setup.renderer.root, "diff-scroll") as ScrollBoxRenderable;
79
+
80
+ // the cursor row is the one painted with the cursor background
81
+ const cursorScreenRow = (): number => {
82
+ const lines = setup.captureSpans().lines;
83
+ for (let index = 0; index < lines.length; index++) {
84
+ if (lines[index]!.spans.some((span) => hex(span.bg) === DARK.cursorBackground)) return index;
85
+ }
86
+ return -1;
87
+ };
88
+
89
+ // Act - walk the cursor down through the whole diff, sampling the scroll each step
90
+ const scrollTops: number[] = [];
91
+ const screenRows: number[] = [];
92
+ for (let step = 0; step < rows.length; step++) {
93
+ scrollTops.push(scrollbox.scrollTop);
94
+ screenRows.push(cursorScreenRow());
95
+ await press(setup, "j");
96
+ }
97
+
98
+ // Assert - scrollTop only ever grows going down (never rebounds/jitters)
99
+ for (let step = 1; step < scrollTops.length; step++) {
100
+ expect(scrollTops[step]!).toBeGreaterThanOrEqual(scrollTops[step - 1]!);
101
+ }
102
+
103
+ // Assert - the cursor never scrolls off screen (the pre-fix bug walked it off
104
+ // the viewport as it passed each wrapped card)
105
+ expect(screenRows.every((row) => row >= 0)).toBe(true);
106
+
107
+ // Assert - through the steady scroll region (not the initial fill, not the
108
+ // final clamp at the content end) the cursor holds ONE stable row near the
109
+ // bottom; pre-fix it drifted down a row per wrapped card
110
+ const maxScroll = scrollbox.scrollHeight - scrollbox.height;
111
+ const steadyRows = screenRows.filter(
112
+ (_, step) => scrollTops[step]! > 0 && scrollTops[step]! < maxScroll,
113
+ );
114
+ expect(steadyRows.length).toBeGreaterThan(0);
115
+ expect(new Set(steadyRows).size).toBe(1);
116
+ const pinnedRow = steadyRows[0]!;
117
+ const viewportBottom = scrollbox.height;
118
+ // the cursor sits a couple of rows inside the bottom edge, per the follow rule
119
+ expect(viewportBottom - pinnedRow).toBeGreaterThanOrEqual(2);
120
+ expect(viewportBottom - pinnedRow).toBeLessThanOrEqual(3);
121
+
122
+ setup.renderer.destroy();
123
+ }, 25000);
@@ -0,0 +1,78 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { DiffSheet } from "./DiffSheet";
5
+ import { FIXTURE_PATCH, fixtureDiffRows } from "./story-fixtures";
6
+
7
+ export const meta: StoryMeta = { title: "DiffSheet" };
8
+
9
+ export const SignedRows: Story = {
10
+ render: () => <DiffSheet rows={fixtureDiffRows()} cursor={0} annotations={[]} />,
11
+ expectedColors: [DARK.insertedForeground, DARK.deletedForeground],
12
+ size: { width: 90, height: 16 },
13
+ };
14
+
15
+ // A modified line keeps the diff color on the changed words and dims the
16
+ // unchanged prefix to the dim token, so the intra-line change stands out.
17
+ export const IntralineWordDiff: Story = {
18
+ render: () => <DiffSheet rows={fixtureDiffRows()} cursor={0} annotations={[]} />,
19
+ expectedColors: [DARK.insertedForeground, DARK.deletedForeground, DARK.textDim],
20
+ size: { width: 90, height: 16 },
21
+ };
22
+
23
+ export const CursorOnAddedLine: Story = {
24
+ render: () => <DiffSheet rows={fixtureDiffRows()} cursor={4} annotations={[]} />,
25
+ expectedColors: [DARK.cursorBackground],
26
+ size: { width: 90, height: 16 },
27
+ };
28
+
29
+ export const ComposingOnLine: Story = {
30
+ render: () => (
31
+ <DiffSheet
32
+ rows={fixtureDiffRows()}
33
+ cursor={4}
34
+ annotations={[]}
35
+ compose={{
36
+ kind: "comment",
37
+ rowIndex: 4,
38
+ quote: " private items = new Map();",
39
+ draft: {
40
+ text: "Map needs an eviction story.",
41
+ onInput: () => {},
42
+ onSave: () => {},
43
+ onCancel: () => {},
44
+ },
45
+ }}
46
+ />
47
+ ),
48
+ expectedColors: [DARK.accent],
49
+ size: { width: 90, height: 16 },
50
+ };
51
+
52
+ export const AnnotatedLine: Story = {
53
+ render: () => (
54
+ <DiffSheet
55
+ rows={fixtureDiffRows()}
56
+ cursor={0}
57
+ annotations={[
58
+ {
59
+ id: "a_story_diff",
60
+ kind: "comment",
61
+ anchor: {
62
+ // diff anchors quote the row text verbatim, trailing newline included
63
+ quote:
64
+ FIXTURE_PATCH.split("\n")
65
+ .find((line) => line.startsWith("+ "))!
66
+ .slice(1) + "\n",
67
+ prefix: "",
68
+ suffix: "",
69
+ },
70
+ body: "Map needs an eviction story.",
71
+ createdAt: "2026-01-01T00:00:00Z",
72
+ },
73
+ ]}
74
+ />
75
+ ),
76
+ expectedColors: [DARK.markCommentBackground],
77
+ size: { width: 90, height: 16 },
78
+ };
@@ -0,0 +1,62 @@
1
+ import { afterEach, beforeEach, expect, test } from "bun:test";
2
+ import { testRender } from "@opentui/react/test-utils";
3
+ import type { RGBA } from "@opentui/core";
4
+ import React from "react";
5
+ import { DiffSheet } from "./DiffSheet";
6
+ import { diffRows } from "../view-diff";
7
+ import { DARK } from "../theme";
8
+ import { allowEventLoopUpdates } from "../test-support";
9
+
10
+ const PATCH = `diff --git a/src/x.ts b/src/x.ts
11
+ --- a/src/x.ts
12
+ +++ b/src/x.ts
13
+ @@ -1,3 +1,3 @@
14
+ export class Store {
15
+ - const limit = 100;
16
+ + const limit = 250;
17
+ }
18
+ `;
19
+
20
+ function hex(color: RGBA): string {
21
+ const [red, green, blue] = color.toInts();
22
+ return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
23
+ }
24
+
25
+ beforeEach(() => allowEventLoopUpdates());
26
+ afterEach(() => allowEventLoopUpdates());
27
+
28
+ test("renders tree-sitter colors, with the changed word in the diff color on top", async () => {
29
+ // Arrange
30
+ const setup = await testRender(<DiffSheet rows={diffRows(PATCH)} cursor={0} annotations={[]} />, {
31
+ width: 60,
32
+ height: 12,
33
+ });
34
+
35
+ // Act
36
+ // syntax highlighting resolves off the render path; pump until a keyword paints
37
+ let keywordAccent = false;
38
+ for (let attempt = 0; attempt < 60 && !keywordAccent; attempt++) {
39
+ await setup.renderOnce();
40
+ await new Promise((resolve) => setTimeout(resolve, 20));
41
+ for (const line of setup.captureSpans().lines) {
42
+ for (const span of line.spans) {
43
+ if (span.text.includes("export") && hex(span.fg) === DARK.accent) keywordAccent = true;
44
+ }
45
+ }
46
+ }
47
+
48
+ // Assert
49
+ expect(keywordAccent).toBe(true);
50
+ // the intra-line changed number keeps the diff color over its syntax color
51
+ const changedColors = new Set<string>();
52
+ for (const line of setup.captureSpans().lines) {
53
+ for (const span of line.spans) {
54
+ if (span.text.includes("250")) changedColors.add(hex(span.fg));
55
+ if (span.text.includes("100")) changedColors.add(hex(span.fg));
56
+ }
57
+ }
58
+ expect(changedColors.has(DARK.insertedForeground)).toBe(true);
59
+ expect(changedColors.has(DARK.deletedForeground)).toBe(true);
60
+
61
+ setup.renderer.destroy();
62
+ }, 25000);