@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.
Files changed (94) hide show
  1. package/package.json +27 -26
  2. package/src/App.autoclose.test.tsx +7 -4
  3. package/src/App.diff.test.tsx +130 -4
  4. package/src/App.export.test.tsx +22 -4
  5. package/src/App.plan-review.test.tsx +127 -27
  6. package/src/App.readonly.test.tsx +31 -8
  7. package/src/App.rendering.test.tsx +12 -3
  8. package/src/App.review-panel.test.tsx +11 -3
  9. package/src/App.submit-confirm.test.tsx +12 -7
  10. package/src/App.test.tsx +46 -9
  11. package/src/App.toast.test.tsx +10 -3
  12. package/src/App.tsx +400 -100
  13. package/src/App.walk.test.tsx +30 -10
  14. package/src/attribution.ts +8 -2
  15. package/src/clipboard.ts +5 -1
  16. package/src/components/AnnotationCard.stories.tsx +28 -9
  17. package/src/components/AnnotationCard.tsx +53 -38
  18. package/src/components/Breadcrumb.tsx +7 -1
  19. package/src/components/CodeBlock.stories.tsx +19 -2
  20. package/src/components/CodeBlock.tsx +24 -9
  21. package/src/components/CompletionOverlay.tsx +13 -3
  22. package/src/components/ConfirmCard.stories.tsx +22 -3
  23. package/src/components/ConfirmCard.tsx +22 -5
  24. package/src/components/ConfirmDialog.tsx +9 -1
  25. package/src/components/DiffSheet.scroll.test.tsx +123 -0
  26. package/src/components/DiffSheet.stories.tsx +18 -2
  27. package/src/components/DiffSheet.syntax.test.tsx +62 -0
  28. package/src/components/DiffSheet.tsx +184 -98
  29. package/src/components/InboxList.stories.tsx +16 -3
  30. package/src/components/InboxList.tsx +17 -10
  31. package/src/components/KeybindsDialog.tsx +19 -3
  32. package/src/components/MarkerPopover.stories.tsx +58 -0
  33. package/src/components/MarkerPopover.tsx +102 -0
  34. package/src/components/MenuBar.stories.tsx +5 -1
  35. package/src/components/MenuBar.tsx +18 -2
  36. package/src/components/PlanSheet.stories.tsx +34 -2
  37. package/src/components/PlanSheet.tsx +141 -32
  38. package/src/components/PromptDialog.tsx +18 -2
  39. package/src/components/ReviewPanel.stories.tsx +13 -3
  40. package/src/components/ReviewPanel.tsx +8 -4
  41. package/src/components/ReviewRail.stories.tsx +145 -72
  42. package/src/components/ReviewRail.tsx +177 -38
  43. package/src/components/SettingsDialog.stories.tsx +51 -3
  44. package/src/components/SettingsDialog.tsx +8 -3
  45. package/src/components/SettingsRows.tsx +19 -3
  46. package/src/components/Toast.tsx +17 -2
  47. package/src/components/WalkWizard.stories.tsx +13 -2
  48. package/src/components/WalkWizard.tsx +9 -3
  49. package/src/components/__snapshots__/stories.test.tsx.snap +281 -83
  50. package/src/components/diff-sheet-layout.test.ts +127 -0
  51. package/src/components/diff-sheet-layout.ts +167 -0
  52. package/src/components/primitives/Button.tsx +5 -1
  53. package/src/components/primitives/Dialog.tsx +9 -1
  54. package/src/components/primitives/Tabs.tsx +24 -4
  55. package/src/components/stories-app.tsx +34 -6
  56. package/src/components/story-fixtures.ts +11 -3
  57. package/src/components/story.ts +3 -1
  58. package/src/components/syntax-highlight.ts +47 -12
  59. package/src/components/theme-context.stories.tsx +6 -1
  60. package/src/components/theme-context.tsx +7 -1
  61. package/src/config.test.ts +178 -2
  62. package/src/config.ts +121 -23
  63. package/src/diff-hunk-curate.test.ts +315 -0
  64. package/src/diff-hunk-curate.ts +233 -0
  65. package/src/diff-intraline.test.ts +164 -0
  66. package/src/diff-intraline.ts +132 -0
  67. package/src/diff-syntax.test.ts +96 -0
  68. package/src/diff-syntax.ts +154 -0
  69. package/src/editor.test.ts +13 -4
  70. package/src/editor.ts +31 -5
  71. package/src/herdr.ts +8 -2
  72. package/src/index.ts +9 -1
  73. package/src/integrations.ts +3 -1
  74. package/src/intent-dispatch.test.ts +193 -9
  75. package/src/intent-dispatch.ts +151 -18
  76. package/src/key-bindings.test.ts +19 -9
  77. package/src/key-bindings.ts +36 -14
  78. package/src/keymap.test.ts +171 -26
  79. package/src/keymap.ts +83 -16
  80. package/src/review-panel.test.ts +3 -1
  81. package/src/review-panel.ts +9 -2
  82. package/src/serve.test.ts +11 -3
  83. package/src/session-controller.curate.test.ts +253 -0
  84. package/src/session-controller.share.test.ts +65 -17
  85. package/src/session-controller.ts +301 -26
  86. package/src/share.ts +41 -8
  87. package/src/test-support.ts +16 -6
  88. package/src/theme-presets.ts +168 -0
  89. package/src/theme.ts +1 -3
  90. package/src/view-diff.ts +15 -4
  91. package/src/view-plan.test.ts +40 -6
  92. package/src/view-plan.ts +71 -28
  93. package/src/walk.test.ts +7 -2
  94. package/src/walk.ts +7 -2
