@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.67

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 (160) hide show
  1. package/package.json +6 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +79 -112
  4. package/src/App.plan-review.test.tsx +23 -205
  5. package/src/App.prototype.test.tsx +1 -8
  6. package/src/App.readonly.test.tsx +9 -27
  7. package/src/App.submit-confirm.test.tsx +19 -72
  8. package/src/App.test.tsx +136 -31
  9. package/src/App.tsx +491 -322
  10. package/src/App.walk.test.tsx +15 -16
  11. package/src/App.workbench.test.tsx +390 -0
  12. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  13. package/src/annotation-palette.ts +61 -0
  14. package/src/app-screens.tsx +197 -77
  15. package/src/app-view-model.ts +9 -69
  16. package/src/components/AnnotationCard.stories.tsx +1 -1
  17. package/src/components/AnnotationCard.tsx +1 -1
  18. package/src/components/AnnotationCards.stories.tsx +87 -0
  19. package/src/components/AnnotationCards.tsx +303 -0
  20. package/src/components/AppHeader.stories.tsx +50 -0
  21. package/src/components/AppHeader.tsx +83 -0
  22. package/src/components/AppShell.stories.tsx +89 -0
  23. package/src/components/AppShell.tsx +239 -0
  24. package/src/components/AppShell.zoom.test.tsx +94 -0
  25. package/src/components/Breadcrumb.stories.tsx +1 -1
  26. package/src/components/ChangesColumn.stories.tsx +39 -0
  27. package/src/components/ChangesColumn.tsx +170 -0
  28. package/src/components/CodeBlock.stories.tsx +1 -1
  29. package/src/components/CompletionOverlay.stories.tsx +1 -1
  30. package/src/components/ConfirmCard.stories.tsx +1 -1
  31. package/src/components/ConfirmCard.tsx +3 -4
  32. package/src/components/ConfirmDialog.stories.tsx +1 -1
  33. package/src/components/DiffSheet.scroll.test.tsx +38 -55
  34. package/src/components/DiffSheet.split.test.tsx +140 -0
  35. package/src/components/DiffSheet.stories.tsx +56 -54
  36. package/src/components/DiffSheet.syntax.test.tsx +24 -7
  37. package/src/components/DiffSheet.tsx +607 -276
  38. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  39. package/src/components/DiscussionMarkerRail.tsx +181 -0
  40. package/src/components/EditorGrid.stories.tsx +59 -0
  41. package/src/components/EditorGrid.tabs.test.tsx +105 -0
  42. package/src/components/EditorGrid.tsx +338 -0
  43. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  44. package/src/components/FileContentsView.stories.tsx +16 -0
  45. package/src/components/FileContentsView.test.tsx +29 -0
  46. package/src/components/FileContentsView.tsx +78 -0
  47. package/src/components/InboxList.stories.tsx +23 -8
  48. package/src/components/InboxList.tsx +188 -33
  49. package/src/components/KeybindsDialog.stories.tsx +1 -1
  50. package/src/components/MarkerPopover.stories.tsx +1 -1
  51. package/src/components/MenuBar.stories.tsx +3 -12
  52. package/src/components/MenuBar.tsx +25 -56
  53. package/src/components/PaneGrid.stories.tsx +84 -0
  54. package/src/components/PaneGrid.tsx +128 -0
  55. package/src/components/PanelColumn.stories.tsx +25 -0
  56. package/src/components/PanelColumn.tsx +95 -0
  57. package/src/components/ProjectTreeView.stories.tsx +25 -0
  58. package/src/components/ProjectTreeView.test.tsx +32 -0
  59. package/src/components/ProjectTreeView.tsx +82 -0
  60. package/src/components/PromptDialog.stories.tsx +1 -1
  61. package/src/components/PromptDialog.tsx +8 -1
  62. package/src/components/PrototypeSheet.stories.tsx +1 -1
  63. package/src/components/RootOverlay.stories.tsx +44 -0
  64. package/src/components/RootOverlay.tsx +35 -0
  65. package/src/components/SettingsDialog.stories.tsx +7 -1
  66. package/src/components/SettingsDialog.tsx +114 -67
  67. package/src/components/SettingsRows.stories.tsx +1 -1
  68. package/src/components/ShellHeader.stories.tsx +50 -0
  69. package/src/components/ShellHeader.tsx +137 -0
  70. package/src/components/SurfaceScrollbar.stories.tsx +34 -0
  71. package/src/components/SurfaceScrollbar.test.tsx +111 -0
  72. package/src/components/SurfaceScrollbar.tsx +93 -0
  73. package/src/components/ThreadFooter.stories.tsx +30 -0
  74. package/src/components/ThreadFooter.test.tsx +31 -0
  75. package/src/components/ThreadFooter.tsx +54 -0
  76. package/src/components/ThreadView.slash.test.tsx +113 -0
  77. package/src/components/ThreadView.stories.tsx +1 -1
  78. package/src/components/ThreadView.tsx +62 -1352
  79. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  80. package/src/components/ThreadsSidebar.tsx +70 -0
  81. package/src/components/Toast.stories.tsx +1 -1
  82. package/src/components/Tooltip.stories.tsx +26 -0
  83. package/src/components/Tooltip.tsx +72 -0
  84. package/src/components/TreePane.stories.tsx +1 -1
  85. package/src/components/WalkWizard.stories.tsx +1 -1
  86. package/src/components/WelcomeSurface.stories.tsx +12 -0
  87. package/src/components/WelcomeSurface.tsx +52 -0
  88. package/src/components/__snapshots__/stories.test.tsx.snap +732 -572
  89. package/src/components/agent-launcher.stories.tsx +1 -1
  90. package/src/components/diff-sheet-layout.test.ts +125 -55
  91. package/src/components/diff-sheet-layout.ts +3 -103
  92. package/src/components/editor-grid.test.ts +86 -0
  93. package/src/components/editor-grid.ts +138 -0
  94. package/src/components/fade-title.test.ts +49 -0
  95. package/src/components/fade-title.ts +61 -0
  96. package/src/components/file-tree.test.ts +60 -0
  97. package/src/components/file-tree.ts +112 -0
  98. package/src/components/pages/Workspace.stories.tsx +181 -0
  99. package/src/components/primitives/Button.stories.tsx +1 -1
  100. package/src/components/primitives/Card.stories.tsx +1 -1
  101. package/src/components/primitives/Dialog.stories.tsx +1 -1
  102. package/src/components/primitives/IconButton.stories.tsx +36 -0
  103. package/src/components/primitives/IconButton.tsx +62 -0
  104. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  105. package/src/components/primitives/Tabs.stories.tsx +1 -1
  106. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  107. package/src/components/primitives/Tree.stories.tsx +124 -0
  108. package/src/components/primitives/Tree.tsx +96 -0
  109. package/src/components/primitives/icons.ts +61 -0
  110. package/src/components/primitives/tree-model.test.ts +99 -0
  111. package/src/components/primitives/tree-model.ts +137 -0
  112. package/src/components/quick-actions-editor.stories.tsx +1 -1
  113. package/src/components/session-tree.test.ts +134 -0
  114. package/src/components/session-tree.ts +108 -0
  115. package/src/components/stories-app.tsx +142 -82
  116. package/src/components/story-tree.test.ts +100 -0
  117. package/src/components/story.ts +55 -0
  118. package/src/components/theme-context.stories.tsx +1 -1
  119. package/src/config.test.ts +20 -49
  120. package/src/config.ts +26 -28
  121. package/src/diff-fold.test.ts +78 -0
  122. package/src/diff-fold.ts +126 -0
  123. package/src/diff-syntax.test.ts +16 -0
  124. package/src/diff-syntax.ts +6 -3
  125. package/src/discussions.ts +79 -0
  126. package/src/intent-dispatch.test.ts +31 -11
  127. package/src/intent-dispatch.ts +93 -48
  128. package/src/key-bindings.test.ts +1 -1
  129. package/src/keymap.test.ts +3 -25
  130. package/src/keymap.ts +17 -18
  131. package/src/mark-runs.ts +120 -0
  132. package/src/pty.ts +10 -2
  133. package/src/run.ts +17 -7
  134. package/src/serve.test.ts +4 -8
  135. package/src/session-controller.curate.test.ts +75 -0
  136. package/src/session-controller.share.test.ts +1 -0
  137. package/src/session-controller.tree.test.ts +26 -0
  138. package/src/session-controller.ts +160 -31
  139. package/src/slash-palette.ts +83 -0
  140. package/src/split-diff.test.ts +110 -0
  141. package/src/split-diff.ts +149 -0
  142. package/src/test-support.test.ts +3 -3
  143. package/src/test-support.ts +39 -8
  144. package/src/theme.ts +4 -4
  145. package/src/thread-chords.test.ts +25 -9
  146. package/src/thread-chords.ts +33 -8
  147. package/src/use-annotation-surface.tsx +837 -0
  148. package/src/use-changes-workbench.ts +151 -0
  149. package/src/use-frame-measure.ts +38 -0
  150. package/src/use-settings-dialog.tsx +23 -24
  151. package/src/use-terminal-virtualizer.ts +233 -0
  152. package/src/view-diff.ts +114 -0
  153. package/src/App.review-panel.test.tsx +0 -140
  154. package/src/App.tree.test.tsx +0 -224
  155. package/src/components/ReviewPanel.stories.tsx +0 -100
  156. package/src/components/ReviewPanel.tsx +0 -103
  157. package/src/components/ReviewRail.stories.tsx +0 -232
  158. package/src/components/ReviewRail.tsx +0 -327
  159. package/src/review-panel.test.ts +0 -113
  160. package/src/review-panel.ts +0 -83
