@cueloop/client 0.1.0-alpha.68 → 0.1.0-alpha.69

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 +7 -6
  2. package/src/App.appearance.test.tsx +8 -6
  3. package/src/App.diff.test.tsx +3 -3
  4. package/src/App.export.test.tsx +9 -3
  5. package/src/App.plan-review.test.tsx +10 -8
  6. package/src/App.prototype-markdown.test.tsx +92 -0
  7. package/src/App.prototype.test.tsx +17 -28
  8. package/src/App.readonly.test.tsx +3 -3
  9. package/src/App.rendering.test.tsx +3 -4
  10. package/src/App.test.tsx +14 -18
  11. package/src/App.tsx +117 -42
  12. package/src/App.walk.test.tsx +2 -2
  13. package/src/App.workbench.test.tsx +85 -14
  14. package/src/app-screens.tsx +26 -10
  15. package/src/app-view-model.ts +17 -3
  16. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  17. package/src/components/AnnotatableFileView.test.tsx +102 -0
  18. package/src/components/AnnotatableFileView.tsx +126 -0
  19. package/src/components/AnnotationCards.tsx +6 -6
  20. package/src/components/AppShell.tsx +62 -15
  21. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  22. package/src/components/BareWorkbenchFileView.tsx +69 -0
  23. package/src/components/ChangesColumn.tsx +3 -2
  24. package/src/components/{DiffSheet.scroll.test.tsx → DiffContentView.scroll.test.tsx} +2 -2
  25. package/src/components/{DiffSheet.split.test.tsx → DiffContentView.split.test.tsx} +3 -3
  26. package/src/components/{DiffSheet.stories.tsx → DiffContentView.stories.tsx} +3 -3
  27. package/src/components/{DiffSheet.syntax.test.tsx → DiffContentView.syntax.test.tsx} +2 -2
  28. package/src/components/{DiffSheet.tsx → DiffContentView.tsx} +187 -28
  29. package/src/components/DiscussionMarkerRail.tsx +2 -2
  30. package/src/components/EditorGrid.tabs.test.tsx +17 -4
  31. package/src/components/EditorGrid.tsx +49 -19
  32. package/src/components/FileContentsView.test.tsx +3 -3
  33. package/src/components/FileContentsView.tsx +37 -17
  34. package/src/components/{SurfaceScrollbar.stories.tsx → OverlayScrollbar.stories.tsx} +5 -6
  35. package/src/components/{SurfaceScrollbar.test.tsx → OverlayScrollbar.test.tsx} +41 -13
  36. package/src/components/{SurfaceScrollbar.tsx → OverlayScrollbar.tsx} +49 -13
  37. package/src/components/ProjectTreeView.tsx +3 -2
  38. package/src/components/{PrototypeSheet.stories.tsx → PrototypeContentView.stories.tsx} +4 -5
  39. package/src/components/{PrototypeSheet.tsx → PrototypeContentView.tsx} +58 -58
  40. package/src/components/ScrollArea.stories.tsx +20 -0
  41. package/src/components/ScrollArea.tsx +44 -0
  42. package/src/components/SettingsDialog.tsx +3 -2
  43. package/src/components/{InboxList.stories.tsx → ThreadTree.stories.tsx} +5 -5
  44. package/src/components/{InboxList.tsx → ThreadTree.tsx} +6 -6
  45. package/src/components/ThreadsSidebar.tsx +5 -4
  46. package/src/components/TreePane.tsx +3 -2
  47. package/src/components/WelcomePlayground.stories.tsx +12 -0
  48. package/src/components/WelcomePlayground.test.tsx +47 -0
  49. package/src/components/WelcomePlayground.tsx +140 -0
  50. package/src/components/__snapshots__/stories.test.tsx.snap +163 -283
  51. package/src/components/{diff-sheet-layout.test.ts → diff-content-view-layout.test.ts} +1 -1
  52. package/src/components/{diff-sheet-layout.ts → diff-content-view-layout.ts} +1 -1
  53. package/src/config.test.ts +19 -6
  54. package/src/config.ts +26 -6
  55. package/src/intent-dispatch.test.ts +2 -0
  56. package/src/intent-dispatch.ts +1 -1
  57. package/src/key-bindings.test.ts +1 -1
  58. package/src/key-bindings.ts +5 -1
  59. package/src/keymap.ts +1 -1
  60. package/src/prototype-browser.ts +8 -0
  61. package/src/prototype-pixels.tsx +35 -0
  62. package/src/ready-signal.test.ts +85 -0
  63. package/src/ready-signal.ts +52 -0
  64. package/src/session-controller.live-diff.test.ts +198 -0
  65. package/src/session-controller.ts +141 -26
  66. package/src/slash-palette.test.ts +54 -0
  67. package/src/slash-palette.ts +32 -18
  68. package/src/test-support.ts +42 -3
  69. package/src/theme-presets.ts +20 -0
  70. package/src/theme.ts +18 -0
  71. package/src/thread-chords.ts +1 -1
  72. package/src/use-annotation-surface.tsx +12 -2
  73. package/src/use-settings-dialog.tsx +4 -4
  74. package/src/view-diff.test.ts +44 -1
  75. package/src/view-diff.ts +81 -0
  76. package/src/components/AnnotationCard.compose-rows.test.ts +0 -49
  77. package/src/components/AnnotationCard.stories.tsx +0 -94
  78. package/src/components/AnnotationCard.tsx +0 -250
  79. package/src/components/MarkerPopover.stories.tsx +0 -58
  80. package/src/components/MarkerPopover.tsx +0 -109
  81. package/src/components/WelcomeSurface.stories.tsx +0 -12
  82. package/src/components/WelcomeSurface.tsx +0 -52
