@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90

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 (316) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +77 -51
  10. package/src/App.diff.test.tsx +259 -45
  11. package/src/App.export.test.tsx +58 -41
  12. package/src/App.focus.test.ts +89 -0
  13. package/src/App.identity.test.tsx +78 -0
  14. package/src/App.plan-render.test.tsx +209 -0
  15. package/src/App.plan-review.test.tsx +330 -161
  16. package/src/App.prototype-markdown.test.tsx +128 -0
  17. package/src/App.prototype.test.tsx +176 -0
  18. package/src/App.readonly.test.tsx +129 -67
  19. package/src/App.regression.test.tsx +94 -0
  20. package/src/App.rendering.test.tsx +61 -35
  21. package/src/App.share-dialog.test.tsx +172 -0
  22. package/src/App.submit-confirm.test.tsx +264 -0
  23. package/src/App.test.tsx +402 -126
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1402 -1060
  26. package/src/App.visual.test.tsx +57 -0
  27. package/src/App.walk.test.tsx +299 -0
  28. package/src/App.workbench.test.tsx +774 -0
  29. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  30. package/src/annotation-palette.ts +62 -0
  31. package/src/ansi-screen.test.ts +28 -0
  32. package/src/ansi-screen.ts +72 -0
  33. package/src/app-key-state.ts +61 -0
  34. package/src/app-screens.tsx +577 -0
  35. package/src/app-view-model.ts +168 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/client-extension-registry.test.tsx +61 -0
  39. package/src/client-extension-registry.ts +159 -0
  40. package/src/clipboard.test.ts +23 -0
  41. package/src/clipboard.ts +33 -0
  42. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  43. package/src/components/AnnotatableFileView.test.tsx +102 -0
  44. package/src/components/AnnotatableFileView.tsx +134 -0
  45. package/src/components/AnnotationCards.stories.tsx +86 -0
  46. package/src/components/AnnotationCards.tsx +341 -0
  47. package/src/components/AnsiScreen.stories.tsx +12 -0
  48. package/src/components/AnsiScreen.tsx +28 -0
  49. package/src/components/AppHeader.stories.tsx +50 -0
  50. package/src/components/AppHeader.tsx +83 -0
  51. package/src/components/AppShell.brand.test.tsx +58 -0
  52. package/src/components/AppShell.extensions.test.tsx +191 -0
  53. package/src/components/AppShell.stories.tsx +89 -0
  54. package/src/components/AppShell.tsx +362 -0
  55. package/src/components/AppShell.zoom.test.tsx +135 -0
  56. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  57. package/src/components/BareWorkbenchFileView.tsx +64 -0
  58. package/src/components/Breadcrumb.stories.tsx +19 -0
  59. package/src/components/Breadcrumb.tsx +45 -0
  60. package/src/components/ChangesColumn.stories.tsx +44 -0
  61. package/src/components/ChangesColumn.tsx +199 -0
  62. package/src/components/ChangesFileTree.test.tsx +61 -0
  63. package/src/components/CodeBlock.stories.tsx +42 -0
  64. package/src/components/CodeBlock.tsx +84 -0
  65. package/src/components/CompletionOverlay.stories.tsx +37 -0
  66. package/src/components/CompletionOverlay.tsx +77 -0
  67. package/src/components/ConfirmCard.stories.tsx +37 -0
  68. package/src/components/ConfirmCard.tsx +150 -0
  69. package/src/components/ConfirmDialog.stories.tsx +21 -0
  70. package/src/components/ConfirmDialog.tsx +72 -0
  71. package/src/components/DiffContentView.scroll.test.tsx +149 -0
  72. package/src/components/DiffContentView.split.test.tsx +140 -0
  73. package/src/components/DiffContentView.stories.tsx +80 -0
  74. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  75. package/src/components/DiffContentView.tsx +906 -0
  76. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  77. package/src/components/DiscussionMarkerRail.tsx +186 -0
  78. package/src/components/EditorGrid.stories.tsx +59 -0
  79. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  80. package/src/components/EditorGrid.tsx +426 -0
  81. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  82. package/src/components/ExtensionZone.stories.tsx +28 -0
  83. package/src/components/ExtensionZone.tsx +145 -0
  84. package/src/components/FileContentsView.stories.tsx +16 -0
  85. package/src/components/FileContentsView.test.tsx +29 -0
  86. package/src/components/FileContentsView.tsx +98 -0
  87. package/src/components/GridTabContent.stories.tsx +59 -0
  88. package/src/components/GridTabContent.tsx +182 -0
  89. package/src/components/HandleChipsInput.stories.tsx +24 -0
  90. package/src/components/HandleChipsInput.test.tsx +35 -0
  91. package/src/components/HandleChipsInput.tsx +145 -0
  92. package/src/components/KeybindsDialog.stories.tsx +31 -0
  93. package/src/components/KeybindsDialog.tsx +103 -0
  94. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  95. package/src/components/MarkdownGridBlock.tsx +72 -0
  96. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  97. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  98. package/src/components/MarkdownThreadEditor.tsx +162 -0
  99. package/src/components/MenuBar.stories.tsx +28 -0
  100. package/src/components/MenuBar.tsx +44 -0
  101. package/src/components/NavModeHint.stories.tsx +40 -0
  102. package/src/components/NavModeHint.tsx +31 -0
  103. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  104. package/src/components/OverlayScrollbar.test.tsx +139 -0
  105. package/src/components/OverlayScrollbar.tsx +129 -0
  106. package/src/components/PaneGrid.stories.tsx +84 -0
  107. package/src/components/PaneGrid.tsx +128 -0
  108. package/src/components/PanelColumn.stories.tsx +25 -0
  109. package/src/components/PanelColumn.tsx +98 -0
  110. package/src/components/ProjectTreeView.stories.tsx +25 -0
  111. package/src/components/ProjectTreeView.test.tsx +75 -0
  112. package/src/components/ProjectTreeView.tsx +125 -0
  113. package/src/components/PromptDialog.stories.tsx +36 -0
  114. package/src/components/PromptDialog.test.tsx +61 -0
  115. package/src/components/PromptDialog.tsx +99 -0
  116. package/src/components/PrototypeContentView.stories.tsx +19 -0
  117. package/src/components/PrototypeContentView.tsx +446 -0
  118. package/src/components/RootOverlay.stories.tsx +45 -0
  119. package/src/components/RootOverlay.tsx +48 -0
  120. package/src/components/ScrollArea.stories.tsx +20 -0
  121. package/src/components/ScrollArea.tsx +44 -0
  122. package/src/components/SettingsDialog.stories.tsx +117 -0
  123. package/src/components/SettingsDialog.tsx +186 -0
  124. package/src/components/SettingsRows.stories.tsx +18 -0
  125. package/src/components/SettingsRows.tsx +112 -0
  126. package/src/components/ShareDialog.stories.tsx +121 -0
  127. package/src/components/ShareDialog.tsx +552 -0
  128. package/src/components/ShellHeader.stories.tsx +50 -0
  129. package/src/components/ShellHeader.tsx +137 -0
  130. package/src/components/SlashComposer.stories.tsx +37 -0
  131. package/src/components/SlashComposer.tsx +87 -0
  132. package/src/components/ThreadFooter.stories.tsx +30 -0
  133. package/src/components/ThreadFooter.test.tsx +47 -0
  134. package/src/components/ThreadFooter.tsx +76 -0
  135. package/src/components/ThreadTree.stories.tsx +46 -0
  136. package/src/components/ThreadTree.tsx +255 -0
  137. package/src/components/ThreadView.selection.test.tsx +951 -0
  138. package/src/components/ThreadView.slash.test.tsx +146 -0
  139. package/src/components/ThreadView.stories.tsx +26 -0
  140. package/src/components/ThreadView.tsx +525 -0
  141. package/src/components/Toast.stories.tsx +19 -0
  142. package/src/components/Toast.tsx +52 -0
  143. package/src/components/Tooltip.stories.tsx +26 -0
  144. package/src/components/Tooltip.tsx +72 -0
  145. package/src/components/TreePane.stories.tsx +106 -0
  146. package/src/components/TreePane.tsx +157 -0
  147. package/src/components/WalkWizard.stories.tsx +100 -0
  148. package/src/components/WalkWizard.tsx +150 -0
  149. package/src/components/WelcomePlayground.stories.tsx +12 -0
  150. package/src/components/WelcomePlayground.test.tsx +49 -0
  151. package/src/components/WelcomePlayground.tsx +139 -0
  152. package/src/components/WorkspacePanels.stories.tsx +36 -0
  153. package/src/components/WorkspacePanels.tsx +116 -0
  154. package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
  155. package/src/components/diff-content-view-layout.test.ts +198 -0
  156. package/src/components/diff-content-view-layout.ts +84 -0
  157. package/src/components/editor-grid.test.ts +86 -0
  158. package/src/components/editor-grid.ts +138 -0
  159. package/src/components/file-tree.test.ts +60 -0
  160. package/src/components/file-tree.ts +112 -0
  161. package/src/components/menu-control.stories.tsx +35 -0
  162. package/src/components/menu-control.tsx +43 -0
  163. package/src/components/pages/Workspace.stories.tsx +181 -0
  164. package/src/components/primitives/Button.stories.tsx +32 -0
  165. package/src/components/primitives/Button.tsx +59 -0
  166. package/src/components/primitives/Card.stories.tsx +25 -0
  167. package/src/components/primitives/Card.tsx +63 -0
  168. package/src/components/primitives/Dialog.stories.tsx +24 -0
  169. package/src/components/primitives/Dialog.test.tsx +37 -0
  170. package/src/components/primitives/Dialog.tsx +71 -0
  171. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  172. package/src/components/primitives/DialogActions.tsx +39 -0
  173. package/src/components/primitives/IconButton.stories.tsx +36 -0
  174. package/src/components/primitives/IconButton.tsx +62 -0
  175. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  176. package/src/components/primitives/StatusBar.tsx +23 -0
  177. package/src/components/primitives/Switch.stories.tsx +17 -0
  178. package/src/components/primitives/Switch.tsx +32 -0
  179. package/src/components/primitives/Tabs.stories.tsx +29 -0
  180. package/src/components/primitives/Tabs.tsx +102 -0
  181. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  182. package/src/components/primitives/Toolbar.tsx +14 -0
  183. package/src/components/primitives/Tree.stories.tsx +124 -0
  184. package/src/components/primitives/Tree.tsx +100 -0
  185. package/src/components/primitives/frame.test.ts +12 -0
  186. package/src/components/primitives/frame.ts +17 -0
  187. package/src/components/primitives/icons.ts +61 -0
  188. package/src/components/primitives/tree-model.test.ts +99 -0
  189. package/src/components/primitives/tree-model.ts +137 -0
  190. package/src/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/components/quick-actions-editor.test.tsx +117 -0
  192. package/src/components/quick-actions-editor.tsx +226 -0
  193. package/src/components/share-dialog-store.ts +58 -0
  194. package/src/components/stories-app.tsx +193 -0
  195. package/src/components/stories.test.tsx +74 -0
  196. package/src/components/story-fixtures.ts +102 -0
  197. package/src/components/story-tree.test.ts +108 -0
  198. package/src/components/story.ts +159 -0
  199. package/src/components/syntax-highlight.ts +133 -0
  200. package/src/components/terminal-pane.test.tsx +40 -0
  201. package/src/components/terminal-pane.ts +199 -0
  202. package/src/components/theme-context.stories.tsx +46 -0
  203. package/src/components/theme-context.tsx +37 -0
  204. package/src/components/thread-tree.test.ts +134 -0
  205. package/src/components/thread-tree.ts +109 -0
  206. package/src/components/truncate-text.ts +7 -0
  207. package/src/components/truncate-title.test.ts +31 -0
  208. package/src/components/truncate-title.ts +15 -0
  209. package/src/config-identity.test.ts +78 -0
  210. package/src/config.test.ts +579 -34
  211. package/src/config.ts +533 -57
  212. package/src/diff-fold.test.ts +78 -0
  213. package/src/diff-fold.ts +126 -0
  214. package/src/diff-hunk-curate.test.ts +315 -0
  215. package/src/diff-hunk-curate.ts +153 -0
  216. package/src/diff-intraline.test.ts +238 -0
  217. package/src/diff-intraline.ts +236 -0
  218. package/src/diff-syntax.test.ts +112 -0
  219. package/src/diff-syntax.ts +179 -0
  220. package/src/discussions.ts +79 -0
  221. package/src/editor.test.ts +149 -0
  222. package/src/editor.ts +139 -9
  223. package/src/ghostty-terminal.test.ts +85 -0
  224. package/src/ghostty-terminal.ts +199 -0
  225. package/src/github-identity.test.ts +58 -0
  226. package/src/github-identity.ts +41 -0
  227. package/src/herdr.test.ts +9 -34
  228. package/src/herdr.ts +25 -21
  229. package/src/index.ts +28 -0
  230. package/src/integrations.test.ts +41 -0
  231. package/src/integrations.ts +32 -0
  232. package/src/intent-dispatch.test.ts +724 -0
  233. package/src/intent-dispatch.ts +690 -0
  234. package/src/key-bindings.test.ts +156 -0
  235. package/src/key-bindings.ts +459 -0
  236. package/src/keymap.test.ts +456 -115
  237. package/src/keymap.ts +315 -81
  238. package/src/kitty-image.test.ts +66 -0
  239. package/src/kitty-image.ts +138 -0
  240. package/src/launch-layout.test.ts +67 -0
  241. package/src/launch-layout.ts +47 -0
  242. package/src/mark-runs.ts +121 -0
  243. package/src/markdown-editor-syntax-style.ts +50 -0
  244. package/src/markdown-grid.test.ts +101 -0
  245. package/src/markdown-grid.ts +203 -0
  246. package/src/markdown-highlight.test.ts +61 -0
  247. package/src/markdown-highlight.ts +122 -0
  248. package/src/pasted-image.test.ts +67 -0
  249. package/src/pasted-image.ts +60 -0
  250. package/src/perf/otlp-trace.test.ts +116 -0
  251. package/src/perf/otlp-trace.ts +130 -0
  252. package/src/perf/perf-report.ts +17 -0
  253. package/src/perf/perf-timings.test.ts +18 -0
  254. package/src/perf/perf-timings.ts +38 -0
  255. package/src/prototype-browser.test.ts +81 -0
  256. package/src/prototype-browser.ts +329 -0
  257. package/src/prototype-pixels.tsx +35 -0
  258. package/src/pty.test.ts +81 -0
  259. package/src/pty.ts +245 -0
  260. package/src/ready-signal.test.ts +85 -0
  261. package/src/ready-signal.ts +52 -0
  262. package/src/run.ts +85 -12
  263. package/src/serve.test.ts +24 -12
  264. package/src/serve.ts +52 -10
  265. package/src/share.ts +237 -0
  266. package/src/skills.test.ts +74 -0
  267. package/src/skills.ts +75 -0
  268. package/src/slash-palette.test.ts +103 -0
  269. package/src/slash-palette.ts +129 -0
  270. package/src/split-diff.test.ts +110 -0
  271. package/src/split-diff.ts +149 -0
  272. package/src/test-support.test.ts +264 -0
  273. package/src/test-support.ts +369 -0
  274. package/src/theme-presets.test.ts +51 -0
  275. package/src/theme-presets.ts +219 -0
  276. package/src/theme.ts +88 -19
  277. package/src/thread-chords.test.ts +79 -0
  278. package/src/thread-chords.ts +163 -0
  279. package/src/thread-code-syntax.ts +45 -0
  280. package/src/thread-controller.curate.test.ts +476 -0
  281. package/src/thread-controller.derivation.test.ts +62 -0
  282. package/src/thread-controller.live-diff.test.ts +322 -0
  283. package/src/thread-controller.optimistic.test.ts +69 -0
  284. package/src/thread-controller.share.test.ts +561 -0
  285. package/src/thread-controller.switch-cache.test.ts +176 -0
  286. package/src/thread-controller.tree.test.ts +261 -0
  287. package/src/thread-controller.ts +2050 -0
  288. package/src/thread-selection.test.ts +210 -0
  289. package/src/thread-selection.ts +243 -0
  290. package/src/tree-view.test.ts +128 -0
  291. package/src/tree-view.ts +153 -0
  292. package/src/use-annotation-surface.tsx +975 -0
  293. package/src/use-changes-workbench.ts +190 -0
  294. package/src/use-frame-measure.ts +41 -0
  295. package/src/use-remember-layout.ts +31 -0
  296. package/src/use-repo-changes.ts +87 -0
  297. package/src/use-settings-dialog.tsx +364 -0
  298. package/src/use-terminal-virtualizer.ts +259 -0
  299. package/src/use-thread-body-editing.tsx +58 -0
  300. package/src/version.ts +5 -0
  301. package/src/view-diff.test.ts +279 -9
  302. package/src/view-diff.ts +460 -24
  303. package/src/view-plan.regression.test.ts +60 -0
  304. package/src/view-plan.test.ts +545 -0
  305. package/src/view-plan.ts +1019 -0
  306. package/src/visual/contrast.test.ts +35 -0
  307. package/src/visual/contrast.ts +33 -0
  308. package/src/walk.test.ts +121 -0
  309. package/src/walk.ts +74 -0
  310. package/src/workbench-snapshot.test.ts +99 -0
  311. package/src/workbench-snapshot.ts +59 -0
  312. package/src/session-controller.ts +0 -398
  313. package/src/syntax.test.ts +0 -41
  314. package/src/syntax.ts +0 -88
  315. package/src/view.test.ts +0 -157
  316. package/src/view.ts +0 -364
