@cueloop/client 0.1.0-alpha.92 → 0.1.0-alpha.93

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 (37) hide show
  1. package/package.json +5 -5
  2. package/src/App.deleted-scroll.test.tsx +186 -0
  3. package/src/App.diff.test.tsx +3 -4
  4. package/src/App.test.tsx +1 -0
  5. package/src/App.toast.test.tsx +27 -1
  6. package/src/App.tsx +21 -12
  7. package/src/App.workbench.test.tsx +208 -2
  8. package/src/app-screens.tsx +34 -8
  9. package/src/components/AppShell.zoom.test.tsx +16 -3
  10. package/src/components/ChangesColumn.tsx +10 -4
  11. package/src/components/ChangesFileTree.test.tsx +122 -4
  12. package/src/components/DiffContentView.scroll.test.tsx +285 -1
  13. package/src/components/DiffContentView.tsx +214 -56
  14. package/src/components/EditorGrid.split.test.tsx +118 -0
  15. package/src/components/EditorGrid.stories.tsx +27 -0
  16. package/src/components/EditorGrid.tsx +170 -50
  17. package/src/components/EditorGrid.zoom.test.tsx +65 -1
  18. package/src/components/GridTabContent.actions.test.tsx +151 -0
  19. package/src/components/GridTabContent.stories.tsx +2 -2
  20. package/src/components/GridTabContent.tsx +5 -1
  21. package/src/components/ProjectTreeView.test.tsx +166 -8
  22. package/src/components/ProjectTreeView.tsx +53 -27
  23. package/src/components/ScrollArea.tsx +21 -2
  24. package/src/components/Toast.tsx +3 -1
  25. package/src/components/WelcomePlayground.test.tsx +28 -1
  26. package/src/components/__snapshots__/stories.test.tsx.snap +41 -21
  27. package/src/components/editor-grid.test.ts +87 -0
  28. package/src/components/editor-grid.ts +142 -6
  29. package/src/components/primitives/Tree.tsx +51 -15
  30. package/src/thread-controller.curate.test.ts +27 -0
  31. package/src/thread-controller.ts +4 -6
  32. package/src/use-annotation-surface.tsx +86 -19
  33. package/src/use-changes-workbench.ts +30 -18
  34. package/src/use-frame-measure.ts +40 -7
  35. package/src/use-settings-dialog.tsx +1 -13
  36. package/src/use-shared-keyboard.ts +55 -0
  37. package/src/use-terminal-virtualizer.ts +8 -2
@@ -7,13 +7,14 @@ import type { ProjectPanelMode } from "./components/AppShell";
7
7
  import type { LaunchLayout } from "./launch-layout";
8
8
  import {
9
9
  activateTab,
10
- addTab,
11
10
  changesTab,
12
11
  closeTab,
13
12
  containsGroup,
14
- fileTab,
15
13
  firstGroupId,
14
+ keepFileTab,
16
15
  makeGroup,
16
+ openChangesTab,
17
+ openFileTab,
17
18
  splitGroup,
18
19
  welcomeTab,
19
20
  type EditorNode,
@@ -73,9 +74,10 @@ export interface ChangesWorkbench {
73
74
  toggleZoom: () => void;
74
75
  focusGroup: (groupId: string) => void;
75
76
  activate: (groupId: string, tabId: string) => void;
77
+ keepTab: (groupId: string, tabId: string) => void;
76
78
  close: (groupId: string, tabId: string) => void;
77
79
  split: (groupId: string, direction: SplitDirection) => void;
78
- openFile: (path: string, fileView: "diff" | "contents") => void;
80
+ openFile: (path: string, fileView: "diff" | "contents", persistent?: boolean) => void;
79
81
  /** Re-open defaults when the session changes: a diff opens the region in changed-files mode. */
80
82
  syncSession: (sessionId: string | undefined, isDiff: boolean) => void;
81
83
  }
@@ -121,16 +123,16 @@ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesW
121
123
  }
122
124
  };
123
125
  const toggleChanges = (): void => {
124
- if (projectMode === "changes") {
125
- setChangesOpen(false);
126
- setProjectMode("tree");
127
- // never leave zoom on with the Changes editor gone - the Thread pane would stay hidden
128
- setZoomed(false);
129
- } else {
130
- setChangesOpen(true);
131
- setProjectOpen(true);
132
- setProjectMode("changes");
133
- }
126
+ setChangesOpen(true);
127
+ setProjectOpen(true);
128
+ setProjectMode("changes");
129
+ setGrid((tree) => {
130
+ const result = openChangesTab(tree, focusedGroup ?? firstGroupId(tree));
131
+
132
+ setFocusedGroup(result.focusGroupId);
133
+
134
+ return result.tree;
135
+ });
134
136
  };
135
137
  const toggleProject = (): void => {
136
138
  setProjectOpen(true);
@@ -160,13 +162,22 @@ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesW
160
162
  return result.tree;
161
163
  });
162
164
  };