@@ -7,6 +7,7 @@
7
7
  * stories catalog) drive it without a second key handler inside.
8
8
  */
9
9
 
10
+ import { ScrollArea } from "./ScrollArea";
10
11
  import React from "react";
11
12
  import { useTerminalDimensions } from "@opentui/react";
12
13
  import type { Theme } from "../theme";
@@ -68,7 +69,7 @@ function KeybindsBody({
68
69
  tokens: Theme;
69
70
  }): React.ReactNode {
70
71
  return (
71
- <scrollbox style={{ flexGrow: 1 }} focused={false}>
72
+ <ScrollArea>
72
73
  {sections.map((section) => (
73
74
  <box key={section.title} style={{ flexDirection: "column" }}>
74
75
  <text fg={tokens.accent}>{section.title}</text>
@@ -81,7 +82,7 @@ function KeybindsBody({
81
82
  <text> </text>
82
83
  </box>
83
84
  ))}
84
- </scrollbox>
85
+ </ScrollArea>
85
86
  );
86
87
  }
87
88
 
@@ -1,13 +1,13 @@
1
1
  import React from "react";
2
2
  import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
- import { InboxList } from "./InboxList";
4
+ import { ThreadTree } from "./ThreadTree";
5
5
  import { groupInbox } from "./session-tree";
6
6
  import { fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
7
7
 
8
- export const meta: StoryMeta = { title: "Surfaces/InboxList" };
8
+ export const meta: StoryMeta = { title: "Surfaces/ThreadTree" };
9
9
 
10
- /** InboxList is the body under App's header/menu chrome; a frame gives it height. */
10
+ /** ThreadTree is the body under App's header/menu chrome; a frame gives it height. */
11
11
  function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode {
12
12
  return <box style={{ flexDirection: "column", width: "100%", height: "100%" }}>{children}</box>;
13
13
  }
@@ -28,7 +28,7 @@ export const TwoPending: Story = {
28
28
 
29
29
  return (
30
30
  <BodyFrame>
31
- <InboxList rows={rows} cursor={0} />
31
+ <ThreadTree rows={rows} cursor={0} />
32
32
  </BodyFrame>
33
33
  );
34
34
  },
@@ -39,7 +39,7 @@ export const TwoPending: Story = {
39
39
  export const Empty: Story = {
40
40
  render: () => (
41
41
  <BodyFrame>
42
- <InboxList rows={[]} cursor={0} />
42
+ <ThreadTree rows={[]} cursor={0} />
43
43
  </BodyFrame>
44
44
  ),
45
45
  size: { width: 80, height: 8 },
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The pending-review inbox: a Pinned / Projects / Threads tree. Selection stays
2
+ * The sidebar thread tree: a Pinned / Projects / Threads listing. Selection stays
3
3
  * with the keyboard grammar - the cursor indexes the flat thread order and this
4
4
  * component renders the snapshot. A hovered or selected thread reveals a kebab
5
5
  * that opens an inline pin / rename / delete menu; long titles fade on the right
@@ -14,7 +14,7 @@ import { NERD } from "./primitives/icons";
14
14
  import { fadeTitle } from "./fade-title";
15
15
  import { threadTitle, type InboxRow } from "./session-tree";
16
16
 
17
- export interface InboxListProps {
17
+ export interface ThreadTreeProps {
18
18
  rows: InboxRow[];
19
19
  cursor: number;
20
20
  /** The open thread's id; highlights it instead of the cursor (the left sidebar case). */
@@ -148,7 +148,7 @@ function ThreadRow(props: ThreadRowProps): React.ReactNode {
148
148
  );
149
149
  }
150
150
 
151
- export function InboxList({
151
+ export function ThreadTree({
152
152
  rows,
153
153
  cursor,
154
154
  activeId,
@@ -159,7 +159,7 @@ export function InboxList({
159
159
  onPin,
160
160
  onRename,
161
161
  theme,
162
- }: InboxListProps): React.ReactNode {
162
+ }: ThreadTreeProps): React.ReactNode {
163
163
  const tokens = useComponentTheme(theme);
164
164
  const [actionsForId, setActionsForId] = useState<string | null>(null);
165
165
  // leave room for the 3-char prefix, the border, and the hover kebab so a row
@@ -168,9 +168,9 @@ export function InboxList({
168
168
  const closeMenu = (): void => setActionsForId(null);
169
169
 
170
170
  return (
171
- <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
171
+ <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1 }}>
172
172
  {rows.length === 0 ? (
173
- <text fg={tokens.textDim}>nothing waiting for review</text>
173
+ <text fg={tokens.textDim}>no threads</text>
174
174
  ) : (
175
175
  rows.map((row) => {
176
176
  if (row.kind === "section") {
@@ -2,9 +2,10 @@
2
2
  // thread. A click jumps to that thread; the open thread stays highlighted. Renders
3
3
  // nothing when collapsed so the thread reclaims the width.
4
4
 
5
+ import { ScrollArea } from "./ScrollArea";
5
6
  import React from "react";
6
7
  import type { Theme } from "../theme";
7
- import { InboxList } from "./InboxList";
8
+ import { ThreadTree } from "./ThreadTree";
8
9
  import type { InboxRow } from "./session-tree";
9
10
 
10
11
  export interface ThreadsSidebarProps {
@@ -51,8 +52,8 @@ export function ThreadsSidebar({
51
52
  borderColor: theme?.border,
52
53
  }}
53
54
  >
54
- <scrollbox style={{ flexGrow: 1 }} focused={false}>
55
- <InboxList
55
+ <ScrollArea>
56
+ <ThreadTree
56
57
  rows={rows}
57
58
  cursor={cursor}
58
59
  activeId={activeId}
@@ -64,7 +65,7 @@ export function ThreadsSidebar({
64
65
  onRename={onRename}
65
66
  theme={theme}
66
67
  />
67
- </scrollbox>
68
+ </ScrollArea>
68
69
  </box>
69
70
  );
70
71
  }
@@ -6,6 +6,7 @@
6
6
  * path to the same primitives the option chords reach.
7
7
  */
8
8
 
9
+ import { ScrollArea } from "./ScrollArea";
9
10
  import React, { useEffect, useRef } from "react";
10
11
  import { createTextAttributes, type ScrollBoxRenderable } from "@opentui/core";
11
12
  import type { Theme } from "../theme";
@@ -84,7 +85,7 @@ export function TreePane({
84
85
 
85
86
  return (
86
87
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
87
- <scrollbox ref={scrollRef} style={{ flexGrow: 1 }} focused={false}>
88
+ <ScrollArea scrollRef={scrollRef}>
88
89
  {rows.map((row) => {
89
90
  const selected = row.entryId === selectedEntryId;
90
91
  const foreground = row.onPath ? tokens.text : tokens.textDim;
@@ -112,7 +113,7 @@ export function TreePane({
112
113
  </box>
113
114
  );
114
115
  })}
115
- </scrollbox>
116
+ </ScrollArea>
116
117
  {canMove || canFork ? (
117
118
  <box style={{ paddingLeft: 1 }}>
118
119
  <Toolbar>
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { WelcomePlayground } from "./WelcomePlayground";
5
+
6
+ export const meta: StoryMeta = { title: "Surfaces/WelcomePlayground" };
7
+
8
+ export const Welcome: Story = {
9
+ render: () => <WelcomePlayground version="0.1.0-alpha.66" quickActions={[]} theme={DARK} />,
10
+ expectedColors: [DARK.textDim],
11
+ size: { width: 72, height: 20 },
12
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The welcome page is a real annotation surface, but ephemeral: selecting its copy and typing leaves
3
+ * a comment that shows inline, exactly as a file does, and it lives only in the component - there is
4
+ * no controller or daemon behind it, so nothing is persisted. The copy prompts the two first gestures.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import React from "react";
10
+ import { WelcomePlayground } from "./WelcomePlayground";
11
+ import { DARK } from "../theme";
12
+ import { dragText, typeText, pressKey, waitForText } from "../test-support";
13
+
14
+ describe("WelcomePlayground", () => {
15
+ test("the copy prompts the select-and-type and the slash gestures", async () => {
16
+ const setup = await testRender(
17
+ <WelcomePlayground version="1.2.3" quickActions={[]} theme={DARK} />,
18
+ { width: 80, height: 24 },
19
+ );
20
+
21
+ await waitForText(setup, "Getting started");
22
+ const frame = setup.captureCharFrame();
23
+
24
+ expect(frame).toContain("start typing to leave your first comment");
25
+ expect(frame).toContain('type "/"');
26
+
27
+ setup.renderer.destroy();
28
+ });
29
+
30
+ test("selecting the copy and typing leaves a comment inline, exactly like a file", async () => {
31
+ const setup = await testRender(
32
+ <WelcomePlayground version="1.2.3" quickActions={[]} theme={DARK} />,
33
+ { width: 80, height: 24 },
34
+ );
35
+
36
+ // Act - select the practice line, type a comment, send it
37
+ await waitForText(setup, "quick brown fox");
38
+ await dragText(setup, "quick brown fox", "quick brown fox", "quick brown fox".length);
39
+ await typeText(setup, "my first note");
40
+ await pressKey(setup, "RETURN", { meta: true });
41
+
42
+ // Assert - the note renders on the surface, no controller or daemon involved
43
+ await waitForText(setup, "my first note");
44
+
45
+ setup.renderer.destroy();
46
+ });
47
+ });
@@ -0,0 +1,140 @@
1
+ // The welcome page as a live annotation playground: the onboarding copy renders through the same
2
+ // diff sheet a file uses, so selecting text and typing leaves a comment with the identical gesture a
3
+ // real review uses. The notes are ephemeral - there is no file or thread behind onboarding copy, so
4
+ // they live only in this component's state and are never stored or re-anchored. The copy teaches the
5
+ // two first gestures: select-and-type to comment, and "/" for the quick actions and skills.
6
+
7
+ import React, { useMemo, useState } from "react";
8
+ import {
9
+ makeAnchor,
10
+ newAnnotationId,
11
+ SCHEMA_VERSION,
12
+ type Annotation,
13
+ type ReviewSession,
14
+ } from "@cueloop/schema";
15
+ import type { Theme } from "../theme";
16
+ import type { QuickAction } from "../config";
17
+ import { DiffContentView } from "./DiffContentView";
18
+ import { diffRowBlocks, fileContentsRows, marksByRows } from "../view-diff";
19
+
20
+ export interface WelcomePlaygroundProps {
21
+ version: string;
22
+ quickActions: QuickAction[];
23
+ /** Reports whether the playground composer is open, so the shell suspends its inbox keys while typing. */
24
+ onComposingChange?: (composing: boolean) => void;
25
+ theme?: Theme;
26
+ }
27
+
28
+ const DOCS_URL = "https://cueloop.dev/docs/";
29
+
30
+ /** The playground copy: a heading, the two guided prompts, a line to practice on, and the links. */
31
+ function welcomeCopy(version: string): string {
32
+ return [
33
+ "Getting started",
34
+ "Comment on agent-authored work.",
35
+ "",
36
+ "This page is a playground. Nothing you write here is saved.",
37
+ "",
38
+ "Select the line below and start typing to leave your first comment:",
39
+ "",
40
+ " The quick brown fox jumps over the lazy dog.",
41
+ "",
42
+ 'While composing, type "/" to see the quick actions and skills.',
43
+ "",
44
+ "Start",
45
+ " - select a thread",
46
+ " - inspect files and changes",
47
+ "",
48
+ "Learn",
49
+ ` - docs ${DOCS_URL}`,
50
+ "",
51
+ "Version",
52
+ ` - cueloop v${version}`,
53
+ ].join("\n");
54
+ }
55
+
56
+ /** An ephemeral thread that holds nothing but the playground's notes, so the surface groups
57
+ * discussions exactly as a real review does without any of it reaching the daemon. */
58
+ function playgroundThread(annotations: Annotation[]): ReviewSession {
59
+ return {
60
+ schemaVersion: SCHEMA_VERSION,
61
+ id: "welcome",
62
+ workspace: { repoRoot: "", branch: "main" },
63
+ artifact: { type: "plan", content: "", meta: {} },
64
+ revisions: [],
65
+ annotations,
66
+ verdict: null,
67
+ status: "pending",
68
+ createdAt: "1970-01-01T00:00:00.000Z",
69
+ };
70
+ }
71
+
72
+ export function WelcomePlayground(props: WelcomePlaygroundProps): React.ReactNode {
73
+ const [annotations, setAnnotations] = useState<Annotation[]>([]);
74
+ const rows = useMemo(
75
+ () => fileContentsRows("welcome", welcomeCopy(props.version)),
76
+ [props.version],
77
+ );
78
+ const session = playgroundThread(annotations);
79
+
80
+ return (
81
+ <DiffContentView
82
+ rows={rows}
83
+ session={session}
84
+ marks={marksByRows(annotations, rows)}
85
+ quickActions={props.quickActions}
86
+ observer={false}
87
+ commentsEnabled
88
+ fileView
89
+ onComposingChange={props.onComposingChange}
90
+ onAnnotate={(span, body) =>
91
+ setAnnotations((current) => [
92
+ ...current,
93
+ {
94
+ id: newAnnotationId(),
95
+ kind: "comment",
96
+ anchor: makeAnchor(
97
+ diffRowBlocks(rows),
98
+ span.start.blockIndex,
99
+ span.start.char,
100
+ span.end.char,
101
+ span.end.blockIndex,
102
+ ),
103
+ target: { kind: "welcome" },
104
+ body,
105
+ createdAt: new Date().toISOString(),
106
+ },
107
+ ])
108
+ }
109
+ onReply={(rootAnnotationId, body) =>
110
+ setAnnotations((current) => {
111
+ const root = current.find((annotation) => annotation.id === rootAnnotationId);
112
+
113
+ if (!root) return current;
114
+
115
+ return [
116
+ ...current,
117
+ {
118
+ id: newAnnotationId(),
119
+ kind: "comment",
120
+ anchor: root.anchor,
121
+ target: { kind: "welcome" },
122
+ body,
123
+ replyTo: rootAnnotationId,
124
+ createdAt: new Date().toISOString(),
125
+ },
126
+ ];
127
+ })
128
+ }
129
+ onUpdateAnnotation={(id, body) =>
130
+ setAnnotations((current) =>
131
+ current.map((annotation) =>
132
+ annotation.id === id ? { ...annotation, body } : annotation,
133
+ ),
134
+ )
135
+ }
136
+ onExit={() => {}}
137
+ theme={props.theme}
138
+ />
139
+ );
140
+ }