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

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,241 @@
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
+ for (const line of text.split("\n")) {
92
+ visualRowCount += Math.max(1, Math.ceil(line.length / usableWidth));
93
+ }
94
+ return Math.min(cap, Math.max(1, visualRowCount));
95
+ }
96
+
97
+ function DraftEditor({
98
+ draft,
99
+ rows,
100
+ onRowsChange,
101
+ theme,
102
+ }: {
103
+ draft: AnnotationDraft;
104
+ /** Current visible height in rows; owned by the parent so the card frame agrees. */
105
+ rows: number;
106
+ onRowsChange: (rows: number) => void;
107
+ theme?: Theme;
108
+ }): React.ReactNode {
109
+ const tokens = useComponentTheme(theme);
110
+ const editorRef = useRef<TextareaRenderable | null>(null);
111
+ // re-edit opens with the caret after the existing body, like a text field
112
+ const initialTextLength = useRef(draft.text.length);
113
+ useEffect(() => {
114
+ const editor = editorRef.current;
115
+ if (!editor) return;
116
+ editor.cursorOffset = initialTextLength.current;
117
+ // the box is laid out now, so its width is known: size to the wrapped body
118
+ onRowsChange(composeRowCount(editor.plainText, editor.width));
119
+ }, [onRowsChange]);
120
+ return (
121
+ <>
122
+ <textarea
123
+ ref={editorRef}
124
+ focused
125
+ initialValue={draft.text}
126
+ placeholder="write a note..."
127
+ keyBindings={COMPOSE_KEY_BINDINGS}
128
+ {...(draft.onSubmit ? { onSubmit: draft.onSubmit } : {})}
129
+ onContentChange={() => {
130
+ const editor = editorRef.current;
131
+ if (!editor) return;
132
+ draft.onInput(editor.plainText);
133
+ onRowsChange(composeRowCount(editor.plainText, editor.width));
134
+ }}
135
+ style={{
136
+ height: rows,
137
+ backgroundColor: "transparent",
138
+ focusedBackgroundColor: "transparent",
139
+ textColor: tokens.text,
140
+ focusedTextColor: tokens.text,
141
+ }}
142
+ />
143
+ <Toolbar>
144
+ <Button variant="solid" marginRight={2} onPress={draft.onSave} theme={theme}>
145
+ {" Save "}
146
+ </Button>
147
+ <Button onPress={draft.onCancel} theme={theme}>
148
+ {" Cancel "}
149
+ </Button>
150
+ </Toolbar>
151
+ </>
152
+ );
153
+ }
154
+
155
+ export function AnnotationCard({
156
+ id,
157
+ kind,
158
+ quote,
159
+ draft,
160
+ saved,
161
+ theme,
162
+ }: AnnotationCardProps): React.ReactNode {
163
+ const tokens = useComponentTheme(theme);
164
+ // the composer wears the same blue as the saved comment card it becomes
165
+ const kindColor = tokens.blue;
166
+ const activeDraft = draft ?? saved?.editing ?? null;
167
+ // The composer's visible height, grown from the wrapped text. It is shared
168
+ // with the card frame (contentRows) so the border never clips the textarea
169
+ // or the toolbar. Seeded from hard newlines, then corrected once the mounted
170
+ // textarea reports its measured width.
171
+ const [editorRowCount, setEditorRowCount] = useState(() =>
172
+ // Width 0 is the "layout not known yet" contract: count hard newlines only,
173
+ // until the mounted textarea reports its measured width below.
174
+ composeRowCount(activeDraft?.text ?? "", 0),
175
+ );
176
+ if (draft) {
177
+ const verb = "comment on";
178
+ return (
179
+ <Card
180
+ title={` ${verb} "${truncateToSingleLine(quote, 40)}" `}
181
+ contentRows={editorRowCount + 1}
182
+ borderColor={kindColor}
183
+ backgroundColor="transparent"
184
+ theme={theme}
185
+ >
186
+ <DraftEditor
187
+ draft={draft}
188
+ rows={editorRowCount}
189
+ onRowsChange={setEditorRowCount}
190
+ theme={theme}
191
+ />
192
+ </Card>
193
+ );
194
+ }
195
+ const card = saved!;
196
+ // border = "what + who" (+ an orphaned flag when the anchor no longer resolves)
197
+ const flags = card.isOrphan ? ["ORPHANED"] : [];
198
+ const title = ` ${[kind.toUpperCase(), card.author, ...flags].join(" · ")} `;
199
+ // every saved card is bordered, so the border + side padding always eat this width
200
+ const borderInset = 4;
201
+ // a bordered box must declare a height or it collapses: quote + body, or the editor
202
+ const contentRows = card.editing ? editorRowCount + 2 : 2;
203
+ return (
204
+ <box
205
+ id={id}
206
+ title={title}
207
+ style={{
208
+ flexDirection: "column",
209
+ height: cardHeight(contentRows),
210
+ border: true,
211
+ borderStyle: FRAME_BORDER_STYLE,
212
+ // the border (and its title) always wears the card's tone - salmon for
213
+ // own notes, blue for a collaborator's; the card background stays
214
+ // transparent so it sits flat on the terminal theme. Selection reads
215
+ // from the quote line taking the card's tone (below) and from the
216
+ // matching text highlight in the document.
217
+ borderColor: card.tone,
218
+ backgroundColor: "transparent",
219
+ paddingLeft: 1,
220
+ paddingRight: 1,
221
+ }}
222
+ onMouseUp={card.onPress}
223
+ >
224
+ <text
225
+ fg={card.isSelected ? card.tone : tokens.textDim}
226
+ >{`"${truncateToSingleLine(quote, 26 - borderInset)}"`}</text>
227
+ {card.editing ? (
228
+ <DraftEditor
229
+ draft={card.editing}
230
+ rows={editorRowCount}
231
+ onRowsChange={setEditorRowCount}
232
+ theme={theme}
233
+ />
234
+ ) : (
235
+ <text fg={card.isOrphan ? tokens.textDim : tokens.textMuted}>
236
+ {truncateToSingleLine(card.body, 28 - borderInset)}
237
+ </text>
238
+ )}
239
+ </box>
240
+ );
241
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { Breadcrumb } from "./Breadcrumb";
5
+
6
+ export const meta: StoryMeta = { title: "Breadcrumb" };
7
+
8
+ export const SessionHeader: Story = {
9
+ render: () => (
10
+ <Breadcrumb
11
+ items={[
12
+ { label: "cueloop", tone: "accent" },
13
+ { label: "Migration Plan · rev 2", tone: "dim" },
14
+ { label: "resolved: approve", tone: "green" },
15
+ ]}
16
+ />
17
+ ),
18
+ expectedColors: [DARK.accent, DARK.green],
19
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * One-row breadcrumb trail: toned segments joined by " · " on the panel
3
+ * surface. The app header is a breadcrumb (product · artifact · revision)
4
+ * followed by state badges; each item picks a semantic tone.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { Theme } from "../theme";
9
+ import { useComponentTheme } from "./theme-context";
10
+
11
+ export type BreadcrumbTone = "accent" | "dim" | "text" | "green";
12
+
13
+ export interface BreadcrumbItem {
14
+ label: string;
15
+ tone?: BreadcrumbTone;
16
+ }
17
+
18
+ export interface BreadcrumbProps {
19
+ items: BreadcrumbItem[];
20
+ theme?: Theme;
21
+ }
22
+
23
+ export function Breadcrumb({ items, theme }: BreadcrumbProps): React.ReactNode {
24
+ const tokens = useComponentTheme(theme);
25
+ const toneColor = (tone?: BreadcrumbTone): string =>
26
+ tone === "accent"
27
+ ? tokens.accent
28
+ : tone === "green"
29
+ ? tokens.green
30
+ : tone === "text"
31
+ ? tokens.text
32
+ : tokens.textDim;
33
+ return (
34
+ <box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1, flexDirection: "row" }}>
35
+ <text fg={tokens.text}>
36
+ {items.map((item, index) => (
37
+ <span key={index} fg={toneColor(item.tone)}>
38
+ {index === 0 ? item.label : ` · ${item.label}`}
39
+ </span>
40
+ ))}
41
+ </text>
42
+ </box>
43
+ );
44
+ }
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { CodeBlock } from "./CodeBlock";
5
+
6
+ export const meta: StoryMeta = { title: "CodeBlock" };
7
+
8
+ const SNIPPET = `export function gate(full: number) {
9
+ // threshold
10
+ return full < 0.6 ? "blocked" : "clear";
11
+ }`;
12
+
13
+ export const Typescript: Story = {
14
+ render: () => <CodeBlock language="ts" content={SNIPPET} />,
15
+ expectedColors: [DARK.elevated],
16
+ };
17
+
18
+ export const CursorAnnotatedEdited: Story = {
19
+ render: () => (
20
+ <CodeBlock
21
+ language="ts"
22
+ content={SNIPPET}
23
+ isCursor
24
+ isAnnotated
25
+ changeTag="edited"
26
+ marginTop={1}
27
+ />
28
+ ),
29
+ expectedColors: [DARK.accent],
30
+ };
31
+
32
+ export const UnknownLanguage: Story = {
33
+ render: () => (
34
+ <CodeBlock language="brainfog" content={"plain verbatim lines\n with indentation"} />
35
+ ),
36
+ };
37
+
38
+ /** A cut code block: struck-through gray content, no [cut] tag. */
39
+ export const Cut: Story = {
40
+ render: () => <CodeBlock language="ts" content={SNIPPET} cut />,
41
+ expectedColors: [DARK.elevated],
42
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Fenced code block: verbatim lines in a contained elevated box, highlighted
3
+ * by the native code renderable (tree-sitter). Unstyled text draws first, so
4
+ * the content never flashes empty while highlights resolve; unknown languages
5
+ * stay unstyled.
6
+ */
7
+
8
+ import React from "react";
9
+ import { createTextAttributes } from "@opentui/core";
10
+ import type { Theme } from "../theme";
11
+ import { useComponentTheme } from "./theme-context";
12
+ import { filetypeFor, syntaxStyleFor } from "./syntax-highlight";
13
+
14
+ /** A cut code block reads as removed: struck through and grayed, never red. */
15
+ const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
16
+
17
+ export interface CodeBlockProps {
18
+ id?: string;
19
+ /** Markdown fence info string ("tsx", "python", ...). */
20
+ language?: string;
21
+ content: string;
22
+ isCursor?: boolean;
23
+ /** Vertical rhythm above the block. */
24
+ marginTop?: number;
25
+ isAnnotated?: boolean;
26
+ /** Working-copy change tag rendered after the language label. */
27
+ changeTag?: "new" | "edited";
28
+ /** A cut (removed) block: struck-through gray content, no tag. */
29
+ cut?: boolean;
30
+ theme?: Theme;
31
+ }
32
+
33
+ export function CodeBlock({
34
+ id,
35
+ language,
36
+ content,
37
+ isCursor = false,
38
+ marginTop = 0,
39
+ isAnnotated = false,
40
+ changeTag,
41
+ cut = false,
42
+ theme,
43
+ }: CodeBlockProps): React.ReactNode {
44
+ const tokens = useComponentTheme(theme);
45
+ const tagColor = changeTag === "new" ? tokens.green : tokens.accent;
46
+ return (
47
+ <box id={id} style={{ flexDirection: "column", marginTop }}>
48
+ <text>
49
+ <span fg={isCursor ? tokens.accent : tokens.textDim}>{isCursor ? "▎ " : " "}</span>
50
+ <span fg={tokens.textDim}>{language ?? "code"}</span>
51
+ {isAnnotated ? <span fg={tokens.accent}> ◆</span> : null}
52
+ {changeTag ? <span fg={tagColor}> [{changeTag}]</span> : null}
53
+ </text>
54
+ <box
55
+ style={{
56
+ flexDirection: "column",
57
+ backgroundColor: tokens.elevated,
58
+ paddingLeft: 2,
59
+ paddingRight: 2,
60
+ paddingTop: 1,
61
+ paddingBottom: 1,
62
+ marginLeft: 2,
63
+ }}
64
+ >
65
+ {cut ? (
66
+ <text style={{ wrapMode: "none" }} selectable={false}>
67
+ <span fg={tokens.textDim} attributes={CUT_ATTRIBUTES}>
68
+ {content}
69
+ </span>
70
+ </text>
71
+ ) : (
72
+ <code
73
+ content={content}
74
+ filetype={filetypeFor(language)}
75
+ syntaxStyle={syntaxStyleFor(tokens)}
76
+ selectable={false}
77
+ style={{ wrapMode: "none", fg: tokens.textMuted, bg: tokens.elevated }}
78
+ />
79
+ )}
80
+ </box>
81
+ </box>
82
+ );
83
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { CompletionOverlay } from "./CompletionOverlay";
5
+
6
+ export const meta: StoryMeta = { title: "CompletionOverlay" };
7
+
8
+ export const ApprovedPrompt: Story = {
9
+ render: () => <CompletionOverlay verdict="approve" completion={{ phase: "prompt" }} status="" />,
10
+ expectedColors: [DARK.green],
11
+ };
12
+
13
+ export const FeedbackCounting: Story = {
14
+ render: () => (
15
+ <CompletionOverlay
16
+ verdict="request_changes"
17
+ completion={{ phase: "counting", remaining: 3 }}
18
+ status="exported to vault/cueloop/plan.md"
19
+ returnsTo="agent/worker-3"
20
+ />
21
+ ),
22
+ expectedColors: [DARK.accent],
23
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Post-submit hand-back overlay: the agent is unblocked, the reviewer either
3
+ * closes now, opts into auto-close, or dismisses back to the resolved view.
4
+ * The latest status line (e.g. the vault-export path) stays visible here.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { VerdictKind } from "@cueloop/schema";
9
+ import type { Theme } from "../theme";
10
+ import { useComponentTheme } from "./theme-context";
11
+
12
+ export interface CompletionOverlayProps {
13
+ verdict: VerdictKind;
14
+ completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
15
+ /** Latest status line (e.g. the vault-export path) stays visible here. */
16
+ status: string;
17
+ /** Where focus goes on close (the agent's pane), when known. */
18
+ returnsTo?: string;
19
+ theme?: Theme;
20
+ }
21
+
22
+ export function CompletionOverlay({
23
+ verdict,
24
+ completion,
25
+ status,
26
+ returnsTo,
27
+ theme,
28
+ }: CompletionOverlayProps): React.ReactNode {
29
+ const tokens = useComponentTheme(theme);
30
+ const approved = verdict === "approve";
31
+ return (
32
+ <box
33
+ style={{
34
+ width: "100%",
35
+ height: "100%",
36
+ backgroundColor: tokens.background,
37
+ flexDirection: "column",
38
+ justifyContent: "center",
39
+ alignItems: "center",
40
+ }}
41
+ >
42
+ <text fg={approved ? tokens.green : tokens.accent}>
43
+ {approved ? "review approved" : "feedback sent"}
44
+ </text>
45
+ <text> </text>
46
+ <text fg={tokens.text}>
47
+ The agent has your {approved ? "approval" : "feedback"} and is unblocked.
48
+ </text>
49
+ {returnsTo ? <text fg={tokens.textDim}>returning to {returnsTo} on close</text> : null}
50
+ {status ? <text fg={tokens.textDim}>{status}</text> : null}
51
+ <text> </text>
52
+ <text fg={tokens.textDim}>
53
+ close [return]
54
+ {completion.phase === "counting" ? ` · closing in ${completion.remaining}s` : ""}
55
+ {" · return to plan [esc] · always [a]"}
56
+ </text>
57
+ </box>
58
+ );
59
+ }
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { ConfirmCard } from "./ConfirmCard";
5
+
6
+ export const meta: StoryMeta = { title: "ConfirmCard" };
7
+
8
+ const callbacks = {
9
+ onInput: () => {},
10
+ onSelectVerdict: () => {},
11
+ onSubmit: () => {},
12
+ onCancel: () => {},
13
+ };
14
+
15
+ export const ApproveDefault: Story = {
16
+ render: () => <ConfirmCard verdict="approve" summary="" {...callbacks} />,
17
+ expectedColors: [DARK.green, DARK.accent],
18
+ size: { width: 40, height: 14 },
19
+ };
20
+
21
+ export const RequestChanges: Story = {
22
+ render: () => (
23
+ <ConfirmCard verdict="request_changes" summary="Tighten the steps." {...callbacks} />
24
+ ),
25
+ expectedColors: [DARK.red],
26
+ size: { width: 40, height: 14 },
27
+ };
28
+
29
+ export const WithViewedSummary: Story = {
30
+ render: () => (
31
+ <ConfirmCard verdict="approve" summary="" viewedSummary="2/3 files viewed" {...callbacks} />
32
+ ),
33
+ expectedColors: [DARK.green, DARK.textDim],
34
+ size: { width: 40, height: 14 },
35
+ };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The Submit button expanded into a bordered confirm card at the rail bottom:
3
+ * the verdict selector (arrow keys or click), the optional summary, and plain
4
+ * word-buttons - key hints live in the status line only. The bordered height
5
+ * derives from the content rows through Card, so layout and render never drift.
6
+ */
7
+
8
+ import React from "react";
9
+ import type { VerdictKind } from "@cueloop/schema";
10
+ import type { Theme } from "../theme";
11
+ import { useComponentTheme } from "./theme-context";
12
+ import { Card } from "./primitives/Card";
13
+ import { Button } from "./primitives/Button";
14
+ import { Toolbar } from "./primitives/Toolbar";
15
+
16
+ export const VERDICTS: VerdictKind[] = ["comment", "approve", "request_changes"];
17
+
18
+ /** Selector words in the confirm card - one word per verdict. */
19
+ export const VERDICT_LABEL: Record<VerdictKind, string> = {
20
+ comment: "Comment",
21
+ approve: "Approve",
22
+ request_changes: "Changes",
23
+ };
24
+
25
+ export interface ConfirmCardProps {
26
+ verdict: VerdictKind;
27
+ summary: string;
28
+ /**
29
+ * The guided walk's honest coverage line for diff sessions, e.g.
30
+ * "2/3 files viewed". Undefined = no walk data, the row does not render.
31
+ */
32
+ viewedSummary?: string;
33
+ onInput: (summary: string) => void;
34
+ onSelectVerdict: (verdict: VerdictKind) => void;
35
+ onSubmit: () => void;
36
+ onCancel: () => void;
37
+ theme?: Theme;
38
+ }
39
+
40
+ /** 1-row verdict selector, spacer, summary input, spacer, buttons. */
41
+ const CONFIRM_CONTENT_ROWS = 5;
42
+
43
+ export function verdictColor(verdict: VerdictKind, tokens: Theme): string {
44
+ return verdict === "approve"
45
+ ? tokens.green
46
+ : verdict === "request_changes"
47
+ ? tokens.red
48
+ : tokens.blue;
49
+ }
50
+
51
+ /**
52
+ * The verdict selector: one row of pressable words, matching the reading
53
+ * direction of a choice between three peers. Selection stays controlled by
54
+ * the grammar (←/→ cycle the verdict); a click selects directly. The
55
+ * selected verdict wears brackets and its color.
56
+ */
57
+ function VerdictSelector({
58
+ verdict,
59
+ onSelectVerdict,
60
+ theme,
61
+ }: {
62
+ verdict: VerdictKind;
63
+ onSelectVerdict: (verdict: VerdictKind) => void;
64
+ theme?: Theme;
65
+ }): React.ReactNode {
66
+ const tokens = useComponentTheme(theme);
67
+ return (
68
+ <box style={{ flexDirection: "row", height: 1 }}>
69
+ {VERDICTS.map((candidate) => (
70
+ <box
71
+ key={candidate}
72
+ style={{ paddingRight: 1 }}
73
+ onMouseUp={() => onSelectVerdict(candidate)}
74
+ >
75
+ <text fg={candidate === verdict ? verdictColor(candidate, tokens) : tokens.textDim}>
76
+ {candidate === verdict
77
+ ? `[${VERDICT_LABEL[candidate]}]`
78
+ : ` ${VERDICT_LABEL[candidate]} `}
79
+ </text>
80
+ </box>
81
+ ))}
82
+ </box>
83
+ );
84
+ }
85
+
86
+ export function ConfirmCard({
87
+ verdict,
88
+ summary,
89
+ viewedSummary,
90
+ onInput,
91
+ onSelectVerdict,
92
+ onSubmit,
93
+ onCancel,
94
+ theme,
95
+ }: ConfirmCardProps): React.ReactNode {
96
+ const tokens = useComponentTheme(theme);
97
+ return (
98
+ <Card
99
+ title=" submit review "
100
+ contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 2 : 0)}
101
+ borderColor={tokens.text}
102
+ backgroundColor="transparent"
103
+ marginRight={1}
104
+ theme={theme}
105
+ >
106
+ {viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
107
+ {viewedSummary !== undefined ? <box style={{ height: 1 }} /> : null}
108
+ <VerdictSelector verdict={verdict} onSelectVerdict={onSelectVerdict} theme={theme} />
109
+ <box style={{ height: 1 }} />
110
+ <input
111
+ focused
112
+ value={summary}
113
+ onInput={onInput}
114
+ placeholder="summary for the agent (optional)"
115
+ />
116
+ <box style={{ height: 1 }} />
117
+ <Toolbar>
118
+ <Button variant="solid" marginRight={2} onPress={onSubmit} theme={theme}>
119
+ {" Submit "}
120
+ </Button>
121
+ <Button onPress={onCancel} theme={theme}>
122
+ {" Cancel "}
123
+ </Button>
124
+ </Toolbar>
125
+ </Card>
126
+ );
127
+ }