@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81

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 (307) 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 +40 -32
  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 +176 -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 +428 -0
  16. package/src/App.prototype-markdown.test.tsx +92 -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 +222 -0
  23. package/src/App.test.tsx +333 -131
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1338 -608
  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 +701 -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 +574 -0
  35. package/src/app-view-model.ts +162 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/clipboard.test.ts +23 -0
  39. package/src/clipboard.ts +33 -0
  40. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  41. package/src/components/AnnotatableFileView.test.tsx +102 -0
  42. package/src/components/AnnotatableFileView.tsx +134 -0
  43. package/src/components/AnnotationCards.stories.tsx +86 -0
  44. package/src/components/AnnotationCards.tsx +341 -0
  45. package/src/components/AnsiScreen.stories.tsx +12 -0
  46. package/src/components/AnsiScreen.tsx +28 -0
  47. package/src/components/AppHeader.stories.tsx +50 -0
  48. package/src/components/AppHeader.tsx +83 -0
  49. package/src/components/AppShell.brand.test.tsx +58 -0
  50. package/src/components/AppShell.stories.tsx +89 -0
  51. package/src/components/AppShell.tsx +329 -0
  52. package/src/components/AppShell.zoom.test.tsx +135 -0
  53. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  54. package/src/components/BareWorkbenchFileView.tsx +64 -0
  55. package/src/components/Breadcrumb.stories.tsx +19 -0
  56. package/src/components/Breadcrumb.tsx +45 -0
  57. package/src/components/ChangesColumn.stories.tsx +44 -0
  58. package/src/components/ChangesColumn.tsx +199 -0
  59. package/src/components/ChangesFileTree.test.tsx +61 -0
  60. package/src/components/CodeBlock.stories.tsx +42 -0
  61. package/src/components/CodeBlock.tsx +84 -0
  62. package/src/components/CompletionOverlay.stories.tsx +37 -0
  63. package/src/components/CompletionOverlay.tsx +77 -0
  64. package/src/components/ConfirmCard.stories.tsx +37 -0
  65. package/src/components/ConfirmCard.tsx +153 -0
  66. package/src/components/ConfirmDialog.stories.tsx +21 -0
  67. package/src/components/ConfirmDialog.tsx +72 -0
  68. package/src/components/DiffContentView.scroll.test.tsx +121 -0
  69. package/src/components/DiffContentView.split.test.tsx +140 -0
  70. package/src/components/DiffContentView.stories.tsx +80 -0
  71. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  72. package/src/components/DiffContentView.tsx +876 -0
  73. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  74. package/src/components/DiscussionMarkerRail.tsx +186 -0
  75. package/src/components/EditorGrid.stories.tsx +59 -0
  76. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  77. package/src/components/EditorGrid.tsx +426 -0
  78. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  79. package/src/components/FileContentsView.stories.tsx +16 -0
  80. package/src/components/FileContentsView.test.tsx +29 -0
  81. package/src/components/FileContentsView.tsx +98 -0
  82. package/src/components/GridTabContent.stories.tsx +59 -0
  83. package/src/components/GridTabContent.tsx +182 -0
  84. package/src/components/HandleChipsInput.stories.tsx +24 -0
  85. package/src/components/HandleChipsInput.test.tsx +35 -0
  86. package/src/components/HandleChipsInput.tsx +145 -0
  87. package/src/components/KeybindsDialog.stories.tsx +31 -0
  88. package/src/components/KeybindsDialog.tsx +103 -0
  89. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  90. package/src/components/MarkdownGridBlock.tsx +72 -0
  91. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  92. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  93. package/src/components/MarkdownThreadEditor.tsx +162 -0
  94. package/src/components/MenuBar.stories.tsx +28 -0
  95. package/src/components/MenuBar.tsx +44 -0
  96. package/src/components/NavModeHint.stories.tsx +40 -0
  97. package/src/components/NavModeHint.tsx +31 -0
  98. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  99. package/src/components/OverlayScrollbar.test.tsx +139 -0
  100. package/src/components/OverlayScrollbar.tsx +129 -0
  101. package/src/components/PaneGrid.stories.tsx +84 -0
  102. package/src/components/PaneGrid.tsx +128 -0
  103. package/src/components/PanelColumn.stories.tsx +25 -0
  104. package/src/components/PanelColumn.tsx +98 -0
  105. package/src/components/ProjectTreeView.stories.tsx +25 -0
  106. package/src/components/ProjectTreeView.test.tsx +59 -0
  107. package/src/components/ProjectTreeView.tsx +111 -0
  108. package/src/components/PromptDialog.stories.tsx +36 -0
  109. package/src/components/PromptDialog.test.tsx +61 -0
  110. package/src/components/PromptDialog.tsx +99 -0
  111. package/src/components/PrototypeContentView.stories.tsx +19 -0
  112. package/src/components/PrototypeContentView.tsx +446 -0
  113. package/src/components/RootOverlay.stories.tsx +45 -0
  114. package/src/components/RootOverlay.tsx +48 -0
  115. package/src/components/ScrollArea.stories.tsx +20 -0
  116. package/src/components/ScrollArea.tsx +44 -0
  117. package/src/components/SettingsDialog.stories.tsx +117 -0
  118. package/src/components/SettingsDialog.tsx +186 -0
  119. package/src/components/SettingsRows.stories.tsx +18 -0
  120. package/src/components/SettingsRows.tsx +112 -0
  121. package/src/components/ShareDialog.stories.tsx +121 -0
  122. package/src/components/ShareDialog.tsx +552 -0
  123. package/src/components/ShellHeader.stories.tsx +50 -0
  124. package/src/components/ShellHeader.tsx +137 -0
  125. package/src/components/SlashComposer.stories.tsx +37 -0
  126. package/src/components/SlashComposer.tsx +87 -0
  127. package/src/components/ThreadFooter.stories.tsx +30 -0
  128. package/src/components/ThreadFooter.test.tsx +47 -0
  129. package/src/components/ThreadFooter.tsx +76 -0
  130. package/src/components/ThreadTree.stories.tsx +46 -0
  131. package/src/components/ThreadTree.tsx +255 -0
  132. package/src/components/ThreadView.selection.test.tsx +899 -0
  133. package/src/components/ThreadView.slash.test.tsx +146 -0
  134. package/src/components/ThreadView.stories.tsx +26 -0
  135. package/src/components/ThreadView.tsx +472 -0
  136. package/src/components/Toast.stories.tsx +19 -0
  137. package/src/components/Toast.tsx +52 -0
  138. package/src/components/Tooltip.stories.tsx +26 -0
  139. package/src/components/Tooltip.tsx +72 -0
  140. package/src/components/TreePane.stories.tsx +106 -0
  141. package/src/components/TreePane.tsx +157 -0
  142. package/src/components/WalkWizard.stories.tsx +100 -0
  143. package/src/components/WalkWizard.tsx +150 -0
  144. package/src/components/WelcomePlayground.stories.tsx +12 -0
  145. package/src/components/WelcomePlayground.test.tsx +47 -0
  146. package/src/components/WelcomePlayground.tsx +142 -0
  147. package/src/components/__snapshots__/stories.test.tsx.snap +2238 -0
  148. package/src/components/diff-content-view-layout.test.ts +198 -0
  149. package/src/components/diff-content-view-layout.ts +84 -0
  150. package/src/components/editor-grid.test.ts +86 -0
  151. package/src/components/editor-grid.ts +138 -0
  152. package/src/components/file-tree.test.ts +60 -0
  153. package/src/components/file-tree.ts +112 -0
  154. package/src/components/menu-control.stories.tsx +35 -0
  155. package/src/components/menu-control.tsx +43 -0
  156. package/src/components/pages/Workspace.stories.tsx +181 -0
  157. package/src/components/primitives/Button.stories.tsx +32 -0
  158. package/src/components/primitives/Button.tsx +56 -0
  159. package/src/components/primitives/Card.stories.tsx +25 -0
  160. package/src/components/primitives/Card.tsx +63 -0
  161. package/src/components/primitives/Dialog.stories.tsx +24 -0
  162. package/src/components/primitives/Dialog.test.tsx +37 -0
  163. package/src/components/primitives/Dialog.tsx +71 -0
  164. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  165. package/src/components/primitives/DialogActions.tsx +39 -0
  166. package/src/components/primitives/IconButton.stories.tsx +36 -0
  167. package/src/components/primitives/IconButton.tsx +62 -0
  168. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  169. package/src/components/primitives/StatusBar.tsx +23 -0
  170. package/src/components/primitives/Switch.stories.tsx +17 -0
  171. package/src/components/primitives/Switch.tsx +32 -0
  172. package/src/components/primitives/Tabs.stories.tsx +29 -0
  173. package/src/components/primitives/Tabs.tsx +102 -0
  174. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  175. package/src/components/primitives/Toolbar.tsx +14 -0
  176. package/src/components/primitives/Tree.stories.tsx +124 -0
  177. package/src/components/primitives/Tree.tsx +100 -0
  178. package/src/components/primitives/frame.test.ts +12 -0
  179. package/src/components/primitives/frame.ts +17 -0
  180. package/src/components/primitives/icons.ts +61 -0
  181. package/src/components/primitives/tree-model.test.ts +99 -0
  182. package/src/components/primitives/tree-model.ts +137 -0
  183. package/src/components/quick-actions-editor.stories.tsx +58 -0
  184. package/src/components/quick-actions-editor.test.tsx +117 -0
  185. package/src/components/quick-actions-editor.tsx +226 -0
  186. package/src/components/share-dialog-store.ts +58 -0
  187. package/src/components/stories-app.tsx +193 -0
  188. package/src/components/stories.test.tsx +74 -0
  189. package/src/components/story-fixtures.ts +102 -0
  190. package/src/components/story-tree.test.ts +108 -0
  191. package/src/components/story.ts +159 -0
  192. package/src/components/syntax-highlight.ts +133 -0
  193. package/src/components/terminal-pane.test.tsx +40 -0
  194. package/src/components/terminal-pane.ts +199 -0
  195. package/src/components/theme-context.stories.tsx +46 -0
  196. package/src/components/theme-context.tsx +37 -0
  197. package/src/components/thread-tree.test.ts +134 -0
  198. package/src/components/thread-tree.ts +109 -0
  199. package/src/components/truncate-text.ts +7 -0
  200. package/src/components/truncate-title.test.ts +31 -0
  201. package/src/components/truncate-title.ts +15 -0
  202. package/src/config-identity.test.ts +78 -0
  203. package/src/config.test.ts +517 -33
  204. package/src/config.ts +467 -57
  205. package/src/diff-fold.test.ts +78 -0
  206. package/src/diff-fold.ts +126 -0
  207. package/src/diff-hunk-curate.test.ts +315 -0
  208. package/src/diff-hunk-curate.ts +153 -0
  209. package/src/diff-intraline.test.ts +238 -0
  210. package/src/diff-intraline.ts +236 -0
  211. package/src/diff-syntax.test.ts +112 -0
  212. package/src/diff-syntax.ts +179 -0
  213. package/src/discussions.ts +79 -0
  214. package/src/editor.test.ts +149 -0
  215. package/src/editor.ts +139 -9
  216. package/src/ghostty-terminal.test.ts +85 -0
  217. package/src/ghostty-terminal.ts +199 -0
  218. package/src/github-identity.test.ts +58 -0
  219. package/src/github-identity.ts +41 -0
  220. package/src/herdr.test.ts +9 -34
  221. package/src/herdr.ts +25 -21
  222. package/src/index.ts +13 -0
  223. package/src/integrations.test.ts +41 -0
  224. package/src/integrations.ts +32 -0
  225. package/src/intent-dispatch.test.ts +723 -0
  226. package/src/intent-dispatch.ts +692 -0
  227. package/src/key-bindings.test.ts +156 -0
  228. package/src/key-bindings.ts +459 -0
  229. package/src/keymap.test.ts +461 -113
  230. package/src/keymap.ts +321 -79
  231. package/src/kitty-image.test.ts +66 -0
  232. package/src/kitty-image.ts +138 -0
  233. package/src/launch-layout.test.ts +52 -0
  234. package/src/launch-layout.ts +42 -0
  235. package/src/mark-runs.ts +121 -0
  236. package/src/markdown-editor-syntax-style.ts +50 -0
  237. package/src/markdown-grid.test.ts +101 -0
  238. package/src/markdown-grid.ts +203 -0
  239. package/src/markdown-highlight.test.ts +61 -0
  240. package/src/markdown-highlight.ts +122 -0
  241. package/src/pasted-image.test.ts +67 -0
  242. package/src/pasted-image.ts +60 -0
  243. package/src/perf/otlp-trace.test.ts +116 -0
  244. package/src/perf/otlp-trace.ts +130 -0
  245. package/src/perf/perf-report.ts +17 -0
  246. package/src/perf/perf-timings.test.ts +18 -0
  247. package/src/perf/perf-timings.ts +38 -0
  248. package/src/prototype-browser.test.ts +64 -0
  249. package/src/prototype-browser.ts +329 -0
  250. package/src/prototype-pixels.tsx +35 -0
  251. package/src/pty.test.ts +81 -0
  252. package/src/pty.ts +245 -0
  253. package/src/ready-signal.test.ts +85 -0
  254. package/src/ready-signal.ts +52 -0
  255. package/src/run.ts +78 -13
  256. package/src/serve.test.ts +24 -12
  257. package/src/serve.ts +52 -10
  258. package/src/share.ts +237 -0
  259. package/src/skills.test.ts +74 -0
  260. package/src/skills.ts +75 -0
  261. package/src/slash-palette.test.ts +103 -0
  262. package/src/slash-palette.ts +129 -0
  263. package/src/split-diff.test.ts +110 -0
  264. package/src/split-diff.ts +149 -0
  265. package/src/test-support.test.ts +264 -0
  266. package/src/test-support.ts +369 -0
  267. package/src/theme-presets.test.ts +51 -0
  268. package/src/theme-presets.ts +214 -0
  269. package/src/theme.ts +84 -19
  270. package/src/thread-chords.test.ts +79 -0
  271. package/src/thread-chords.ts +163 -0
  272. package/src/thread-controller.curate.test.ts +450 -0
  273. package/src/thread-controller.derivation.test.ts +62 -0
  274. package/src/thread-controller.live-diff.test.ts +322 -0
  275. package/src/thread-controller.optimistic.test.ts +69 -0
  276. package/src/thread-controller.share.test.ts +561 -0
  277. package/src/thread-controller.switch-cache.test.ts +176 -0
  278. package/src/thread-controller.tree.test.ts +261 -0
  279. package/src/thread-controller.ts +1893 -0
  280. package/src/thread-selection.test.ts +210 -0
  281. package/src/thread-selection.ts +243 -0
  282. package/src/tree-view.test.ts +128 -0
  283. package/src/tree-view.ts +148 -0
  284. package/src/use-annotation-surface.tsx +915 -0
  285. package/src/use-changes-workbench.ts +190 -0
  286. package/src/use-frame-measure.ts +41 -0
  287. package/src/use-remember-layout.ts +31 -0
  288. package/src/use-settings-dialog.tsx +320 -0
  289. package/src/use-terminal-virtualizer.ts +233 -0
  290. package/src/use-thread-body-editing.tsx +58 -0
  291. package/src/version.ts +5 -0
  292. package/src/view-diff.test.ts +137 -9
  293. package/src/view-diff.ts +269 -23
  294. package/src/view-plan.regression.test.ts +60 -0
  295. package/src/view-plan.test.ts +456 -0
  296. package/src/view-plan.ts +719 -0
  297. package/src/visual/contrast.test.ts +35 -0
  298. package/src/visual/contrast.ts +33 -0
  299. package/src/walk.test.ts +121 -0
  300. package/src/walk.ts +74 -0
  301. package/src/workbench-snapshot.test.ts +81 -0
  302. package/src/workbench-snapshot.ts +29 -0
  303. package/src/session-controller.ts +0 -345
  304. package/src/syntax.test.ts +0 -41
  305. package/src/syntax.ts +0 -88
  306. package/src/view.test.ts +0 -157
  307. package/src/view.ts +0 -312
