@cueloop/client 0.1.0-alpha.33 → 0.1.0-alpha.35
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.
- package/package.json +27 -26
- package/src/App.autoclose.test.tsx +7 -4
- package/src/App.diff.test.tsx +130 -4
- package/src/App.export.test.tsx +22 -4
- package/src/App.plan-review.test.tsx +127 -27
- package/src/App.readonly.test.tsx +31 -8
- package/src/App.rendering.test.tsx +12 -3
- package/src/App.review-panel.test.tsx +11 -3
- package/src/App.submit-confirm.test.tsx +12 -7
- package/src/App.test.tsx +46 -9
- package/src/App.toast.test.tsx +10 -3
- package/src/App.tsx +400 -100
- package/src/App.walk.test.tsx +30 -10
- package/src/attribution.ts +8 -2
- package/src/clipboard.ts +5 -1
- package/src/components/AnnotationCard.stories.tsx +28 -9
- package/src/components/AnnotationCard.tsx +53 -38
- package/src/components/Breadcrumb.tsx +7 -1
- package/src/components/CodeBlock.stories.tsx +19 -2
- package/src/components/CodeBlock.tsx +24 -9
- package/src/components/CompletionOverlay.tsx +13 -3
- package/src/components/ConfirmCard.stories.tsx +22 -3
- package/src/components/ConfirmCard.tsx +22 -5
- package/src/components/ConfirmDialog.tsx +9 -1
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +18 -2
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +184 -98
- package/src/components/InboxList.stories.tsx +16 -3
- package/src/components/InboxList.tsx +17 -10
- package/src/components/KeybindsDialog.tsx +19 -3
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +102 -0
- package/src/components/MenuBar.stories.tsx +5 -1
- package/src/components/MenuBar.tsx +18 -2
- package/src/components/PlanSheet.stories.tsx +34 -2
- package/src/components/PlanSheet.tsx +141 -32
- package/src/components/PromptDialog.tsx +18 -2
- package/src/components/ReviewPanel.stories.tsx +13 -3
- package/src/components/ReviewPanel.tsx +8 -4
- package/src/components/ReviewRail.stories.tsx +145 -72
- package/src/components/ReviewRail.tsx +177 -38
- package/src/components/SettingsDialog.stories.tsx +51 -3
- package/src/components/SettingsDialog.tsx +8 -3
- package/src/components/SettingsRows.tsx +19 -3
- package/src/components/Toast.tsx +17 -2
- package/src/components/WalkWizard.stories.tsx +13 -2
- package/src/components/WalkWizard.tsx +9 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +281 -83
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.tsx +5 -1
- package/src/components/primitives/Dialog.tsx +9 -1
- package/src/components/primitives/Tabs.tsx +24 -4
- package/src/components/stories-app.tsx +34 -6
- package/src/components/story-fixtures.ts +11 -3
- package/src/components/story.ts +3 -1
- package/src/components/syntax-highlight.ts +47 -12
- package/src/components/theme-context.stories.tsx +6 -1
- package/src/components/theme-context.tsx +7 -1
- package/src/config.test.ts +178 -2
- package/src/config.ts +121 -23
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +13 -4
- package/src/editor.ts +31 -5
- package/src/herdr.ts +8 -2
- package/src/index.ts +9 -1
- package/src/integrations.ts +3 -1
- package/src/intent-dispatch.test.ts +193 -9
- package/src/intent-dispatch.ts +151 -18
- package/src/key-bindings.test.ts +19 -9
- package/src/key-bindings.ts +36 -14
- package/src/keymap.test.ts +171 -26
- package/src/keymap.ts +83 -16
- package/src/review-panel.test.ts +3 -1
- package/src/review-panel.ts +9 -2
- package/src/serve.test.ts +11 -3
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +65 -17
- package/src/session-controller.ts +301 -26
- package/src/share.ts +41 -8
- package/src/test-support.ts +16 -6
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +1 -3
- package/src/view-diff.ts +15 -4
- package/src/view-plan.test.ts +40 -6
- package/src/view-plan.ts +71 -28
- package/src/walk.test.ts +7 -2
- package/src/walk.ts +7 -2
package/src/App.walk.test.tsx
CHANGED
|
@@ -10,7 +10,13 @@ import { DaemonServer } from "@cueloop/daemon";
|
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
12
|
import { DARK } from "./theme";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
isolateUserConfig,
|
|
15
|
+
press,
|
|
16
|
+
waitForState,
|
|
17
|
+
waitForText,
|
|
18
|
+
waitForTextGone,
|
|
19
|
+
} from "./test-support";
|
|
14
20
|
|
|
15
21
|
const PATCH = `diff --git a/src/a.ts b/src/a.ts
|
|
16
22
|
index 111..222 100644
|
|
@@ -59,7 +65,10 @@ afterEach(() => {
|
|
|
59
65
|
});
|
|
60
66
|
|
|
61
67
|
async function renderApp() {
|
|
62
|
-
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
68
|
+
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
69
|
+
width: 120,
|
|
70
|
+
height: 34,
|
|
71
|
+
});
|
|
63
72
|
// Wait for session content, not just the chrome: pressing w before the diff
|
|
64
73
|
// loads drops the walk intent (no session, no diff view) and hangs the test.
|
|
65
74
|
await waitForText(setup, "src/a.ts");
|
|
@@ -75,7 +84,9 @@ function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
|
75
84
|
for (const span of line.spans) {
|
|
76
85
|
if (!span.text.includes(needle)) continue;
|
|
77
86
|
const [red, green, blue] = span.fg.toInts();
|
|
78
|
-
foregrounds.push(
|
|
87
|
+
foregrounds.push(
|
|
88
|
+
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
89
|
+
);
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
return foregrounds;
|
|
@@ -100,7 +111,9 @@ describe("the guided walk", () => {
|
|
|
100
111
|
// Assert
|
|
101
112
|
await waitForText(setup, "file 2 of 3 · 1 viewed");
|
|
102
113
|
// the viewed mark rides the session record, not the client
|
|
103
|
-
await waitForState(setup, () =>
|
|
114
|
+
await waitForState(setup, () =>
|
|
115
|
+
(server.core.sessionGet(session.id).viewedPaths ?? []).includes("src/a.ts"),
|
|
116
|
+
);
|
|
104
117
|
|
|
105
118
|
// Act
|
|
106
119
|
// [ steps back; the revisited card shows its viewed marker
|
|
@@ -128,7 +141,11 @@ describe("the guided walk", () => {
|
|
|
128
141
|
// Assert
|
|
129
142
|
await waitForText(setup, "walk complete");
|
|
130
143
|
expect(setup.captureCharFrame()).toContain("every file viewed (3/3)");
|
|
131
|
-
expect(server.core.sessionGet(session.id).viewedPaths).toEqual([
|
|
144
|
+
expect(server.core.sessionGet(session.id).viewedPaths).toEqual([
|
|
145
|
+
"src/a.ts",
|
|
146
|
+
"src/b.ts",
|
|
147
|
+
"src/c.ts",
|
|
148
|
+
]);
|
|
132
149
|
|
|
133
150
|
// Act
|
|
134
151
|
// the end card's return leaves the walk and opens the rail confirm
|
|
@@ -176,7 +193,10 @@ describe("the guided walk", () => {
|
|
|
176
193
|
await press(first, "]");
|
|
177
194
|
await press(first, "]");
|
|
178
195
|
await waitForText(first, "file 3 of 3 · 2 viewed");
|
|
179
|
-
await waitForState(
|
|
196
|
+
await waitForState(
|
|
197
|
+
first,
|
|
198
|
+
() => (server.core.sessionGet(session.id).viewedPaths ?? []).length === 2,
|
|
199
|
+
);
|
|
180
200
|
first.renderer.destroy();
|
|
181
201
|
|
|
182
202
|
// Act
|
|
@@ -237,17 +257,17 @@ describe("the guided walk", () => {
|
|
|
237
257
|
const setup = await renderApp();
|
|
238
258
|
|
|
239
259
|
// Assert
|
|
240
|
-
// before the walk the
|
|
260
|
+
// before the walk the changed word on the added line wears the insertion color
|
|
241
261
|
await waitForText(setup, "+new line");
|
|
242
|
-
expect(foregroundsOf(setup, "
|
|
262
|
+
expect(foregroundsOf(setup, "new")).toContain(DARK.insertedForeground);
|
|
243
263
|
|
|
244
264
|
// Act
|
|
245
265
|
await press(setup, "w");
|
|
246
266
|
|
|
247
267
|
// Assert
|
|
248
268
|
await waitForText(setup, "file 1 of 3 · 0 viewed");
|
|
249
|
-
// dimmed: the sheet
|
|
250
|
-
expect(foregroundsOf(setup, "
|
|
269
|
+
// dimmed: the sheet's changed word drops to the dim token...
|
|
270
|
+
expect(foregroundsOf(setup, "new")).toEqual([DARK.textDim]);
|
|
251
271
|
// ...while the wizard preview keeps the insertion color
|
|
252
272
|
expect(foregroundsOf(setup, "+export const b = 3;")).toContain(DARK.insertedForeground);
|
|
253
273
|
});
|
package/src/attribution.ts
CHANGED
|
@@ -13,7 +13,9 @@ export const ANONYMOUS_LABEL = "-- anonymous --";
|
|
|
13
13
|
|
|
14
14
|
/** A short, stable handle from a raw author id when no name is known. */
|
|
15
15
|
export function shortHandle(authorId: string): string {
|
|
16
|
-
const base = authorId.startsWith(FINGERPRINT_PREFIX)
|
|
16
|
+
const base = authorId.startsWith(FINGERPRINT_PREFIX)
|
|
17
|
+
? authorId.slice(FINGERPRINT_PREFIX.length)
|
|
18
|
+
: authorId;
|
|
17
19
|
return base.slice(0, 8) || authorId;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -22,7 +24,11 @@ export function shortHandle(authorId: string): string {
|
|
|
22
24
|
* own name, then anonymous for a known-but-unnamed participant, then a short
|
|
23
25
|
* handle for an id we hold no identity for.
|
|
24
26
|
*/
|
|
25
|
-
export function resolveDisplayName(
|
|
27
|
+
export function resolveDisplayName(
|
|
28
|
+
authorId: string,
|
|
29
|
+
participants: Identity[] | undefined,
|
|
30
|
+
overrides: Record<string, string>,
|
|
31
|
+
): string {
|
|
26
32
|
const override = overrides[authorId];
|
|
27
33
|
if (override) return override;
|
|
28
34
|
const identity = participants?.find((participant) => participant.id === authorId);
|
package/src/clipboard.ts
CHANGED
|
@@ -16,7 +16,11 @@ export function clipboardCommands(platform: NodeJS.Platform = process.platform):
|
|
|
16
16
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
17
17
|
for (const command of clipboardCommands()) {
|
|
18
18
|
try {
|
|
19
|
-
const proc = Bun.spawn(command, {
|
|
19
|
+
const proc = Bun.spawn(command, {
|
|
20
|
+
stdin: Buffer.from(text),
|
|
21
|
+
stdout: "ignore",
|
|
22
|
+
stderr: "ignore",
|
|
23
|
+
});
|
|
20
24
|
if ((await proc.exited) === 0) return true;
|
|
21
25
|
} catch {
|
|
22
26
|
// tool not installed - try the next one
|
|
@@ -9,34 +9,51 @@ const draftCallbacks = { onInput: () => {}, onSave: () => {}, onCancel: () => {}
|
|
|
9
9
|
|
|
10
10
|
export const CommentDraft: Story = {
|
|
11
11
|
render: () => (
|
|
12
|
-
<AnnotationCard
|
|
12
|
+
<AnnotationCard
|
|
13
|
+
kind="comment"
|
|
14
|
+
quote="persists sessions"
|
|
15
|
+
draft={{ text: "Which daemon?", ...draftCallbacks }}
|
|
16
|
+
/>
|
|
13
17
|
),
|
|
14
18
|
expectedColors: [DARK.accent],
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
export const
|
|
21
|
+
export const SavedSelected: Story = {
|
|
18
22
|
render: () => (
|
|
19
|
-
<AnnotationCard
|
|
23
|
+
<AnnotationCard
|
|
24
|
+
kind="comment"
|
|
25
|
+
quote="persists sessions"
|
|
26
|
+
saved={{
|
|
27
|
+
body: "Which daemon owns this?",
|
|
28
|
+
isSelected: true,
|
|
29
|
+
isOrphan: false,
|
|
30
|
+
isBlocking: false,
|
|
31
|
+
author: "me",
|
|
32
|
+
editing: null,
|
|
33
|
+
onPress: () => {},
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
20
36
|
),
|
|
21
|
-
expectedColors: [DARK.
|
|
37
|
+
expectedColors: [DARK.accent],
|
|
22
38
|
};
|
|
23
39
|
|
|
24
|
-
export const
|
|
40
|
+
export const SavedCollaborator: Story = {
|
|
25
41
|
render: () => (
|
|
26
42
|
<AnnotationCard
|
|
27
43
|
kind="comment"
|
|
28
44
|
quote="persists sessions"
|
|
29
45
|
saved={{
|
|
30
46
|
body: "Which daemon owns this?",
|
|
31
|
-
isSelected:
|
|
47
|
+
isSelected: false,
|
|
32
48
|
isOrphan: false,
|
|
33
49
|
isBlocking: false,
|
|
50
|
+
author: "Priya",
|
|
34
51
|
editing: null,
|
|
35
52
|
onPress: () => {},
|
|
36
53
|
}}
|
|
37
54
|
/>
|
|
38
55
|
),
|
|
39
|
-
expectedColors: [DARK.
|
|
56
|
+
expectedColors: [DARK.border],
|
|
40
57
|
};
|
|
41
58
|
|
|
42
59
|
export const SavedOrphanBlocking: Story = {
|
|
@@ -49,24 +66,26 @@ export const SavedOrphanBlocking: Story = {
|
|
|
49
66
|
isSelected: false,
|
|
50
67
|
isOrphan: true,
|
|
51
68
|
isBlocking: true,
|
|
69
|
+
author: "me",
|
|
52
70
|
editing: null,
|
|
53
71
|
onPress: () => {},
|
|
54
72
|
}}
|
|
55
73
|
/>
|
|
56
74
|
),
|
|
57
|
-
expectedColors: [DARK.
|
|
75
|
+
expectedColors: [DARK.border],
|
|
58
76
|
};
|
|
59
77
|
|
|
60
78
|
export const SavedEditing: Story = {
|
|
61
79
|
render: () => (
|
|
62
80
|
<AnnotationCard
|
|
63
|
-
kind="
|
|
81
|
+
kind="comment"
|
|
64
82
|
quote="move the store"
|
|
65
83
|
saved={{
|
|
66
84
|
body: "old body",
|
|
67
85
|
isSelected: true,
|
|
68
86
|
isOrphan: false,
|
|
69
87
|
isBlocking: false,
|
|
88
|
+
author: "me",
|
|
70
89
|
editing: { text: "new body", ...draftCallbacks },
|
|
71
90
|
onPress: () => {},
|
|
72
91
|
}}
|
|
@@ -32,10 +32,8 @@ export interface AnnotationSaved {
|
|
|
32
32
|
isSelected: boolean;
|
|
33
33
|
isOrphan: boolean;
|
|
34
34
|
isBlocking: boolean;
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/** The viewer's own note when collaborators are also present (e.g. "me"). Set = tag-in-border, accent. */
|
|
38
|
-
selfLabel?: string;
|
|
35
|
+
/** Border-title author: a collaborator's display name, or "me" for the reviewer's own note. */
|
|
36
|
+
author: string;
|
|
39
37
|
/** Non-null while the card body is being rewritten in place. */
|
|
40
38
|
editing: AnnotationDraft | null;
|
|
41
39
|
onPress: () => void;
|
|
@@ -76,7 +74,11 @@ export const COMPOSE_MAX_ROWS = 4;
|
|
|
76
74
|
* newlines only. Beyond the cap the textarea scrolls internally and keeps the
|
|
77
75
|
* caret line in view.
|
|
78
76
|
*/
|
|
79
|
-
export function composeRowCount(
|
|
77
|
+
export function composeRowCount(
|
|
78
|
+
text: string,
|
|
79
|
+
contentWidth: number,
|
|
80
|
+
cap: number = COMPOSE_MAX_ROWS,
|
|
81
|
+
): number {
|
|
80
82
|
const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
|
|
81
83
|
let visualRowCount = 0;
|
|
82
84
|
for (const line of text.split("\n")) {
|
|
@@ -107,7 +109,7 @@ function DraftEditor({
|
|
|
107
109
|
editor.cursorOffset = initialTextLength.current;
|
|
108
110
|
// the box is laid out now, so its width is known: size to the wrapped body
|
|
109
111
|
onRowsChange(composeRowCount(editor.plainText, editor.width));
|
|
110
|
-
}, []);
|
|
112
|
+
}, [onRowsChange]);
|
|
111
113
|
return (
|
|
112
114
|
<>
|
|
113
115
|
<textarea
|
|
@@ -135,16 +137,23 @@ function DraftEditor({
|
|
|
135
137
|
{" Save "}
|
|
136
138
|
</Button>
|
|
137
139
|
<Button onPress={draft.onCancel} theme={theme}>
|
|
138
|
-
{" Cancel
|
|
140
|
+
{" Cancel "}
|
|
139
141
|
</Button>
|
|
140
142
|
</Toolbar>
|
|
141
143
|
</>
|
|
142
144
|
);
|
|
143
145
|
}
|
|
144
146
|
|
|
145
|
-
export function AnnotationCard({
|
|
147
|
+
export function AnnotationCard({
|
|
148
|
+
id,
|
|
149
|
+
kind,
|
|
150
|
+
quote,
|
|
151
|
+
draft,
|
|
152
|
+
saved,
|
|
153
|
+
theme,
|
|
154
|
+
}: AnnotationCardProps): React.ReactNode {
|
|
146
155
|
const tokens = useComponentTheme(theme);
|
|
147
|
-
const kindColor =
|
|
156
|
+
const kindColor = tokens.accent;
|
|
148
157
|
const activeDraft = draft ?? saved?.editing ?? null;
|
|
149
158
|
// The composer's visible height, grown from the wrapped text. It is shared
|
|
150
159
|
// with the card frame (contentRows) so the border never clips the textarea
|
|
@@ -156,57 +165,63 @@ export function AnnotationCard({ id, kind, quote, draft, saved, theme }: Annotat
|
|
|
156
165
|
composeRowCount(activeDraft?.text ?? "", 0),
|
|
157
166
|
);
|
|
158
167
|
if (draft) {
|
|
159
|
-
const verb =
|
|
168
|
+
const verb = "comment on";
|
|
160
169
|
return (
|
|
161
170
|
<Card
|
|
162
171
|
title={` ${verb} "${truncateToSingleLine(quote, 40)}" `}
|
|
163
172
|
contentRows={editorRowCount + 1}
|
|
164
173
|
borderColor={kindColor}
|
|
165
174
|
backgroundColor="transparent"
|
|
166
|
-
marginLeft={2}
|
|
167
|
-
marginRight={2}
|
|
168
175
|
theme={theme}
|
|
169
176
|
>
|
|
170
|
-
<DraftEditor
|
|
177
|
+
<DraftEditor
|
|
178
|
+
draft={draft}
|
|
179
|
+
rows={editorRowCount}
|
|
180
|
+
onRowsChange={setEditorRowCount}
|
|
181
|
+
theme={theme}
|
|
182
|
+
/>
|
|
171
183
|
</Card>
|
|
172
184
|
);
|
|
173
185
|
}
|
|
174
186
|
const card = saved!;
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const borderInset = borderLabel ? 4 : 0;
|
|
187
|
+
// border = "what + who" (+ any exceptional flag); body = the content only
|
|
188
|
+
const flags = [card.isBlocking ? "BLOCKING" : null, card.isOrphan ? "ORPHANED" : null].filter(
|
|
189
|
+
(flag): flag is string => flag !== null,
|
|
190
|
+
);
|
|
191
|
+
const title = ` ${[kind.toUpperCase(), card.author, ...flags].join(" · ")} `;
|
|
192
|
+
// every saved card is bordered, so the border + side padding always eat this width
|
|
193
|
+
const borderInset = 4;
|
|
194
|
+
// a bordered box must declare a height or it collapses: quote + body, or the editor
|
|
195
|
+
const contentRows = card.editing ? editorRowCount + 2 : 2;
|
|
185
196
|
return (
|
|
186
197
|
<box
|
|
187
198
|
id={id}
|
|
188
|
-
title={
|
|
199
|
+
title={title}
|
|
189
200
|
style={{
|
|
190
201
|
flexDirection: "column",
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
height: cardHeight(contentRows),
|
|
203
|
+
border: true,
|
|
204
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
205
|
+
// cards stay transparent in every theme; selection is the bright
|
|
206
|
+
// kind-colored border, while unselected cards recede to a dim border
|
|
207
|
+
borderColor: card.isSelected || card.editing ? kindColor : tokens.border,
|
|
208
|
+
paddingLeft: 1,
|
|
209
|
+
paddingRight: 1,
|
|
196
210
|
}}
|
|
197
211
|
onMouseUp={card.onPress}
|
|
198
212
|
>
|
|
199
|
-
<text fg={
|
|
200
|
-
{marker}
|
|
201
|
-
{kind.toUpperCase()}
|
|
202
|
-
{card.isBlocking ? <span fg={tokens.red}> · BLOCKING</span> : null}
|
|
203
|
-
<span fg={tokens.textDim}>{card.isOrphan ? " · ORPHANED" : " · pending"}</span>
|
|
204
|
-
</text>
|
|
205
|
-
<text fg={tokens.textDim}>{indent}"{truncateToSingleLine(quote, 26 - borderInset)}"</text>
|
|
213
|
+
<text fg={tokens.textDim}>"{truncateToSingleLine(quote, 26 - borderInset)}"</text>
|
|
206
214
|
{card.editing ? (
|
|
207
|
-
<DraftEditor
|
|
215
|
+
<DraftEditor
|
|
216
|
+
draft={card.editing}
|
|
217
|
+
rows={editorRowCount}
|
|
218
|
+
onRowsChange={setEditorRowCount}
|
|
219
|
+
theme={theme}
|
|
220
|
+
/>
|
|
208
221
|
) : (
|
|
209
|
-
<text fg={card.isOrphan ? tokens.textDim : tokens.textMuted}>
|
|
222
|
+
<text fg={card.isOrphan ? tokens.textDim : tokens.textMuted}>
|
|
223
|
+
{truncateToSingleLine(card.body, 28 - borderInset)}
|
|
224
|
+
</text>
|
|
210
225
|
)}
|
|
211
226
|
</box>
|
|
212
227
|
);
|
|
@@ -23,7 +23,13 @@ export interface BreadcrumbProps {
|
|
|
23
23
|
export function Breadcrumb({ items, theme }: BreadcrumbProps): React.ReactNode {
|
|
24
24
|
const tokens = useComponentTheme(theme);
|
|
25
25
|
const toneColor = (tone?: BreadcrumbTone): string =>
|
|
26
|
-
tone === "accent"
|
|
26
|
+
tone === "accent"
|
|
27
|
+
? tokens.accent
|
|
28
|
+
: tone === "green"
|
|
29
|
+
? tokens.green
|
|
30
|
+
: tone === "text"
|
|
31
|
+
? tokens.text
|
|
32
|
+
: tokens.textDim;
|
|
27
33
|
return (
|
|
28
34
|
<box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1, flexDirection: "row" }}>
|
|
29
35
|
<text fg={tokens.text}>
|
|
@@ -16,10 +16,27 @@ export const Typescript: Story = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export const CursorAnnotatedEdited: Story = {
|
|
19
|
-
render: () =>
|
|
19
|
+
render: () => (
|
|
20
|
+
<CodeBlock
|
|
21
|
+
language="ts"
|
|
22
|
+
content={SNIPPET}
|
|
23
|
+
isCursor
|
|
24
|
+
isAnnotated
|
|
25
|
+
changeTag="edited"
|
|
26
|
+
marginTop={1}
|
|
27
|
+
/>
|
|
28
|
+
),
|
|
20
29
|
expectedColors: [DARK.accent],
|
|
21
30
|
};
|
|
22
31
|
|
|
23
32
|
export const UnknownLanguage: Story = {
|
|
24
|
-
render: () =>
|
|
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],
|
|
25
42
|
};
|
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React from "react";
|
|
9
|
+
import { createTextAttributes } from "@opentui/core";
|
|
9
10
|
import type { Theme } from "../theme";
|
|
10
11
|
import { useComponentTheme } from "./theme-context";
|
|
11
12
|
import { filetypeFor, syntaxStyleFor } from "./syntax-highlight";
|
|
12
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
|
+
|
|
13
17
|
export interface CodeBlockProps {
|
|
14
18
|
id?: string;
|
|
15
19
|
/** Markdown fence info string ("tsx", "python", ...). */
|
|
@@ -20,7 +24,9 @@ export interface CodeBlockProps {
|
|
|
20
24
|
marginTop?: number;
|
|
21
25
|
isAnnotated?: boolean;
|
|
22
26
|
/** Working-copy change tag rendered after the language label. */
|
|
23
|
-
changeTag?: "
|
|
27
|
+
changeTag?: "new" | "edited";
|
|
28
|
+
/** A cut (removed) block: struck-through gray content, no tag. */
|
|
29
|
+
cut?: boolean;
|
|
24
30
|
theme?: Theme;
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -32,10 +38,11 @@ export function CodeBlock({
|
|
|
32
38
|
marginTop = 0,
|
|
33
39
|
isAnnotated = false,
|
|
34
40
|
changeTag,
|
|
41
|
+
cut = false,
|
|
35
42
|
theme,
|
|
36
43
|
}: CodeBlockProps): React.ReactNode {
|
|
37
44
|
const tokens = useComponentTheme(theme);
|
|
38
|
-
const tagColor = changeTag === "
|
|
45
|
+
const tagColor = changeTag === "new" ? tokens.green : tokens.accent;
|
|
39
46
|
return (
|
|
40
47
|
<box id={id} style={{ flexDirection: "column", marginTop }}>
|
|
41
48
|
<text>
|
|
@@ -55,13 +62,21 @@ export function CodeBlock({
|
|
|
55
62
|
marginLeft: 2,
|
|
56
63
|
}}
|
|
57
64
|
>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
)}
|
|
65
80
|
</box>
|
|
66
81
|
</box>
|
|
67
82
|
);
|
|
@@ -19,7 +19,13 @@ export interface CompletionOverlayProps {
|
|
|
19
19
|
theme?: Theme;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function CompletionOverlay({
|
|
22
|
+
export function CompletionOverlay({
|
|
23
|
+
verdict,
|
|
24
|
+
completion,
|
|
25
|
+
status,
|
|
26
|
+
returnsTo,
|
|
27
|
+
theme,
|
|
28
|
+
}: CompletionOverlayProps): React.ReactNode {
|
|
23
29
|
const tokens = useComponentTheme(theme);
|
|
24
30
|
const approved = verdict === "approve";
|
|
25
31
|
return (
|
|
@@ -33,9 +39,13 @@ export function CompletionOverlay({ verdict, completion, status, returnsTo, them
|
|
|
33
39
|
alignItems: "center",
|
|
34
40
|
}}
|
|
35
41
|
>
|
|
36
|
-
<text fg={approved ? tokens.green : tokens.accent}>
|
|
42
|
+
<text fg={approved ? tokens.green : tokens.accent}>
|
|
43
|
+
{approved ? "review approved" : "feedback sent"}
|
|
44
|
+
</text>
|
|
37
45
|
<text> </text>
|
|
38
|
-
<text fg={tokens.text}>
|
|
46
|
+
<text fg={tokens.text}>
|
|
47
|
+
The agent has your {approved ? "approval" : "feedback"} and is unblocked.
|
|
48
|
+
</text>
|
|
39
49
|
{returnsTo ? <text fg={tokens.textDim}>returning to {returnsTo} on close</text> : null}
|
|
40
50
|
{status ? <text fg={tokens.textDim}>{status}</text> : null}
|
|
41
51
|
<text> </text>
|
|
@@ -5,17 +5,36 @@ import { ConfirmCard } from "./ConfirmCard";
|
|
|
5
5
|
|
|
6
6
|
export const meta: StoryMeta = { title: "ConfirmCard" };
|
|
7
7
|
|
|
8
|
-
const callbacks = {
|
|
8
|
+
const callbacks = {
|
|
9
|
+
onInput: () => {},
|
|
10
|
+
onSelectVerdict: () => {},
|
|
11
|
+
onSubmit: () => {},
|
|
12
|
+
onCancel: () => {},
|
|
13
|
+
};
|
|
9
14
|
|
|
10
15
|
export const ApproveDefault: Story = {
|
|
11
|
-
render: () =>
|
|
16
|
+
render: () => (
|
|
17
|
+
<ConfirmCard
|
|
18
|
+
verdict="approve"
|
|
19
|
+
summary=""
|
|
20
|
+
annotationCount={0}
|
|
21
|
+
blockingCount={0}
|
|
22
|
+
{...callbacks}
|
|
23
|
+
/>
|
|
24
|
+
),
|
|
12
25
|
expectedColors: [DARK.green, DARK.accent],
|
|
13
26
|
size: { width: 40, height: 14 },
|
|
14
27
|
};
|
|
15
28
|
|
|
16
29
|
export const RequestChanges: Story = {
|
|
17
30
|
render: () => (
|
|
18
|
-
<ConfirmCard
|
|
31
|
+
<ConfirmCard
|
|
32
|
+
verdict="request_changes"
|
|
33
|
+
summary="Tighten the steps."
|
|
34
|
+
annotationCount={3}
|
|
35
|
+
blockingCount={1}
|
|
36
|
+
{...callbacks}
|
|
37
|
+
/>
|
|
19
38
|
),
|
|
20
39
|
expectedColors: [DARK.red],
|
|
21
40
|
size: { width: 40, height: 14 },
|
|
@@ -44,7 +44,11 @@ export interface ConfirmCardProps {
|
|
|
44
44
|
const CONFIRM_CONTENT_ROWS = 7;
|
|
45
45
|
|
|
46
46
|
export function verdictColor(verdict: VerdictKind, tokens: Theme): string {
|
|
47
|
-
return verdict === "approve"
|
|
47
|
+
return verdict === "approve"
|
|
48
|
+
? tokens.green
|
|
49
|
+
: verdict === "request_changes"
|
|
50
|
+
? tokens.red
|
|
51
|
+
: tokens.blue;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -66,9 +70,15 @@ function VerdictSelector({
|
|
|
66
70
|
return (
|
|
67
71
|
<box style={{ flexDirection: "row", height: 1 }}>
|
|
68
72
|
{VERDICTS.map((candidate) => (
|
|
69
|
-
<box
|
|
73
|
+
<box
|
|
74
|
+
key={candidate}
|
|
75
|
+
style={{ paddingRight: 1 }}
|
|
76
|
+
onMouseUp={() => onSelectVerdict(candidate)}
|
|
77
|
+
>
|
|
70
78
|
<text fg={candidate === verdict ? verdictColor(candidate, tokens) : tokens.textDim}>
|
|
71
|
-
{candidate === verdict
|
|
79
|
+
{candidate === verdict
|
|
80
|
+
? `[${VERDICT_LABEL[candidate]}]`
|
|
81
|
+
: ` ${VERDICT_LABEL[candidate]} `}
|
|
72
82
|
</text>
|
|
73
83
|
</box>
|
|
74
84
|
))}
|
|
@@ -98,12 +108,19 @@ export function ConfirmCard({
|
|
|
98
108
|
marginRight={1}
|
|
99
109
|
theme={theme}
|
|
100
110
|
>
|
|
101
|
-
<text
|
|
111
|
+
<text
|
|
112
|
+
fg={tokens.textDim}
|
|
113
|
+
>{`${annotationCount} annotations · ${blockingCount} blocking`}</text>
|
|
102
114
|
{viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
|
|
103
115
|
<box style={{ height: 1 }} />
|
|
104
116
|
<VerdictSelector verdict={verdict} onSelectVerdict={onSelectVerdict} theme={theme} />
|
|
105
117
|
<box style={{ height: 1 }} />
|
|
106
|
-
<input
|
|
118
|
+
<input
|
|
119
|
+
focused
|
|
120
|
+
value={summary}
|
|
121
|
+
onInput={onInput}
|
|
122
|
+
placeholder="summary for the agent (optional)"
|
|
123
|
+
/>
|
|
107
124
|
<box style={{ height: 1 }} />
|
|
108
125
|
<Toolbar>
|
|
109
126
|
<Button variant="solid" marginRight={2} onPress={onSubmit} theme={theme}>
|
|
@@ -39,7 +39,15 @@ export function ConfirmDialog({
|
|
|
39
39
|
if (!isOpen) return null;
|
|
40
40
|
return (
|
|
41
41
|
<Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
|
|
42
|
-
<box
|
|
42
|
+
<box
|
|
43
|
+
style={{
|
|
44
|
+
flexDirection: "column",
|
|
45
|
+
flexGrow: 1,
|
|
46
|
+
paddingLeft: 1,
|
|
47
|
+
paddingRight: 1,
|
|
48
|
+
paddingTop: 1,
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
43
51
|
<text fg={tokens.text}>{message}</text>
|
|
44
52
|
<box style={{ flexGrow: 1 }} />
|
|
45
53
|
<Toolbar>
|