@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.44",
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.44",
30
- "@cueloop/extension-api": "0.1.0-alpha.44",
31
- "@cueloop/integration-obsidian": "0.1.0-alpha.44",
32
- "@cueloop/schema": "0.1.0-alpha.44",
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 selected fill (elevated) clearing to a transparent card
632
+ // shows as the card's quote line dropping the selection tone (blue)
619
633
  await press(setup, "escape");
620
- await waitForState(setup, () => !backgroundsOf(setup, "Anchor me").includes(DARK.elevated));
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 bright kind-colored border (transparent
154
- // fill); a selected card is the only accent-colored border in the frame
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.accent));
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, honest counts, verdict selector, word-buttons
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("0 annotations");
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, "1 annotations · 0 blocking");
151
+ await waitForText(setup, "[Changes]"); // pending items: request changes default
153
152
  const frame = setup.captureCharFrame();
154
- expect(frame).toContain("1 annotations · 0 blocking");
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, "12 annotations · 0 blocking");
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("12 annotations · 0 blocking");
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 { annotationBlocking, type ReviewRailHandle } from "./components/ReviewRail";
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
- // a mouse drag leaves a native selection: turn it into a word span so the
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
- {isDiff ? (
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,
@@ -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
- await waitForText(setup, "0 annotations");
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.accent],
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.accent,
31
+ tone: DARK.blue,
33
32
  editing: null,
34
33
  onPress: () => {},
35
34
  }}
36
35
  />
37
36
  ),
38
- expectedColors: [DARK.accent],
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 SavedOrphanBlocking: Story = {
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.accent,
69
+ tone: DARK.blue,
73
70
  editing: null,
74
71
  onPress: () => {},
75
72
  }}
76
73
  />
77
74
  ),
78
- expectedColors: [DARK.accent],
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.accent,
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 (own = salmon, collaborator = blue). */
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
- const kindColor = tokens.accent;
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" (+ any exceptional flag); body = the content only
190
- const flags = [card.isBlocking ? "BLOCKING" : null, card.isOrphan ? "ORPHANED" : null].filter(
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; selection reads from a filled
209
- // background so the color stays put whether or not the card is active
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: card.isSelected || card.editing ? tokens.elevated : "transparent",
211
+ backgroundColor: "transparent",
212
212
  paddingLeft: 1,
213
213
  paddingRight: 1,
214
214
  }}
215
215
  onMouseUp={card.onPress}
216
216
  >
217
- <text fg={tokens.textDim}>"{truncateToSingleLine(quote, 26 - borderInset)}"</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
- * honest counts, the verdict selector (arrow keys or click), the optional
4
- * summary, and plain word-buttons - key hints live in the status line only.
5
- * The bordered height derives from the content rows through Card, so layout
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
- /** Counts, spacer, 1-row verdict selector, spacer, summary input, spacer, buttons. */
44
- const CONFIRM_CONTENT_ROWS = 7;
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 ? 1 : 0)}
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 style={{ flexDirection: "column", alignItems: "flex-start" }}>
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}>