@@ -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,320 @@
1
+ import React, { useState, type Dispatch, type SetStateAction } from "react";
2
+ import {
3
+ DEFAULT_QUICK_ACTIONS,
4
+ persistActions,
5
+ persistAutoClose,
6
+ persistDiffView,
7
+ persistTheme,
8
+ type AutoClose,
9
+ type DiffViewMode,
10
+ type QuickAction,
11
+ } from "./config";
12
+ import {
13
+ composeTheme,
14
+ THEME_LABELS,
15
+ THEME_NAMES,
16
+ type Appearance,
17
+ type ThemeName,
18
+ } from "./theme-presets";
19
+ import type { Theme } from "./theme";
20
+ import type { SettingsCategory } from "./components/SettingsDialog";
21
+ import { QuickActionsEditor } from "./components/quick-actions-editor";
22
+
23
+ export interface SettingsNav {
24
+ categoryId: string;
25
+ rowIndex: number;
26
+ zone: "nav" | "body";
27
+ }
28
+
29
+ export interface SettingsDialogModel {
30
+ settingsNav: SettingsNav;
31
+ actionsExpandedIndex: number | null;
32
+ setActionsExpandedIndex: Dispatch<SetStateAction<number | null>>;
33
+ settingsCategories: SettingsCategory[];
34
+ settingsValues: Record<string, string>;
35
+ cycleSetting: (rowKey: string) => void;
36
+ handleSettingsKey: (name: string) => void;
37
+ onCategorySelect: (categoryId: string) => void;
38
+ /** Open the settings dialog focused on the left nav, from a fresh state. */
39
+ openSettings: () => void;
40
+ }
41
+
42
+ const DOWN_KEYS = new Set(["j", "down"]);
43
+ const UP_KEYS = new Set(["k", "up"]);
44
+ const ENTER_BODY_KEYS = new Set(["l", "right", "tab", "return"]);
45
+ const BACK_TO_NAV_KEYS = new Set(["h", "left", "tab"]);
46
+ const ACTIVATE_KEYS = new Set(["return", "space", "l"]);
47
+
48
+ function moveNavZone(
49
+ name: string,
50
+ categoryIndex: number,
51
+ categories: SettingsCategory[],
52
+ ): SettingsNav | null {
53
+ if (DOWN_KEYS.has(name))
54
+ return {
55
+ categoryId: categories[Math.min(categories.length - 1, categoryIndex + 1)]!.id,
56
+ rowIndex: 0,
57
+ zone: "nav",
58
+ };
59
+ if (UP_KEYS.has(name))
60
+ return { categoryId: categories[Math.max(0, categoryIndex - 1)]!.id, rowIndex: 0, zone: "nav" };
61
+ if (ENTER_BODY_KEYS.has(name))
62
+ return { categoryId: categories[categoryIndex]!.id, rowIndex: 0, zone: "body" };
63
+
64
+ return null;
65
+ }
66
+
67
+ function moveBodyRow(name: string, nav: SettingsNav, rowCount: number): SettingsNav | null {
68
+ if (DOWN_KEYS.has(name)) return { ...nav, rowIndex: Math.min(rowCount - 1, nav.rowIndex + 1) };
69
+ if (UP_KEYS.has(name)) return { ...nav, rowIndex: Math.max(0, nav.rowIndex - 1) };
70
+ if (BACK_TO_NAV_KEYS.has(name)) return { ...nav, zone: "nav" };
71
+
72
+ return null;
73
+ }
74
+
75
+ export function useSettingsDialog(params: {
76
+ theme: Theme;
77
+ appearance: Appearance;
78
+ autoClose: AutoClose;
79
+ setAutoClose: Dispatch<SetStateAction<AutoClose>>;
80
+ diffView: DiffViewMode;
81
+ setDiffView: Dispatch<SetStateAction<DiffViewMode>>;
82
+ themeName: ThemeName;
83
+ setThemeName: Dispatch<SetStateAction<ThemeName>>;
84
+ themeOverrides: Partial<Theme>;
85
+ setTheme: Dispatch<SetStateAction<Theme>>;
86
+ quickActions: QuickAction[];
87
+ setQuickActions: Dispatch<SetStateAction<QuickAction[]>>;
88
+ setMenuDialog: Dispatch<SetStateAction<"keybinds" | "settings" | null>>;
89
+ identityName?: string;
90
+ identityProvider: "typed" | "github";
91
+ onSyncGithubIdentity: () => void;
92
+ onRenameDisplayName: () => void;
93
+ }): SettingsDialogModel {
94
+ const {
95
+ theme,
96
+ appearance,
97
+ autoClose,
98
+ setAutoClose,
99
+ diffView,
100
+ setDiffView,
101
+ themeName,
102
+ setThemeName,
103
+ themeOverrides,
104
+ setTheme,
105
+ quickActions,
106
+ setQuickActions,
107
+ setMenuDialog,
108
+ identityName,
109
+ identityProvider,
110
+ onSyncGithubIdentity,
111
+ onRenameDisplayName,
112
+ } = params;
113
+
114
+ // open focused on the left nav, so up/down browses categories until l/tab/enter enters the body
115
+ const [settingsNav, setSettingsNav] = useState<SettingsNav>({
116
+ categoryId: "general",
117
+ rowIndex: 0,
118
+ zone: "nav",
119
+ });
120
+ const [actionsExpandedIndex, setActionsExpandedIndex] = useState<number | null>(null);
121
+ const [actionsExpandedField, setActionsExpandedField] = useState<"prompt" | "metadata">("prompt");
122
+
123
+ const commitActions = (next: QuickAction[]): void => {
124
+ setQuickActions(next);
125
+ persistActions(next);
126
+ };
127
+ const editActionPrompt = (index: number, prompt: string): void => {
128
+ // a blank title fails the config schema and would vanish on reload, so never persist one
129
+ if (prompt.trim().length === 0) return;
130
+ commitActions(
131
+ quickActions.map((action, actionIndex) =>
132
+ actionIndex === index ? { ...action, prompt } : action,
133
+ ),
134
+ );
135
+ };
136
+ const editActionMetadata = (index: number, metadata: string): void =>
137
+ commitActions(
138
+ quickActions.map((action, actionIndex) =>
139
+ actionIndex === index
140
+ ? { ...action, metadata: metadata.trim() ? metadata : undefined }
141
+ : action,
142
+ ),
143
+ );
144
+ const resetActions = (): void => {
145
+ setActionsExpandedIndex(null);
146
+ commitActions(DEFAULT_QUICK_ACTIONS.map((action) => ({ ...action })));
147
+ };
148
+ const addAction = (): void => {
149
+ setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: quickActions.length }));
150
+ commitActions([...quickActions, { prompt: "New action" }]);
151
+ };
152
+
153
+ const settingsCategories: SettingsCategory[] = [
154
+ {
155
+ id: "general",
156
+ name: "General",
157
+ rows: [
158
+ {
159
+ key: "autoClose",
160
+ label: "Auto-close on submit",
161
+ kind: "cycle",
162
+ options: ["off", "3s", "10s"],
163
+ },
164
+ {
165
+ key: "diffView",
166
+ label: "Diff view",
167
+ kind: "cycle",
168
+ options: ["Split", "Stacked"],
169
+ },
170
+ ],
171
+ },
172
+ {
173
+ id: "account",
174
+ name: "Account",
175
+ rows: [
176
+ { key: "displayName", label: "Display name", kind: "text" },
177
+ { key: "identitySource", label: "Source", kind: "text" },
178
+ { key: "syncGithub", label: "Sync from GitHub", kind: "text" },
179
+ ],
180
+ },
181
+ {
182
+ id: "appearance",
183
+ name: "Appearance",
184
+ rows: [
185
+ {
186
+ key: "theme",
187
+ label: "Theme",
188
+ kind: "cycle",
189
+ options: THEME_NAMES.map((name) => THEME_LABELS[name]),
190
+ },
191
+ ],
192
+ },
193
+ {
194
+ id: "actions",
195
+ name: "Actions",
196
+ rows: [],
197
+ customBody: (
198
+ <QuickActionsEditor
199
+ actions={quickActions}
200
+ selectedIndex={settingsNav.categoryId === "actions" ? settingsNav.rowIndex : -1}
201
+ expandedIndex={actionsExpandedIndex}
202
+ expandedField={actionsExpandedField}
203
+ onToggleExpand={(index, field) => {
204
+ setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: index }));
205
+ setActionsExpandedField(field);
206
+ setActionsExpandedIndex((current) => (current === index ? null : index));
207
+ }}
208
+ onEditPrompt={editActionPrompt}
209
+ onEditMetadata={editActionMetadata}
210
+ onReset={resetActions}
211
+ onAdd={addAction}
212
+ theme={theme}
213
+ />
214
+ ),
215
+ },
216
+ {
217
+ id: "keybinds",
218
+ name: "Keybinds",
219
+ rows: [],
220
+ },
221
+ ];
222
+ const settingsValues = {
223
+ autoClose: autoClose === "off" ? "off" : `${autoClose}s`,
224
+ diffView: diffView === "split" ? "Split" : "Stacked",
225
+ theme: THEME_LABELS[themeName],
226
+ displayName: identityName ?? "-- not set --",
227
+ identitySource: identityProvider === "github" ? "GitHub" : "typed",
228
+ syncGithub: "enter to sync",
229
+ };
230
+ const cycleSetting = (rowKey: string): void => {
231
+ if (rowKey === "autoClose") {
232
+ const next: AutoClose = autoClose === "off" ? 3 : autoClose === 3 ? 10 : "off";
233
+
234
+ setAutoClose(next);
235
+ persistAutoClose(next);
236
+ } else if (rowKey === "diffView") {
237
+ const next: DiffViewMode = diffView === "stacked" ? "split" : "stacked";
238
+
239
+ setDiffView(next);
240
+ persistDiffView(next);
241
+ } else if (rowKey === "theme") {
242
+ const next = THEME_NAMES[(THEME_NAMES.indexOf(themeName) + 1) % THEME_NAMES.length]!;
243
+
244
+ setThemeName(next);
245
+ setTheme(composeTheme(next, themeOverrides, appearance));
246
+ persistTheme(next);
247
+ } else if (rowKey === "syncGithub") {
248
+ onSyncGithubIdentity();
249
+ } else if (rowKey === "displayName") {
250
+ onRenameDisplayName();
251
+ }
252
+ };
253
+
254
+ const openSettings = (): void => {
255
+ setSettingsNav({ categoryId: "general", rowIndex: 0, zone: "nav" });
256
+ setActionsExpandedIndex(null);
257
+ setMenuDialog("settings");
258
+ };
259
+
260
+ const onCategorySelect = (categoryId: string): void => {
261
+ // ignore a select that names no real category, so the key handler never dereferences nothing
262
+ if (!settingsCategories.some((category) => category.id === categoryId)) return;
263
+ setActionsExpandedIndex(null);
264
+ setSettingsNav({ categoryId, rowIndex: 0, zone: "body" });
265
+ };
266
+
267
+ const handleSettingsKey = (name: string): void => {
268
+ // an open system-prompt input owns typing; only esc (close it) escapes here
269
+ if (actionsExpandedIndex !== null) {
270
+ if (name === "escape") setActionsExpandedIndex(null);
271
+
272
+ return;
273
+ }
274
+ if (name === "escape") return void setMenuDialog(null);
275
+ const categoryIndex = settingsCategories.findIndex(
276
+ (category) => category.id === settingsNav.categoryId,
277
+ );
278
+ const category = settingsCategories[categoryIndex]!;
279
+
280
+ if (settingsNav.zone === "nav") {
281
+ const next = moveNavZone(name, categoryIndex, settingsCategories);
282
+
283
+ if (next) setSettingsNav(next);
284
+
285
+ return;
286
+ }
287
+ // the Actions category is a list of quick actions plus a trailing "add" row
288
+ if (category.id === "actions") {
289
+ const moved = moveBodyRow(name, settingsNav, quickActions.length + 1);
290
+
291
+ if (moved) return void setSettingsNav(moved);
292
+ if (ACTIVATE_KEYS.has(name)) {
293
+ if (settingsNav.rowIndex === quickActions.length) addAction();
294
+ else {
295
+ setActionsExpandedField("prompt");
296
+ setActionsExpandedIndex(settingsNav.rowIndex);
297
+ }
298
+ }
299
+
300
+ return;
301
+ }
302
+ const moved = moveBodyRow(name, settingsNav, category.rows.length);
303
+
304
+ if (moved) return void setSettingsNav(moved);
305
+ if (name === "return" || name === "space")
306
+ cycleSetting(category.rows[settingsNav.rowIndex]!.key);
307
+ };
308
+
309
+ return {
310
+ settingsNav,
311
+ actionsExpandedIndex,
312
+ setActionsExpandedIndex,
313
+ settingsCategories,
314
+ settingsValues,
315
+ cycleSetting,
316
+ handleSettingsKey,
317
+ onCategorySelect,
318
+ openSettings,
319
+ };
320
+ }