@@ -2,14 +2,17 @@ import React from "react";
2
2
  import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { ReviewRail } from "./ReviewRail";
5
- import { FIXTURE_ANNOTATIONS, fixturePlanSession } from "./story-fixtures";
5
+ import { FIXTURE_ANNOTATIONS, fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
6
6
 
7
7
  export const meta: StoryMeta = { title: "ReviewRail" };
8
8
 
9
9
  const callbacks = {
10
+ curationItems: [],
10
11
  onTabChange: () => {},
11
12
  onSelectCard: () => {},
12
13
  onActivateCard: () => {},
14
+ onSelectCuration: () => {},
15
+ onUndoCuration: () => {},
13
16
  onSubmitRequest: () => {},
14
17
  };
15
18
 
@@ -22,19 +25,19 @@ export const AnnotationStack: Story = {
22
25
  render: () => (
23
26
  <RailFrame>
24
27
  <ReviewRail
25
- session={fixturePlanSession()}
26
- authorNames={{}}
27
- selectedId={FIXTURE_ANNOTATIONS[0]!.id}
28
- resolvedIds={new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id))}
29
- railTab="review"
30
- pendingCount={2}
31
- cardEdit={null}
32
- submitConfirm={null}
33
- {...callbacks}
28
+ session={fixturePlanSession()}
29
+ authorNames={{}}
30
+ selectedId={FIXTURE_ANNOTATIONS[0]!.id}
31
+ resolvedIds={new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id))}
32
+ railTab="review"
33
+ pendingCount={2}
34
+ cardEdit={null}
35
+ submitConfirm={null}
36
+ {...callbacks}
34
37
  />
35
38
  </RailFrame>
36
39
  ),
37
- expectedColors: [DARK.green],
40
+ expectedColors: [DARK.accent],
38
41
  size: { width: 40, height: 24 },
39
42
  };
40
43
 
