@cueloop/client 0.1.0-alpha.74 → 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.
Files changed (59) hide show
  1. package/package.json +5 -5
  2. package/src/App.focus.test.ts +147 -0
  3. package/src/App.regression.test.tsx +94 -0
  4. package/src/App.test.tsx +11 -8
  5. package/src/App.tsx +476 -220
  6. package/src/App.visual.test.tsx +57 -0
  7. package/src/App.workbench.test.tsx +41 -22
  8. package/src/app-screens.tsx +168 -123
  9. package/src/components/AppShell.brand.test.tsx +1 -1
  10. package/src/components/AppShell.tsx +14 -1
  11. package/src/components/ChangesColumn.tsx +40 -12
  12. package/src/components/ChangesFileTree.test.tsx +61 -0
  13. package/src/components/ConfirmDialog.tsx +8 -1
  14. package/src/components/DiffContentView.tsx +7 -1
  15. package/src/components/DiscussionMarkerRail.tsx +2 -2
  16. package/src/components/EditorGrid.tsx +84 -26
  17. package/src/components/GridTabContent.tsx +10 -1
  18. package/src/components/PanelColumn.tsx +3 -0
  19. package/src/components/ProjectTreeView.test.tsx +39 -12
  20. package/src/components/ProjectTreeView.tsx +38 -10
  21. package/src/components/PromptDialog.test.tsx +61 -0
  22. package/src/components/PromptDialog.tsx +16 -3
  23. package/src/components/RootOverlay.stories.tsx +1 -0
  24. package/src/components/RootOverlay.tsx +19 -6
  25. package/src/components/SettingsDialog.stories.tsx +3 -3
  26. package/src/components/SettingsDialog.tsx +3 -4
  27. package/src/components/ThreadFooter.test.tsx +12 -7
  28. package/src/components/ThreadFooter.tsx +18 -4
  29. package/src/components/ThreadTree.tsx +79 -36
  30. package/src/components/ThreadView.slash.test.tsx +36 -0
  31. package/src/components/ThreadView.tsx +7 -1
  32. package/src/components/WelcomePlayground.tsx +2 -0
  33. package/src/components/__snapshots__/stories.test.tsx.snap +60 -51
  34. package/src/components/menu-control.stories.tsx +35 -0
  35. package/src/components/menu-control.tsx +43 -0
  36. package/src/components/primitives/Dialog.test.tsx +37 -0
  37. package/src/components/primitives/Dialog.tsx +5 -0
  38. package/src/components/primitives/icons.ts +1 -1
  39. package/src/components/quick-actions-editor.stories.tsx +2 -0
  40. package/src/components/quick-actions-editor.test.tsx +35 -1
  41. package/src/components/quick-actions-editor.tsx +23 -22
  42. package/src/components/session-tree.test.ts +3 -3
  43. package/src/components/session-tree.ts +1 -1
  44. package/src/config.ts +1 -0
  45. package/src/diff-intraline.ts +26 -5
  46. package/src/perf/otlp-trace.test.ts +116 -0
  47. package/src/perf/otlp-trace.ts +130 -0
  48. package/src/perf/perf-report.ts +17 -0
  49. package/src/perf/perf-timings.test.ts +18 -0
  50. package/src/perf/perf-timings.ts +38 -0
  51. package/src/run.ts +22 -2
  52. package/src/thread-chords.test.ts +33 -1
  53. package/src/thread-chords.ts +114 -29
  54. package/src/thread-controller.ts +4 -3
  55. package/src/use-annotation-surface.tsx +22 -0
  56. package/src/use-frame-measure.ts +4 -1
  57. package/src/use-settings-dialog.tsx +9 -7
  58. package/src/visual/contrast.test.ts +35 -0
  59. 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);
@@ -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 (when wide)",
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
- onToggleExpand={(index) => {
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 setActionsExpandedIndex(settingsNav.rowIndex);
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
+ }