@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60
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 +5 -5
- package/src/App.autoclose.test.tsx +7 -0
- package/src/App.diff.test.tsx +11 -0
- package/src/App.export.test.tsx +5 -0
- package/src/App.plan-review.test.tsx +42 -0
- package/src/App.prototype.test.tsx +11 -0
- package/src/App.readonly.test.tsx +10 -0
- package/src/App.rendering.test.tsx +4 -0
- package/src/App.review-panel.test.tsx +7 -0
- package/src/App.submit-confirm.test.tsx +11 -0
- package/src/App.test.tsx +13 -0
- package/src/App.toast.test.tsx +2 -0
- package/src/App.tsx +27 -0
- package/src/App.walk.test.tsx +10 -0
- package/src/app-key-state.ts +1 -0
- package/src/app-screens.tsx +4 -0
- package/src/app-view-model.ts +21 -0
- package/src/attribution.ts +4 -0
- package/src/clipboard.ts +3 -0
- package/src/components/AnnotationCard.tsx +9 -0
- package/src/components/Breadcrumb.tsx +1 -0
- package/src/components/CodeBlock.tsx +1 -0
- package/src/components/CompletionOverlay.tsx +1 -0
- package/src/components/ConfirmCard.tsx +2 -0
- package/src/components/ConfirmDialog.tsx +2 -0
- package/src/components/DiffSheet.scroll.test.tsx +12 -0
- package/src/components/DiffSheet.syntax.test.tsx +3 -0
- package/src/components/DiffSheet.tsx +19 -0
- package/src/components/InboxList.tsx +2 -0
- package/src/components/KeybindsDialog.tsx +1 -0
- package/src/components/MarkerPopover.tsx +1 -0
- package/src/components/MenuBar.tsx +1 -0
- package/src/components/PlanSheet.tsx +26 -0
- package/src/components/PromptDialog.tsx +2 -0
- package/src/components/PrototypeSheet.tsx +27 -0
- package/src/components/ReviewPanel.tsx +2 -0
- package/src/components/ReviewRail.tsx +1 -0
- package/src/components/SettingsDialog.tsx +6 -0
- package/src/components/SettingsRows.tsx +4 -0
- package/src/components/Toast.tsx +1 -0
- package/src/components/agent-launcher.test.tsx +1 -0
- package/src/components/agent-launcher.tsx +2 -0
- package/src/components/diff-sheet-layout.test.ts +1 -0
- package/src/components/diff-sheet-layout.ts +17 -0
- package/src/components/plan-sheet-run-style.ts +4 -0
- package/src/components/primitives/Button.tsx +1 -0
- package/src/components/primitives/Card.tsx +1 -0
- package/src/components/primitives/Dialog.tsx +2 -0
- package/src/components/primitives/StatusBar.tsx +1 -0
- package/src/components/primitives/Tabs.tsx +4 -0
- package/src/components/quick-actions-editor.tsx +2 -0
- package/src/components/stories-app.tsx +1 -0
- package/src/components/stories.test.tsx +4 -0
- package/src/components/story.ts +6 -0
- package/src/components/syntax-highlight.ts +10 -0
- package/src/components/terminal-pane.ts +12 -0
- package/src/components/theme-context.tsx +1 -0
- package/src/components/truncate-text.ts +1 -0
- package/src/config.test.ts +18 -0
- package/src/config.ts +27 -0
- package/src/diff-hunk-curate.ts +20 -0
- package/src/diff-intraline.test.ts +1 -0
- package/src/diff-intraline.ts +14 -0
- package/src/diff-syntax.ts +22 -0
- package/src/editor.test.ts +6 -0
- package/src/editor.ts +12 -0
- package/src/ghostty-terminal.test.ts +2 -0
- package/src/ghostty-terminal.ts +8 -0
- package/src/herdr.test.ts +2 -0
- package/src/herdr.ts +3 -0
- package/src/integrations.ts +2 -0
- package/src/intent-dispatch.test.ts +6 -0
- package/src/intent-dispatch.ts +37 -0
- package/src/key-bindings.test.ts +4 -0
- package/src/key-bindings.ts +22 -0
- package/src/keymap.test.ts +13 -0
- package/src/keymap.ts +43 -0
- package/src/kitty-image.ts +9 -0
- package/src/prototype-browser.test.ts +1 -0
- package/src/prototype-browser.ts +22 -0
- package/src/pty.test.ts +3 -0
- package/src/pty.ts +15 -0
- package/src/run.ts +1 -0
- package/src/serve.test.ts +1 -0
- package/src/serve.ts +3 -0
- package/src/session-controller.curate.test.ts +7 -0
- package/src/session-controller.share.test.ts +11 -0
- package/src/session-controller.ts +79 -0
- package/src/share.ts +7 -0
- package/src/test-support.test.ts +5 -0
- package/src/test-support.ts +10 -0
- package/src/theme-presets.ts +1 -0
- package/src/use-settings-dialog.tsx +12 -0
- package/src/view-diff.test.ts +3 -0
- package/src/view-diff.ts +5 -0
- package/src/view-plan.test.ts +7 -0
- package/src/view-plan.ts +59 -0
- package/src/walk.ts +4 -0
|
@@ -69,14 +69,17 @@ function cellHeightOverWidth(renderer: {
|
|
|
69
69
|
terminalHeight: number;
|
|
70
70
|
}): number {
|
|
71
71
|
const resolution = renderer.resolution;
|
|
72
|
+
|
|
72
73
|
if (!resolution || renderer.terminalWidth < 1 || renderer.terminalHeight < 1) {
|
|
73
74
|
return FALLBACK_CELL_HEIGHT_OVER_WIDTH;
|
|
74
75
|
}
|
|
76
|
+
|
|
75
77
|
return resolution.height / renderer.terminalHeight / (resolution.width / renderer.terminalWidth);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
function regionOf(box: BoxRenderable | null): CellRegion | null {
|
|
79
81
|
if (!box || box.width < 1 || box.height < 1) return null;
|
|
82
|
+
|
|
80
83
|
return { column: box.x, row: box.y, columns: box.width, rows: box.height };
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -97,9 +100,11 @@ function captureConfig(
|
|
|
97
100
|
renderer: CaptureRenderer,
|
|
98
101
|
): { viewport: PrototypeViewport; deviceScaleFactor: number } {
|
|
99
102
|
const resolution = renderer.resolution;
|
|
103
|
+
|
|
100
104
|
if (resolution && renderer.terminalWidth >= 1 && renderer.terminalHeight >= 1) {
|
|
101
105
|
const cellWidth = resolution.width / renderer.terminalWidth;
|
|
102
106
|
const cellHeight = resolution.height / renderer.terminalHeight;
|
|
107
|
+
|
|
103
108
|
return {
|
|
104
109
|
viewport: {
|
|
105
110
|
width: Math.max(1, Math.round(region.columns * cellWidth)),
|
|
@@ -108,6 +113,7 @@ function captureConfig(
|
|
|
108
113
|
deviceScaleFactor: 1,
|
|
109
114
|
};
|
|
110
115
|
}
|
|
116
|
+
|
|
111
117
|
return {
|
|
112
118
|
viewport: {
|
|
113
119
|
width: CAPTURE_WIDTH,
|
|
@@ -139,6 +145,7 @@ function PrototypeSheetImpl({
|
|
|
139
145
|
const paintRef = useRef<() => void>(() => undefined);
|
|
140
146
|
const launchRef = useRef<() => void>(() => undefined);
|
|
141
147
|
const hiddenRef = useRef(hidden);
|
|
148
|
+
|
|
142
149
|
useEffect(() => {
|
|
143
150
|
hiddenRef.current = hidden;
|
|
144
151
|
});
|
|
@@ -182,10 +189,12 @@ function PrototypeSheetImpl({
|
|
|
182
189
|
deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
|
|
183
190
|
transmittedRef.current = null;
|
|
184
191
|
}
|
|
192
|
+
|
|
185
193
|
return;
|
|
186
194
|
}
|
|
187
195
|
const region = regionOf(regionRef.current);
|
|
188
196
|
const png = pngRef.current;
|
|
197
|
+
|
|
189
198
|
if (!region || !png) return;
|
|
190
199
|
if (transmittedRef.current !== png) {
|
|
191
200
|
transmitKittyImage(write, png, region, PROTOTYPE_IMAGE_ID, medium);
|
|
@@ -194,12 +203,15 @@ function PrototypeSheetImpl({
|
|
|
194
203
|
placeKittyImage(write, region, PROTOTYPE_IMAGE_ID);
|
|
195
204
|
}
|
|
196
205
|
};
|
|
206
|
+
|
|
197
207
|
paintRef.current = paint;
|
|
198
208
|
const onFrame = (): void => {
|
|
199
209
|
launchRef.current();
|
|
200
210
|
paint();
|
|
201
211
|
};
|
|
212
|
+
|
|
202
213
|
renderer.on("frame", onFrame);
|
|
214
|
+
|
|
203
215
|
return () => {
|
|
204
216
|
renderer.off("frame", onFrame);
|
|
205
217
|
if (write) deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
|
|
@@ -211,6 +223,7 @@ function PrototypeSheetImpl({
|
|
|
211
223
|
}, [composing, onComposingChange]);
|
|
212
224
|
|
|
213
225
|
const unmountedRef = useRef(false);
|
|
226
|
+
|
|
214
227
|
useEffect(() => {
|
|
215
228
|
return () => {
|
|
216
229
|
unmountedRef.current = true;
|
|
@@ -221,13 +234,16 @@ function PrototypeSheetImpl({
|
|
|
221
234
|
const launch = (): void => {
|
|
222
235
|
const region = regionOf(regionRef.current);
|
|
223
236
|
const caps = renderer?.capabilities;
|
|
237
|
+
|
|
224
238
|
if (launchedRef.current || !region) return;
|
|
225
239
|
if (caps && caps.kitty_graphics === false) {
|
|
226
240
|
setStatus("unsupported");
|
|
241
|
+
|
|
227
242
|
return;
|
|
228
243
|
}
|
|
229
244
|
launchedRef.current = true;
|
|
230
245
|
const capture = captureConfig(region, renderer!);
|
|
246
|
+
|
|
231
247
|
viewportRef.current = capture.viewport;
|
|
232
248
|
void (async () => {
|
|
233
249
|
try {
|
|
@@ -236,10 +252,12 @@ function PrototypeSheetImpl({
|
|
|
236
252
|
viewport: capture.viewport,
|
|
237
253
|
deviceScaleFactor: capture.deviceScaleFactor,
|
|
238
254
|
});
|
|
255
|
+
|
|
239
256
|
// the sheet may have unmounted while Chromium was launching; close the
|
|
240
257
|
// late arrival instead of leaking the process
|
|
241
258
|
if (unmountedRef.current) {
|
|
242
259
|
void browser.close();
|
|
260
|
+
|
|
243
261
|
return;
|
|
244
262
|
}
|
|
245
263
|
browserRef.current = browser;
|
|
@@ -251,12 +269,14 @@ function PrototypeSheetImpl({
|
|
|
251
269
|
}
|
|
252
270
|
})();
|
|
253
271
|
};
|
|
272
|
+
|
|
254
273
|
useEffect(() => {
|
|
255
274
|
launchRef.current = launch;
|
|
256
275
|
});
|
|
257
276
|
|
|
258
277
|
const refresh = async (): Promise<void> => {
|
|
259
278
|
const browser = browserRef.current;
|
|
279
|
+
|
|
260
280
|
if (browser) setPng(await browser.screenshot());
|
|
261
281
|
};
|
|
262
282
|
|
|
@@ -278,6 +298,7 @@ function PrototypeSheetImpl({
|
|
|
278
298
|
const onRegionMouseDown = (event: { x: number; y: number }): void => {
|
|
279
299
|
const browser = browserRef.current;
|
|
280
300
|
const region = regionOf(regionRef.current);
|
|
301
|
+
|
|
281
302
|
if (status !== "ready" || !browser || !region) return;
|
|
282
303
|
const geometry = {
|
|
283
304
|
x: region.column,
|
|
@@ -286,12 +307,15 @@ function PrototypeSheetImpl({
|
|
|
286
307
|
height: region.rows,
|
|
287
308
|
};
|
|
288
309
|
const cssPoint = imageCellToCss(event, geometry, viewportRef.current);
|
|
310
|
+
|
|
289
311
|
if (!cssPoint) return;
|
|
290
312
|
void (async () => {
|
|
291
313
|
const element = await browser.elementAt(cssPoint.x, cssPoint.y);
|
|
314
|
+
|
|
292
315
|
if (!element) return;
|
|
293
316
|
const cell = cssBoxToCell(element.box, geometry, viewportRef.current);
|
|
294
317
|
const topRaw = cell.row - geometry.y;
|
|
318
|
+
|
|
295
319
|
setSelected(element);
|
|
296
320
|
setOverlayCell({
|
|
297
321
|
left: Math.max(0, cell.column - geometry.x),
|
|
@@ -308,8 +332,10 @@ function PrototypeSheetImpl({
|
|
|
308
332
|
|
|
309
333
|
const onRegionScroll = (event: { scroll?: { direction: string } }): void => {
|
|
310
334
|
const browser = browserRef.current;
|
|
335
|
+
|
|
311
336
|
if (status !== "ready" || !browser) return;
|
|
312
337
|
const delta = event.scroll?.direction === "up" ? -SCROLL_STEP : SCROLL_STEP;
|
|
338
|
+
|
|
313
339
|
// a selection would drift once the page scrolls under it, so drop it
|
|
314
340
|
clearSelection();
|
|
315
341
|
void browser
|
|
@@ -419,5 +445,6 @@ function statusLine(status: SheetStatus, errorMessage: string): string {
|
|
|
419
445
|
if (status === "loading") return "rendering prototype…";
|
|
420
446
|
if (status === "unsupported")
|
|
421
447
|
return "prototype preview needs a graphics terminal (kitty or ghostty)";
|
|
448
|
+
|
|
422
449
|
return `prototype preview failed: ${errorMessage}`;
|
|
423
450
|
}
|
|
@@ -49,6 +49,7 @@ function CompactRail({
|
|
|
49
49
|
theme?: Theme;
|
|
50
50
|
}): React.ReactNode {
|
|
51
51
|
const tokens = useComponentTheme(theme);
|
|
52
|
+
|
|
52
53
|
return (
|
|
53
54
|
<box
|
|
54
55
|
style={{
|
|
@@ -88,6 +89,7 @@ export function ReviewPanel({
|
|
|
88
89
|
theme,
|
|
89
90
|
}: ReviewPanelProps): React.ReactNode {
|
|
90
91
|
if (mode === "hidden") return null;
|
|
92
|
+
|
|
91
93
|
return (
|
|
92
94
|
<>
|
|
93
95
|
<ReviewDivider onGrab={onDividerGrab} />
|
|
@@ -283,6 +283,7 @@ function RemovalCard({
|
|
|
283
283
|
Math.max(1, previewLines.length) + (overflow > 0 ? 1 : 0) + (isSelected ? 1 : 0);
|
|
284
284
|
// border = "what + who": a diff reject or a plan cut, always the reviewer's own
|
|
285
285
|
const title = ` ${item.source === "diff" ? "REJECT" : "CUT"} · me `;
|
|
286
|
+
|
|
286
287
|
return (
|
|
287
288
|
<box id={`removal-card-${item.id}`} onMouseUp={() => onSelect(item.id)}>
|
|
288
289
|
<Card
|
|
@@ -50,10 +50,13 @@ export function SettingsDialog({
|
|
|
50
50
|
}: SettingsDialogProps): React.ReactNode {
|
|
51
51
|
const tokens = useComponentTheme(theme);
|
|
52
52
|
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
53
|
+
|
|
53
54
|
if (!isOpen) return null;
|
|
54
55
|
const category =
|
|
55
56
|
categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
|
|
57
|
+
|
|
56
58
|
if (!category) return null;
|
|
59
|
+
|
|
57
60
|
return (
|
|
58
61
|
<Dialog
|
|
59
62
|
isOpen
|
|
@@ -68,6 +71,7 @@ export function SettingsDialog({
|
|
|
68
71
|
<text fg={tokens.textDim}>SETTINGS</text>
|
|
69
72
|
{categories.map((candidate) => {
|
|
70
73
|
const isActive = candidate.id === category.id;
|
|
74
|
+
|
|
71
75
|
return (
|
|
72
76
|
<box
|
|
73
77
|
key={candidate.id}
|
|
@@ -92,6 +96,7 @@ export function SettingsDialog({
|
|
|
92
96
|
{category.rows.map((row, rowIndex) => {
|
|
93
97
|
const isActive = activeZone === "body" && rowIndex === activeRowIndex;
|
|
94
98
|
const value = values[row.key];
|
|
99
|
+
|
|
95
100
|
if (row.kind === "toggle") {
|
|
96
101
|
return (
|
|
97
102
|
<ToggleRow
|
|
@@ -116,6 +121,7 @@ export function SettingsDialog({
|
|
|
116
121
|
/>
|
|
117
122
|
);
|
|
118
123
|
}
|
|
124
|
+
|
|
119
125
|
return (
|
|
120
126
|
<TextRow
|
|
121
127
|
key={row.key}
|
|
@@ -27,6 +27,7 @@ interface RowShellProps {
|
|
|
27
27
|
|
|
28
28
|
function RowShell({ label, isActive, onPress, theme, children }: RowShellProps): React.ReactNode {
|
|
29
29
|
const tokens = useComponentTheme(theme);
|
|
30
|
+
|
|
30
31
|
return (
|
|
31
32
|
<box
|
|
32
33
|
style={{
|
|
@@ -60,6 +61,7 @@ export function ToggleRow({
|
|
|
60
61
|
theme,
|
|
61
62
|
}: ToggleRowProps): React.ReactNode {
|
|
62
63
|
const tokens = useComponentTheme(theme);
|
|
64
|
+
|
|
63
65
|
return (
|
|
64
66
|
<RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
|
|
65
67
|
<text fg={value ? tokens.green : tokens.accent}>{value ? "on" : "off"}</text>
|
|
@@ -83,6 +85,7 @@ export function CycleRow({
|
|
|
83
85
|
theme,
|
|
84
86
|
}: CycleRowProps): React.ReactNode {
|
|
85
87
|
const tokens = useComponentTheme(theme);
|
|
88
|
+
|
|
86
89
|
return (
|
|
87
90
|
<RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
|
|
88
91
|
<text fg={tokens.accent}>{value}</text>
|
|
@@ -100,6 +103,7 @@ export interface TextRowProps {
|
|
|
100
103
|
|
|
101
104
|
export function TextRow({ label, value, isActive, onPress, theme }: TextRowProps): React.ReactNode {
|
|
102
105
|
const tokens = useComponentTheme(theme);
|
|
106
|
+
|
|
103
107
|
return (
|
|
104
108
|
<RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
|
|
105
109
|
<text fg={tokens.accent}>{value}</text>
|
package/src/components/Toast.tsx
CHANGED
|
@@ -14,6 +14,7 @@ export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
|
|
|
14
14
|
const tokens = useComponentTheme(theme);
|
|
15
15
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
16
16
|
const width = Math.min(terminalWidth - 6, Math.max(28, body.length + 4));
|
|
17
|
+
|
|
17
18
|
return (
|
|
18
19
|
<Dialog
|
|
19
20
|
isOpen
|
|
@@ -43,6 +43,7 @@ describe("AgentLauncher", () => {
|
|
|
43
43
|
// Assert - all three launcher names present, in launch order
|
|
44
44
|
const names = HARNESS_LAUNCHERS.map((harness) => harness.name);
|
|
45
45
|
const positions = names.map((name) => frame.indexOf(name));
|
|
46
|
+
|
|
46
47
|
expect(positions.every((position) => position >= 0)).toBe(true);
|
|
47
48
|
expect(positions).toEqual(positions.toSorted((left, right) => left - right));
|
|
48
49
|
});
|
|
@@ -81,6 +81,7 @@ export function AgentLauncher({
|
|
|
81
81
|
|
|
82
82
|
const launch = (harness: HarnessLauncher): void => {
|
|
83
83
|
const seed = seedContext ? planHandoffBriefing(session.id) : undefined;
|
|
84
|
+
|
|
84
85
|
if (embeddedTerminalAvailable()) setRunning({ harness, seed });
|
|
85
86
|
else onLaunchHarness(harness.command, seed);
|
|
86
87
|
};
|
|
@@ -94,6 +95,7 @@ export function AgentLauncher({
|
|
|
94
95
|
useEffect(() => {
|
|
95
96
|
if (!running) return onAgentTerminal?.(null);
|
|
96
97
|
onAgentTerminal?.({ write: (data) => paneRef.current?.write(data), detach });
|
|
98
|
+
|
|
97
99
|
return () => onAgentTerminal?.(null);
|
|
98
100
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
99
101
|
}, [running]);
|
|
@@ -63,6 +63,7 @@ describe("segmentRows", () => {
|
|
|
63
63
|
// Assert
|
|
64
64
|
expect(segments.map((segment) => segment.kind)).toEqual(["header", "header", "chunk"]);
|
|
65
65
|
const chunk = segments[2];
|
|
66
|
+
|
|
66
67
|
expect(chunk?.kind).toBe("chunk");
|
|
67
68
|
if (chunk?.kind === "chunk") expect(chunk.annotation?.id).toBe("a1");
|
|
68
69
|
});
|
|
@@ -35,14 +35,17 @@ export function annotatedRowsByIndex(
|
|
|
35
35
|
annotations: Annotation[],
|
|
36
36
|
): Map<number, Annotation> {
|
|
37
37
|
const byRow = new Map<number, Annotation>();
|
|
38
|
+
|
|
38
39
|
for (const annotation of annotations) {
|
|
39
40
|
const rowIndex = rows.findIndex(
|
|
40
41
|
(row) =>
|
|
41
42
|
row.text === annotation.anchor.quote &&
|
|
42
43
|
(row.kind === "ctx" || row.kind === "add" || row.kind === "del"),
|
|
43
44
|
);
|
|
45
|
+
|
|
44
46
|
if (rowIndex !== -1) byRow.set(rowIndex, annotation);
|
|
45
47
|
}
|
|
48
|
+
|
|
46
49
|
return byRow;
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -66,6 +69,7 @@ export function segmentRows(
|
|
|
66
69
|
|
|
67
70
|
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
68
71
|
const row = rows[rowIndex]!;
|
|
72
|
+
|
|
69
73
|
if (row.kind === "file" || row.kind === "hunk") {
|
|
70
74
|
closeChunk(null);
|
|
71
75
|
segments.push({ kind: "header", rowIndex, row });
|
|
@@ -76,6 +80,7 @@ export function segmentRows(
|
|
|
76
80
|
chunk.push(row);
|
|
77
81
|
|
|
78
82
|
const annotation = annotatedByRow.get(rowIndex);
|
|
83
|
+
|
|
79
84
|
if (annotation) {
|
|
80
85
|
closeChunk(annotation);
|
|
81
86
|
chunkStart = rowIndex + 1;
|
|
@@ -85,6 +90,7 @@ export function segmentRows(
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
closeChunk(null);
|
|
93
|
+
|
|
88
94
|
return segments;
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -93,6 +99,7 @@ export function segmentRows(
|
|
|
93
99
|
export function rowContentOffsets(segments: DiffSegment[]): number[] {
|
|
94
100
|
const offsets: number[] = [];
|
|
95
101
|
let contentY = 0;
|
|
102
|
+
|
|
96
103
|
for (const segment of segments) {
|
|
97
104
|
if (segment.kind === "header") {
|
|
98
105
|
offsets[segment.rowIndex] = contentY;
|
|
@@ -104,6 +111,7 @@ export function rowContentOffsets(segments: DiffSegment[]): number[] {
|
|
|
104
111
|
contentY += segment.rows.length + (segment.annotation ? 1 : 0);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
114
|
+
|
|
107
115
|
return offsets;
|
|
108
116
|
}
|
|
109
117
|
|
|
@@ -125,27 +133,33 @@ export function coloredRowSpans(
|
|
|
125
133
|
const isModifiedLine = intralineRuns !== undefined;
|
|
126
134
|
|
|
127
135
|
const spans: ColoredSpan[] = [];
|
|
136
|
+
|
|
128
137
|
for (let column = 0; column < text.length; column++) {
|
|
129
138
|
const color = changed[column]
|
|
130
139
|
? baseColor
|
|
131
140
|
: (syntaxColorByColumn[column] ?? (isModifiedLine ? tokens.textDim : baseColor));
|
|
132
141
|
const previous = spans[spans.length - 1];
|
|
142
|
+
|
|
133
143
|
if (previous && previous.foreground === color) previous.text += text[column];
|
|
134
144
|
else spans.push({ text: text[column]!, foreground: color });
|
|
135
145
|
}
|
|
146
|
+
|
|
136
147
|
return spans;
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
/** Which columns are the intra-line change on a modified line. */
|
|
140
151
|
function changedColumns(length: number, intralineRuns: IntralineRun[] | undefined): boolean[] {
|
|
141
152
|
const changed = Array.from({ length }, () => false);
|
|
153
|
+
|
|
142
154
|
if (!intralineRuns) return changed;
|
|
143
155
|
let offset = 0;
|
|
156
|
+
|
|
144
157
|
for (const run of intralineRuns) {
|
|
145
158
|
if (run.changed)
|
|
146
159
|
for (let index = 0; index < run.text.length; index++) changed[offset + index] = true;
|
|
147
160
|
offset += run.text.length;
|
|
148
161
|
}
|
|
162
|
+
|
|
149
163
|
return changed;
|
|
150
164
|
}
|
|
151
165
|
|
|
@@ -156,12 +170,15 @@ function syntaxColorColumns(
|
|
|
156
170
|
tokens: Theme,
|
|
157
171
|
): Array<string | undefined> {
|
|
158
172
|
const colors: Array<string | undefined> = Array.from({ length }, () => undefined);
|
|
173
|
+
|
|
159
174
|
for (const span of syntaxSpans ?? []) {
|
|
160
175
|
const color = colorForSyntaxGroup(span.group, tokens);
|
|
176
|
+
|
|
161
177
|
if (!color) continue;
|
|
162
178
|
for (let column = span.start; column < span.end && column < length; column++) {
|
|
163
179
|
colors[column] = color;
|
|
164
180
|
}
|
|
165
181
|
}
|
|
182
|
+
|
|
166
183
|
return colors;
|
|
167
184
|
}
|
|
@@ -29,6 +29,7 @@ function headingForeground(block: DisplayBlock, tokens: Theme): string | undefin
|
|
|
29
29
|
if (block.kind === "h1") return tokens.text;
|
|
30
30
|
if (block.kind === "h2") return tokens.textMuted;
|
|
31
31
|
if (block.kind === "h3") return tokens.textDim;
|
|
32
|
+
|
|
32
33
|
return undefined;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -39,6 +40,7 @@ function blockBaseStyle(block: DisplayBlock, tokens: Theme): BlockBaseStyle {
|
|
|
39
40
|
// block-level base: headings bold, quotes muted italic; inline roles compose on top
|
|
40
41
|
const baseFg = headingFg ?? (isQuote ? tokens.textMuted : tokens.text);
|
|
41
42
|
const baseAttributes = (isHeading ? HEADING_ATTRIBUTES : 0) | (isQuote ? QUOTE_ATTRIBUTES : 0);
|
|
43
|
+
|
|
42
44
|
return { baseFg, baseAttributes };
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -49,6 +51,7 @@ function inlineRoleStyle(
|
|
|
49
51
|
): { fg?: string; bg?: string; attributes?: number } {
|
|
50
52
|
const { baseFg, baseAttributes } = base;
|
|
51
53
|
const withBase = (roleAttributes: number): number => baseAttributes | roleAttributes;
|
|
54
|
+
|
|
52
55
|
switch (run.role) {
|
|
53
56
|
case "ins":
|
|
54
57
|
return { fg: tokens.insertedForeground, attributes: baseAttributes || undefined };
|
|
@@ -82,5 +85,6 @@ export function runStyle(
|
|
|
82
85
|
): { fg?: string; bg?: string; attributes?: number } {
|
|
83
86
|
// a cut block reads as removed: every run struck through and grayed, never red
|
|
84
87
|
if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
|
|
88
|
+
|
|
85
89
|
return inlineRoleStyle(run, blockBaseStyle(block, tokens), tokens);
|
|
86
90
|
}
|
|
@@ -14,6 +14,7 @@ export interface StatusBarProps {
|
|
|
14
14
|
|
|
15
15
|
export function StatusBar({ theme, children }: StatusBarProps): React.ReactNode {
|
|
16
16
|
const tokens = useComponentTheme(theme);
|
|
17
|
+
|
|
17
18
|
return (
|
|
18
19
|
<box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1 }}>
|
|
19
20
|
<text fg={tokens.textDim}>{children}</text>
|
|
@@ -59,14 +59,17 @@ export interface TabListProps {
|
|
|
59
59
|
|
|
60
60
|
export function TabList({ children }: TabListProps): React.ReactNode {
|
|
61
61
|
const tabs = useContext(TabsContext);
|
|
62
|
+
|
|
62
63
|
if (!tabs) throw new Error("TabList must render inside Tabs");
|
|
63
64
|
const tokens = useComponentTheme(tabs.theme);
|
|
64
65
|
const items: { id: string; label: string }[] = [];
|
|
66
|
+
|
|
65
67
|
React.Children.forEach(children, (child) => {
|
|
66
68
|
if (React.isValidElement<TabProps>(child) && typeof child.props.id === "string") {
|
|
67
69
|
items.push({ id: child.props.id, label: child.props.children });
|
|
68
70
|
}
|
|
69
71
|
});
|
|
72
|
+
|
|
70
73
|
return (
|
|
71
74
|
<box
|
|
72
75
|
style={{
|
|
@@ -81,6 +84,7 @@ export function TabList({ children }: TabListProps): React.ReactNode {
|
|
|
81
84
|
>
|
|
82
85
|
{items.map((item) => {
|
|
83
86
|
const selected = item.id === tabs.selectedKey;
|
|
87
|
+
|
|
84
88
|
return (
|
|
85
89
|
<box
|
|
86
90
|
key={item.id}
|
|
@@ -40,6 +40,7 @@ export function QuickActionsEditor({
|
|
|
40
40
|
theme,
|
|
41
41
|
}: QuickActionsEditorProps): React.ReactNode {
|
|
42
42
|
const tokens = useComponentTheme(theme);
|
|
43
|
+
|
|
43
44
|
return (
|
|
44
45
|
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
45
46
|
<box style={{ flexDirection: "row" }}>
|
|
@@ -89,6 +90,7 @@ function ActionRow({
|
|
|
89
90
|
}): React.ReactNode {
|
|
90
91
|
const tokens = useComponentTheme(theme);
|
|
91
92
|
const inputRef = useRef<TextareaRenderable | null>(null);
|
|
93
|
+
|
|
92
94
|
return (
|
|
93
95
|
<box style={{ flexDirection: "column" }}>
|
|
94
96
|
<box
|
|
@@ -17,6 +17,7 @@ const stories = await loadStories();
|
|
|
17
17
|
function hexOf(color: RGBA | null | undefined): string | undefined {
|
|
18
18
|
if (!color) return undefined;
|
|
19
19
|
const [red, green, blue] = color.toInts();
|
|
20
|
+
|
|
20
21
|
return "#" + [red, green, blue].map((part) => part!.toString(16).padStart(2, "0")).join("");
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -36,6 +37,7 @@ describe("stories catalog", () => {
|
|
|
36
37
|
|
|
37
38
|
// Act
|
|
38
39
|
const setup = await testRender(story.render(), size);
|
|
40
|
+
|
|
39
41
|
allowEventLoopUpdates();
|
|
40
42
|
await setup.waitForVisualIdle();
|
|
41
43
|
const frame = setup.captureCharFrame();
|
|
@@ -44,10 +46,12 @@ describe("stories catalog", () => {
|
|
|
44
46
|
expect(frame).toMatchSnapshot(`${moduleTitle}/${storyName}`);
|
|
45
47
|
if (story.expectedColors?.length) {
|
|
46
48
|
const seenColors = new Set<string>();
|
|
49
|
+
|
|
47
50
|
for (const line of setup.captureSpans().lines) {
|
|
48
51
|
for (const span of line.spans) {
|
|
49
52
|
const foregroundHex = hexOf(span.fg);
|
|
50
53
|
const backgroundHex = hexOf(span.bg);
|
|
54
|
+
|
|
51
55
|
if (foregroundHex) seenColors.add(foregroundHex);
|
|
52
56
|
if (backgroundHex) seenColors.add(backgroundHex);
|
|
53
57
|
}
|
package/src/components/story.ts
CHANGED
|
@@ -41,15 +41,18 @@ export async function loadStories(): Promise<LoadedStory[]> {
|
|
|
41
41
|
const glob = new Bun.Glob("**/*.stories.tsx");
|
|
42
42
|
const files = [...glob.scanSync({ cwd: import.meta.dir })].sort();
|
|
43
43
|
const loaded: LoadedStory[] = [];
|
|
44
|
+
|
|
44
45
|
for (const file of files) {
|
|
45
46
|
const moduleExports = (await import(`${import.meta.dir}/${file}`)) as Record<string, unknown>;
|
|
46
47
|
const meta = moduleExports["meta"] as StoryMeta | undefined;
|
|
47
48
|
const moduleTitle = meta?.title ?? file.replace(/\.stories\.tsx$/, "");
|
|
49
|
+
|
|
48
50
|
for (const [exportName, exported] of Object.entries(moduleExports)) {
|
|
49
51
|
if (exportName === "meta" || !isStory(exported)) continue;
|
|
50
52
|
loaded.push({ moduleTitle, storyName: exportName, story: exported });
|
|
51
53
|
}
|
|
52
54
|
}
|
|
55
|
+
|
|
53
56
|
return loaded;
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -58,12 +61,15 @@ export function componentFilesMissingStories(): string[] {
|
|
|
58
61
|
const componentGlob = new Bun.Glob("**/*.tsx");
|
|
59
62
|
const files = [...componentGlob.scanSync({ cwd: import.meta.dir })];
|
|
60
63
|
const missing: string[] = [];
|
|
64
|
+
|
|
61
65
|
for (const file of files) {
|
|
62
66
|
if (file.endsWith(".stories.tsx") || file.endsWith(".test.tsx")) continue;
|
|
63
67
|
if (file.endsWith(".prototype.tsx")) continue;
|
|
64
68
|
if (file === "stories-app.tsx") continue;
|
|
65
69
|
const storiesSibling = file.replace(/\.tsx$/, ".stories.tsx");
|
|
70
|
+
|
|
66
71
|
if (!files.includes(storiesSibling)) missing.push(file);
|
|
67
72
|
}
|
|
73
|
+
|
|
68
74
|
return missing;
|
|
69
75
|
}
|
|
@@ -17,6 +17,7 @@ import type { Theme } from "../theme";
|
|
|
17
17
|
/** Markdown fence info -> tree-sitter filetype, with the aliases we accept. */
|
|
18
18
|
export function filetypeFor(language?: string): string | undefined {
|
|
19
19
|
const info = (language ?? "").toLowerCase();
|
|
20
|
+
|
|
20
21
|
if (!info) return undefined;
|
|
21
22
|
const aliases: Record<string, string> = {
|
|
22
23
|
ts: "typescript",
|
|
@@ -29,6 +30,7 @@ export function filetypeFor(language?: string): string | undefined {
|
|
|
29
30
|
zsh: "bash",
|
|
30
31
|
yml: "yaml",
|
|
31
32
|
};
|
|
33
|
+
|
|
32
34
|
return infoStringToFiletype(aliases[info] ?? info) ?? aliases[info] ?? info;
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -36,9 +38,12 @@ export function filetypeFor(language?: string): string | undefined {
|
|
|
36
38
|
export function filetypeForPath(path: string): string | undefined {
|
|
37
39
|
const basename = path.split("/").pop() ?? path;
|
|
38
40
|
const byBasename = basenameToFiletype.get(basename);
|
|
41
|
+
|
|
39
42
|
if (byBasename) return byBasename;
|
|
40
43
|
const dotIndex = basename.lastIndexOf(".");
|
|
44
|
+
|
|
41
45
|
if (dotIndex <= 0) return undefined;
|
|
46
|
+
|
|
42
47
|
return extensionToFiletype.get(basename.slice(dotIndex + 1).toLowerCase());
|
|
43
48
|
}
|
|
44
49
|
|
|
@@ -75,9 +80,12 @@ const syntaxStyleCache = new WeakMap<Theme, SyntaxStyle>();
|
|
|
75
80
|
|
|
76
81
|
export function syntaxStyleFor(theme: Theme): SyntaxStyle {
|
|
77
82
|
const cached = syntaxStyleCache.get(theme);
|
|
83
|
+
|
|
78
84
|
if (cached) return cached;
|
|
79
85
|
const style = SyntaxStyle.fromStyles(syntaxGroupStyles(theme));
|
|
86
|
+
|
|
80
87
|
syntaxStyleCache.set(theme, style);
|
|
88
|
+
|
|
81
89
|
return style;
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -85,8 +93,10 @@ export function syntaxStyleFor(theme: Theme): SyntaxStyle {
|
|
|
85
93
|
export function colorForSyntaxGroup(group: string, theme: Theme): string | undefined {
|
|
86
94
|
const styles = syntaxGroupStyles(theme);
|
|
87
95
|
const direct = styles[group];
|
|
96
|
+
|
|
88
97
|
if (direct) return direct.fg;
|
|
89
98
|
// capture names are dotted (e.g. "keyword.control"); match the broadest prefix
|
|
90
99
|
const base = group.split(".")[0]!;
|
|
100
|
+
|
|
91
101
|
return styles[base]?.fg;
|
|
92
102
|
}
|