@cueloop/client 0.1.0-alpha.67 → 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 (85) 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/ghostty-terminal.test.ts +2 -1
  56. package/src/ghostty-terminal.ts +27 -4
  57. package/src/intent-dispatch.test.ts +2 -0
  58. package/src/intent-dispatch.ts +1 -1
  59. package/src/key-bindings.test.ts +1 -1
  60. package/src/key-bindings.ts +5 -1
  61. package/src/keymap.ts +1 -1
  62. package/src/prototype-browser.ts +8 -0
  63. package/src/prototype-pixels.tsx +35 -0
  64. package/src/ready-signal.test.ts +85 -0
  65. package/src/ready-signal.ts +52 -0
  66. package/src/session-controller.live-diff.test.ts +198 -0
  67. package/src/session-controller.ts +141 -26
  68. package/src/slash-palette.test.ts +54 -0
  69. package/src/slash-palette.ts +32 -18
  70. package/src/test-support.test.ts +12 -0
  71. package/src/test-support.ts +69 -10
  72. package/src/theme-presets.ts +20 -0
  73. package/src/theme.ts +18 -0
  74. package/src/thread-chords.ts +1 -1
  75. package/src/use-annotation-surface.tsx +12 -2
  76. package/src/use-settings-dialog.tsx +4 -4
  77. package/src/view-diff.test.ts +44 -1
  78. package/src/view-diff.ts +81 -0
  79. package/src/components/AnnotationCard.compose-rows.test.ts +0 -49
  80. package/src/components/AnnotationCard.stories.tsx +0 -94
  81. package/src/components/AnnotationCard.tsx +0 -250
  82. package/src/components/MarkerPopover.stories.tsx +0 -58
  83. package/src/components/MarkerPopover.tsx +0 -109
  84. package/src/components/WelcomeSurface.stories.tsx +0 -12
  85. package/src/components/WelcomeSurface.tsx +0 -52
