@cueloop/client 0.1.0-alpha.66 → 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.
- package/package.json +6 -5
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +71 -39
- package/src/App.plan-review.test.tsx +20 -21
- package/src/App.readonly.test.tsx +7 -3
- package/src/App.test.tsx +73 -14
- package/src/App.tsx +375 -129
- package/src/App.walk.test.tsx +14 -15
- package/src/App.workbench.test.tsx +390 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +61 -0
- package/src/app-screens.tsx +176 -45
- package/src/app-view-model.ts +7 -65
- package/src/components/AnnotationCard.tsx +1 -1
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +47 -42
- package/src/components/AppShell.tsx +212 -100
- package/src/components/AppShell.zoom.test.tsx +94 -0
- package/src/components/ChangesColumn.tsx +49 -26
- package/src/components/DiffSheet.scroll.test.tsx +38 -55
- package/src/components/DiffSheet.split.test.tsx +140 -0
- package/src/components/DiffSheet.stories.tsx +55 -53
- package/src/components/DiffSheet.syntax.test.tsx +24 -7
- package/src/components/DiffSheet.tsx +607 -276
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +105 -0
- package/src/components/EditorGrid.tsx +338 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +78 -0
- package/src/components/InboxList.tsx +159 -30
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +82 -0
- package/src/components/PromptDialog.tsx +8 -1
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -0
- package/src/components/SettingsDialog.tsx +2 -1
- package/src/components/SurfaceScrollbar.stories.tsx +34 -0
- package/src/components/SurfaceScrollbar.test.tsx +111 -0
- package/src/components/SurfaceScrollbar.tsx +93 -0
- package/src/components/ThreadFooter.stories.tsx +2 -2
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +12 -7
- package/src/components/ThreadView.tsx +62 -1440
- package/src/components/ThreadsSidebar.tsx +14 -1
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/WelcomeSurface.stories.tsx +12 -0
- package/src/components/WelcomeSurface.tsx +52 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +509 -559
- package/src/components/diff-sheet-layout.test.ts +125 -55
- package/src/components/diff-sheet-layout.ts +3 -103
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +15 -0
- package/src/components/file-tree.ts +59 -4
- package/src/components/pages/Workspace.stories.tsx +2 -2
- package/src/components/primitives/IconButton.tsx +21 -2
- package/src/components/primitives/Tree.tsx +13 -4
- package/src/components/primitives/icons.ts +27 -4
- package/src/components/session-tree.test.ts +20 -0
- package/src/components/session-tree.ts +16 -2
- package/src/components/stories-app.tsx +2 -2
- package/src/config.test.ts +20 -49
- package/src/config.ts +26 -28
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-syntax.test.ts +16 -0
- package/src/diff-syntax.ts +6 -3
- package/src/discussions.ts +79 -0
- package/src/intent-dispatch.test.ts +31 -11
- package/src/intent-dispatch.ts +93 -48
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +3 -25
- package/src/keymap.ts +17 -18
- package/src/mark-runs.ts +120 -0
- package/src/pty.ts +10 -2
- package/src/run.ts +17 -7
- package/src/serve.test.ts +4 -8
- package/src/session-controller.curate.test.ts +75 -0
- package/src/session-controller.share.test.ts +1 -0
- package/src/session-controller.tree.test.ts +26 -0
- package/src/session-controller.ts +153 -29
- package/src/slash-palette.ts +83 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +3 -3
- package/src/test-support.ts +39 -8
- package/src/thread-chords.test.ts +25 -9
- package/src/thread-chords.ts +33 -8
- package/src/use-annotation-surface.tsx +837 -0
- package/src/use-changes-workbench.ts +151 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +14 -24
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/view-diff.ts +114 -0
- package/src/components/ReviewPanel.stories.tsx +0 -100
- package/src/components/ReviewPanel.tsx +0 -103
- package/src/components/ReviewRail.stories.tsx +0 -232
- package/src/components/ReviewRail.tsx +0 -327
- package/src/review-panel.test.ts +0 -113
- package/src/review-panel.ts +0 -83
|
@@ -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
|
-
}
|
package/src/review-panel.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/review-panel.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure geometry and state for the resizable review panel. The panel shows the
|
|
3
|
-
* review rail in one of three modes and the expanded rail resizes within a
|
|
4
|
-
* clamp; App owns the React state and the mouse wiring, this module owns the
|
|
5
|
-
* arithmetic so the layout math stays unit-tested and out of the JSX. Bounds
|
|
6
|
-
* and the compact width carry over verbatim from the approved prototype.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Expanded (full rail), compact (count + dots strip), or hidden (nothing). */
|
|
10
|
-
export type ReviewPanelMode = "expanded" | "compact" | "hidden";
|
|
11
|
-
|
|
12
|
-
/** Expanded-rail width bounds, in terminal columns. */
|
|
13
|
-
export const REVIEW_MIN_WIDTH = 24;
|
|
14
|
-
export const REVIEW_MAX_WIDTH = 50;
|
|
15
|
-
export const REVIEW_DEFAULT_WIDTH = 34;
|
|
16
|
-
/**
|
|
17
|
-
* Columns the plan/diff column keeps for itself: the expanded rail yields past
|
|
18
|
-
* this so a narrow terminal (or a wide persisted width) can never crowd the
|
|
19
|
-
* plan out to nothing - the whole reason the panel is resizable. On terminals
|
|
20
|
-
* wide enough for the full 50-col rail this bound never binds.
|
|
21
|
-
*/
|
|
22
|
-
export const REVIEW_PLAN_MIN_WIDTH = 30;
|
|
23
|
-
/** The compact strip is a fixed narrow column: count on top, one dot per card. */
|
|
24
|
-
export const REVIEW_COMPACT_WIDTH = 6;
|
|
25
|
-
/** Keyboard resize step, in columns, for the widen/narrow keys. */
|
|
26
|
-
export const REVIEW_RESIZE_STEP = 2;
|
|
27
|
-
|
|
28
|
-
/** Round a desired width to a whole column and clamp it to the bounds. */
|
|
29
|
-
export function clampWidth(
|
|
30
|
-
desiredWidth: number,
|
|
31
|
-
min = REVIEW_MIN_WIDTH,
|
|
32
|
-
max = REVIEW_MAX_WIDTH,
|
|
33
|
-
): number {
|
|
34
|
-
return Math.max(min, Math.min(max, Math.round(desiredWidth)));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The widest the expanded rail may be on a given terminal, reserving the plan
|
|
39
|
-
* its minimum and one column for the divider. Never drops below REVIEW_MIN_WIDTH
|
|
40
|
-
* (an extremely narrow terminal is the auto-collapse case, handled elsewhere).
|
|
41
|
-
*/
|
|
42
|
-
export function maxWidthForTerminal(terminalWidth: number): number {
|
|
43
|
-
return Math.max(
|
|
44
|
-
REVIEW_MIN_WIDTH,
|
|
45
|
-
Math.min(REVIEW_MAX_WIDTH, terminalWidth - REVIEW_PLAN_MIN_WIDTH - 1),
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Clamp a desired rail width to the bounds AND the terminal, so the plan keeps its room. */
|
|
50
|
-
export function resolveReviewWidth(desiredWidth: number, terminalWidth: number): number {
|
|
51
|
-
return clampWidth(desiredWidth, REVIEW_MIN_WIDTH, maxWidthForTerminal(terminalWidth));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The keybinding cycle: expanded -> compact -> hidden -> expanded. */
|
|
55
|
-
export function cycleReviewPanelMode(mode: ReviewPanelMode): ReviewPanelMode {
|
|
56
|
-
return mode === "expanded" ? "compact" : mode === "compact" ? "hidden" : "expanded";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The clickable chevron toggles only between expanded and compact - hidden is
|
|
61
|
-
* reachable by keybinding alone, so it never leaves a leftover tab behind. A
|
|
62
|
-
* toggle from hidden still opens the panel rather than doing nothing.
|
|
63
|
-
*/
|
|
64
|
-
export function toggleReviewPanelMode(mode: ReviewPanelMode): ReviewPanelMode {
|
|
65
|
-
return mode === "expanded" ? "compact" : "expanded";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Rail width from an absolute mouse column while dragging the divider: the rail
|
|
70
|
-
* spans from the mouse column to the terminal's right edge, then clamps.
|
|
71
|
-
*/
|
|
72
|
-
export function widthFromMouseColumn(mouseColumn: number, terminalWidth: number): number {
|
|
73
|
-
return resolveReviewWidth(terminalWidth - mouseColumn, terminalWidth);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The divider is a single-column stack of vertical glyphs (a text block, not a
|
|
78
|
-
* bordered box - a one-column box with a border renders a full box). One glyph
|
|
79
|
-
* per row, at least one so the column never collapses.
|
|
80
|
-
*/
|
|
81
|
-
export function reviewRowsToDivider(rows: number): string {
|
|
82
|
-
return Array.from({ length: Math.max(1, rows) }, () => "│").join("\n");
|
|
83
|
-
}
|