@@ -42,38 +45,103 @@ export const MixedOwnershipStack: Story = {
42
45
  render: () => (
43
46
  <RailFrame>
44
47
  <ReviewRail
45
- session={fixturePlanSession({
46
- participants: [{ id: "SHA256:collab1", provider: "ssh", name: "Priya" }],
47
- annotations: [
48
+ session={fixturePlanSession({
49
+ participants: [{ id: "SHA256:collab1", provider: "ssh", name: "Priya" }],
50
+ annotations: [
51
+ {
52
+ id: "own_note",
53
+ kind: "comment",
54
+ anchor: { quote: "persists sessions", prefix: "The daemon ", suffix: " to disk" },
55
+ body: "Which daemon owns this?",
56
+ createdAt: "2026-01-01T00:00:00Z",
57
+ },
58
+ {
59
+ id: "collab_note",
60
+ kind: "comment",
61
+ anchor: { quote: "move the store", prefix: "", suffix: "" },
62
+ body: "move it behind one interface",
63
+ author: "SHA256:collab1",
64
+ createdAt: "2026-01-01T00:00:01Z",
65
+ },
66
+ ],
67
+ })}
68
+ authorNames={{}}
69
+ selectedId="own_note"
70
+ resolvedIds={new Set(["own_note", "collab_note"])}
71
+ railTab="review"
72
+ pendingCount={2}
73
+ cardEdit={null}
74
+ submitConfirm={null}
75
+ {...callbacks}
76
+ />
77
+ </RailFrame>
78
+ ),
79
+ // every annotation border is accent now
80
+ expectedColors: [DARK.accent],
81
+ size: { width: 40, height: 24 },
82
+ };
83
+
84
+ export const Removals: Story = {
85
+ render: () => (
86
+ <RailFrame>
87
+ <ReviewRail
88
+ session={fixtureDiffSession()}
89
+ authorNames={{}}
90
+ resolvedIds={null}
91
+ railTab="review"
92
+ pendingCount={0}
93
+ cardEdit={null}
94
+ submitConfirm={null}
95
+ {...callbacks}
96
+ curationItems={[
48
97
  {
49
- id: "own_note",
50
- kind: "comment",
51
- anchor: { quote: "persists sessions", prefix: "The daemon ", suffix: " to disk" },
52
- body: "Which daemon owns this?",
53
- createdAt: "2026-01-01T00:00:00Z",
98
+ id: "diff:src/store.ts#0#1",
99
+ source: "diff",
100
+ preview: ["- private items = [];", "+ private items = new Map();"],
101
+ revealIndex: 3,
54
102
  },
55
103
  {
56
- id: "collab_note",
57
- kind: "suggestion",
58
- anchor: { quote: "move the store", prefix: "", suffix: "" },
59
- body: "move it behind one interface",
60
- author: "SHA256:collab1",
61
- createdAt: "2026-01-01T00:00:01Z",
104
+ id: "diff:src/store.ts#1#hunk",
105
+ source: "diff",
106
+ preview: ["- return null;", "+ return fallback();", "+ log(fallback);"],
107
+ revealIndex: 9,
62
108
  },
63
- ],
64
- })}
65
- authorNames={{}}
66
- selectedId="own_note"
67
- resolvedIds={new Set(["own_note", "collab_note"])}
68
- railTab="review"
69
- pendingCount={2}
70
- cardEdit={null}
71
- submitConfirm={null}
72
- {...callbacks}
109
+ ]}
110
+ selectedCurationId="diff:src/store.ts#0#1"
73
111
  />
74
112
  </RailFrame>
75
113
  ),
76
- expectedColors: [DARK.accent, DARK.blue],
114
+ expectedColors: [DARK.red],
115
+ size: { width: 40, height: 24 },
116
+ };
117
+
118
+ /** A plan cut and an annotation interleave in one line-ordered stack. */
119
+ export const InterleavedStack: Story = {
120
+ render: () => (
121
+ <RailFrame>
122
+ <ReviewRail
123
+ session={fixturePlanSession()}
124
+ authorNames={{}}
125
+ selectedId={FIXTURE_ANNOTATIONS[0]!.id}
126
+ resolvedIds={new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id))}
127
+ railTab="review"
128
+ pendingCount={2}
129
+ cardEdit={null}
130
+ submitConfirm={null}
131
+ {...callbacks}
132
+ curationItems={[
133
+ {
134
+ id: "plan:6-6",
135
+ source: "plan",
136
+ preview: ["- add recovery"],
137
+ revealIndex: 1,
138
+ },
139
+ ]}
140
+ annotationPositions={new Map([[FIXTURE_ANNOTATIONS[0]!.id, 5]])}
141
+ />
142
+ </RailFrame>
143
+ ),
144
+ expectedColors: [DARK.accent],
77
145
  size: { width: 40, height: 24 },
78
146
  };
79
147
 
