@cueloop/client 0.1.0-alpha.44 → 0.1.0-alpha.46
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 +6 -5
- package/src/App.plan-review.test.tsx +16 -2
- package/src/App.readonly.test.tsx +3 -3
- package/src/App.submit-confirm.test.tsx +6 -8
- package/src/App.tsx +39 -23
- package/src/App.walk.test.tsx +2 -1
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/components/AnnotationCard.stories.tsx +7 -11
- package/src/components/AnnotationCard.tsx +13 -11
- package/src/components/ConfirmCard.stories.tsx +3 -24
- package/src/components/ConfirmCard.tsx +7 -15
- package/src/components/MarkerPopover.tsx +7 -1
- package/src/components/PlanSheet.tsx +115 -48
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +228 -0
- package/src/components/ReviewRail.stories.tsx +0 -2
- package/src/components/ReviewRail.tsx +2 -9
- package/src/components/__snapshots__/stories.test.tsx.snap +37 -9
- package/src/intent-dispatch.test.ts +1 -0
- package/src/prototype-browser.test.ts +55 -0
- package/src/prototype-browser.ts +218 -0
- package/src/session-controller.ts +14 -0
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +105 -0
- package/src/view-plan.ts +96 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.46",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -26,16 +26,17 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
30
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
29
|
+
"@cueloop/daemon": "0.1.0-alpha.46",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.46",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.46",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.46",
|
|
33
33
|
"@opentui/core": "^0.5.2",
|
|
34
34
|
"@opentui/keymap": "0.5.2",
|
|
35
35
|
"@opentui/react": "^0.5.2",
|
|
36
36
|
"@opentui/ssh": "0.5.2",
|
|
37
37
|
"@pierre/diffs": "^1.3.5",
|
|
38
38
|
"diff": "^8",
|
|
39
|
+
"puppeteer-core": "^25.8.0",
|
|
39
40
|
"react": "^19.2.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
@@ -87,6 +87,20 @@ function backgroundsOf(setup: Setup, needle: string): string[] {
|
|
|
87
87
|
return backgrounds;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
91
|
+
const foregrounds: string[] = [];
|
|
92
|
+
for (const line of setup.captureSpans().lines) {
|
|
93
|
+
for (const span of line.spans) {
|
|
94
|
+
if (!span.text.includes(needle)) continue;
|
|
95
|
+
const [red, green, blue] = span.fg.toInts();
|
|
96
|
+
foregrounds.push(
|
|
97
|
+
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return foregrounds;
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
/** Move the cursor to "The daemon persists sessions to disk atomically." */
|
|
91
105
|
async function toContextParagraph(setup: Setup): Promise<void> {
|
|
92
106
|
await press(setup, "j");
|
|
@@ -615,9 +629,9 @@ describe("edit-exit reconciliation", () => {
|
|
|
615
629
|
|
|
616
630
|
// Act
|
|
617
631
|
// deselect the card so e reaches the editor hand-off, then edit; deselection
|
|
618
|
-
// shows as the card's
|
|
632
|
+
// shows as the card's quote line dropping the selection tone (blue)
|
|
619
633
|
await press(setup, "escape");
|
|
620
|
-
await waitForState(setup, () => !
|
|
634
|
+
await waitForState(setup, () => !foregroundsOf(setup, "The daemon").includes(DARK.blue));
|
|
621
635
|
await press(setup, "e");
|
|
622
636
|
|
|
623
637
|
// Assert
|
|
@@ -150,10 +150,10 @@ describe("observer navigation still works", () => {
|
|
|
150
150
|
await press(setup, "n");
|
|
151
151
|
await setup.renderOnce();
|
|
152
152
|
|
|
153
|
-
// Assert - focus shows as the card's
|
|
154
|
-
//
|
|
153
|
+
// Assert - focus shows as the card's blue border (transparent fill); a
|
|
154
|
+
// comment card is the only blue-bordered box in the frame
|
|
155
155
|
await waitForText(setup, "COMMENT · me");
|
|
156
|
-
await waitForState(setup, () => hasBorderColor(setup, DARK.
|
|
156
|
+
await waitForState(setup, () => hasBorderColor(setup, DARK.blue));
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
159
|
|
|
@@ -87,9 +87,8 @@ describe("rail submit confirm", () => {
|
|
|
87
87
|
// Assert
|
|
88
88
|
await waitForText(setup, "submit review");
|
|
89
89
|
const frame = setup.captureCharFrame();
|
|
90
|
-
// the card: title,
|
|
90
|
+
// the card: title, verdict selector, word-buttons
|
|
91
91
|
expect(frame).toContain("submit review");
|
|
92
|
-
expect(frame).toContain("0 annotations · 0 blocking");
|
|
93
92
|
expect(frame).toContain("[Approve]"); // nothing pending: approve default
|
|
94
93
|
expect(frame).toContain(" Submit ");
|
|
95
94
|
expect(frame).toContain(" Cancel ");
|
|
@@ -136,7 +135,7 @@ describe("rail submit confirm", () => {
|
|
|
136
135
|
// Assert
|
|
137
136
|
// a bare ESC settles after the parser's escape-sequence window
|
|
138
137
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
139
|
-
expect(frame).not.toContain("
|
|
138
|
+
expect(frame).not.toContain("submit review");
|
|
140
139
|
expect(frame).toContain("Submit review");
|
|
141
140
|
});
|
|
142
141
|
|
|
@@ -149,10 +148,9 @@ describe("rail submit confirm", () => {
|
|
|
149
148
|
await press(setup, "enter");
|
|
150
149
|
|
|
151
150
|
// Assert
|
|
152
|
-
await waitForText(setup, "
|
|
151
|
+
await waitForText(setup, "[Changes]"); // pending items: request changes default
|
|
153
152
|
const frame = setup.captureCharFrame();
|
|
154
|
-
expect(frame).toContain("
|
|
155
|
-
expect(frame).toContain("[Changes]"); // pending items: request changes default
|
|
153
|
+
expect(frame).toContain("submit review");
|
|
156
154
|
|
|
157
155
|
// Act
|
|
158
156
|
await setup.mockInput.typeText("Tighten the steps.");
|
|
@@ -183,7 +181,7 @@ describe("rail submit confirm", () => {
|
|
|
183
181
|
await press(setup, "enter");
|
|
184
182
|
|
|
185
183
|
// Assert
|
|
186
|
-
await waitForText(setup, "
|
|
184
|
+
await waitForText(setup, "[Changes]");
|
|
187
185
|
// the card is pinned outside the scrollbox; the stack stays scrolled off the top
|
|
188
186
|
expect(setup.captureCharFrame()).not.toContain("note 01");
|
|
189
187
|
|
|
@@ -196,7 +194,7 @@ describe("rail submit confirm", () => {
|
|
|
196
194
|
// Assert
|
|
197
195
|
await waitForText(setup, "note 12");
|
|
198
196
|
const scrolledWithCard = setup.captureCharFrame();
|
|
199
|
-
expect(scrolledWithCard).toContain("
|
|
197
|
+
expect(scrolledWithCard).toContain("submit review");
|
|
200
198
|
expect(scrolledWithCard).not.toContain("note 01");
|
|
201
199
|
});
|
|
202
200
|
|
package/src/App.tsx
CHANGED
|
@@ -58,7 +58,8 @@ import { CLIENT_VERSION } from "./version";
|
|
|
58
58
|
import { Breadcrumb, type BreadcrumbItem } from "./components/Breadcrumb";
|
|
59
59
|
import { PlanSheet, type PlanSheetHandle } from "./components/PlanSheet";
|
|
60
60
|
import { DiffSheet } from "./components/DiffSheet";
|
|
61
|
-
import {
|
|
61
|
+
import { PrototypeSheet } from "./components/PrototypeSheet";
|
|
62
|
+
import { type ReviewRailHandle } from "./components/ReviewRail";
|
|
62
63
|
import type { AgentTerminalHandle } from "./components/agent-launcher";
|
|
63
64
|
import { ReviewPanel } from "./components/ReviewPanel";
|
|
64
65
|
import {
|
|
@@ -251,6 +252,7 @@ export function App({
|
|
|
251
252
|
}, [marks]);
|
|
252
253
|
const resolved = session?.status === "resolved";
|
|
253
254
|
const isDiff = session?.artifact.type === "diff";
|
|
255
|
+
const isPrototype = session?.artifact.type === "prototype";
|
|
254
256
|
// sort position per annotation so the rail interleaves annotation and removal
|
|
255
257
|
// cards in one line-ordered stack: a diff row carries its blockIndex; a plan
|
|
256
258
|
// annotation resolves to the display index it marked
|
|
@@ -853,30 +855,36 @@ export function App({
|
|
|
853
855
|
}
|
|
854
856
|
: null;
|
|
855
857
|
|
|
858
|
+
// a mouse drag leaves a native selection: turn it into a word span so the
|
|
859
|
+
// marker popover opens at the dragged range, mirroring the `v` grammar.
|
|
860
|
+
// Returns whether a native selection existed (the release ended a drag).
|
|
861
|
+
const activateSpanFromSelection = (preferredIndex?: number): boolean => {
|
|
862
|
+
if (!renderer?.hasSelection) return false;
|
|
863
|
+
// the release block re-anchors the span, so each drag replaces the last
|
|
864
|
+
const selection = planSheetRef.current?.readSelection(preferredIndex);
|
|
865
|
+
const block = selection ? display[selection.displayIndex] : undefined;
|
|
866
|
+
const span =
|
|
867
|
+
selection && block
|
|
868
|
+
? spanFromRange(selection.displayIndex, displayText(block), selection.start, selection.end)
|
|
869
|
+
: null;
|
|
870
|
+
if (span) setMode({ type: "span", span });
|
|
871
|
+
return true;
|
|
872
|
+
};
|
|
873
|
+
|
|
856
874
|
const onLineActivate = (displayIndex: number): void => {
|
|
857
|
-
|
|
858
|
-
// marker popover opens at the dragged range, mirroring the `v` grammar
|
|
859
|
-
if (renderer?.hasSelection) {
|
|
860
|
-
// the release block re-anchors the span, so each drag replaces the last
|
|
861
|
-
const selection = planSheetRef.current?.readSelection(displayIndex);
|
|
862
|
-
const block = selection ? display[selection.displayIndex] : undefined;
|
|
863
|
-
const span =
|
|
864
|
-
selection && block
|
|
865
|
-
? spanFromRange(
|
|
866
|
-
selection.displayIndex,
|
|
867
|
-
displayText(block),
|
|
868
|
-
selection.start,
|
|
869
|
-
selection.end,
|
|
870
|
-
)
|
|
871
|
-
: null;
|
|
872
|
-
if (span) setMode({ type: "span", span });
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
+
if (activateSpanFromSelection(displayIndex)) return;
|
|
875
876
|
setCursor(displayIndex);
|
|
876
877
|
const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
|
|
877
878
|
if (annotationId) selectCardFromDocument(annotationId);
|
|
878
879
|
};
|
|
879
880
|
|
|
881
|
+
// a drag released outside any block's text (the gutter, past a line end, a
|
|
882
|
+
// gap between blocks) never reaches a block handler; the sheet-level release
|
|
883
|
+
// still turns the finished drag into a span
|
|
884
|
+
const onSelectionRelease = (): void => {
|
|
885
|
+
activateSpanFromSelection();
|
|
886
|
+
};
|
|
887
|
+
|
|
880
888
|
const onEditRequest = (): void => {
|
|
881
889
|
// A share viewer/observer has no Edit affordance (the button is hidden), so
|
|
882
890
|
// this is owner-only; stay silent rather than nag if it is ever reached.
|
|
@@ -916,8 +924,6 @@ export function App({
|
|
|
916
924
|
? {
|
|
917
925
|
verdict: mode.verdict,
|
|
918
926
|
summary: mode.summary,
|
|
919
|
-
annotationCount: reviewerAnnotations(activeSession).length,
|
|
920
|
-
blockingCount: reviewerAnnotations(activeSession).filter(annotationBlocking).length,
|
|
921
927
|
// walk coverage keeps partial passes honest at the verdict
|
|
922
928
|
viewedSummary:
|
|
923
929
|
isDiff && activeSession.viewedPaths !== undefined
|
|
@@ -1021,7 +1027,16 @@ export function App({
|
|
|
1021
1027
|
<box style={{ width: railFootprint }} />
|
|
1022
1028
|
</box>
|
|
1023
1029
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
1024
|
-
{
|
|
1030
|
+
{isPrototype ? (
|
|
1031
|
+
<PrototypeSheet
|
|
1032
|
+
prototypePath={activeSession.artifact.meta.prototypePath ?? ""}
|
|
1033
|
+
quickActions={quickActions}
|
|
1034
|
+
canComment={isOwner && !resolved}
|
|
1035
|
+
onCommentElement={(element, body) =>
|
|
1036
|
+
controller.annotatePrototype(element.selector, element.quote, body)
|
|
1037
|
+
}
|
|
1038
|
+
/>
|
|
1039
|
+
) : isDiff ? (
|
|
1025
1040
|
// the sheet dims to reading-quiet colors while the wizard has focus
|
|
1026
1041
|
<DiffSheet
|
|
1027
1042
|
rows={rows}
|
|
@@ -1044,6 +1059,7 @@ export function App({
|
|
|
1044
1059
|
popover={popoverState}
|
|
1045
1060
|
editOrphanCount={editOrphanCount}
|
|
1046
1061
|
onLineActivate={onLineActivate}
|
|
1062
|
+
onSelectionRelease={onSelectionRelease}
|
|
1047
1063
|
/>
|
|
1048
1064
|
)}
|
|
1049
1065
|
<ReviewPanel
|
|
@@ -1056,7 +1072,7 @@ export function App({
|
|
|
1056
1072
|
session: activeSession,
|
|
1057
1073
|
authorNames,
|
|
1058
1074
|
selectedId: focusedAnnotationId,
|
|
1059
|
-
resolvedIds: isDiff ? null : resolvedIds,
|
|
1075
|
+
resolvedIds: isDiff || isPrototype ? null : resolvedIds,
|
|
1060
1076
|
curationItems,
|
|
1061
1077
|
selectedCurationId,
|
|
1062
1078
|
annotationPositions,
|
package/src/App.walk.test.tsx
CHANGED
|
@@ -242,7 +242,8 @@ describe("the guided walk", () => {
|
|
|
242
242
|
await press(setup, "escape");
|
|
243
243
|
await waitForTextGone(setup, "agent note");
|
|
244
244
|
await press(setup, "enter");
|
|
245
|
-
|
|
245
|
+
// nothing pending despite the note, so the confirm card defaults to approve
|
|
246
|
+
await waitForText(setup, "[Approve]");
|
|
246
247
|
await press(setup, "enter");
|
|
247
248
|
|
|
248
249
|
// Assert
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
|
+
|
|
3
|
+
exports[`view-plan regression harness (M0) blockRuns output is stable across the representative blocks 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"runs": [
|
|
7
|
+
{
|
|
8
|
+
"role": "plain",
|
|
9
|
+
"start": 0,
|
|
10
|
+
"text": "Heading with ",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"role": "strong",
|
|
14
|
+
"start": 15,
|
|
15
|
+
"text": "bold",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
"text": "Heading with **bold**",
|
|
19
|
+
"type": "same",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"runs": [
|
|
23
|
+
{
|
|
24
|
+
"role": "plain",
|
|
25
|
+
"start": 0,
|
|
26
|
+
"text": "A paragraph with ",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"role": "strong",
|
|
30
|
+
"start": 19,
|
|
31
|
+
"text": "strong",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"role": "plain",
|
|
35
|
+
"start": 27,
|
|
36
|
+
"text": ", ",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"role": "em",
|
|
40
|
+
"start": 30,
|
|
41
|
+
"text": "em",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"role": "plain",
|
|
45
|
+
"start": 33,
|
|
46
|
+
"text": ", ",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"role": "code",
|
|
50
|
+
"start": 36,
|
|
51
|
+
"text": "code",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"role": "plain",
|
|
55
|
+
"start": 41,
|
|
56
|
+
"text": ", ",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"role": "strike",
|
|
60
|
+
"start": 45,
|
|
61
|
+
"text": "strike",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"role": "plain",
|
|
65
|
+
"start": 53,
|
|
66
|
+
"text": " and a ",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"href": "https://example.com",
|
|
70
|
+
"role": "link",
|
|
71
|
+
"start": 61,
|
|
72
|
+
"text": "label",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"role": "plain",
|
|
76
|
+
"start": 88,
|
|
77
|
+
"text": ".",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
"text": "A paragraph with **strong**, *em*, \`code\`, ~~strike~~ and a [label](https://example.com).",
|
|
81
|
+
"type": "same",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"runs": [
|
|
85
|
+
{
|
|
86
|
+
"role": "plain",
|
|
87
|
+
"start": 0,
|
|
88
|
+
"text": "a list item with ",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"role": "em",
|
|
92
|
+
"start": 18,
|
|
93
|
+
"text": "emphasis",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
"text": "a list item with *emphasis*",
|
|
97
|
+
"type": "same",
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
`;
|
|
@@ -15,7 +15,7 @@ export const CommentDraft: Story = {
|
|
|
15
15
|
draft={{ text: "Which daemon?", ...draftCallbacks }}
|
|
16
16
|
/>
|
|
17
17
|
),
|
|
18
|
-
expectedColors: [DARK.
|
|
18
|
+
expectedColors: [DARK.blue],
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export const SavedSelected: Story = {
|
|
@@ -27,15 +27,14 @@ export const SavedSelected: Story = {
|
|
|
27
27
|
body: "Which daemon owns this?",
|
|
28
28
|
isSelected: true,
|
|
29
29
|
isOrphan: false,
|
|
30
|
-
isBlocking: false,
|
|
31
30
|
author: "me",
|
|
32
|
-
tone: DARK.
|
|
31
|
+
tone: DARK.blue,
|
|
33
32
|
editing: null,
|
|
34
33
|
onPress: () => {},
|
|
35
34
|
}}
|
|
36
35
|
/>
|
|
37
36
|
),
|
|
38
|
-
expectedColors: [DARK.
|
|
37
|
+
expectedColors: [DARK.blue],
|
|
39
38
|
};
|
|
40
39
|
|
|
41
40
|
export const SavedCollaborator: Story = {
|
|
@@ -47,7 +46,6 @@ export const SavedCollaborator: Story = {
|
|
|
47
46
|
body: "Which daemon owns this?",
|
|
48
47
|
isSelected: false,
|
|
49
48
|
isOrphan: false,
|
|
50
|
-
isBlocking: false,
|
|
51
49
|
author: "Priya",
|
|
52
50
|
tone: DARK.blue,
|
|
53
51
|
editing: null,
|
|
@@ -58,7 +56,7 @@ export const SavedCollaborator: Story = {
|
|
|
58
56
|
expectedColors: [DARK.blue],
|
|
59
57
|
};
|
|
60
58
|
|
|
61
|
-
export const
|
|
59
|
+
export const SavedOrphan: Story = {
|
|
62
60
|
render: () => (
|
|
63
61
|
<AnnotationCard
|
|
64
62
|
kind="comment"
|
|
@@ -67,15 +65,14 @@ export const SavedOrphanBlocking: Story = {
|
|
|
67
65
|
body: "The passage is gone.",
|
|
68
66
|
isSelected: false,
|
|
69
67
|
isOrphan: true,
|
|
70
|
-
isBlocking: true,
|
|
71
68
|
author: "me",
|
|
72
|
-
tone: DARK.
|
|
69
|
+
tone: DARK.blue,
|
|
73
70
|
editing: null,
|
|
74
71
|
onPress: () => {},
|
|
75
72
|
}}
|
|
76
73
|
/>
|
|
77
74
|
),
|
|
78
|
-
expectedColors: [DARK.
|
|
75
|
+
expectedColors: [DARK.blue],
|
|
79
76
|
};
|
|
80
77
|
|
|
81
78
|
export const SavedEditing: Story = {
|
|
@@ -87,9 +84,8 @@ export const SavedEditing: Story = {
|
|
|
87
84
|
body: "old body",
|
|
88
85
|
isSelected: true,
|
|
89
86
|
isOrphan: false,
|
|
90
|
-
isBlocking: false,
|
|
91
87
|
author: "me",
|
|
92
|
-
tone: DARK.
|
|
88
|
+
tone: DARK.blue,
|
|
93
89
|
editing: { text: "new body", ...draftCallbacks },
|
|
94
90
|
onPress: () => {},
|
|
95
91
|
}}
|
|
@@ -31,10 +31,9 @@ export interface AnnotationSaved {
|
|
|
31
31
|
body: string;
|
|
32
32
|
isSelected: boolean;
|
|
33
33
|
isOrphan: boolean;
|
|
34
|
-
isBlocking: boolean;
|
|
35
34
|
/** Border-title author: a collaborator's display name, or "me" for the reviewer's own note. */
|
|
36
35
|
author: string;
|
|
37
|
-
/** Border + title color for this card (
|
|
36
|
+
/** Border + title color for this card (comment cards read blue). */
|
|
38
37
|
tone: string;
|
|
39
38
|
/** Non-null while the card body is being rewritten in place. */
|
|
40
39
|
editing: AnnotationDraft | null;
|
|
@@ -155,7 +154,8 @@ export function AnnotationCard({
|
|
|
155
154
|
theme,
|
|
156
155
|
}: AnnotationCardProps): React.ReactNode {
|
|
157
156
|
const tokens = useComponentTheme(theme);
|
|
158
|
-
|
|
157
|
+
// the composer wears the same blue as the saved comment card it becomes
|
|
158
|
+
const kindColor = tokens.blue;
|
|
159
159
|
const activeDraft = draft ?? saved?.editing ?? null;
|
|
160
160
|
// The composer's visible height, grown from the wrapped text. It is shared
|
|
161
161
|
// with the card frame (contentRows) so the border never clips the textarea
|
|
@@ -186,10 +186,8 @@ export function AnnotationCard({
|
|
|
186
186
|
);
|
|
187
187
|
}
|
|
188
188
|
const card = saved!;
|
|
189
|
-
// border = "what + who" (+
|
|
190
|
-
const flags =
|
|
191
|
-
(flag): flag is string => flag !== null,
|
|
192
|
-
);
|
|
189
|
+
// border = "what + who" (+ an orphaned flag when the anchor no longer resolves)
|
|
190
|
+
const flags = card.isOrphan ? ["ORPHANED"] : [];
|
|
193
191
|
const title = ` ${[kind.toUpperCase(), card.author, ...flags].join(" · ")} `;
|
|
194
192
|
// every saved card is bordered, so the border + side padding always eat this width
|
|
195
193
|
const borderInset = 4;
|
|
@@ -205,16 +203,20 @@ export function AnnotationCard({
|
|
|
205
203
|
border: true,
|
|
206
204
|
borderStyle: FRAME_BORDER_STYLE,
|
|
207
205
|
// the border (and its title) always wears the card's tone - salmon for
|
|
208
|
-
// own notes, blue for a collaborator's;
|
|
209
|
-
//
|
|
206
|
+
// own notes, blue for a collaborator's; the card background stays
|
|
207
|
+
// transparent so it sits flat on the terminal theme. Selection reads
|
|
208
|
+
// from the quote line taking the card's tone (below) and from the
|
|
209
|
+
// matching text highlight in the document.
|
|
210
210
|
borderColor: card.tone,
|
|
211
|
-
backgroundColor:
|
|
211
|
+
backgroundColor: "transparent",
|
|
212
212
|
paddingLeft: 1,
|
|
213
213
|
paddingRight: 1,
|
|
214
214
|
}}
|
|
215
215
|
onMouseUp={card.onPress}
|
|
216
216
|
>
|
|
217
|
-
<text
|
|
217
|
+
<text
|
|
218
|
+
fg={card.isSelected ? card.tone : tokens.textDim}
|
|
219
|
+
>{`"${truncateToSingleLine(quote, 26 - borderInset)}"`}</text>
|
|
218
220
|
{card.editing ? (
|
|
219
221
|
<DraftEditor
|
|
220
222
|
draft={card.editing}
|
|
@@ -13,28 +13,14 @@ const callbacks = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const ApproveDefault: Story = {
|
|
16
|
-
render: () =>
|
|
17
|
-
<ConfirmCard
|
|
18
|
-
verdict="approve"
|
|
19
|
-
summary=""
|
|
20
|
-
annotationCount={0}
|
|
21
|
-
blockingCount={0}
|
|
22
|
-
{...callbacks}
|
|
23
|
-
/>
|
|
24
|
-
),
|
|
16
|
+
render: () => <ConfirmCard verdict="approve" summary="" {...callbacks} />,
|
|
25
17
|
expectedColors: [DARK.green, DARK.accent],
|
|
26
18
|
size: { width: 40, height: 14 },
|
|
27
19
|
};
|
|
28
20
|
|
|
29
21
|
export const RequestChanges: Story = {
|
|
30
22
|
render: () => (
|
|
31
|
-
<ConfirmCard
|
|
32
|
-
verdict="request_changes"
|
|
33
|
-
summary="Tighten the steps."
|
|
34
|
-
annotationCount={3}
|
|
35
|
-
blockingCount={1}
|
|
36
|
-
{...callbacks}
|
|
37
|
-
/>
|
|
23
|
+
<ConfirmCard verdict="request_changes" summary="Tighten the steps." {...callbacks} />
|
|
38
24
|
),
|
|
39
25
|
expectedColors: [DARK.red],
|
|
40
26
|
size: { width: 40, height: 14 },
|
|
@@ -42,14 +28,7 @@ export const RequestChanges: Story = {
|
|
|
42
28
|
|
|
43
29
|
export const WithViewedSummary: Story = {
|
|
44
30
|
render: () => (
|
|
45
|
-
<ConfirmCard
|
|
46
|
-
verdict="approve"
|
|
47
|
-
summary=""
|
|
48
|
-
annotationCount={1}
|
|
49
|
-
blockingCount={0}
|
|
50
|
-
viewedSummary="2/3 files viewed"
|
|
51
|
-
{...callbacks}
|
|
52
|
-
/>
|
|
31
|
+
<ConfirmCard verdict="approve" summary="" viewedSummary="2/3 files viewed" {...callbacks} />
|
|
53
32
|
),
|
|
54
33
|
expectedColors: [DARK.green, DARK.textDim],
|
|
55
34
|
size: { width: 40, height: 14 },
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Submit button expanded into a bordered confirm card at the rail bottom:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* and render never drift.
|
|
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.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import React from "react";
|
|
@@ -26,8 +25,6 @@ export const VERDICT_LABEL: Record<VerdictKind, string> = {
|
|
|
26
25
|
export interface ConfirmCardProps {
|
|
27
26
|
verdict: VerdictKind;
|
|
28
27
|
summary: string;
|
|
29
|
-
annotationCount: number;
|
|
30
|
-
blockingCount: number;
|
|
31
28
|
/**
|
|
32
29
|
* The guided walk's honest coverage line for diff sessions, e.g.
|
|
33
30
|
* "2/3 files viewed". Undefined = no walk data, the row does not render.
|
|
@@ -40,8 +37,8 @@ export interface ConfirmCardProps {
|
|
|
40
37
|
theme?: Theme;
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
/**
|
|
44
|
-
const CONFIRM_CONTENT_ROWS =
|
|
40
|
+
/** 1-row verdict selector, spacer, summary input, spacer, buttons. */
|
|
41
|
+
const CONFIRM_CONTENT_ROWS = 5;
|
|
45
42
|
|
|
46
43
|
export function verdictColor(verdict: VerdictKind, tokens: Theme): string {
|
|
47
44
|
return verdict === "approve"
|
|
@@ -89,8 +86,6 @@ function VerdictSelector({
|
|
|
89
86
|
export function ConfirmCard({
|
|
90
87
|
verdict,
|
|
91
88
|
summary,
|
|
92
|
-
annotationCount,
|
|
93
|
-
blockingCount,
|
|
94
89
|
viewedSummary,
|
|
95
90
|
onInput,
|
|
96
91
|
onSelectVerdict,
|
|
@@ -102,17 +97,14 @@ export function ConfirmCard({
|
|
|
102
97
|
return (
|
|
103
98
|
<Card
|
|
104
99
|
title=" submit review "
|
|
105
|
-
contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ?
|
|
100
|
+
contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 2 : 0)}
|
|
106
101
|
borderColor={tokens.text}
|
|
107
102
|
backgroundColor="transparent"
|
|
108
103
|
marginRight={1}
|
|
109
104
|
theme={theme}
|
|
110
105
|
>
|
|
111
|
-
<text
|
|
112
|
-
fg={tokens.textDim}
|
|
113
|
-
>{`${annotationCount} annotations · ${blockingCount} blocking`}</text>
|
|
114
106
|
{viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
|
|
115
|
-
<box style={{ height: 1 }} />
|
|
107
|
+
{viewedSummary !== undefined ? <box style={{ height: 1 }} /> : null}
|
|
116
108
|
<VerdictSelector verdict={verdict} onSelectVerdict={onSelectVerdict} theme={theme} />
|
|
117
109
|
<box style={{ height: 1 }} />
|
|
118
110
|
<input
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from "react";
|
|
11
|
+
import type { MouseEvent } from "@opentui/core";
|
|
11
12
|
import type { Theme } from "../theme";
|
|
12
13
|
import type { QuickAction } from "../config";
|
|
13
14
|
import { useComponentTheme } from "./theme-context";
|
|
@@ -56,7 +57,12 @@ export function MarkerPopover({
|
|
|
56
57
|
};
|
|
57
58
|
const open = view === "actions";
|
|
58
59
|
return (
|
|
59
|
-
<box
|
|
60
|
+
<box
|
|
61
|
+
style={{ flexDirection: "column", alignItems: "flex-start" }}
|
|
62
|
+
// a release on the popover is the popover's own click; it must never
|
|
63
|
+
// bubble to the sheet's selection-release fallback and re-enter span mode
|
|
64
|
+
onMouseUp={(event: MouseEvent) => event.stopPropagation()}
|
|
65
|
+
>
|
|
60
66
|
<box style={{ ...cardStyle, height: cardHeight(1) }}>
|
|
61
67
|
<box style={{ flexDirection: "row" }}>
|
|
62
68
|
<text fg={tokens.accent} onMouseUp={onComment}>
|