@@ -1,250 +0,0 @@
1
- /**
2
- * One annotation component in three modes: a draft composer (bordered card
3
- * inline in the document flow), a saved rail card, and the rail card editing
4
- * its body in place. Compose, saved, and re-edit share this rendering path,
5
- * so the kind-color and quote treatment can never drift between them.
6
- *
7
- * Bodies are multiline: the composer is a textarea (wrap, undo/redo). Plain ⏎
8
- * stays the save key while ⌥/Alt+⏎ and shift+⏎ insert a new line (the Slack
9
- * convention). The box auto-grows with the wrapped text up to a cap, then
10
- * scrolls internally.
11
- */
12
-
13
- import React, { useEffect, useRef, useState } from "react";
14
- import type { KeyBinding, TextareaRenderable } from "@opentui/core";
15
- import type { Theme } from "../theme";
16
- import { useComponentTheme } from "./theme-context";
17
- import { Card, cardHeight } from "./primitives/Card";
18
- import { FRAME_BORDER_STYLE } from "./primitives/frame";
19
- import { Button } from "./primitives/Button";
20
- import { Toolbar } from "./primitives/Toolbar";
21
- import { truncateToSingleLine } from "./truncate-text";
22
-
23
- export interface AnnotationDraft {
24
- text: string;
25
- onInput: (text: string) => void;
26
- onSave: () => void;
27
- onCancel: () => void;
28
- /**
29
- * When set, ⏎ in the textarea saves through this handler. The plan and diff
30
- * composers leave it unset because the app keymap owns ⏎ there; the prototype
31
- * composer sets it because the app keymap is suppressed while it is open.
32
- */
33
- onSubmit?: () => void;
34
- }
35
-
36
- export interface AnnotationSaved {
37
- body: string;
38
- isSelected: boolean;
39
- isOrphan: boolean;
40
- /** Border-title author: a collaborator's display name, or "me" for the reviewer's own note. */
41
- author: string;
42
- /** Border + title color for this card (comment cards read blue). */
43
- tone: string;
44
- /** Non-null while the card body is being rewritten in place. */
45
- editing: AnnotationDraft | null;
46
- onPress: () => void;
47
- }
48
-
49
- export interface AnnotationCardProps {
50
- id?: string;
51
- kind: string;
52
- quote: string;
53
- draft?: AnnotationDraft;
54
- saved?: AnnotationSaved;
55
- theme?: Theme;
56
- }
57
-
58
- /**
59
- * ⏎ submits (which keeps the textarea from inserting its default newline; the
60
- * grammar owns the actual save). ⌥/Alt+⏎ and shift+⏎ insert a newline so the
61
- * body can grow multiline without leaving the composer.
62
- *
63
- * Option/Alt reaches a binding as `meta`, not `option`: a binding key here is
64
- * built from name+ctrl+shift+meta+super only, so the newline binding matches on
65
- * `meta`. This also overrides the textarea default that maps meta+⏎ to submit.
66
- */
67
- const COMPOSE_KEY_BINDINGS: KeyBinding[] = [
68
- { name: "return", action: "submit" },
69
- { name: "return", meta: true, action: "newline" },
70
- { name: "return", shift: true, action: "newline" },
71
- ];
72
-
73
- /** The composer never grows past this many rows; beyond it the textarea scrolls. */
74
- export const COMPOSE_MAX_ROWS = 4;
75
-
76
- /**
77
- * Visible row count for the composer, so a soft-wrapped long line grows the box
78
- * just like a hard newline does. Each logical line takes `ceil(length /
79
- * contentWidth)` visual rows (at least one), summed over the hard-newline lines
80
- * and capped. A non-positive width (before layout is known) counts hard
81
- * newlines only. Beyond the cap the textarea scrolls internally and keeps the
82
- * caret line in view.
83
- */
84
- export function composeRowCount(
85
- text: string,
86
- contentWidth: number,
87
- cap: number = COMPOSE_MAX_ROWS,
88
- ): number {
89
- const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
90
- let visualRowCount = 0;
91
-
92
- for (const line of text.split("\n")) {
93
- visualRowCount += Math.max(1, Math.ceil(line.length / usableWidth));
94
- }
95
-
96
- return Math.min(cap, Math.max(1, visualRowCount));
97
- }
98
-
99
- function DraftEditor({
100
- draft,
101
- rows,
102
- onRowsChange,
103
- theme,
104
- }: {
105
- draft: AnnotationDraft;
106
- /** Current visible height in rows; owned by the parent so the card frame agrees. */
107
- rows: number;
108
- onRowsChange: (rows: number) => void;
109
- theme?: Theme;
110
- }): React.ReactNode {
111
- const tokens = useComponentTheme(theme);
112
- const editorRef = useRef<TextareaRenderable | null>(null);
113
- // re-edit opens with the caret after the existing body, like a text field
114
- const initialTextLength = useRef(draft.text.length);
115
-
116
- useEffect(() => {
117
- const editor = editorRef.current;
118
-
119
- if (!editor) return;
120
- editor.cursorOffset = initialTextLength.current;
121
- // the box is laid out now, so its width is known: size to the wrapped body
122
- onRowsChange(composeRowCount(editor.plainText, editor.width));
123
- }, [onRowsChange]);
124
-
125
- return (
126
- <>
127
- <textarea
128
- ref={editorRef}
129
- focused
130
- initialValue={draft.text}
131
- placeholder="write a note..."
132
- keyBindings={COMPOSE_KEY_BINDINGS}
133
- {...(draft.onSubmit ? { onSubmit: draft.onSubmit } : {})}
134
- onContentChange={() => {
135
- const editor = editorRef.current;
136
-
137
- if (!editor) return;
138
- draft.onInput(editor.plainText);
139
- onRowsChange(composeRowCount(editor.plainText, editor.width));
140
- }}
141
- style={{
142
- height: rows,
143
- backgroundColor: "transparent",
144
- focusedBackgroundColor: "transparent",
145
- textColor: tokens.text,
146
- focusedTextColor: tokens.text,
147
- }}
148
- />
149
- <Toolbar>
150
- <Button variant="solid" marginRight={2} onPress={draft.onSave} theme={theme}>
151
- {" Save "}
152
- </Button>
153
- <Button onPress={draft.onCancel} theme={theme}>
154
- {" Cancel "}
155
- </Button>
156
- </Toolbar>
157
- </>
158
- );
159
- }
160
-
161
- export function AnnotationCard({
162
- id,
163
- kind,
164
- quote,
165
- draft,
166
- saved,
167
- theme,
168
- }: AnnotationCardProps): React.ReactNode {
169
- const tokens = useComponentTheme(theme);
170
- // the composer wears the same blue as the saved comment card it becomes
171
- const kindColor = tokens.blue;
172
- const activeDraft = draft ?? saved?.editing ?? null;
173
- // The composer's visible height, grown from the wrapped text. It is shared
174
- // with the card frame (contentRows) so the border never clips the textarea
175
- // or the toolbar. Seeded from hard newlines, then corrected once the mounted
176
- // textarea reports its measured width.
177
- const [editorRowCount, setEditorRowCount] = useState(() =>
178
- // Width 0 is the "layout not known yet" contract: count hard newlines only,
179
- // until the mounted textarea reports its measured width below.
180
- composeRowCount(activeDraft?.text ?? "", 0),
181
- );
182
-
183
- if (draft) {
184
- const primitive = "comment on";
185
-
186
- return (
187
- <Card
188
- title={` ${primitive} "${truncateToSingleLine(quote, 20)}" `}
189
- contentRows={editorRowCount + 1}
190
- borderColor={kindColor}
191
- backgroundColor="transparent"
192
- theme={theme}
193
- >
194
- <DraftEditor
195
- draft={draft}
196
- rows={editorRowCount}
197
- onRowsChange={setEditorRowCount}
198
- theme={theme}
199
- />
200
- </Card>
201
- );
202
- }
203
- const card = saved!;
204
- // border = "what + who" (+ an orphaned flag when the anchor no longer resolves)
205
- const flags = card.isOrphan ? ["ORPHANED"] : [];
206
- const title = ` ${[kind.toUpperCase(), card.author, ...flags].join(" · ")} `;
207
- // every saved card is bordered, so the border + side padding always eat this width
208
- const borderInset = 4;
209
- // a bordered box must declare a height or it collapses: quote + body, or the editor
210
- const contentRows = card.editing ? editorRowCount + 2 : 2;
211
-
212
- return (
213
- <box
214
- id={id}
215
- title={title}
216
- style={{
217
- flexDirection: "column",
218
- height: cardHeight(contentRows),
219
- border: true,
220
- borderStyle: FRAME_BORDER_STYLE,
221
- // the border (and its title) always wears the card's tone - salmon for
222
- // own notes, blue for a collaborator's; the card background stays
223
- // transparent so it sits flat on the terminal theme. Selection reads
224
- // from the quote line taking the card's tone (below) and from the
225
- // matching text highlight in the document.
226
- borderColor: card.tone,
227
- backgroundColor: "transparent",
228
- paddingLeft: 1,
229
- paddingRight: 1,
230
- }}
231
- onMouseUp={card.onPress}
232
- >
233
- <text
234
- fg={card.isSelected ? card.tone : tokens.textDim}
235
- >{`"${truncateToSingleLine(quote, 26 - borderInset)}"`}</text>
236
- {card.editing ? (
237
- <DraftEditor
238
- draft={card.editing}
239
- rows={editorRowCount}
240
- onRowsChange={setEditorRowCount}
241
- theme={theme}
242
- />
243
- ) : (
244
- <text fg={card.isOrphan ? tokens.textDim : tokens.textMuted}>
245
- {truncateToSingleLine(card.body, 28 - borderInset)}
246
- </text>
247
- )}
248
- </box>
249
- );
250
- }
@@ -1,58 +0,0 @@
1
- import React from "react";
2
- import { DARK } from "../theme";
3
- import { DEFAULT_QUICK_ACTIONS } from "../config";
4
- import type { Story, StoryMeta } from "./story";
5
- import { MarkerPopover } from "./MarkerPopover";
6
-
7
- export const meta: StoryMeta = { title: "Overlays/MarkerPopover" };
8
-
9
- const callbacks = {
10
- onComment: () => {},
11
- onCut: () => {},
12
- onOpenActions: () => {},
13
- onClose: () => {},
14
- onPickAction: () => {},
15
- onBack: () => {},
16
- };
17
-
18
- export const Toolbar: Story = {
19
- render: () => (
20
- <MarkerPopover
21
- view="toolbar"
22
- actions={DEFAULT_QUICK_ACTIONS}
23
- actionIndex={0}
24
- canCut
25
- {...callbacks}
26
- />
27
- ),
28
- expectedColors: [DARK.accent, DARK.red, DARK.textMuted, DARK.textDim],
29
- size: { width: 60, height: 4 },
30
- };
31
-
32
- export const ToolbarNoCut: Story = {
33
- render: () => (
34
- <MarkerPopover
35
- view="toolbar"
36
- actions={DEFAULT_QUICK_ACTIONS}
37
- actionIndex={0}
38
- canCut={false}
39
- {...callbacks}
40
- />
41
- ),
42
- expectedColors: [DARK.accent, DARK.textMuted, DARK.textDim],
43
- size: { width: 60, height: 4 },
44
- };
45
-
46
- export const ActionsList: Story = {
47
- render: () => (
48
- <MarkerPopover
49
- view="actions"
50
- actions={DEFAULT_QUICK_ACTIONS}
51
- actionIndex={2}
52
- canCut
53
- {...callbacks}
54
- />
55
- ),
56
- expectedColors: [DARK.accent, DARK.textMuted, DARK.textDim],
57
- size: { width: 60, height: 8 },
58
- };
@@ -1,109 +0,0 @@
1
- /**
2
- * The marker-actions popover: the visible form of plan span mode, floated as a
3
- * bordered card above the marked block. The toolbar card is always shown -
4
- * `comment · cut · actions · [x]`, each label keyboard-shortcut-backed and
5
- * clickable. Pressing `actions` drops a second card below it listing the
6
- * quick-actions; picking one inserts its preset comment. Plan-only: the diff
7
- * review has no span.
8
- */
9
-
10
- import React from "react";
11
- import type { MouseEvent } from "@opentui/core";
12
- import type { Theme } from "../theme";
13
- import type { QuickAction } from "../config";
14
- import { useComponentTheme } from "./theme-context";
15
- import { cardHeight } from "./primitives/Card";
16
- import { FRAME_BORDER_STYLE } from "./primitives/frame";
17
-
18
- export interface MarkerPopoverProps {
19
- /** "toolbar" is the bar alone; "actions" also drops the quick-actions card. */
20
- view: "toolbar" | "actions";
21
- actions: QuickAction[];
22
- /** Highlighted action in the dropdown (accent); ignored in toolbar view. */
23
- actionIndex: number;
24
- /** Show the cut affordance: owner-only, like every other plan-edit control. */
25
- canCut: boolean;
26
- onComment: () => void;
27
- onCut: () => void;
28
- onOpenActions: () => void;
29
- onClose: () => void;
30
- onPickAction: (index: number) => void;
31
- onBack: () => void;
32
- theme?: Theme;
33
- }
34
-
35
- export function MarkerPopover({
36
- view,
37
- actions,
38
- actionIndex,
39
- canCut,
40
- onComment,
41
- onCut,
42
- onOpenActions,
43
- onClose,
44
- onPickAction,
45
- onBack,
46
- theme,
47
- }: MarkerPopoverProps): React.ReactNode {
48
- const tokens = useComponentTheme(theme);
49
- const cardStyle = {
50
- border: true,
51
- borderStyle: FRAME_BORDER_STYLE,
52
- borderColor: tokens.border,
53
- backgroundColor: tokens.elevated,
54
- flexDirection: "column" as const,
55
- paddingLeft: 1,
56
- paddingRight: 1,
57
- };
58
- const open = view === "actions";
59
-
60
- return (
61
- <box
62
- style={{ flexDirection: "column", alignItems: "flex-start" }}
63
- // a release on the popover is the popover's own click; it must never
64
- // bubble to the sheet's selection-release fallback and re-enter span mode
65
- onMouseUp={(event: MouseEvent) => event.stopPropagation()}
66
- >
67
- <box style={{ ...cardStyle, height: cardHeight(1) }}>
68
- <box style={{ flexDirection: "row" }}>
69
- <text fg={tokens.accent} onMouseUp={onComment}>
70
- comment
71
- </text>
72
- <text fg={tokens.textDim}> · </text>
73
- {canCut ? (
74
- <>
75
- <text fg={tokens.red} onMouseUp={onCut}>
76
- cut
77
- </text>
78
- <text fg={tokens.textDim}> · </text>
79
- </>
80
- ) : null}
81
- <text fg={open ? tokens.accent : tokens.textMuted} onMouseUp={onOpenActions}>
82
- actions
83
- </text>
84
- <text fg={tokens.textDim}> · </text>
85
- <text fg={tokens.textDim} onMouseUp={onClose}>
86
- [x]
87
- </text>
88
- </box>
89
- </box>
90
- {open ? (
91
- <box style={{ ...cardStyle, height: cardHeight(actions.length + 1) }}>
92
- {actions.map((action, index) => (
93
- <text
94
- key={index}
95
- fg={index === actionIndex ? tokens.accent : tokens.textMuted}
96
- onMouseUp={() => onPickAction(index)}
97
- >
98
- {index === actionIndex ? "› " : " "}
99
- {action.prompt}
100
- </text>
101
- ))}
102
- <text fg={tokens.textDim} onMouseUp={onBack}>
103
- {" "}‹ back
104
- </text>
105
- </box>
106
- ) : null}
107
- </box>
108
- );
109
- }
@@ -1,12 +0,0 @@
1
- import React from "react";
2
- import { DARK } from "../theme";
3
- import type { Story, StoryMeta } from "./story";
4
- import { WelcomeSurface } from "./WelcomeSurface";
5
-
6
- export const meta: StoryMeta = { title: "Surfaces/WelcomeSurface" };
7
-
8
- export const Welcome: Story = {
9
- render: () => <WelcomeSurface version="0.1.0-alpha.66" theme={DARK} />,
10
- expectedColors: [DARK.accent],
11
- size: { width: 60, height: 16 },
12
- };
@@ -1,52 +0,0 @@
1
- // The Welcome body: the disposable default tab's contents in the Changes panel,
2
- // shown when nothing else is open. Points at where to start, the docs, and what
3
- // shipped in this build. The tab chrome (label + close) lives in the panel header.
4
-
5
- import React from "react";
6
- import type { Theme } from "../theme";
7
-
8
- const DOCS_URL = "www.cueloop.dev";
9
-
10
- export interface WelcomeSurfaceProps {
11
- version: string;
12
- theme?: Theme;
13
- }
14
-
15
- function Section({
16
- title,
17
- children,
18
- theme,
19
- }: {
20
- title: string;
21
- children: React.ReactNode;
22
- theme?: Theme;
23
- }): React.ReactNode {
24
- return (
25
- <box style={{ flexDirection: "column", flexShrink: 0, marginTop: 1 }}>
26
- <text fg={theme?.text}>{title}</text>
27
- {children}
28
- </box>
29
- );
30
- }
31
-
32
- export function WelcomeSurface({ version, theme }: WelcomeSurfaceProps): React.ReactNode {
33
- return (
34
- <box
35
- style={{ flexDirection: "column", flexGrow: 1, flexShrink: 0, paddingLeft: 2, paddingTop: 1 }}
36
- >
37
- <text fg={theme?.accent}>Welcome to cueloop</text>
38
- <text fg={theme?.textDim}>a review loop for coding agents</text>
39
- <Section title="Start" theme={theme}>
40
- <text fg={theme?.textDim}>· select a thread on the left</text>
41
- <text fg={theme?.textDim}>· or run cueloop plan / cueloop diff from a repo</text>
42
- </Section>
43
- <Section title="Learn" theme={theme}>
44
- <text fg={theme?.textDim}>{`· docs ${DOCS_URL}`}</text>
45
- <text fg={theme?.textDim}>{`· changelog ${DOCS_URL}/changelog`}</text>
46
- </Section>
47
- <Section title="What's new" theme={theme}>
48
- <text fg={theme?.textDim}>{`· cueloop v${version}`}</text>
49
- </Section>
50
- </box>
51
- );
52
- }