@cueloop/client 0.1.0-alpha.75 → 0.1.0-alpha.76
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.focus.test.ts +147 -0
- package/src/App.regression.test.tsx +94 -0
- package/src/App.test.tsx +11 -8
- package/src/App.tsx +476 -220
- package/src/App.visual.test.tsx +57 -0
- package/src/App.workbench.test.tsx +41 -22
- package/src/app-screens.tsx +168 -123
- package/src/components/AppShell.brand.test.tsx +1 -1
- package/src/components/AppShell.tsx +14 -1
- package/src/components/ChangesColumn.tsx +40 -12
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/ConfirmDialog.tsx +8 -1
- package/src/components/DiffContentView.tsx +7 -1
- package/src/components/DiscussionMarkerRail.tsx +2 -2
- package/src/components/EditorGrid.tsx +84 -26
- package/src/components/GridTabContent.tsx +10 -1
- package/src/components/PanelColumn.tsx +3 -0
- package/src/components/ProjectTreeView.test.tsx +39 -12
- package/src/components/ProjectTreeView.tsx +38 -10
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +16 -3
- package/src/components/RootOverlay.stories.tsx +1 -0
- package/src/components/RootOverlay.tsx +19 -6
- package/src/components/SettingsDialog.stories.tsx +3 -3
- package/src/components/SettingsDialog.tsx +3 -4
- package/src/components/ThreadFooter.test.tsx +12 -7
- package/src/components/ThreadFooter.tsx +18 -4
- package/src/components/ThreadTree.tsx +79 -36
- package/src/components/ThreadView.slash.test.tsx +36 -0
- package/src/components/ThreadView.tsx +7 -1
- package/src/components/WelcomePlayground.tsx +2 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +60 -51
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +5 -0
- package/src/components/primitives/icons.ts +1 -1
- package/src/components/quick-actions-editor.stories.tsx +2 -0
- package/src/components/quick-actions-editor.test.tsx +35 -1
- package/src/components/quick-actions-editor.tsx +23 -22
- package/src/components/session-tree.test.ts +3 -3
- package/src/components/session-tree.ts +1 -1
- package/src/config.ts +1 -0
- package/src/diff-intraline.ts +26 -5
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/run.ts +22 -2
- package/src/thread-chords.test.ts +33 -1
- package/src/thread-chords.ts +114 -29
- package/src/thread-controller.ts +4 -3
- package/src/use-annotation-surface.tsx +22 -0
- package/src/use-frame-measure.ts +4 -1
- package/src/use-settings-dialog.tsx +9 -7
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "./thread-selection";
|
|
38
38
|
import { annotationPaletteFor, type AnnotationPalette } from "./annotation-palette";
|
|
39
39
|
import { printableSequence, type MarkRange, type VisualLine } from "./mark-runs";
|
|
40
|
+
import { matchesLeader } from "./thread-chords";
|
|
40
41
|
import {
|
|
41
42
|
activeSlashToken,
|
|
42
43
|
isStandaloneSlashQuery,
|
|
@@ -103,6 +104,8 @@ export interface AnnotationSurfaceOptions {
|
|
|
103
104
|
onAnnotate: (span: TextSpan, body: string) => void;
|
|
104
105
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
105
106
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
107
|
+
leaderCombos?: readonly string[];
|
|
108
|
+
onLeaderCommand?: (key: KeyEvent) => void;
|
|
106
109
|
onExit: () => void;
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -176,6 +179,8 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
176
179
|
onAnnotate,
|
|
177
180
|
onReply,
|
|
178
181
|
onUpdateAnnotation,
|
|
182
|
+
leaderCombos,
|
|
183
|
+
onLeaderCommand,
|
|
179
184
|
onExit,
|
|
180
185
|
} = options;
|
|
181
186
|
const palette = annotationPaletteFor(tokens);
|
|
@@ -190,6 +195,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
190
195
|
return { head: start, anchor: start };
|
|
191
196
|
});
|
|
192
197
|
const [compose, setCompose] = useState<ComposeState | null>(null);
|
|
198
|
+
const leaderPending = useRef(false);
|
|
193
199
|
|
|
194
200
|
useEffect(() => {
|
|
195
201
|
onComposingChange?.(compose !== null);
|
|
@@ -657,10 +663,26 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
657
663
|
|
|
658
664
|
if (activeCompose) return handleComposeKey(key, activeCompose);
|
|
659
665
|
if (key.name === "escape") {
|
|
666
|
+
if (leaderPending.current) {
|
|
667
|
+
leaderPending.current = false;
|
|
668
|
+
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
660
671
|
if (focusedDiscussion !== null) return setFocusedDiscussion(null);
|
|
661
672
|
|
|
662
673
|
return collapseCaret();
|
|
663
674
|
}
|
|
675
|
+
if (leaderPending.current) {
|
|
676
|
+
leaderPending.current = false;
|
|
677
|
+
onLeaderCommand?.(key);
|
|
678
|
+
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (leaderCombos && matchesLeader(key, leaderCombos)) {
|
|
682
|
+
leaderPending.current = true;
|
|
683
|
+
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
664
686
|
if (handleDiscussionVerb(key)) return;
|
|
665
687
|
if (handleCaretKey(key)) return;
|
|
666
688
|
startTyping(key);
|
package/src/use-frame-measure.ts
CHANGED
|
@@ -12,11 +12,14 @@ export function useFrameMeasure<T>(
|
|
|
12
12
|
read: () => T,
|
|
13
13
|
isEqual: (left: T, right: T) => boolean,
|
|
14
14
|
initial: T,
|
|
15
|
+
active = true,
|
|
15
16
|
): T {
|
|
16
17
|
const renderer = useRenderer();
|
|
17
18
|
const [value, setValue] = useState(initial);
|
|
18
19
|
|
|
19
20
|
useEffect(() => {
|
|
21
|
+
// a measurement only needed while a popover is open subscribes no per-frame listener when closed
|
|
22
|
+
if (!active) return;
|
|
20
23
|
const measure = (): void => {
|
|
21
24
|
const next = read();
|
|
22
25
|
|
|
@@ -32,7 +35,7 @@ export function useFrameMeasure<T>(
|
|
|
32
35
|
renderer?.off("resize", measure);
|
|
33
36
|
};
|
|
34
37
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
-
}, [renderer]);
|
|
38
|
+
}, [renderer, active]);
|
|
36
39
|
|
|
37
40
|
return value;
|
|
38
41
|
}
|
|
@@ -106,6 +106,7 @@ export function useSettingsDialog(params: {
|
|
|
106
106
|
zone: "body",
|
|
107
107
|
});
|
|
108
108
|
const [actionsExpandedIndex, setActionsExpandedIndex] = useState<number | null>(null);
|
|
109
|
+
const [actionsExpandedField, setActionsExpandedField] = useState<"prompt" | "metadata">("prompt");
|
|
109
110
|
|
|
110
111
|
const commitActions = (next: QuickAction[]): void => {
|
|
111
112
|
setQuickActions(next);
|
|
@@ -141,7 +142,6 @@ export function useSettingsDialog(params: {
|
|
|
141
142
|
{
|
|
142
143
|
id: "general",
|
|
143
144
|
name: "General",
|
|
144
|
-
description: "submission behaviour",
|
|
145
145
|
rows: [
|
|
146
146
|
{
|
|
147
147
|
key: "autoClose",
|
|
@@ -151,7 +151,7 @@ export function useSettingsDialog(params: {
|
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
key: "diffView",
|
|
154
|
-
label: "Diff view
|
|
154
|
+
label: "Diff view",
|
|
155
155
|
kind: "cycle",
|
|
156
156
|
options: ["Split", "Stacked"],
|
|
157
157
|
},
|
|
@@ -160,7 +160,6 @@ export function useSettingsDialog(params: {
|
|
|
160
160
|
{
|
|
161
161
|
id: "appearance",
|
|
162
162
|
name: "Appearance",
|
|
163
|
-
description: "the color theme",
|
|
164
163
|
rows: [
|
|
165
164
|
{
|
|
166
165
|
key: "theme",
|
|
@@ -173,15 +172,16 @@ export function useSettingsDialog(params: {
|
|
|
173
172
|
{
|
|
174
173
|
id: "actions",
|
|
175
174
|
name: "Actions",
|
|
176
|
-
description: "quick-action comments",
|
|
177
175
|
rows: [],
|
|
178
176
|
customBody: (
|
|
179
177
|
<QuickActionsEditor
|
|
180
178
|
actions={quickActions}
|
|
181
179
|
selectedIndex={settingsNav.categoryId === "actions" ? settingsNav.rowIndex : -1}
|
|
182
180
|
expandedIndex={actionsExpandedIndex}
|
|
183
|
-
|
|
181
|
+
expandedField={actionsExpandedField}
|
|
182
|
+
onToggleExpand={(index, field) => {
|
|
184
183
|
setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: index }));
|
|
184
|
+
setActionsExpandedField(field);
|
|
185
185
|
setActionsExpandedIndex((current) => (current === index ? null : index));
|
|
186
186
|
}}
|
|
187
187
|
onEditPrompt={editActionPrompt}
|
|
@@ -195,7 +195,6 @@ export function useSettingsDialog(params: {
|
|
|
195
195
|
{
|
|
196
196
|
id: "keybinds",
|
|
197
197
|
name: "Keybinds",
|
|
198
|
-
description: "keyboard reference",
|
|
199
198
|
rows: [],
|
|
200
199
|
},
|
|
201
200
|
];
|
|
@@ -258,7 +257,10 @@ export function useSettingsDialog(params: {
|
|
|
258
257
|
if (moved) return void setSettingsNav(moved);
|
|
259
258
|
if (ACTIVATE_KEYS.has(name)) {
|
|
260
259
|
if (settingsNav.rowIndex === quickActions.length) addAction();
|
|
261
|
-
else
|
|
260
|
+
else {
|
|
261
|
+
setActionsExpandedField("prompt");
|
|
262
|
+
setActionsExpandedIndex(settingsNav.rowIndex);
|
|
263
|
+
}
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
return;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { themeForName } from "../theme-presets";
|
|
3
|
+
import { contrastRatio, relativeLuminance, rgbColorFromHex } from "./contrast";
|
|
4
|
+
|
|
5
|
+
test("relativeLuminance ranks black below white", () => {
|
|
6
|
+
expect(relativeLuminance([0, 0, 0])).toBe(0);
|
|
7
|
+
expect(relativeLuminance([255, 255, 255])).toBeCloseTo(1, 5);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("contrastRatio is symmetric and peaks at black on white", () => {
|
|
11
|
+
expect(contrastRatio([0, 0, 0], [255, 255, 255])).toBeCloseTo(21, 0);
|
|
12
|
+
expect(contrastRatio([255, 255, 255], [0, 0, 0])).toBeCloseTo(21, 0);
|
|
13
|
+
expect(contrastRatio([100, 100, 100], [100, 100, 100])).toBeCloseTo(1, 5);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// The branded theme is terminal-through, so text sits on a real terminal background, not pure black or
|
|
17
|
+
// white; gate each theme against a representative one so a low-contrast token cannot hide behind an extreme.
|
|
18
|
+
const TERMINAL_BACKGROUND = {
|
|
19
|
+
dark: rgbColorFromHex("#1e1e2e"),
|
|
20
|
+
light: rgbColorFromHex("#f0f0f0"),
|
|
21
|
+
} as const;
|
|
22
|
+
const MIN_NORMAL_TEXT_CONTRAST = 4.5;
|
|
23
|
+
|
|
24
|
+
for (const mode of ["dark", "light"] as const) {
|
|
25
|
+
test(`${mode} theme keeps text, muted text, and accent legible on its terminal`, () => {
|
|
26
|
+
const theme = themeForName("cueloop", mode);
|
|
27
|
+
const background = TERMINAL_BACKGROUND[mode];
|
|
28
|
+
|
|
29
|
+
for (const token of [theme.text, theme.textMuted, theme.accent]) {
|
|
30
|
+
expect(contrastRatio(rgbColorFromHex(token), background)).toBeGreaterThanOrEqual(
|
|
31
|
+
MIN_NORMAL_TEXT_CONTRAST,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** WCAG relative luminance and contrast ratio for the visual-regression accessibility gate. */
|
|
2
|
+
|
|
3
|
+
export type RgbColor = readonly [number, number, number];
|
|
4
|
+
|
|
5
|
+
/** Parse a `#rrggbb` hexadecimal colour into an sRGB triple (channels 0-255). */
|
|
6
|
+
export function rgbColorFromHex(hex: string): RgbColor {
|
|
7
|
+
const value = hex.replace("#", "");
|
|
8
|
+
|
|
9
|
+
return [
|
|
10
|
+
Number.parseInt(value.slice(0, 2), 16),
|
|
11
|
+
Number.parseInt(value.slice(2, 4), 16),
|
|
12
|
+
Number.parseInt(value.slice(4, 6), 16),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** WCAG relative luminance of an sRGB colour, 0 (black) to 1 (white). */
|
|
17
|
+
export function relativeLuminance(rgb: RgbColor): number {
|
|
18
|
+
const linear = rgb.map((channel) => {
|
|
19
|
+
const ratio = channel / 255;
|
|
20
|
+
|
|
21
|
+
return ratio <= 0.03928 ? ratio / 12.92 : ((ratio + 0.055) / 1.055) ** 2.4;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return 0.2126 * linear[0]! + 0.7152 * linear[1]! + 0.0722 * linear[2]!;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** WCAG contrast ratio between two sRGB colours, 1 (identical) to 21 (black on white). */
|
|
28
|
+
export function contrastRatio(foreground: RgbColor, background: RgbColor): number {
|
|
29
|
+
const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background));
|
|
30
|
+
const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background));
|
|
31
|
+
|
|
32
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
33
|
+
}
|