@@ -1,232 +0,0 @@
1
- import React from "react";
2
- import { DARK } from "../theme";
3
- import type { Story, StoryMeta } from "./story";
4
- import { ReviewRail } from "./ReviewRail";
5
- import { FIXTURE_ANNOTATIONS, fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
6
-
7
- export const meta: StoryMeta = { title: "ReviewRail" };
8
-
9
- /** An empty Tree tab: the stories here show the review tab. */
10
- const tree = {
11
- rows: [],
12
- canMove: true,
13
- canFork: true,
14
- onSelect: () => {},
15
- onGo: () => {},
16
- onBranch: () => {},
17
- onLabel: () => {},
18
- onFork: () => {},
19
- onForkAndShare: () => {},
20
- };
21
-
22
- const callbacks = {
23
- curationItems: [],
24
- tree,
25
- onTabChange: () => {},
26
- onSelectCard: () => {},
27
- onActivateCard: () => {},
28
- onSelectCuration: () => {},
29
- onUndoCuration: () => {},
30
- onSubmitRequest: () => {},
31
- onLaunchHarness: () => {},
32
- };
33
-
34
- /** The rail expects a height-constrained parent, like the app's main row. */
35
- function RailFrame({ children }: { children: React.ReactNode }): React.ReactNode {
36
- return <box style={{ flexDirection: "row", width: "100%", height: "100%" }}>{children}</box>;
37
- }
38
-
39
- export const AnnotationStack: Story = {
40
- render: () => (
41
- <RailFrame>
42
- <ReviewRail
43
- session={fixturePlanSession()}
44
- authorNames={{}}
45
- selectedId={FIXTURE_ANNOTATIONS[0]!.id}
46
- resolvedIds={new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id))}
47
- railTab="review"
48
- pendingCount={2}
49
- cardEdit={null}
50
- submitConfirm={null}
51
- {...callbacks}
52
- />
53
- </RailFrame>
54
- ),
55
- expectedColors: [DARK.accent],
56
- size: { width: 40, height: 24 },
57
- };
58
-
59
- export const MixedOwnershipStack: Story = {
60
- render: () => (
61
- <RailFrame>
62
- <ReviewRail
63
- session={fixturePlanSession({
64
- participants: [{ id: "SHA256:collab1", provider: "ssh", name: "Priya" }],
65
- annotations: [
66
- {
67
- id: "own_note",
68
- kind: "comment",
69
- anchor: { quote: "persists sessions", prefix: "The daemon ", suffix: " to disk" },
70
- body: "Which daemon owns this?",
71
- createdAt: "2026-01-01T00:00:00Z",
72
- },
73
- {
74
- id: "collab_note",
75
- kind: "comment",
76
- anchor: { quote: "move the store", prefix: "", suffix: "" },
77
- body: "move it behind one interface",
78
- author: "SHA256:collab1",
79
- createdAt: "2026-01-01T00:00:01Z",
80
- },
81
- ],
82
- })}
83
- authorNames={{}}
84
- selectedId="own_note"
85
- resolvedIds={new Set(["own_note", "collab_note"])}
86
- railTab="review"
87
- pendingCount={2}
88
- cardEdit={null}
89
- submitConfirm={null}
90
- {...callbacks}
91
- />
92
- </RailFrame>
93
- ),
94
- // every annotation border is accent now
95
- expectedColors: [DARK.accent],
96
- size: { width: 40, height: 24 },
97
- };
98
-
99
- export const Removals: Story = {
100
- render: () => (
101
- <RailFrame>
102
- <ReviewRail
103
- session={fixtureDiffSession()}
104
- authorNames={{}}
105
- resolvedIds={null}
106
- railTab="review"
107
- pendingCount={0}
108
- cardEdit={null}
109
- submitConfirm={null}
110
- {...callbacks}
111
- curationItems={[
112
- {
113
- id: "diff:src/store.ts#0#1",
114
- source: "diff",
115
- preview: ["- private items = [];", "+ private items = new Map();"],
116
- revealIndex: 3,
117
- },
118
- {
119
- id: "diff:src/store.ts#1#hunk",
120
- source: "diff",
121
- preview: ["- return null;", "+ return fallback();", "+ log(fallback);"],
122
- revealIndex: 9,
123
- },
124
- ]}
125
- selectedCurationId="diff:src/store.ts#0#1"
126
- />
127
- </RailFrame>
128
- ),
129
- expectedColors: [DARK.red],
130
- size: { width: 40, height: 24 },
131
- };
132
-
133
- /** A plan cut and an annotation interleave in one line-ordered stack. */
134
- export const InterleavedStack: Story = {
135
- render: () => (
136
- <RailFrame>
137
- <ReviewRail
138
- session={fixturePlanSession()}
139
- authorNames={{}}
140
- selectedId={FIXTURE_ANNOTATIONS[0]!.id}
141
- resolvedIds={new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id))}
142
- railTab="review"
143
- pendingCount={2}
144
- cardEdit={null}
145
- submitConfirm={null}
146
- {...callbacks}
147
- curationItems={[
148
- {
149
- id: "plan:6-6",
150
- source: "plan",
151
- preview: ["- add recovery"],
152
- revealIndex: 1,
153
- },
154
- ]}
155
- annotationPositions={new Map([[FIXTURE_ANNOTATIONS[0]!.id, 5]])}
156
- />
157
- </RailFrame>
158
- ),
159
- expectedColors: [DARK.accent],
160
- size: { width: 40, height: 24 },
161
- };
162
-
163
- export const AgentTab: Story = {
164
- render: () => (
165
- <RailFrame>
166
- <ReviewRail
167
- session={fixturePlanSession()}
168
- authorNames={{}}
169
- resolvedIds={new Set()}
170
- railTab="agent"
171
- pendingCount={2}
172
- cardEdit={null}
173
- submitConfirm={null}
174
- {...callbacks}
175
- />
176
- </RailFrame>
177
- ),
178
- size: { width: 40, height: 24 },
179
- };
180
-
181
- export const SubmitConfirmOpen: Story = {
182
- render: () => (
183
- <RailFrame>
184
- <ReviewRail
185
- session={fixturePlanSession()}
186
- authorNames={{}}
187
- resolvedIds={new Set()}
188
- railTab="review"
189
- pendingCount={2}
190
- cardEdit={null}
191
- submitConfirm={{
192
- verdict: "request_changes",
193
- summary: "",
194
- onInput: () => {},
195
- onSelectVerdict: () => {},
196
- onSubmit: () => {},
197
- onCancel: () => {},
198
- }}
199
- {...callbacks}
200
- />
201
- </RailFrame>
202
- ),
203
- expectedColors: [DARK.red],
204
- size: { width: 40, height: 30 },
205
- };
206
-
207
- export const Resolved: Story = {
208
- render: () => (
209
- <RailFrame>
210
- <ReviewRail
211
- session={fixturePlanSession({
212
- status: "resolved",
213
- verdict: {
214
- kind: "approve",
215
- summary: "",
216
- feedback: "",
217
- resolvedAt: "2026-01-01T00:00:00Z",
218
- },
219
- })}
220
- authorNames={{}}
221
- resolvedIds={new Set()}
222
- railTab="review"
223
- pendingCount={0}
224
- cardEdit={null}
225
- submitConfirm={null}
226
- {...callbacks}
227
- />
228
- </RailFrame>
229
- ),
230
- expectedColors: [DARK.green],
231
- size: { width: 40, height: 24 },
232
- };
@@ -1,327 +0,0 @@
1
- /**
2
- * The right review rail: tab strip (Review/Tree/Agent), the scrollable annotation
3
- * stack, and the pinned submit affordance. The confirm card sits OUTSIDE the
4
- * scrollbox so the stack scrolls while the card stays at the rail bottom.
5
- * Card reveal goes through the scrollbox's own scrollChildIntoView - no
6
- * hardcoded card-height arithmetic.
7
- */
8
-
9
- import React, { forwardRef, useImperativeHandle, useRef } from "react";
10
- import { createTextAttributes, type ScrollBoxRenderable } from "@opentui/core";
11
- import { isAddressed, isAgentNote, type Annotation, type ReviewSession } from "@cueloop/schema";
12
- import type { CurationItem } from "../session-controller";
13
- import type { Theme } from "../theme";
14
- import { useComponentTheme } from "./theme-context";
15
- import { Tabs, Tab, TabList } from "./primitives/Tabs";
16
- import { Button } from "./primitives/Button";
17
- import { Toolbar } from "./primitives/Toolbar";
18
- import { AnnotationCard, type AnnotationDraft } from "./AnnotationCard";
19
- import { ConfirmCard, type ConfirmCardProps } from "./ConfirmCard";
20
- import { AgentLauncher, type AgentTerminalHandle } from "./agent-launcher";
21
- import { TreePane, type TreePaneProps } from "./TreePane";
22
- import { Card } from "./primitives/Card";
23
- import { truncateToSingleLine } from "./truncate-text";
24
- import { resolveDisplayName } from "../attribution";
25
-
26
- export type RailTab = "review" | "agent" | "tree";
27
-
28
- export interface RailCardEdit extends AnnotationDraft {
29
- id: string;
30
- }
31
-
32
- export interface ReviewRailHandle {
33
- /** Scroll the card for an annotation into view (selection symmetry). */
34
- revealCard(annotationId: string): void;
35
- }
36
-
37
- export interface ReviewRailProps {
38
- session: ReviewSession;
39
- /** Planner-local author renames (config [authors]); override the registry name. */
40
- authorNames: Record<string, string>;
41
- selectedId?: string;
42
- /** Ids whose anchor resolved; null = orphan display off (diff view). */
43
- resolvedIds: Set<string> | null;
44
- /** Removed pieces (diff rejections / plan cuts) interleaved into the card stack. */
45
- curationItems: CurationItem[];
46
- /** The selected curation item, highlighted and the undo target. */
47
- selectedCurationId?: string;
48
- /**
49
- * Sort position per annotation id (diff row index / plan display index), so
50
- * annotation and removal cards interleave in one position-ordered stack.
51
- */
52
- annotationPositions?: Map<string, number>;
53
- railTab: RailTab;
54
- pendingCount: number;
55
- cardEdit: RailCardEdit | null;
56
- submitConfirm: Omit<ConfirmCardProps, "theme"> | null;
57
- onTabChange: (tab: RailTab) => void;
58
- onSelectCard: (id: string) => void;
59
- onActivateCard: (id: string) => void;
60
- /** Select a curation item (reveals its diff row); pressing undo restores it. */
61
- onSelectCuration: (id: string) => void;
62
- /** Restore a curation item (the selected card's undo button, same as the u key). */
63
- onUndoCuration: (id: string) => void;
64
- onSubmitRequest: () => void;
65
- /** Launch a bring-your-own harness in the rail (Agent tab); seedText is the plan-context briefing. */
66
- onLaunchHarness: (command: string, seedText?: string) => void;
67
- /** In-tab agent terminal handle (embedded path); null when detached. Lets the app route keys to it. */
68
- onAgentTerminal?: (handle: AgentTerminalHandle | null) => void;
69
- /** The Tree tab: the session's history rows and the primitives that move it. */
70
- tree: Omit<TreePaneProps, "theme">;
71
- /** Rail column width; the app derives it from the persisted review layout. */
72
- width?: number;
73
- /**
74
- * When set, renders the muted `>` chevron pinned at the rail's bottom-left,
75
- * one column from the divider. Clicking it collapses the panel to compact.
76
- */
77
- onCollapse?: () => void;
78
- theme?: Theme;
79
- }
80
-
81
- export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function ReviewRail(
82
- {
83
- session,
84
- authorNames,
85
- selectedId,
86
- resolvedIds,
87
- curationItems,
88
- selectedCurationId,
89
- annotationPositions,
90
- railTab,
91
- cardEdit,
92
- submitConfirm,
93
- onTabChange,
94
- onSelectCard,
95
- onActivateCard,
96
- onSelectCuration,
97
- onUndoCuration,
98
- onSubmitRequest,
99
- onLaunchHarness,
100
- onAgentTerminal,
101
- tree,
102
- width = 34,
103
- onCollapse,
104
- theme,
105
- }: ReviewRailProps,
106
- handleRef,
107
- ): React.ReactNode {
108
- const tokens = useComponentTheme(theme);
109
- const scrollRef = useRef<ScrollBoxRenderable | null>(null);
110
- // addressed annotations leave the card list; a dim count keeps them honest
111
- const openAnnotations = session.annotations.filter((annotation) => !isAddressed(annotation));
112
- const addressedCount = session.annotations.length - openAnnotations.length;
113
-
114
- useImperativeHandle(handleRef, () => ({
115
- revealCard: (annotationId: string): void => {
116
- try {
117
- scrollRef.current?.scrollChildIntoView(`annotation-card-${annotationId}`);
118
- } catch {
119
- // reveal is best-effort; selection state is already correct
120
- }
121
- },
122
- }));
123
-
124
- const renderAnnotationCard = (annotation: Annotation): React.ReactNode => (
125
- <AnnotationCard
126
- key={annotation.id}
127
- id={`annotation-card-${annotation.id}`}
128
- kind={annotation.kind}
129
- quote={annotation.anchor.quote}
130
- theme={theme}
131
- saved={{
132
- body: annotation.body,
133
- isSelected: annotation.id === selectedId,
134
- isOrphan: resolvedIds !== null && !resolvedIds.has(annotation.id),
135
- // border-title author: a collaborator's name, "agent" for agent notes, else "me"
136
- author: annotation.author
137
- ? resolveDisplayName(annotation.author, session.participants, authorNames)
138
- : isAgentNote(annotation)
139
- ? "agent"
140
- : "me",
141
- tone: tokens.blue,
142
- editing:
143
- cardEdit && cardEdit.id === annotation.id
144
- ? {
145
- text: cardEdit.text,
146
- onInput: cardEdit.onInput,
147
- onSave: cardEdit.onSave,
148
- onCancel: cardEdit.onCancel,
149
- }
150
- : null,
151
- onPress: () =>
152
- annotation.id === selectedId
153
- ? onActivateCard(annotation.id)
154
- : onSelectCard(annotation.id),
155
- }}
156
- />
157
- );
158
-
159
- // one position-ordered stack: annotation and removal cards interleaved by their
160
- // source line (diff row / plan block), original order breaking ties
161
- const railEntries: { sort: number; order: number; node: React.ReactNode }[] = [
162
- ...openAnnotations.map((annotation, order) => ({
163
- sort: annotationPositions?.get(annotation.id) ?? Number.MAX_SAFE_INTEGER,
164
- order,
165
- node: renderAnnotationCard(annotation),
166
- })),
167
- ...curationItems.map((item, index) => ({
168
- sort: item.revealIndex,
169
- order: openAnnotations.length + index,
170
- node: (
171
- <RemovalCard
172
- key={item.id}
173
- item={item}
174
- isSelected={item.id === selectedCurationId}
175
- onSelect={onSelectCuration}
176
- onUndo={onUndoCuration}
177
- theme={theme}
178
- />
179
- ),
180
- })),
181
- ].sort((left, right) => left.sort - right.sort || left.order - right.order);
182
-
183
- return (
184
- <box
185
- style={{
186
- width,
187
- backgroundColor: tokens.panel,
188
- flexDirection: "column",
189
- paddingBottom: 1,
190
- }}
191
- >
192
- <Tabs
193
- selectedKey={railTab}
194
- onSelectionChange={(key) => {
195
- if (key === "review" || key === "agent" || key === "tree") onTabChange(key);
196
- }}
197
- theme={theme}
198
- >
199
- <TabList>
200
- <Tab id="review">Review</Tab>
201
- <Tab id="tree">Tree</Tab>
202
- <Tab id="agent">Agent</Tab>
203
- </TabList>
204
- </Tabs>
205
- {railTab === "tree" ? (
206
- <TreePane {...tree} theme={theme} />
207
- ) : railTab === "agent" ? (
208
- <box style={{ flexGrow: 1, flexDirection: "column" }}>
209
- <AgentLauncher
210
- session={session}
211
- onLaunchHarness={onLaunchHarness}
212
- onAgentTerminal={onAgentTerminal}
213
- theme={theme}
214
- />
215
- </box>
216
- ) : openAnnotations.length === 0 && curationItems.length === 0 ? (
217
- <box style={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
218
- <text fg={tokens.textDim}>
219
- {session.annotations.length === 0 ? "no annotations yet" : "all annotations addressed"}
220
- </text>
221
- </box>
222
- ) : (
223
- <box style={{ flexGrow: 1, flexDirection: "column" }}>
224
- {addressedCount > 0 ? (
225
- <text fg={tokens.textDim}>✓ {addressedCount} addressed by revision</text>
226
- ) : null}
227
- <scrollbox ref={scrollRef} style={{ flexGrow: 1 }} focused={false}>
228
- {railEntries.map((entry) => entry.node)}
229
- </scrollbox>
230
- </box>
231
- )}
232
- {/* the confirm card keeps full width, above the footer row */}
233
- {railTab === "review" && submitConfirm ? (
234
- <ConfirmCard {...submitConfirm} theme={theme} />
235
- ) : null}
236
- {/* footer row: collapse chevron left-bound, the submit affordance inline
237
- beside it, both on the rail's last row (the plan's bottom-border height) */}
238
- <box style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
239
- {onCollapse ? (
240
- <box onMouseUp={onCollapse}>
241
- <text fg={tokens.textDim}>{">"}</text>
242
- </box>
243
- ) : null}
244
- {railTab === "review" && !submitConfirm ? (
245
- session.status === "resolved" ? (
246
- <text fg={tokens.green}>resolved: {session.verdict!.kind.replace("_", " ")}</text>
247
- ) : (
248
- <Button variant="accent-text" onPress={onSubmitRequest} theme={theme}>
249
- Submit review
250
- </Button>
251
- )
252
- ) : null}
253
- </box>
254
- </box>
255
- );
256
- });
257
-
258
- /** A removal preview reads as removed content: struck through and dimmed. */
259
- const REMOVAL_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
260
-
261
- /** Preview lines a removal card shows before it collapses to a "+N more" row. */
262
- const REMOVAL_PREVIEW_LINES = 4;
263
-
264
- /** Rough content width inside a removal card's border + padding, for truncation. */
265
- const REMOVAL_PREVIEW_WIDTH = 24;
266
-
267
- /**
268
- * A removed piece as a full display card - the same border/padding/selection
269
- * chrome as a saved annotation card - previewing the removed content struck
270
- * through. Unified across diff (a curated-out hunk/change) and plan (a cut
271
- * block). Clicking selects (App reveals its source line); the undo key restores.
272
- */
273
- function RemovalCard({
274
- item,
275
- isSelected,
276
- onSelect,
277
- onUndo,
278
- theme,
279
- }: {
280
- item: CurationItem;
281
- isSelected: boolean;
282
- onSelect: (id: string) => void;
283
- onUndo: (id: string) => void;
284
- theme?: Theme;
285
- }): React.ReactNode {
286
- const tokens = useComponentTheme(theme);
287
- const previewLines = item.preview.slice(0, REMOVAL_PREVIEW_LINES);
288
- const overflow = item.preview.length - previewLines.length;
289
- // body = the struck-through preview (at least one row) + an overflow row, plus
290
- // the undo button's own row when selected (mirrors the draft card's Save/Cancel)
291
- const contentRows =
292
- Math.max(1, previewLines.length) + (overflow > 0 ? 1 : 0) + (isSelected ? 1 : 0);
293
- // border = "what + who": a diff reject or a plan cut, always the reviewer's own
294
- const title = ` ${item.source === "diff" ? "REJECT" : "CUT"} · me `;
295
-
296
- return (
297
- <box id={`removal-card-${item.id}`} onMouseUp={() => onSelect(item.id)}>
298
- <Card
299
- title={title}
300
- contentRows={contentRows}
301
- borderColor={tokens.red}
302
- backgroundColor="transparent"
303
- theme={theme}
304
- >
305
- {previewLines.length === 0 ? (
306
- <text fg={tokens.textDim}> </text>
307
- ) : (
308
- previewLines.map((line, lineIndex) => (
309
- <text key={lineIndex} fg={tokens.textDim}>
310
- <span attributes={REMOVAL_ATTRIBUTES}>
311
- {truncateToSingleLine(line, REMOVAL_PREVIEW_WIDTH)}
312
- </span>
313
- </text>
314
- ))
315
- )}
316
- {overflow > 0 ? <text fg={tokens.textDim}>{` …+${overflow} more`}</text> : null}
317
- {isSelected ? (
318
- <Toolbar>
319
- <Button variant="solid" onPress={() => onUndo(item.id)} theme={theme}>
320
- {" undo "}
321
- </Button>
322
- </Toolbar>
323
- ) : null}
324
- </Card>
325
- </box>
326
- );
327
- }
@@ -1,113 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- REVIEW_DEFAULT_WIDTH,
4
- REVIEW_MAX_WIDTH,
5
- REVIEW_MIN_WIDTH,
6
- REVIEW_PLAN_MIN_WIDTH,
7
- REVIEW_RESIZE_STEP,
8
- clampWidth,
9
- cycleReviewPanelMode,
10
- maxWidthForTerminal,
11
- resolveReviewWidth,
12
- reviewRowsToDivider,
13
- toggleReviewPanelMode,
14
- widthFromMouseColumn,
15
- } from "./review-panel";
16
-
17
- describe("clampWidth", () => {
18
- test("keeps a width inside the bounds untouched", () => {
19
- expect(clampWidth(REVIEW_DEFAULT_WIDTH)).toBe(REVIEW_DEFAULT_WIDTH);
20
- });
21
-
22
- test("clamps below the minimum and above the maximum", () => {
23
- expect(clampWidth(REVIEW_MIN_WIDTH - 10)).toBe(REVIEW_MIN_WIDTH);
24
- expect(clampWidth(REVIEW_MAX_WIDTH + 10)).toBe(REVIEW_MAX_WIDTH);
25
- });
26
-
27
- test("rounds a fractional column to a whole one", () => {
28
- expect(clampWidth(30.7)).toBe(31);
29
- });
30
-
31
- test("honors explicit bounds", () => {
32
- expect(clampWidth(100, 10, 20)).toBe(20);
33
- expect(clampWidth(5, 10, 20)).toBe(10);
34
- });
35
- });
36
-
37
- describe("cycleReviewPanelMode", () => {
38
- test("cycles expanded -> compact -> hidden -> expanded", () => {
39
- expect(cycleReviewPanelMode("expanded")).toBe("compact");
40
- expect(cycleReviewPanelMode("compact")).toBe("hidden");
41
- expect(cycleReviewPanelMode("hidden")).toBe("expanded");
42
- });
43
- });
44
-
45
- describe("toggleReviewPanelMode", () => {
46
- test("the chevron toggles only between expanded and compact, never hidden", () => {
47
- expect(toggleReviewPanelMode("expanded")).toBe("compact");
48
- expect(toggleReviewPanelMode("compact")).toBe("expanded");
49
- // a hidden panel has no chevron; a stray toggle still opens it
50
- expect(toggleReviewPanelMode("hidden")).toBe("expanded");
51
- });
52
- });
53
-
54
- describe("widthFromMouseColumn", () => {
55
- test("measures the rail from the mouse column to the right edge", () => {
56
- // divider dragged to column 80 of a 120-wide terminal -> a 40-col rail
57
- expect(widthFromMouseColumn(80, 120)).toBe(40);
58
- });
59
-
60
- test("clamps the dragged width to the bounds", () => {
61
- expect(widthFromMouseColumn(0, 120)).toBe(REVIEW_MAX_WIDTH);
62
- expect(widthFromMouseColumn(119, 120)).toBe(REVIEW_MIN_WIDTH);
63
- });
64
-
65
- test("cannot drag the rail wide enough to starve the plan on a narrow terminal", () => {
66
- // dragging to the far left edge of a 60-col terminal still leaves the plan its minimum
67
- expect(widthFromMouseColumn(0, 60)).toBe(60 - REVIEW_PLAN_MIN_WIDTH - 1);
68
- });
69
-
70
- test("the resize step widens and narrows within the clamp", () => {
71
- expect(clampWidth(REVIEW_DEFAULT_WIDTH + REVIEW_RESIZE_STEP)).toBe(
72
- REVIEW_DEFAULT_WIDTH + REVIEW_RESIZE_STEP,
73
- );
74
- expect(clampWidth(REVIEW_MIN_WIDTH - REVIEW_RESIZE_STEP)).toBe(REVIEW_MIN_WIDTH);
75
- });
76
- });
77
-
78
- describe("maxWidthForTerminal", () => {
79
- test("allows the full rail on a wide terminal", () => {
80
- expect(maxWidthForTerminal(120)).toBe(REVIEW_MAX_WIDTH);
81
- });
82
-
83
- test("reserves the plan its minimum on a narrow terminal", () => {
84
- // 60-col terminal: 30 for the plan and 1 for the divider leaves 29 for the rail
85
- expect(maxWidthForTerminal(60)).toBe(60 - REVIEW_PLAN_MIN_WIDTH - 1);
86
- });
87
-
88
- test("never drops below the rail minimum, however narrow the terminal", () => {
89
- expect(maxWidthForTerminal(40)).toBe(REVIEW_MIN_WIDTH);
90
- });
91
- });
92
-
93
- describe("resolveReviewWidth", () => {
94
- test("leaves a width that already fits untouched", () => {
95
- expect(resolveReviewWidth(REVIEW_DEFAULT_WIDTH, 120)).toBe(REVIEW_DEFAULT_WIDTH);
96
- });
97
-
98
- test("shrinks a wide persisted width so the plan keeps its room", () => {
99
- // a saved 50-col rail on a 60-col terminal would leave the plan 9 cols; clamp it back
100
- expect(resolveReviewWidth(REVIEW_MAX_WIDTH, 60)).toBe(60 - REVIEW_PLAN_MIN_WIDTH - 1);
101
- });
102
- });
103
-
104
- describe("reviewRowsToDivider", () => {
105
- test("builds one vertical glyph per row", () => {
106
- expect(reviewRowsToDivider(3)).toBe("│\n│\n│");
107
- });
108
-
109
- test("never collapses to an empty column", () => {
110
- expect(reviewRowsToDivider(0)).toBe("│");
111
- expect(reviewRowsToDivider(-4)).toBe("│");
112
- });
113
- });