@@ -81,14 +149,14 @@ export const AgentTab: Story = {
81
149
  render: () => (
82
150
  <RailFrame>
83
151
  <ReviewRail
84
- session={fixturePlanSession()}
85
- authorNames={{}}
86
- resolvedIds={new Set()}
87
- railTab="agent"
88
- pendingCount={2}
89
- cardEdit={null}
90
- submitConfirm={null}
91
- {...callbacks}
152
+ session={fixturePlanSession()}
153
+ authorNames={{}}
154
+ resolvedIds={new Set()}
155
+ railTab="agent"
156
+ pendingCount={2}
157
+ cardEdit={null}
158
+ submitConfirm={null}
159
+ {...callbacks}
92
160
  />
93
161
  </RailFrame>
94
162
  ),
@@ -99,23 +167,23 @@ export const SubmitConfirmOpen: Story = {
99
167
  render: () => (
100
168
  <RailFrame>
101
169
  <ReviewRail
102
- session={fixturePlanSession()}
103
- authorNames={{}}
104
- resolvedIds={new Set()}
105
- railTab="review"
106
- pendingCount={2}
107
- cardEdit={null}
108
- submitConfirm={{
109
- verdict: "request_changes",
110
- summary: "",
111
- annotationCount: 2,
112
- blockingCount: 0,
113
- onInput: () => {},
114
- onSelectVerdict: () => {},
115
- onSubmit: () => {},
116
- onCancel: () => {},
117
- }}
118
- {...callbacks}
170
+ session={fixturePlanSession()}
171
+ authorNames={{}}
172
+ resolvedIds={new Set()}
173
+ railTab="review"
174
+ pendingCount={2}
175
+ cardEdit={null}
176
+ submitConfirm={{
177
+ verdict: "request_changes",
178
+ summary: "",
179
+ annotationCount: 2,
180
+ blockingCount: 0,
181
+ onInput: () => {},
182
+ onSelectVerdict: () => {},
183
+ onSubmit: () => {},
184
+ onCancel: () => {},
185
+ }}
186
+ {...callbacks}
119
187
  />
120
188
  </RailFrame>
121
189
  ),
@@ -127,17 +195,22 @@ export const Resolved: Story = {
127
195
  render: () => (
128
196
  <RailFrame>
129
197
  <ReviewRail
130
- session={fixturePlanSession({
131
- status: "resolved",
132
- verdict: { kind: "approve", summary: "", feedback: "", resolvedAt: "2026-01-01T00:00:00Z" },
133
- })}
134
- authorNames={{}}
135
- resolvedIds={new Set()}
136
- railTab="review"
137
- pendingCount={0}
138
- cardEdit={null}
139
- submitConfirm={null}
140
- {...callbacks}
198
+ session={fixturePlanSession({
199
+ status: "resolved",
200
+ verdict: {
201
+ kind: "approve",
202
+ summary: "",
203
+ feedback: "",
204
+ resolvedAt: "2026-01-01T00:00:00Z",
205
+ },
206
+ })}
207
+ authorNames={{}}
208
+ resolvedIds={new Set()}
209
+ railTab="review"
210
+ pendingCount={0}
211
+ cardEdit={null}
212
+ submitConfirm={null}
213
+ {...callbacks}
141
214
  />
142
215
  </RailFrame>
143
216
  ),
@@ -7,14 +7,18 @@
7
7
  */
8
8
 
9
9
  import React, { forwardRef, useImperativeHandle, useRef } from "react";
10
- import type { ScrollBoxRenderable } from "@opentui/core";
10
+ import { createTextAttributes, type ScrollBoxRenderable } from "@opentui/core";
11
11
  import { isAddressed, isAgentNote, type Annotation, type ReviewSession } from "@cueloop/schema";
12
+ import type { CurationItem } from "../session-controller";
12
13
  import type { Theme } from "../theme";
13
14
  import { useComponentTheme } from "./theme-context";
14
15
  import { Tabs, Tab, TabList } from "./primitives/Tabs";
15
16
  import { Button } from "./primitives/Button";
17
+ import { Toolbar } from "./primitives/Toolbar";
16
18
  import { AnnotationCard, type AnnotationDraft } from "./AnnotationCard";
17
19
  import { ConfirmCard, type ConfirmCardProps } from "./ConfirmCard";
20
+ import { Card } from "./primitives/Card";
21
+ import { truncateToSingleLine } from "./truncate-text";
18
22
  import { resolveDisplayName } from "../attribution";
19
23
 
20
24
  export type RailTab = "review" | "agent";
@@ -35,6 +39,15 @@ export interface ReviewRailProps {
35
39
  selectedId?: string;
36
40
  /** Ids whose anchor resolved; null = orphan display off (diff view). */
37
41
  resolvedIds: Set<string> | null;
42
+ /** Removed pieces (diff rejections / plan cuts) interleaved into the card stack. */
43
+ curationItems: CurationItem[];
44
+ /** The selected curation item, highlighted and the undo target. */
45
+ selectedCurationId?: string;
46
+ /**
47
+ * Sort position per annotation id (diff row index / plan display index), so
48
+ * annotation and removal cards interleave in one position-ordered stack.
49
+ */
50
+ annotationPositions?: Map<string, number>;
38
51
  railTab: RailTab;
39
52
  pendingCount: number;
40
53
  cardEdit: RailCardEdit | null;
@@ -42,6 +55,10 @@ export interface ReviewRailProps {
42
55
  onTabChange: (tab: RailTab) => void;
43
56
  onSelectCard: (id: string) => void;
44
57
  onActivateCard: (id: string) => void;
58
+ /** Select a curation item (reveals its diff row); pressing undo restores it. */
59
+ onSelectCuration: (id: string) => void;
60
+ /** Restore a curation item (the selected card's undo button, same as the u key). */
61
+ onUndoCuration: (id: string) => void;
45
62
  onSubmitRequest: () => void;
46
63
  /** Rail column width; the app derives it from the persisted review layout. */
47
64
  width?: number;
@@ -64,13 +81,17 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
64
81
  authorNames,
65
82
  selectedId,
66
83
  resolvedIds,
84
+ curationItems,
85
+ selectedCurationId,
86
+ annotationPositions,
67
87
  railTab,
68
- pendingCount,
69
88
  cardEdit,
70
89
  submitConfirm,
71
90
  onTabChange,
72
91
  onSelectCard,
73
92
  onActivateCard,
93
+ onSelectCuration,
94
+ onUndoCuration,
74
95
  onSubmitRequest,
75
96
  width = 34,
76
97
  onCollapse,
@@ -83,7 +104,6 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
83
104
  // addressed annotations leave the card list; a dim count keeps them honest
84
105
  const openAnnotations = session.annotations.filter((annotation) => !isAddressed(annotation));
85
106
  const addressedCount = session.annotations.length - openAnnotations.length;
86
- const collaboratorsPresent = session.annotations.some((annotation) => annotation.author);
87
107
 
88
108
  useImperativeHandle(handleRef, () => ({
89
109
  revealCard: (annotationId: string): void => {
@@ -95,9 +115,80 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
95
115
  },
96
116
  }));
97
117
 
118
+ const renderAnnotationCard = (annotation: Annotation): React.ReactNode => (
119
+ <AnnotationCard
120
+ key={annotation.id}
121
+ id={`annotation-card-${annotation.id}`}
122
+ kind={annotation.kind}
123
+ quote={annotation.anchor.quote}
124
+ theme={theme}
125
+ saved={{
126
+ body: annotation.body,
127
+ isSelected: annotation.id === selectedId,
128
+ isOrphan: resolvedIds !== null && !resolvedIds.has(annotation.id),
129
+ isBlocking: annotationBlocking(annotation),
130
+ // border-title author: a collaborator's name, "agent" for agent notes, else "me"
131
+ author: annotation.author
132
+ ? resolveDisplayName(annotation.author, session.participants, authorNames)
133
+ : isAgentNote(annotation)
134
+ ? "agent"
135
+ : "me",
136
+ editing:
137
+ cardEdit && cardEdit.id === annotation.id
138
+ ? {
139
+ text: cardEdit.text,
140
+ onInput: cardEdit.onInput,
141
+ onSave: cardEdit.onSave,
142
+ onCancel: cardEdit.onCancel,
143
+ }
144
+ : null,
145
+ onPress: () =>
146
+ annotation.id === selectedId
147
+ ? onActivateCard(annotation.id)
148
+ : onSelectCard(annotation.id),
149
+ }}
150
+ />
151
+ );
152
+
153
+ // one position-ordered stack: annotation and removal cards interleaved by their
154
+ // source line (diff row / plan block), original order breaking ties
155
+ const railEntries: { sort: number; order: number; node: React.ReactNode }[] = [
156
+ ...openAnnotations.map((annotation, order) => ({
157
+ sort: annotationPositions?.get(annotation.id) ?? Number.MAX_SAFE_INTEGER,
158
+ order,
159
+ node: renderAnnotationCard(annotation),
160
+ })),
161
+ ...curationItems.map((item, index) => ({
162
+ sort: item.revealIndex,
163
+ order: openAnnotations.length + index,
164
+ node: (
165
+ <RemovalCard
166
+ key={item.id}
167
+ item={item}
168
+ isSelected={item.id === selectedCurationId}
169
+ onSelect={onSelectCuration}
170
+ onUndo={onUndoCuration}
171
+ theme={theme}
172
+ />
173
+ ),
174
+ })),
175
+ ].sort((left, right) => left.sort - right.sort || left.order - right.order);
176
+
98
177
  return (
99
- <box style={{ width, backgroundColor: tokens.panel, flexDirection: "column", paddingLeft: 1, paddingBottom: 1 }}>
100
- <Tabs selectedKey={railTab} onSelectionChange={(key) => onTabChange(key as RailTab)} theme={theme}>
178
+ <box
179
+ style={{
180
+ width,
181
+ backgroundColor: tokens.panel,
182
+ flexDirection: "column",
183
+ paddingLeft: 1,
184
+ paddingBottom: 1,
185
+ }}
186
+ >
187
+ <Tabs
188
+ selectedKey={railTab}
189
+ onSelectionChange={(key) => onTabChange(key as RailTab)}
190
+ theme={theme}
191
+ >
101
192
  <TabList>
102
193
  <Tab id="review">Review</Tab>
103
194
  <Tab id="agent">Agent</Tab>
@@ -108,48 +199,26 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
108
199
  <text> </text>
109
200
  <AgentTab session={session} theme={theme} />
110
201
  </box>
111
- ) : openAnnotations.length === 0 ? (
202
+ ) : openAnnotations.length === 0 && curationItems.length === 0 ? (
112
203
  <box style={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
113
- <text fg={tokens.textDim}>{session.annotations.length === 0 ? "no annotations yet" : "all annotations addressed"}</text>
204
+ <text fg={tokens.textDim}>
205
+ {session.annotations.length === 0 ? "no annotations yet" : "all annotations addressed"}
206
+ </text>
114
207
  </box>
115
208
  ) : (
116
209
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
117
- {session.workingCopy !== undefined ? <text fg={tokens.textDim}>± plan edits → one diff</text> : null}
118
- {addressedCount > 0 ? <text fg={tokens.textDim}>✓ {addressedCount} addressed by revision</text> : null}
210
+ {addressedCount > 0 ? (
211
+ <text fg={tokens.textDim}>✓ {addressedCount} addressed by revision</text>
212
+ ) : null}
119
213
  <scrollbox ref={scrollRef} style={{ flexGrow: 1 }} focused={false}>
120
- {openAnnotations.map((annotation) => (
121
- <AnnotationCard
122
- key={annotation.id}
123
- id={`annotation-card-${annotation.id}`}
124
- kind={annotation.kind}
125
- quote={annotation.anchor.quote}
126
- theme={theme}
127
- saved={{
128
- body: annotation.body,
129
- isSelected: annotation.id === selectedId,
130
- isOrphan: resolvedIds !== null && !resolvedIds.has(annotation.id),
131
- isBlocking: annotationBlocking(annotation),
132
- authorLabel: annotation.author ? resolveDisplayName(annotation.author, session.participants, authorNames) : undefined,
133
- selfLabel: !annotation.author && !isAgentNote(annotation) && collaboratorsPresent ? "me" : undefined,
134
- editing:
135
- cardEdit && cardEdit.id === annotation.id
136
- ? {
137
- text: cardEdit.text,
138
- onInput: cardEdit.onInput,
139
- onSave: cardEdit.onSave,
140
- onCancel: cardEdit.onCancel,
141
- }
142
- : null,
143
- onPress: () =>
144
- annotation.id === selectedId ? onActivateCard(annotation.id) : onSelectCard(annotation.id),
145
- }}
146
- />
147
- ))}
214
+ {railEntries.map((entry) => entry.node)}
148
215
  </scrollbox>
149
216
  </box>
150
217
  )}
151
218
  {/* the confirm card keeps full width, above the footer row */}
152
- {railTab !== "agent" && submitConfirm ? <ConfirmCard {...submitConfirm} theme={theme} /> : null}
219
+ {railTab !== "agent" && submitConfirm ? (
220
+ <ConfirmCard {...submitConfirm} theme={theme} />
221
+ ) : null}
153
222
  {/* footer row: collapse chevron left-bound, the submit affordance inline
154
223
  beside it, both on the rail's last row (the plan's bottom-border height) */}
155
224
  <box style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
@@ -172,6 +241,76 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
172
241
  );
173
242
  });
174
243
 
244
+ /** A removal preview reads as removed content: struck through and dimmed. */
245
+ const REMOVAL_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
246
+
247
+ /** Preview lines a removal card shows before it collapses to a "+N more" row. */
248
+ const REMOVAL_PREVIEW_LINES = 4;
249
+
250
+ /** Rough content width inside a removal card's border + padding, for truncation. */
251
+ const REMOVAL_PREVIEW_WIDTH = 24;
252
+
253
+ /**
254
+ * A removed piece as a full display card - the same border/padding/selection
255
+ * chrome as a saved annotation card - previewing the removed content struck
256
+ * through. Unified across diff (a curated-out hunk/change) and plan (a cut
257
+ * block). Clicking selects (App reveals its source line); the undo key restores.
258
+ */
259
+ function RemovalCard({
260
+ item,
261
+ isSelected,
262
+ onSelect,
263
+ onUndo,
264
+ theme,
265
+ }: {
266
+ item: CurationItem;
267
+ isSelected: boolean;
268
+ onSelect: (id: string) => void;
269
+ onUndo: (id: string) => void;
270
+ theme?: Theme;
271
+ }): React.ReactNode {
272
+ const tokens = useComponentTheme(theme);
273
+ const previewLines = item.preview.slice(0, REMOVAL_PREVIEW_LINES);
274
+ const overflow = item.preview.length - previewLines.length;
275
+ // body = the struck-through preview (at least one row) + an overflow row, plus
276
+ // the undo button's own row when selected (mirrors the draft card's Save/Cancel)
277
+ const contentRows =
278
+ Math.max(1, previewLines.length) + (overflow > 0 ? 1 : 0) + (isSelected ? 1 : 0);
279
+ // border = "what + who": a diff reject or a plan cut, always the reviewer's own
280
+ const title = ` ${item.source === "diff" ? "REJECT" : "CUT"} · me `;
281
+ return (
282
+ <box id={`removal-card-${item.id}`} onMouseUp={() => onSelect(item.id)}>
283
+ <Card
284
+ title={title}
285
+ contentRows={contentRows}
286
+ borderColor={isSelected ? tokens.red : tokens.border}
287
+ backgroundColor="transparent"
288
+ theme={theme}
289
+ >
290
+ {previewLines.length === 0 ? (
291
+ <text fg={tokens.textDim}> </text>
292
+ ) : (
293
+ previewLines.map((line, lineIndex) => (
294
+ <text key={lineIndex} fg={tokens.textDim}>
295
+ <span attributes={REMOVAL_ATTRIBUTES}>
296
+ {truncateToSingleLine(line, REMOVAL_PREVIEW_WIDTH)}
297
+ </span>
298
+ </text>
299
+ ))
300
+ )}
301
+ {overflow > 0 ? <text fg={tokens.textDim}>{` …+${overflow} more`}</text> : null}
302
+ {isSelected ? (
303
+ <Toolbar>
304
+ <Button variant="solid" onPress={() => onUndo(item.id)} theme={theme}>
305
+ {" undo "}
306
+ </Button>
307
+ </Toolbar>
308
+ ) : null}
309
+ </Card>
310
+ </box>
311
+ );
312
+ }
313
+
175
314
  /** Agent tab: who submitted, where the session stands, which revision. */
176
315
  function AgentTab({ session, theme }: { session: ReviewSession; theme?: Theme }): React.ReactNode {
177
316
  const tokens = useComponentTheme(theme);
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { DARK } from "../theme";
3
+ import { THEME_PRESETS } from "../theme-presets";
3
4
  import type { Story, StoryMeta } from "./story";
4
5
  import { SettingsDialog, type SettingsCategory } from "./SettingsDialog";
5
6
 
@@ -12,7 +13,12 @@ const CATEGORIES: SettingsCategory[] = [
12
13
  description: "identity and submission",
13
14
  rows: [
14
15
  { key: "displayName", label: "Display name", kind: "text" },
15
- { key: "autoClose", label: "Auto-close on submit", kind: "cycle", options: ["off", "3s", "on"] },
16
+ {
17
+ key: "autoClose",
18
+ label: "Auto-close on submit",
19
+ kind: "cycle",
20
+ options: ["off", "3s", "on"],
21
+ },
16
22
  ],
17
23
  },
18
24
  {
@@ -20,13 +26,37 @@ const CATEGORIES: SettingsCategory[] = [
20
26
  name: "Display",
21
27
  description: "plan width and chrome",
22
28
  rows: [
23
- { key: "planWidth", label: "Plan width", kind: "cycle", options: ["default", "wide", "full"] },
29
+ {
30
+ key: "planWidth",
31
+ label: "Plan width",
32
+ kind: "cycle",
33
+ options: ["default", "wide", "full"],
34
+ },
24
35
  { key: "showLineNumbers", label: "Line numbers", kind: "toggle" },
25
36
  ],
26
37
  },
38
+ {
39
+ id: "appearance",
40
+ name: "Appearance",
41
+ description: "the color theme",
42
+ rows: [
43
+ {
44
+ key: "theme",
45
+ label: "Theme",
46
+ kind: "cycle",
47
+ options: ["cueloop", "Rosé Pine Moon", "Nord"],
48
+ },
49
+ ],
50
+ },
27
51
  ];
28
52
 
29
- const VALUES = { displayName: "amber-heron", autoClose: "3s", planWidth: "default", showLineNumbers: true };
53
+ const VALUES = {
54
+ displayName: "amber-heron",
55
+ autoClose: "3s",
56
+ planWidth: "default",
57
+ showLineNumbers: true,
58
+ theme: "Rosé Pine Moon",
59
+ };
30
60
 
31
61
  export const NavZone: Story = {
32
62
  render: () => (
@@ -61,3 +91,21 @@ export const BodyZone: Story = {
61
91
  expectedColors: [DARK.green],
62
92
  size: { width: 100, height: 30 },
63
93
  };
94
+
95
+ export const AppearanceThemed: Story = {
96
+ render: () => (
97
+ <SettingsDialog
98
+ isOpen
99
+ categories={CATEGORIES}
100
+ values={VALUES}
101
+ activeCategoryId="appearance"
102
+ activeRowIndex={0}
103
+ activeZone="body"
104
+ onCategorySelect={() => {}}
105
+ onRowActivate={() => {}}
106
+ theme={THEME_PRESETS["rose-pine-moon"]}
107
+ />
108
+ ),
109
+ expectedColors: [THEME_PRESETS["rose-pine-moon"].accent],
110
+ size: { width: 100, height: 30 },
111
+ };
@@ -49,7 +49,8 @@ export function SettingsDialog({
49
49
  const tokens = useComponentTheme(theme);
50
50
  const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
51
51
  if (!isOpen) return null;
52
- const category = categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
52
+ const category =
53
+ categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
53
54
  if (!category) return null;
54
55
  return (
55
56
  <Dialog
@@ -68,10 +69,14 @@ export function SettingsDialog({
68
69
  return (
69
70
  <box
70
71
  key={candidate.id}
71
- style={{ backgroundColor: isActive && activeZone === "nav" ? tokens.border : undefined }}
72
+ style={{
73
+ backgroundColor: isActive && activeZone === "nav" ? tokens.border : undefined,
74
+ }}
72
75
  onMouseUp={() => onCategorySelect(candidate.id)}
73
76
  >
74
- <text fg={isActive ? tokens.accent : tokens.textMuted}>{`${isActive ? "› " : " "}${candidate.name}`}</text>
77
+ <text
78
+ fg={isActive ? tokens.accent : tokens.textMuted}
79
+ >{`${isActive ? "› " : " "}${candidate.name}`}</text>
75
80
  </box>
76
81
  );
77
82
  })}