@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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Scene visual regression: clicking a sidebar thread paints its title in the theme's accent, in both
|
|
2
|
+
* themes. Colours come from captureSpans, so a theme or click-feedback regression fails here. */
|
|
3
|
+
|
|
4
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { App } from "./App";
|
|
7
|
+
import { clickText, isolateUserConfig, renderReadyApp, waitForText } from "./test-support";
|
|
8
|
+
import { themeForName } from "./theme-presets";
|
|
9
|
+
import { rgbColorFromHex } from "./visual/contrast";
|
|
10
|
+
import { createTestReviewHome, type TestReviewHome } from "../../../test/helpers/review-home";
|
|
11
|
+
|
|
12
|
+
let reviewHome: TestReviewHome;
|
|
13
|
+
let restoreUserConfig: () => void;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
reviewHome = createTestReviewHome();
|
|
17
|
+
restoreUserConfig = isolateUserConfig(reviewHome.home);
|
|
18
|
+
reviewHome.createPlanSession("# First review\n", "First review");
|
|
19
|
+
reviewHome.createPlanSession("# Second review\n", "Second review");
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
restoreUserConfig();
|
|
23
|
+
reviewHome.cleanup();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** Whether any span containing `needle` is painted in `color` (the thread header carries the title dimly). */
|
|
27
|
+
function hasSpanColor(
|
|
28
|
+
setup: Awaited<ReturnType<typeof renderReadyApp>>,
|
|
29
|
+
needle: string,
|
|
30
|
+
color: readonly [number, number, number],
|
|
31
|
+
): boolean {
|
|
32
|
+
for (const line of setup.captureSpans().lines) {
|
|
33
|
+
for (const span of line.spans) {
|
|
34
|
+
if (!span.text.includes(needle) || !span.fg) continue;
|
|
35
|
+
const [red, green, blue] = span.fg.toInts();
|
|
36
|
+
if (red === color[0] && green === color[1] && blue === color[2]) return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const mode of ["dark", "light"] as const) {
|
|
44
|
+
test(`clicking a thread paints it in the ${mode} accent`, async () => {
|
|
45
|
+
const accent = rgbColorFromHex(themeForName("cueloop", mode).accent);
|
|
46
|
+
const setup = await renderReadyApp(<App home={reviewHome.home} appearance={mode} />, {
|
|
47
|
+
width: 120,
|
|
48
|
+
height: 32,
|
|
49
|
+
});
|
|
50
|
+
await waitForText(setup, "Second review");
|
|
51
|
+
|
|
52
|
+
await clickText(setup, "Second review");
|
|
53
|
+
|
|
54
|
+
expect(hasSpanColor(setup, "Second review", accent)).toBe(true);
|
|
55
|
+
setup.renderer.destroy();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** The four-pane workbench in the virtual terminal, mirroring the browser prototype's interactions:
|
|
2
2
|
* a diff session lays out Threads | Thread | Changes editor | Project sidebar; the diff (±) and tree
|
|
3
3
|
* toggles are mutually exclusive and switching to tree keeps the Changes editor open; a project-tree
|
|
4
|
-
* file opens as a read-only contents tab; the split control offers four directions and
|
|
5
|
-
*
|
|
4
|
+
* file opens as a read-only contents tab; the split control offers four directions and the arrow keys
|
|
5
|
+
* split; dismissing the Changes tab collapses the Changes pane but keeps the Project sidebar;
|
|
6
6
|
* the right-sidebar toggle collapses the region to a thin rail and reopens it; zoom hides the Thread pane
|
|
7
7
|
* while the sidebars stay. */
|
|
8
8
|
|
|
@@ -152,7 +152,7 @@ describe("the four-pane workbench", () => {
|
|
|
152
152
|
expect(frame.split("\n")[HEADER_ROW]!).toContain("README.md");
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
test("the split control offers four directions and
|
|
155
|
+
test("the split control offers four directions and arrow-right makes two groups", async () => {
|
|
156
156
|
const setup = await renderApp();
|
|
157
157
|
|
|
158
158
|
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
@@ -164,17 +164,16 @@ describe("the four-pane workbench", () => {
|
|
|
164
164
|
// the split control appears only on a file tab
|
|
165
165
|
const split = locateText(setup, "split");
|
|
166
166
|
await setup.mockMouse.click(split.column, split.row);
|
|
167
|
-
await waitForText(setup, "
|
|
167
|
+
await waitForText(setup, "right");
|
|
168
168
|
|
|
169
169
|
const menu = setup.captureCharFrame();
|
|
170
|
-
expect(menu).toContain("
|
|
171
|
-
expect(menu).toContain("
|
|
172
|
-
expect(menu).toContain("
|
|
173
|
-
expect(menu).toContain("
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await setup
|
|
177
|
-
// two editor groups now ride the header, each with its own split control
|
|
170
|
+
expect(menu).toContain("left");
|
|
171
|
+
expect(menu).toContain("right");
|
|
172
|
+
expect(menu).toContain("up");
|
|
173
|
+
expect(menu).toContain("down");
|
|
174
|
+
|
|
175
|
+
// the arrow keys drive the popover: right splits the group into two
|
|
176
|
+
await pressKey(setup, "ARROW_RIGHT");
|
|
178
177
|
await waitForState(setup, () => setup.captureCharFrame().split("split").length - 1 >= 2);
|
|
179
178
|
expect((setup.captureCharFrame().match(/README\.md/g) ?? []).length).toBeGreaterThanOrEqual(2);
|
|
180
179
|
});
|
|
@@ -239,11 +238,11 @@ describe("the four-pane workbench", () => {
|
|
|
239
238
|
|
|
240
239
|
const zoom = locateText(setup, NERD.zoom);
|
|
241
240
|
await setup.mockMouse.click(zoom.column, zoom.row);
|
|
242
|
-
await waitForState(setup, () => !setup.captureCharFrame().includes("
|
|
241
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("Select a thread"));
|
|
243
242
|
|
|
244
243
|
const frame = setup.captureCharFrame();
|
|
245
244
|
// the Thread pane's prompt is gone...
|
|
246
|
-
expect(frame).not.toContain("
|
|
245
|
+
expect(frame).not.toContain("Select a thread");
|
|
247
246
|
// ...while the Changes editor and Project sidebar stay
|
|
248
247
|
expect(frame).toContain("store.ts");
|
|
249
248
|
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
@@ -293,7 +292,7 @@ describe("the four-pane workbench", () => {
|
|
|
293
292
|
// zoom the diff - the Thread pane hides and the footer rides the Changes pane
|
|
294
293
|
const zoom = locateText(setup, NERD.zoom);
|
|
295
294
|
await setup.mockMouse.click(zoom.column, zoom.row);
|
|
296
|
-
await waitForState(setup, () => !setup.captureCharFrame().includes("
|
|
295
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("Select a thread"));
|
|
297
296
|
|
|
298
297
|
// switch to the plan thread: it has no Changes editor, so a stranded zoom would blank the middle
|
|
299
298
|
const planRow = locateText(setup, "Zoomed Plan");
|
|
@@ -312,13 +311,13 @@ describe("the four-pane workbench", () => {
|
|
|
312
311
|
// zoom the Changes editor - the Thread pane hides
|
|
313
312
|
const zoom = locateText(setup, NERD.zoom);
|
|
314
313
|
await setup.mockMouse.click(zoom.column, zoom.row);
|
|
315
|
-
await waitForState(setup, () => !setup.captureCharFrame().includes("
|
|
314
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("Select a thread"));
|
|
316
315
|
|
|
317
316
|
// toggling the right sidebar off must not strand a blank screen: zoom exits, the Thread pane returns
|
|
318
317
|
await setup.mockMouse.click(rightToggleColumn(setup), HEADER_ROW);
|
|
319
|
-
await waitForText(setup, "
|
|
318
|
+
await waitForText(setup, "Select a thread");
|
|
320
319
|
const frame = setup.captureCharFrame();
|
|
321
|
-
expect(frame).toContain("
|
|
320
|
+
expect(frame).toContain("Select a thread");
|
|
322
321
|
// the right region is collapsed and the Changes editor is gone
|
|
323
322
|
expect(frame).not.toContain("store.ts");
|
|
324
323
|
expect(diffToggleColumn(setup)).toBe(-1);
|
|
@@ -452,8 +451,8 @@ describe("the bare-launch welcome shell", () => {
|
|
|
452
451
|
/>,
|
|
453
452
|
{ width: 180, height: 14 },
|
|
454
453
|
);
|
|
455
|
-
await waitForText(setup, "
|
|
456
|
-
expect(setup.captureCharFrame()).toContain("
|
|
454
|
+
await waitForText(setup, "Select a thread");
|
|
455
|
+
expect(setup.captureCharFrame()).toContain("Select a thread");
|
|
457
456
|
});
|
|
458
457
|
|
|
459
458
|
test("closing the right region on the vanilla shell remembers it for the next launch", async () => {
|
|
@@ -507,7 +506,27 @@ describe("the bare-launch welcome shell", () => {
|
|
|
507
506
|
expect(frame.split("\n")[HEADER_ROW]!).toContain("Welcome");
|
|
508
507
|
expect(frame).toContain("Getting started");
|
|
509
508
|
// the Thread pane shows its empty state until a thread is opened
|
|
510
|
-
expect(frame).toContain("
|
|
509
|
+
expect(frame).toContain("Select a thread");
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("the Threads sidebar owns the keyboard until a pane is clicked, so nav never touches the editor", async () => {
|
|
513
|
+
const setup = await renderReadyApp(
|
|
514
|
+
<App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
|
|
515
|
+
{ width: 160, height: 24 },
|
|
516
|
+
);
|
|
517
|
+
await waitForText(setup, "Getting started");
|
|
518
|
+
|
|
519
|
+
// focus starts on the Threads sidebar: a letter drives the inbox, it never drafts in the editor
|
|
520
|
+
await typeText(setup, "zzz");
|
|
521
|
+
expect(setup.captureCharFrame()).not.toContain("zzz");
|
|
522
|
+
|
|
523
|
+
// clicking a line in the Welcome editor focuses that pane; now typing composes there
|
|
524
|
+
const line = locateText(setup, "quick brown fox");
|
|
525
|
+
await setup.mockMouse.click(line.column, line.row);
|
|
526
|
+
await typeText(setup, "note");
|
|
527
|
+
await waitForText(setup, "● note");
|
|
528
|
+
|
|
529
|
+
setup.renderer.destroy();
|
|
511
530
|
});
|
|
512
531
|
|
|
513
532
|
test("typing in the welcome composer never reaches the inbox keys", async () => {
|
|
@@ -552,7 +571,7 @@ describe("the bare-launch welcome shell", () => {
|
|
|
552
571
|
// the Threads sidebar and the Thread empty state remain - never a blank shell
|
|
553
572
|
const frame = setup.captureCharFrame();
|
|
554
573
|
expect(frame).toContain("cueloop");
|
|
555
|
-
expect(frame).toContain("
|
|
574
|
+
expect(frame).toContain("Select a thread");
|
|
556
575
|
});
|
|
557
576
|
|
|
558
577
|
test("the changed-files and tree toggles switch navigator mode without collapsing", async () => {
|
package/src/app-screens.tsx
CHANGED
|
@@ -21,8 +21,9 @@ import { SettingsDialog } from "./components/SettingsDialog";
|
|
|
21
21
|
import { CompletionOverlay } from "./components/CompletionOverlay";
|
|
22
22
|
import { ThreadTree } from "./components/ThreadTree";
|
|
23
23
|
import { WelcomePlayground } from "./components/WelcomePlayground";
|
|
24
|
-
import { AppShell, type ProjectPanelMode } from "./components/AppShell";
|
|
24
|
+
import { AppShell, type FocusPane, type ProjectPanelMode } from "./components/AppShell";
|
|
25
25
|
import { EditorGrid } from "./components/EditorGrid";
|
|
26
|
+
import { MenuControlProvider, type MenuControlApi } from "./components/menu-control";
|
|
26
27
|
import { ProjectTreeView } from "./components/ProjectTreeView";
|
|
27
28
|
import { ChangesFileTree } from "./components/ChangesColumn";
|
|
28
29
|
import { BareWorkbenchFileView, draftThread } from "./components/BareWorkbenchFileView";
|
|
@@ -58,6 +59,7 @@ export function MenuChrome(props: {
|
|
|
58
59
|
settingsNav: SettingsNav;
|
|
59
60
|
onCategorySelect: (categoryId: string) => void;
|
|
60
61
|
cycleSetting: (rowKey: string) => void;
|
|
62
|
+
onClose: () => void;
|
|
61
63
|
}): React.ReactNode {
|
|
62
64
|
const {
|
|
63
65
|
menuDialog,
|
|
@@ -68,6 +70,7 @@ export function MenuChrome(props: {
|
|
|
68
70
|
settingsNav,
|
|
69
71
|
onCategorySelect,
|
|
70
72
|
cycleSetting,
|
|
73
|
+
onClose,
|
|
71
74
|
} = props;
|
|
72
75
|
|
|
73
76
|
// the gear opens the settings dialog directly; Keybinds is a leaf in its tree nav
|
|
@@ -85,6 +88,7 @@ export function MenuChrome(props: {
|
|
|
85
88
|
activeZone={settingsNav.zone}
|
|
86
89
|
onCategorySelect={onCategorySelect}
|
|
87
90
|
onRowActivate={(row) => cycleSetting(row.key)}
|
|
91
|
+
onClose={onClose}
|
|
88
92
|
theme={theme}
|
|
89
93
|
/>
|
|
90
94
|
);
|
|
@@ -99,6 +103,7 @@ function WelcomeProjectPanel({
|
|
|
99
103
|
controller,
|
|
100
104
|
onOpenChangedFile,
|
|
101
105
|
onOpenProjectFile,
|
|
106
|
+
focused,
|
|
102
107
|
theme,
|
|
103
108
|
}: {
|
|
104
109
|
mode: ProjectPanelMode;
|
|
@@ -107,6 +112,7 @@ function WelcomeProjectPanel({
|
|
|
107
112
|
onOpenChangedFile: (path: string) => void;
|
|
108
113
|
/** Project tree click -> open the file's read-only contents. */
|
|
109
114
|
onOpenProjectFile: (path: string) => void;
|
|
115
|
+
focused?: boolean;
|
|
110
116
|
theme: Theme;
|
|
111
117
|
}): React.ReactNode {
|
|
112
118
|
const [changes, setChanges] = useState<readonly DiffFileContents[]>([]);
|
|
@@ -129,13 +135,21 @@ function WelcomeProjectPanel({
|
|
|
129
135
|
}, [controller]);
|
|
130
136
|
|
|
131
137
|
if (mode === "changes") {
|
|
132
|
-
return
|
|
138
|
+
return (
|
|
139
|
+
<ChangesFileTree
|
|
140
|
+
files={changes}
|
|
141
|
+
onSelectFile={onOpenChangedFile}
|
|
142
|
+
focused={focused}
|
|
143
|
+
theme={theme}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
return (
|
|
136
149
|
<ProjectTreeView
|
|
137
150
|
loadFiles={() => controller.repoFiles()}
|
|
138
151
|
onSelectFile={onOpenProjectFile}
|
|
152
|
+
focused={focused}
|
|
139
153
|
theme={theme}
|
|
140
154
|
/>
|
|
141
155
|
);
|
|
@@ -150,6 +164,8 @@ function WelcomeProjectPanel({
|
|
|
150
164
|
export function NoThreadShell(props: {
|
|
151
165
|
rows: InboxRow[];
|
|
152
166
|
inboxCursor: number;
|
|
167
|
+
/** Open a sidebar thread and move the cursor onto it, so the row shows its selected backdrop at once. */
|
|
168
|
+
onOpenThread: (id: string) => void;
|
|
153
169
|
mode: Mode;
|
|
154
170
|
theme: Theme;
|
|
155
171
|
controller: ReviewController;
|
|
@@ -159,6 +175,10 @@ export function NoThreadShell(props: {
|
|
|
159
175
|
/** Shared with the thread view, so picking a thread preserves the sidebar. */
|
|
160
176
|
sidebarOpen: boolean;
|
|
161
177
|
onToggleSidebar: () => void;
|
|
178
|
+
focusedPane: FocusPane;
|
|
179
|
+
onFocusPane: (pane: FocusPane) => void;
|
|
180
|
+
/** App owns the one open-menu id, so the bare shell's menus share App's modal keyboard handling. */
|
|
181
|
+
menuControl: MenuControlApi;
|
|
162
182
|
pinnedIds: ReadonlySet<string>;
|
|
163
183
|
onPin: (id: string) => void;
|
|
164
184
|
onRename: (id: string, title: string) => void;
|
|
@@ -172,6 +192,7 @@ export function NoThreadShell(props: {
|
|
|
172
192
|
const {
|
|
173
193
|
rows,
|
|
174
194
|
inboxCursor,
|
|
195
|
+
onOpenThread,
|
|
175
196
|
mode,
|
|
176
197
|
theme,
|
|
177
198
|
controller,
|
|
@@ -180,6 +201,9 @@ export function NoThreadShell(props: {
|
|
|
180
201
|
onOpenMenu,
|
|
181
202
|
sidebarOpen,
|
|
182
203
|
onToggleSidebar,
|
|
204
|
+
focusedPane,
|
|
205
|
+
onFocusPane,
|
|
206
|
+
menuControl,
|
|
183
207
|
pinnedIds,
|
|
184
208
|
onPin,
|
|
185
209
|
onRename,
|
|
@@ -217,7 +241,7 @@ export function NoThreadShell(props: {
|
|
|
217
241
|
observer: false,
|
|
218
242
|
commentsEnabled: true,
|
|
219
243
|
resolved: false,
|
|
220
|
-
suspended:
|
|
244
|
+
suspended: focusedPane !== "changes" || menuControl.openMenuId !== null,
|
|
221
245
|
onComposingChange: onWelcomeComposingChange,
|
|
222
246
|
onObserverBlocked: () => {},
|
|
223
247
|
onCursorChange: () => {},
|
|
@@ -238,133 +262,146 @@ export function NoThreadShell(props: {
|
|
|
238
262
|
|
|
239
263
|
return (
|
|
240
264
|
<ThemeProvider theme={theme}>
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
<MenuControlProvider value={menuControl}>
|
|
266
|
+
<AppShell
|
|
267
|
+
sidebarOpen={sidebarOpen}
|
|
268
|
+
onToggleSidebar={onToggleSidebar}
|
|
269
|
+
onOpenMenu={onOpenMenu}
|
|
270
|
+
onFocusPane={onFocusPane}
|
|
271
|
+
threadsPanel={
|
|
272
|
+
<ScrollArea>
|
|
273
|
+
<ThreadTree
|
|
274
|
+
rows={rows}
|
|
275
|
+
cursor={inboxCursor}
|
|
276
|
+
focused={focusedPane === "threads"}
|
|
277
|
+
pinnedIds={pinnedIds}
|
|
278
|
+
width={30}
|
|
279
|
+
onSelect={onOpenThread}
|
|
280
|
+
onRequestDelete={(id, title) =>
|
|
281
|
+
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
282
|
+
}
|
|
283
|
+
onPin={onPin}
|
|
284
|
+
onRename={onRename}
|
|
285
|
+
theme={theme}
|
|
286
|
+
/>
|
|
287
|
+
</ScrollArea>
|
|
288
|
+
}
|
|
289
|
+
threadTitle=""
|
|
290
|
+
threadPanel={
|
|
291
|
+
<box
|
|
292
|
+
style={{
|
|
293
|
+
flexGrow: 1,
|
|
294
|
+
flexDirection: "column",
|
|
295
|
+
alignItems: "center",
|
|
296
|
+
justifyContent: "center",
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
<text fg={theme.textDim}>Select a thread</text>
|
|
300
|
+
</box>
|
|
301
|
+
}
|
|
302
|
+
changesOpen={workbench.changesOpen}
|
|
303
|
+
projectOpen={workbench.projectOpen}
|
|
304
|
+
onToggleChanges={workbench.toggleChanges}
|
|
305
|
+
onToggleProject={workbench.toggleProject}
|
|
306
|
+
onToggleRight={workbench.toggleRight}
|
|
307
|
+
projectMode={workbench.projectMode}
|
|
308
|
+
zoomHideThread={workbench.zoomed}
|
|
309
|
+
changesPanel={
|
|
310
|
+
<EditorGrid
|
|
311
|
+
tree={workbench.grid}
|
|
312
|
+
focusedGroupId={workbench.activeGroup}
|
|
313
|
+
onFocusGroup={workbench.focusGroup}
|
|
314
|
+
onActivateTab={workbench.activate}
|
|
315
|
+
onCloseTab={workbench.close}
|
|
316
|
+
onSplit={workbench.split}
|
|
317
|
+
onZoom={workbench.toggleZoom}
|
|
318
|
+
zoomed={workbench.zoomed}
|
|
319
|
+
renderTab={(tab) =>
|
|
320
|
+
tab.kind === "welcome" ? (
|
|
321
|
+
<WelcomePlayground
|
|
322
|
+
version={CLIENT_VERSION}
|
|
323
|
+
quickActions={quickActions}
|
|
324
|
+
onComposingChange={onWelcomeComposingChange}
|
|
325
|
+
suspended={focusedPane !== "changes" || menuControl.openMenuId !== null}
|
|
326
|
+
theme={theme}
|
|
327
|
+
/>
|
|
328
|
+
) : tab.fileView === "contents" ? (
|
|
329
|
+
<BareWorkbenchFileView
|
|
330
|
+
path={tab.path ?? ""}
|
|
331
|
+
controller={controller}
|
|
332
|
+
quickActions={quickActions}
|
|
333
|
+
onComposingChange={onWelcomeComposingChange}
|
|
334
|
+
onExit={() => {}}
|
|
335
|
+
theme={theme}
|
|
336
|
+
/>
|
|
337
|
+
) : (
|
|
338
|
+
<GridTabContent
|
|
339
|
+
tab={tab}
|
|
340
|
+
rows={controller.rows()}
|
|
341
|
+
surface={bareSurface}
|
|
342
|
+
rejectedRows={EMPTY_ROWS}
|
|
343
|
+
dimmed={false}
|
|
344
|
+
readFile={(path) => controller.repoReadFile(path)}
|
|
345
|
+
onAddFileComment={(path, anchor, body) =>
|
|
346
|
+
void controller.commentOnWorkbench(
|
|
347
|
+
anchor,
|
|
348
|
+
{ kind: "file", path, rev: "worktree" },
|
|
349
|
+
body,
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
theme={theme}
|
|
353
|
+
/>
|
|
354
|
+
)
|
|
255
355
|
}
|
|
256
|
-
onPin={onPin}
|
|
257
|
-
onRename={onRename}
|
|
258
356
|
theme={theme}
|
|
259
357
|
/>
|
|
260
|
-
</ScrollArea>
|
|
261
|
-
}
|
|
262
|
-
threadTitle=""
|
|
263
|
-
threadPanel={
|
|
264
|
-
<box
|
|
265
|
-
style={{
|
|
266
|
-
flexGrow: 1,
|
|
267
|
-
flexDirection: "column",
|
|
268
|
-
alignItems: "center",
|
|
269
|
-
justifyContent: "center",
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
<text fg={theme.textDim}>{rows.length === 0 ? "no threads" : "Select a thread"}</text>
|
|
273
|
-
</box>
|
|
274
|
-
}
|
|
275
|
-
changesOpen={workbench.changesOpen}
|
|
276
|
-
projectOpen={workbench.projectOpen}
|
|
277
|
-
onToggleChanges={workbench.toggleChanges}
|
|
278
|
-
onToggleProject={workbench.toggleProject}
|
|
279
|
-
onToggleRight={workbench.toggleRight}
|
|
280
|
-
projectMode={workbench.projectMode}
|
|
281
|
-
zoomHideThread={workbench.zoomed}
|
|
282
|
-
changesPanel={
|
|
283
|
-
<EditorGrid
|
|
284
|
-
tree={workbench.grid}
|
|
285
|
-
focusedGroupId={workbench.activeGroup}
|
|
286
|
-
onFocusGroup={workbench.focusGroup}
|
|
287
|
-
onActivateTab={workbench.activate}
|
|
288
|
-
onCloseTab={workbench.close}
|
|
289
|
-
onSplit={workbench.split}
|
|
290
|
-
onZoom={workbench.toggleZoom}
|
|
291
|
-
zoomed={workbench.zoomed}
|
|
292
|
-
renderTab={(tab) =>
|
|
293
|
-
tab.kind === "welcome" ? (
|
|
294
|
-
<WelcomePlayground
|
|
295
|
-
version={CLIENT_VERSION}
|
|
296
|
-
quickActions={quickActions}
|
|
297
|
-
onComposingChange={onWelcomeComposingChange}
|
|
298
|
-
theme={theme}
|
|
299
|
-
/>
|
|
300
|
-
) : tab.fileView === "contents" ? (
|
|
301
|
-
<BareWorkbenchFileView
|
|
302
|
-
path={tab.path ?? ""}
|
|
303
|
-
controller={controller}
|
|
304
|
-
quickActions={quickActions}
|
|
305
|
-
onComposingChange={onWelcomeComposingChange}
|
|
306
|
-
onExit={() => {}}
|
|
307
|
-
theme={theme}
|
|
308
|
-
/>
|
|
309
|
-
) : (
|
|
310
|
-
<GridTabContent
|
|
311
|
-
tab={tab}
|
|
312
|
-
rows={controller.rows()}
|
|
313
|
-
surface={bareSurface}
|
|
314
|
-
rejectedRows={EMPTY_ROWS}
|
|
315
|
-
dimmed={false}
|
|
316
|
-
readFile={(path) => controller.repoReadFile(path)}
|
|
317
|
-
onAddFileComment={(path, anchor, body) =>
|
|
318
|
-
void controller.commentOnWorkbench(
|
|
319
|
-
anchor,
|
|
320
|
-
{ kind: "file", path, rev: "worktree" },
|
|
321
|
-
body,
|
|
322
|
-
)
|
|
323
|
-
}
|
|
324
|
-
theme={theme}
|
|
325
|
-
/>
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
theme={theme}
|
|
329
|
-
/>
|
|
330
|
-
}
|
|
331
|
-
projectPanel={
|
|
332
|
-
<WelcomeProjectPanel
|
|
333
|
-
mode={workbench.projectMode}
|
|
334
|
-
controller={controller}
|
|
335
|
-
onOpenChangedFile={openChangedFile}
|
|
336
|
-
onOpenProjectFile={openProjectFile}
|
|
337
|
-
theme={theme}
|
|
338
|
-
/>
|
|
339
|
-
}
|
|
340
|
-
theme={theme}
|
|
341
|
-
>
|
|
342
|
-
{menuChrome}
|
|
343
|
-
<ConfirmDialog
|
|
344
|
-
isOpen={confirming !== null}
|
|
345
|
-
title=" Delete plan "
|
|
346
|
-
message={
|
|
347
|
-
confirming ? `Delete "${confirming.title}"? This removes the plan and its review.` : ""
|
|
348
358
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
359
|
+
projectPanel={
|
|
360
|
+
<WelcomeProjectPanel
|
|
361
|
+
mode={workbench.projectMode}
|
|
362
|
+
controller={controller}
|
|
363
|
+
onOpenChangedFile={openChangedFile}
|
|
364
|
+
onOpenProjectFile={openProjectFile}
|
|
365
|
+
focused={focusedPane === "project"}
|
|
366
|
+
theme={theme}
|
|
367
|
+
/>
|
|
368
|
+
}
|
|
354
369
|
theme={theme}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
<
|
|
358
|
-
isOpen
|
|
359
|
-
title="
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
370
|
+
>
|
|
371
|
+
{menuChrome}
|
|
372
|
+
<ConfirmDialog
|
|
373
|
+
isOpen={confirming !== null}
|
|
374
|
+
title=" Delete plan "
|
|
375
|
+
message={
|
|
376
|
+
confirming
|
|
377
|
+
? `Delete "${confirming.title}"? This removes the plan and its review.`
|
|
378
|
+
: ""
|
|
379
|
+
}
|
|
380
|
+
onConfirm={() => {
|
|
381
|
+
if (confirming) controller.deleteSession(confirming.sessionId);
|
|
382
|
+
setMode({ type: "normal" });
|
|
383
|
+
}}
|
|
384
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
364
385
|
theme={theme}
|
|
365
386
|
/>
|
|
366
|
-
|
|
367
|
-
|
|
387
|
+
{mode.type === "renameThread" ? (
|
|
388
|
+
<PromptDialog
|
|
389
|
+
isOpen
|
|
390
|
+
title=" rename thread "
|
|
391
|
+
label="new title for this thread:"
|
|
392
|
+
value={mode.text}
|
|
393
|
+
placeholder="a short title"
|
|
394
|
+
onInput={(text) => setMode({ ...mode, text })}
|
|
395
|
+
onSave={() => {
|
|
396
|
+
controller.renameSession(mode.sessionId, mode.text.trim());
|
|
397
|
+
setMode({ type: "normal" });
|
|
398
|
+
}}
|
|
399
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
400
|
+
theme={theme}
|
|
401
|
+
/>
|
|
402
|
+
) : null}
|
|
403
|
+
</AppShell>
|
|
404
|
+
</MenuControlProvider>
|
|
368
405
|
</ThemeProvider>
|
|
369
406
|
);
|
|
370
407
|
}
|
|
@@ -464,6 +501,8 @@ export function TrailingOverlays(props: {
|
|
|
464
501
|
value={mode.text}
|
|
465
502
|
placeholder="their name"
|
|
466
503
|
onInput={(text) => setMode({ ...mode, text })}
|
|
504
|
+
onSave={() => dispatch({ type: "confirmDialog" })}
|
|
505
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
467
506
|
theme={theme}
|
|
468
507
|
/>
|
|
469
508
|
) : null}
|
|
@@ -475,6 +514,8 @@ export function TrailingOverlays(props: {
|
|
|
475
514
|
value={mode.text}
|
|
476
515
|
placeholder="a short title"
|
|
477
516
|
onInput={(text) => setMode({ ...mode, text })}
|
|
517
|
+
onSave={() => dispatch({ type: "confirmDialog" })}
|
|
518
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
478
519
|
theme={theme}
|
|
479
520
|
/>
|
|
480
521
|
) : null}
|
|
@@ -486,6 +527,8 @@ export function TrailingOverlays(props: {
|
|
|
486
527
|
value={mode.text}
|
|
487
528
|
placeholder="your name"
|
|
488
529
|
onInput={(text) => setMode({ ...mode, text })}
|
|
530
|
+
onSave={() => dispatch({ type: "confirmDialog" })}
|
|
531
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
489
532
|
theme={theme}
|
|
490
533
|
/>
|
|
491
534
|
) : null}
|
|
@@ -497,6 +540,8 @@ export function TrailingOverlays(props: {
|
|
|
497
540
|
value={mode.text}
|
|
498
541
|
placeholder={TREE_PROMPTS[mode.ask].placeholder}
|
|
499
542
|
onInput={(text) => setMode({ ...mode, text })}
|
|
543
|
+
onSave={() => dispatch({ type: "confirmDialog" })}
|
|
544
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
500
545
|
theme={theme}
|
|
501
546
|
/>
|
|
502
547
|
) : null}
|
|
@@ -22,7 +22,7 @@ async function brandFrame(): Promise<string[]> {
|
|
|
22
22
|
onOpenMenu={() => {}}
|
|
23
23
|
threadsPanel={<text>threads</text>}
|
|
24
24
|
threadTitle={LONG_TITLE}
|
|
25
|
-
threadPanel={<text>
|
|
25
|
+
threadPanel={<text>Select a thread</text>}
|
|
26
26
|
changesOpen
|
|
27
27
|
projectOpen
|
|
28
28
|
onToggleChanges={() => {}}
|
|
@@ -69,8 +69,11 @@ export interface AppShellProps {
|
|
|
69
69
|
theme?: Theme;
|
|
70
70
|
threadsWidth?: number;
|
|
71
71
|
projectWidth?: number;
|
|
72
|
+
onFocusPane?: (pane: FocusPane) => void;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
export type FocusPane = "threads" | "thread" | "changes" | "project";
|
|
76
|
+
|
|
74
77
|
/** The right region's reserved width: the open project pane, nothing when the region is closed, else the collapsed rail. */
|
|
75
78
|
function rightRegionColumns(
|
|
76
79
|
projectOpen: boolean,
|
|
@@ -122,6 +125,7 @@ export function AppShell({
|
|
|
122
125
|
theme,
|
|
123
126
|
threadsWidth = 30,
|
|
124
127
|
projectWidth = 32,
|
|
128
|
+
onFocusPane,
|
|
125
129
|
}: AppShellProps): React.ReactNode {
|
|
126
130
|
const tokens = theme ?? DARK;
|
|
127
131
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
@@ -214,7 +218,13 @@ export function AppShell({
|
|
|
214
218
|
<TooltipProvider theme={tokens}>
|
|
215
219
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
216
220
|
{sidebarOpen ? (
|
|
217
|
-
<PanelColumn
|
|
221
|
+
<PanelColumn
|
|
222
|
+
width={threadsWidth}
|
|
223
|
+
border="right"
|
|
224
|
+
header={brandChrome}
|
|
225
|
+
onFocus={() => onFocusPane?.("threads")}
|
|
226
|
+
theme={tokens}
|
|
227
|
+
>
|
|
218
228
|
{threadsPanel}
|
|
219
229
|
</PanelColumn>
|
|
220
230
|
) : null}
|
|
@@ -237,6 +247,7 @@ export function AppShell({
|
|
|
237
247
|
reopenRightControl,
|
|
238
248
|
rightRegionClosed,
|
|
239
249
|
)}
|
|
250
|
+
onFocus={() => onFocusPane?.("thread")}
|
|
240
251
|
theme={tokens}
|
|
241
252
|
>
|
|
242
253
|
{threadPanel}
|
|
@@ -244,6 +255,7 @@ export function AppShell({
|
|
|
244
255
|
) : null}
|
|
245
256
|
{changesOpen ? (
|
|
246
257
|
<box
|
|
258
|
+
onMouseDown={() => onFocusPane?.("changes")}
|
|
247
259
|
style={{
|
|
248
260
|
flexDirection: "column",
|
|
249
261
|
flexGrow: 1,
|
|
@@ -266,6 +278,7 @@ export function AppShell({
|
|
|
266
278
|
border="left"
|
|
267
279
|
header={null}
|
|
268
280
|
headerRight={projectToggles}
|
|
281
|
+
onFocus={() => onFocusPane?.("project")}
|
|
269
282
|
theme={tokens}
|
|
270
283
|
>
|
|
271
284
|
{projectPanel}
|