@@ -0,0 +1,190 @@
1
+ // The Changes/Project right-region state machine: the Project pane is the right sidebar and is always
2
+ // present when the region is on, the Changes editor rides on it and never stands alone, and the two
3
+ // toggles are mutually-exclusive file-tree modes. Owns the editor grid (tabs and splits) too.
4
+
5
+ import { useRef, useState } from "react";
6
+ import type { ProjectPanelMode } from "./components/AppShell";
7
+ import type { LaunchLayout } from "./launch-layout";
8
+ import {
9
+ activateTab,
10
+ addTab,
11
+ changesTab,
12
+ closeTab,
13
+ containsGroup,
14
+ fileTab,
15
+ firstGroupId,
16
+ makeGroup,
17
+ splitGroup,
18
+ welcomeTab,
19
+ type EditorNode,
20
+ type SplitDirection,
21
+ } from "./components/editor-grid";
22
+
23
+ /** Seed the workbench for a bare launch: the region opens on a disposable Welcome tab. */
24
+ export interface ChangesWorkbenchOptions {
25
+ seed?: "changes" | "welcome";
26
+ /** The launch layout the right region opens in; overrides the seed's defaults. */
27
+ layout?: LaunchLayout;
28
+ }
29
+
30
+ interface InitialPanes {
31
+ projectOpen: boolean;
32
+ changesOpen: boolean;
33
+ projectMode: ProjectPanelMode;
34
+ zoomed: boolean;
35
+ }
36
+
37
+ /** The right region's opening state: from the launch layout when given, else the welcome-seed defaults. */
38
+ function initialPanes(options?: ChangesWorkbenchOptions): InitialPanes {
39
+ const layout = options?.layout;
40
+
41
+ if (layout) {
42
+ const changesOpen = layout.rightSidebar === "changes";
43
+
44
+ return {
45
+ projectOpen: layout.rightSidebar !== "off",
46
+ changesOpen,
47
+ projectMode: layout.rightSidebar === "project" ? "tree" : "changes",
48
+ // zoom fills the middle with the Changes editor, so a hand-edited layout that zooms without one
49
+ // would strand a blank center with the Thread pane hidden; only honor zoom when Changes is open
50
+ zoomed: layout.zoomChanges && changesOpen,
51
+ };
52
+ }
53
+ const welcomeSeed = options?.seed === "welcome";
54
+
55
+ return {
56
+ projectOpen: welcomeSeed,
57
+ changesOpen: welcomeSeed,
58
+ projectMode: "changes",
59
+ zoomed: false,
60
+ };
61
+ }
62
+
63
+ export interface ChangesWorkbench {
64
+ projectOpen: boolean;
65
+ changesOpen: boolean;
66
+ projectMode: ProjectPanelMode;
67
+ zoomed: boolean;
68
+ grid: EditorNode;
69
+ activeGroup: string;
70
+ toggleRight: () => void;
71
+ toggleChanges: () => void;
72
+ toggleProject: () => void;
73
+ toggleZoom: () => void;
74
+ focusGroup: (groupId: string) => void;
75
+ activate: (groupId: string, tabId: string) => void;
76
+ close: (groupId: string, tabId: string) => void;
77
+ split: (groupId: string, direction: SplitDirection) => void;
78
+ openFile: (path: string, fileView: "diff" | "contents") => void;
79
+ /** Re-open defaults when the session changes: a diff opens the region in changed-files mode. */
80
+ syncSession: (sessionId: string | undefined, isDiff: boolean) => void;
81
+ }
82
+
83
+ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesWorkbench {
84
+ const welcomeSeed = options?.seed === "welcome";
85
+ const initial = initialPanes(options);
86
+ const [projectOpen, setProjectOpen] = useState(initial.projectOpen);
87
+ const [changesOpen, setChangesOpen] = useState(initial.changesOpen);
88
+ const [projectMode, setProjectMode] = useState<ProjectPanelMode>(initial.projectMode);
89
+ const [zoomed, setZoomed] = useState(initial.zoomed);
90
+ const [grid, setGrid] = useState<EditorNode>(() =>
91
+ makeGroup([welcomeSeed ? welcomeTab() : changesTab()]),
92
+ );
93
+ const [focusedGroup, setFocusedGroup] = useState<string | null>(null);
94
+ const rememberedChanges = useRef(welcomeSeed);
95
+ const seenSession = useRef<string | undefined>(undefined);
96
+
97
+ const syncSession = (sessionId: string | undefined, isDiff: boolean): void => {
98
+ if (sessionId === undefined || sessionId === seenSession.current) return;
99
+ seenSession.current = sessionId;
100
+ setProjectOpen(isDiff);
101
+ setChangesOpen(isDiff);
102
+ setProjectMode(isDiff ? "changes" : "tree");
103
+ rememberedChanges.current = isDiff;
104
+ // a non-diff thread has no Changes editor; leaving zoom on would strand the hidden Thread pane
105
+ if (!isDiff) setZoomed(false);
106
+ setGrid(makeGroup([changesTab()]));
107
+ setFocusedGroup(null);
108
+ };
109
+
110
+ const toggleRight = (): void => {
111
+ if (projectOpen) {
112
+ rememberedChanges.current = changesOpen;
113
+ setChangesOpen(false);
114
+ setProjectOpen(false);
115
+ // zoom focuses the Changes editor; collapsing the region returns the Thread pane full width
116
+ setZoomed(false);
117
+ } else {
118
+ setProjectOpen(true);
119
+ setChangesOpen(rememberedChanges.current);
120
+ setProjectMode(rememberedChanges.current ? "changes" : "tree");
121
+ }
122
+ };
123
+ 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
+ }
134
+ };
135
+ const toggleProject = (): void => {
136
+ setProjectOpen(true);
137
+ setProjectMode("tree");
138
+ };
139
+ const close = (groupId: string, tabId: string): void => {
140
+ setGrid((tree) => {
141
+ const pruned = closeTab(tree, groupId, tabId);
142
+ if (pruned === null) {
143
+ setChangesOpen(false);
144
+ setProjectMode("tree");
145
+ // leaving zoom on would hide the Thread pane with no editor to fill the gap
146
+ setZoomed(false);
147
+ return makeGroup([changesTab()]);
148
+ }
149
+ // re-home focus when the closed group was pruned away, so the next open has a live target
150
+ setFocusedGroup((current) =>
151
+ current !== null && containsGroup(pruned, current) ? current : firstGroupId(pruned),
152
+ );
153
+ return pruned;
154
+ });
155
+ };
156
+ const split = (groupId: string, direction: SplitDirection): void => {
157
+ setGrid((tree) => {
158
+ const result = splitGroup(tree, groupId, direction);
159
+ setFocusedGroup(result.focusGroupId);
160
+ return result.tree;
161
+ });
162
+ };
163
+ const openFile = (path: string, fileView: "diff" | "contents"): void => {
164
+ setProjectOpen(true);
165
+ setChangesOpen(true);
166
+ const label = path.split("/").pop() ?? path;
167
+ setGrid((tree) =>
168
+ addTab(tree, focusedGroup ?? firstGroupId(tree), fileTab(label, path, fileView)),
169
+ );
170
+ };
171
+
172
+ return {
173
+ projectOpen,
174
+ changesOpen,
175
+ projectMode,
176
+ zoomed,
177
+ grid,
178
+ activeGroup: focusedGroup ?? firstGroupId(grid),
179
+ toggleRight,
180
+ toggleChanges,
181
+ toggleProject,
182
+ toggleZoom: () => setZoomed((value) => !value),
183
+ focusGroup: setFocusedGroup,
184
+ activate: (groupId, tabId) => setGrid((tree) => activateTab(tree, groupId, tabId)),
185
+ close,
186
+ split,
187
+ openFile,
188
+ syncSession,
189
+ };
190
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Read a layout-dependent value after every rendered frame (and resize),
3
+ * committing only on change. Layout lands after the commit, so a timer
4
+ * would race the visual-idle wait; measuring on the frame event converges
5
+ * deterministically within a frame or two.
6
+ */
7
+
8
+ import { useEffect, useState } from "react";
9
+ import { useRenderer } from "@opentui/react";
10
+
11
+ export function useFrameMeasure<T>(
12
+ read: () => T,
13
+ isEqual: (left: T, right: T) => boolean,
14
+ initial: T,
15
+ active = true,
16
+ ): T {
17
+ const renderer = useRenderer();
18
+ const [value, setValue] = useState(initial);
19
+
20
+ useEffect(() => {
21
+ // a measurement only needed while a popover is open subscribes no per-frame listener when closed
22
+ if (!active) return;
23
+ const measure = (): void => {
24
+ const next = read();
25
+
26
+ setValue((current) => (isEqual(current, next) ? current : next));
27
+ };
28
+
29
+ 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
+ };
37
+ // eslint-disable-next-line react-hooks/exhaustive-deps
38
+ }, [renderer, active]);
39
+
40
+ return value;
41
+ }
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { persistLayout } from "./config";
3
+ import { layoutFromPanes, type LaunchLayout } from "./launch-layout";
4
+
5
+ /**
6
+ * Persist the pane composition whenever the user changes it, so the next bare launch restores it.
7
+ * Only a launch that carried an intended layout (a create-command or the vanilla inbox) remembers;
8
+ * a bare thread open collapses the sidebar and must not clobber the remembered composition. The
9
+ * mount write is skipped: the opening composition is the one just restored.
10
+ */
11
+ export function useRememberLayout(
12
+ layout: LaunchLayout | undefined,
13
+ active: boolean,
14
+ threads: boolean,
15
+ changesOpen: boolean,
16
+ projectOpen: boolean,
17
+ zoomChanges: boolean,
18
+ ): void {
19
+ const settled = useRef(false);
20
+
21
+ useEffect(() => {
22
+ if (!settled.current) {
23
+ settled.current = true;
24
+
25
+ return;
26
+ }
27
+ // only the active workbench persists: on a bare launch App's unused panes must not clobber the shell's
28
+ if (!active || layout === undefined) return;
29
+ persistLayout(layoutFromPanes({ threads, changesOpen, projectOpen, zoomChanges }));
30
+ }, [active, layout, threads, changesOpen, projectOpen, zoomChanges]);
31
+ }
@@ -0,0 +1,87 @@
1
+ import { useEffect, useEffectEvent, useState } from "react";
2
+ import type { DiffFileContents } from "@cueloop/schema";
3
+
4
+ const CHANGES_REFRESH_MS = 2000;
5
+ const EMPTY_CHANGED_FILES: readonly DiffFileContents[] = [];
6
+
7
+ interface RepoChangesOptions {
8
+ loadChanges: () => Promise<readonly DiffFileContents[]>;
9
+ visible: boolean;
10
+ diffSourceKey: string;
11
+ refreshAutomatically?: boolean;
12
+ }
13
+
14
+ interface RepoChangesSnapshot {
15
+ diffSourceKey: string;
16
+ files: readonly DiffFileContents[];
17
+ }
18
+
19
+ function sameChangedFiles(a: readonly DiffFileContents[], b: readonly DiffFileContents[]): boolean {
20
+ return (
21
+ a.length === b.length &&
22
+ a.every(
23
+ (file, index) =>
24
+ file.path === b[index]?.path &&
25
+ file.status === b[index]?.status &&
26
+ file.oldContents === b[index]?.oldContents &&
27
+ file.newContents === b[index]?.newContents,
28
+ )
29
+ );
30
+ }
31
+
32
+ /** Refresh the visible Changes tree from the working repo; keep captured diff reviews fixed. */
33
+ export function useRepoChanges({
34
+ loadChanges,
35
+ visible,
36
+ diffSourceKey,
37
+ refreshAutomatically = true,
38
+ }: RepoChangesOptions): readonly DiffFileContents[] {
39
+ const [snapshot, setSnapshot] = useState<RepoChangesSnapshot | null>(null);
40
+ const loadLatestChanges = useEffectEvent(loadChanges);
41
+
42
+ useEffect(() => {
43
+ if (!visible) return;
44
+
45
+ let cancelled = false;
46
+ let requestInFlight = false;
47
+
48
+ const refreshChanges = async (): Promise<void> => {
49
+ if (requestInFlight) return;
50
+ requestInFlight = true;
51
+
52
+ try {
53
+ const files = await loadLatestChanges();
54
+
55
+ if (!cancelled) {
56
+ setSnapshot((previous) =>
57
+ previous?.diffSourceKey === diffSourceKey && sameChangedFiles(previous.files, files)
58
+ ? previous
59
+ : { diffSourceKey, files },
60
+ );
61
+ }
62
+ } catch {
63
+ if (!cancelled) {
64
+ setSnapshot((previous) =>
65
+ previous?.diffSourceKey === diffSourceKey && previous.files.length === 0
66
+ ? previous
67
+ : { diffSourceKey, files: EMPTY_CHANGED_FILES },
68
+ );
69
+ }
70
+ } finally {
71
+ requestInFlight = false;
72
+ }
73
+ };
74
+
75
+ void refreshChanges();
76
+ const timer = refreshAutomatically
77
+ ? setInterval(() => void refreshChanges(), CHANGES_REFRESH_MS)
78
+ : null;
79
+
80
+ return () => {
81
+ cancelled = true;
82
+ if (timer !== null) clearInterval(timer);
83
+ };
84
+ }, [visible, diffSourceKey, refreshAutomatically]);
85
+
86
+ return snapshot?.diffSourceKey === diffSourceKey ? snapshot.files : EMPTY_CHANGED_FILES;
87
+ }