163
- const openFile = (path: string, fileView: "diff" | "contents"): void => {
165
+ const openFile = (path: string, fileView: "diff" | "contents", persistent = false): void => {
164
166
  setProjectOpen(true);
165
167
  setChangesOpen(true);
166
- const label = path.split("/").pop() ?? path;
167
- setGrid((tree) =>
168
- addTab(tree, focusedGroup ?? firstGroupId(tree), fileTab(label, path, fileView)),
169
- );
168
+ setGrid((tree) => {
169
+ const result = openFileTab(
170
+ tree,
171
+ focusedGroup ?? firstGroupId(tree),
172
+ path,
173
+ fileView,
174
+ persistent,
175
+ );
176
+
177
+ setFocusedGroup(result.focusGroupId);
178
+
179
+ return result.tree;
180
+ });
170
181
  };
171
182
 
172
183
  return {
@@ -182,6 +193,7 @@ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesW
182
193
  toggleZoom: () => setZoomed((value) => !value),
183
194
  focusGroup: setFocusedGroup,
184
195
  activate: (groupId, tabId) => setGrid((tree) => activateTab(tree, groupId, tabId)),
196
+ keepTab: (groupId, tabId) => setGrid((tree) => keepFileTab(tree, groupId, tabId)),
185
197
  close,
186
198
  split,
187
199
  openFile,
@@ -8,6 +8,45 @@
8
8
  import { useEffect, useState } from "react";
9
9
  import { useRenderer } from "@opentui/react";
10
10
 
11
+ type Renderer = NonNullable<ReturnType<typeof useRenderer>>;
12
+ type Measure = () => void;
13
+
14
+ interface FrameSubscriptions {
15
+ measures: Set<Measure>;
16
+ notify: Measure;
17
+ }
18
+
19
+ const subscribers = new WeakMap<Renderer, FrameSubscriptions>();
20
+
21
+ export function subscribeToFrames(renderer: Renderer, measure: Measure): () => void {
22
+ let subscription = subscribers.get(renderer);
23
+
24
+ if (!subscription) {
25
+ const measures = new Set<Measure>();
26
+ const notify = (): void => {
27
+ for (const subscriber of measures) subscriber();
28
+ };
29
+
30
+ subscription = { measures, notify };
31
+ subscribers.set(renderer, subscription);
32
+ renderer.on("frame", notify);
33
+ renderer.on("resize", notify);
34
+ }
35
+
36
+ const { measures, notify } = subscription;
37
+
38
+ measures.add(measure);
39
+
40
+ return () => {
41
+ measures.delete(measure);
42
+ if (measures.size > 0) return;
43
+
44
+ renderer.off("frame", notify);
45
+ renderer.off("resize", notify);
46
+ subscribers.delete(renderer);
47
+ };
48
+ }
49
+
11
50
  export function useFrameMeasure<T>(
12
51
  read: () => T,
13
52
  isEqual: (left: T, right: T) => boolean,
@@ -27,13 +66,7 @@ export function useFrameMeasure<T>(
27
66
  };
28
67
 
29
68
  measure();
30
- renderer?.on("frame", measure);
31
- renderer?.on("resize", measure);
32
-
33
- return () => {
34
- renderer?.off("frame", measure);
35
- renderer?.off("resize", measure);
36
- };
69
+ if (renderer) return subscribeToFrames(renderer, measure);
37
70
  // eslint-disable-next-line react-hooks/exhaustive-deps
38
71
  }, [renderer, active]);
39
72
 
@@ -4,7 +4,6 @@ import {
4
4
  persistActions,
5
5
  persistAutoClose,
6
6
  persistDiffView,
7
- persistReviewSkill,
8
7
  persistReviewWorkspace,
9
8
  persistTheme,
10
9
  type AutoClose,
@@ -94,8 +93,6 @@ export function useSettingsDialog(params: {
94
93
  onSyncGithubIdentity: () => void;
95
94
  onRenameDisplayName: () => void;
96
95
  reviewSkill: string;
97
- reviewSkillOptions: string[];
98
- setReviewSkill: Dispatch<SetStateAction<string>>;
99
96
  reviewWorkspace: ReviewWorkspaceMode;
100
97
  setReviewWorkspace: Dispatch<SetStateAction<ReviewWorkspaceMode>>;
101
98
  }): SettingsDialogModel {
@@ -118,8 +115,6 @@ export function useSettingsDialog(params: {
118
115
  onSyncGithubIdentity,
119
116
  onRenameDisplayName,
120
117
  reviewSkill,
121
- reviewSkillOptions,
122
- setReviewSkill,
123
118
  reviewWorkspace,
124
119
  setReviewWorkspace,
125
120
  } = params;
@@ -210,8 +205,7 @@ export function useSettingsDialog(params: {
210
205
  {
211
206
  key: "reviewSkill",
212
207
  label: "Skill",
213
- kind: "cycle",
214
- options: reviewSkillOptions,
208
+ kind: "text",
215
209
  },
216
210
  {
217
211
  key: "reviewWorkspace",
@@ -281,12 +275,6 @@ export function useSettingsDialog(params: {
281
275
  onSyncGithubIdentity();
282
276
  } else if (rowKey === "displayName") {
283
277
  onRenameDisplayName();
284
- } else if (rowKey === "reviewSkill") {
285
- const current = Math.max(0, reviewSkillOptions.indexOf(reviewSkill));
286
- const next = reviewSkillOptions[(current + 1) % reviewSkillOptions.length]!;
287
-
288
- setReviewSkill(next);
289
- persistReviewSkill(next);
290
278
  } else if (rowKey === "reviewWorkspace") {
291
279
  const next: ReviewWorkspaceMode = reviewWorkspace === "worktree" ? "current" : "worktree";
292
280
 
@@ -0,0 +1,55 @@
1
+ /** One keypress subscription per renderer, even when several editor tiles are mounted. */
2
+
3
+ import { useEffect, useEffectEvent } from "react";
4
+ import { useRenderer } from "@opentui/react";
5
+ import type { KeyEvent } from "@opentui/core";
6
+
7
+ type KeyInput = NonNullable<ReturnType<typeof useRenderer>>["keyInput"];
8
+ type KeyHandler = (key: KeyEvent) => void;
9
+
10
+ interface KeySubscriptions {
11
+ handlers: Set<KeyHandler>;
12
+ notify: KeyHandler;
13
+ }
14
+
15
+ const subscribers = new WeakMap<KeyInput, KeySubscriptions>();
16
+
17
+ function subscribeToKeypress(input: KeyInput, handler: KeyHandler): () => void {
18
+ let subscription = subscribers.get(input);
19
+
20
+ if (!subscription) {
21
+ const handlers = new Set<KeyHandler>();
22
+ const notify = (key: KeyEvent): void => {
23
+ for (const subscriber of handlers) subscriber(key);
24
+ };
25
+
26
+ subscription = { handlers, notify };
27
+ subscribers.set(input, subscription);
28
+ input.on("keypress", notify);
29
+ }
30
+
31
+ const { handlers, notify } = subscription;
32
+
33
+ handlers.add(handler);
34
+
35
+ return () => {
36
+ handlers.delete(handler);
37
+ if (handlers.size > 0) return;
38
+
39
+ input.off("keypress", notify);
40
+ subscribers.delete(input);
41
+ };
42
+ }
43
+
44
+ export function useSharedKeyboard(handler: KeyHandler): void {
45
+ const renderer = useRenderer();
46
+ const stableHandler = useEffectEvent(handler);
47
+
48
+ useEffect(() => {
49
+ const input = renderer?.keyInput;
50
+
51
+ if (!input) return;
52
+
53
+ return subscribeToKeypress(input, stableHandler);
54
+ }, [renderer]);
55
+ }
@@ -12,6 +12,7 @@ import { useEffect, useLayoutEffect, useRef, useState, type RefObject } from "re
12
12
  import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
13
13
  import { useRenderer } from "@opentui/react";
14
14
  import { Virtualizer, type VirtualItem } from "@tanstack/virtual-core";
15
+ import { subscribeToFrames } from "./use-frame-measure";
15
16
 
16
17
  export interface TerminalVirtualizerOptions {
17
18
  scrollbox: RefObject<ScrollBoxRenderable | null>;
@@ -29,6 +30,10 @@ export interface TerminalVirtualizer {
29
30
  measureRef: (index: number) => (renderable: BoxRenderable | null) => void | (() => void);
30
31
  /** Scroll the item into view; "auto" only scrolls when it is off screen. */
31
32
  scrollToIndex: (index: number, align?: "auto" | "start" | "center" | "end") => void;
33
+ /** Measured or estimated visual-row offset of an item. */
34
+ startOfIndex: (index: number) => number | undefined;
35
+ /** Move the native viewport to an exact visual-row offset. */
36
+ scrollToOffset: (offset: number) => void;
32
37
  }
33
38
 
34
39
  /** Visible rows and one-row scrolling for a held mouse mark at a viewport edge. */
@@ -150,9 +155,8 @@ export function useTerminalVirtualizer(options: TerminalVirtualizerOptions): Ter
150
155
  // render, and the callbacks only fire the core when the value actually changed
151
156
  const onFrame = (read: () => void): (() => void) => {
152
157
  read();
153
- renderer?.on("frame", read);
154
158
 
155
- return () => renderer?.off("frame", read);
159
+ return renderer ? subscribeToFrames(renderer, read) : () => {};
156
160
  };
157
161
 
158
162
  const coreOptions = () => ({
@@ -255,5 +259,7 @@ export function useTerminalVirtualizer(options: TerminalVirtualizerOptions): Ter
255
259
  };
256
260
  },
257
261
  scrollToIndex: (index, align = "auto") => virtualizer.scrollToIndex(index, { align }),
262
+ startOfIndex: (index) => virtualizer.getOffsetForIndex(index, "start")?.[0],
263
+ scrollToOffset: (offset) => virtualizer.scrollToOffset(offset, { align: "start" }),
258
264
  };
259
265
  }