@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
package/src/App.tsx
CHANGED
|
@@ -42,8 +42,9 @@ import { Toolbar } from "./components/primitives/Toolbar";
|
|
|
42
42
|
import { groupInbox, projectName, threadTitle } from "./components/session-tree";
|
|
43
43
|
import { ThreadTree } from "./components/ThreadTree";
|
|
44
44
|
import { ChangesFileTree } from "./components/ChangesColumn";
|
|
45
|
+
import { MenuControlProvider, useMenuControlState } from "./components/menu-control";
|
|
45
46
|
import { ProjectTreeView } from "./components/ProjectTreeView";
|
|
46
|
-
import { AppShell, type ProjectPanelMode } from "./components/AppShell";
|
|
47
|
+
import { AppShell, type FocusPane, type ProjectPanelMode } from "./components/AppShell";
|
|
47
48
|
import { EditorGrid } from "./components/EditorGrid";
|
|
48
49
|
import { GridTabContent } from "./components/GridTabContent";
|
|
49
50
|
import { useChangesWorkbench } from "./use-changes-workbench";
|
|
@@ -53,11 +54,15 @@ import { ThreadFooter } from "./components/ThreadFooter";
|
|
|
53
54
|
import { ConfirmCard } from "./components/ConfirmCard";
|
|
54
55
|
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
|
|
55
56
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
diffChordEntries,
|
|
58
|
+
dispatchLeaderCommand,
|
|
59
|
+
leaderCombosFor,
|
|
60
|
+
leaderHint,
|
|
61
|
+
matchesLeader,
|
|
62
|
+
railChordEntries,
|
|
58
63
|
resolveThreadChord,
|
|
59
64
|
THREAD_CHORD_ENTRIES,
|
|
60
|
-
|
|
65
|
+
treeChordEntries,
|
|
61
66
|
} from "./thread-chords";
|
|
62
67
|
import { type DiffFoldControls } from "./components/DiffContentView";
|
|
63
68
|
import { commentCountsByFile } from "./view-diff";
|
|
@@ -86,7 +91,7 @@ import {
|
|
|
86
91
|
TrailingOverlays,
|
|
87
92
|
} from "./app-screens";
|
|
88
93
|
/** A toast clears itself after this idle; esc dismisses it sooner. */
|
|
89
|
-
const TOAST_DISMISS_MS =
|
|
94
|
+
const TOAST_DISMISS_MS = 2000;
|
|
90
95
|
|
|
91
96
|
export interface AppProps {
|
|
92
97
|
home?: string;
|
|
@@ -134,6 +139,46 @@ function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
|
|
|
134
139
|
return menuDialog !== null;
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
/** The drop-up chrome or a floating popover menu (thread actions, editor split) holds the keyboard. */
|
|
143
|
+
function keyboardHeldByMenu(
|
|
144
|
+
menuDialog: "keybinds" | "settings" | null,
|
|
145
|
+
openMenuId: string | null,
|
|
146
|
+
): boolean {
|
|
147
|
+
return menuChromeOpen(menuDialog) || openMenuId !== null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A non-modal toast: escape dismisses it only when nothing else owns escape (no overlay, not span mode). */
|
|
151
|
+
export function toastDismissRequested(
|
|
152
|
+
toastOpen: boolean,
|
|
153
|
+
key: { name: string },
|
|
154
|
+
overlay: KeyState["overlay"],
|
|
155
|
+
spanMode: boolean,
|
|
156
|
+
): boolean {
|
|
157
|
+
return toastOpen && key.name === "escape" && overlay === "none" && !spanMode;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** ctrl+q quits from anywhere, ahead of every focus/menu/overlay gate, with no confirmation. */
|
|
161
|
+
export function quitKeyHandled(
|
|
162
|
+
key: { name: string; ctrl?: boolean },
|
|
163
|
+
onExit?: (code: number) => void,
|
|
164
|
+
): boolean {
|
|
165
|
+
if (!key.ctrl || key.name !== "q") return false;
|
|
166
|
+
onExit?.(0);
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A popover menu is open, so the shell keyboard is modal: escape closes it, every other key is swallowed. */
|
|
172
|
+
function menuModalHandled(
|
|
173
|
+
menuControl: { openMenuId: string | null; closeMenu: () => void },
|
|
174
|
+
key: { name: string },
|
|
175
|
+
): boolean {
|
|
176
|
+
if (menuControl.openMenuId === null) return false;
|
|
177
|
+
if (key.name === "escape") menuControl.closeMenu();
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
137
182
|
/** True while a menu or an overlay owns the keyboard instead of the thread view. */
|
|
138
183
|
function keyboardOwnedElsewhere(menuOwnsKeyboard: boolean, overlay: KeyState["overlay"]): boolean {
|
|
139
184
|
return menuOwnsKeyboard || overlay !== "none";
|
|
@@ -153,6 +198,7 @@ function ProjectPanelBody(props: {
|
|
|
153
198
|
onOpenChangedFile: (path: string) => void;
|
|
154
199
|
onOpenProjectFile: (path: string) => void;
|
|
155
200
|
commentCounts?: ReadonlyMap<string, number>;
|
|
201
|
+
focused?: boolean;
|
|
156
202
|
theme: Theme;
|
|
157
203
|
}): React.ReactNode {
|
|
158
204
|
const [changes, setChanges] = useState<readonly DiffFileContents[]>([]);
|
|
@@ -183,6 +229,7 @@ function ProjectPanelBody(props: {
|
|
|
183
229
|
files={changes}
|
|
184
230
|
onSelectFile={props.onOpenChangedFile}
|
|
185
231
|
commentCounts={props.commentCounts}
|
|
232
|
+
focused={props.focused}
|
|
186
233
|
theme={props.theme}
|
|
187
234
|
/>
|
|
188
235
|
);
|
|
@@ -192,6 +239,7 @@ function ProjectPanelBody(props: {
|
|
|
192
239
|
key={props.reloadKey}
|
|
193
240
|
loadFiles={props.loadProjectFiles}
|
|
194
241
|
onSelectFile={props.onOpenProjectFile}
|
|
242
|
+
focused={props.focused}
|
|
195
243
|
theme={props.theme}
|
|
196
244
|
/>
|
|
197
245
|
);
|
|
@@ -207,7 +255,11 @@ function usableScreenReached(
|
|
|
207
255
|
}
|
|
208
256
|
|
|
209
257
|
/** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
|
|
210
|
-
function cheatsheetFor(
|
|
258
|
+
function cheatsheetFor(
|
|
259
|
+
keyBindings: KeyBindings,
|
|
260
|
+
threadViewActive: boolean,
|
|
261
|
+
hint: string,
|
|
262
|
+
): CheatsheetSection[] {
|
|
211
263
|
const base = keyBindings.cheatsheet();
|
|
212
264
|
|
|
213
265
|
if (!threadViewActive) {
|
|
@@ -217,9 +269,9 @@ function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): Che
|
|
|
217
269
|
return [
|
|
218
270
|
...THREAD_VIEW_CHEATSHEET,
|
|
219
271
|
{ title: "Session", entries: [...THREAD_CHORD_ENTRIES] },
|
|
220
|
-
{ title: "Diff", entries:
|
|
221
|
-
{ title: "Rail", entries:
|
|
222
|
-
{ title: "Tree", entries:
|
|
272
|
+
{ title: "Diff", entries: diffChordEntries(hint) },
|
|
273
|
+
{ title: "Rail", entries: railChordEntries(hint) },
|
|
274
|
+
{ title: "Tree", entries: treeChordEntries(hint) },
|
|
223
275
|
...base.filter((section) => section.title === "Agent terminal"),
|
|
224
276
|
];
|
|
225
277
|
}
|
|
@@ -232,6 +284,125 @@ function initialThreadsOpen(
|
|
|
232
284
|
return layout ? layout.threads : sessionId === undefined;
|
|
233
285
|
}
|
|
234
286
|
|
|
287
|
+
function initialFocusedPane(sessionId: string | undefined, zoomed: boolean): FocusPane {
|
|
288
|
+
if (sessionId === undefined) return "threads";
|
|
289
|
+
|
|
290
|
+
return zoomed ? "changes" : "thread";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function bareShellDefersKeys(
|
|
294
|
+
sessionIsNull: boolean,
|
|
295
|
+
overlayIsNone: boolean,
|
|
296
|
+
focusedPane: FocusPane,
|
|
297
|
+
): boolean {
|
|
298
|
+
return sessionIsNull && overlayIsNone && focusedPane !== "threads";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function surfaceSuspended(base: boolean, focusedPane: FocusPane, pane: FocusPane): boolean {
|
|
302
|
+
return base || focusedPane !== pane;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function threadsNavHandled(params: {
|
|
306
|
+
focusedPane: FocusPane;
|
|
307
|
+
quiet: boolean;
|
|
308
|
+
key: { name: string };
|
|
309
|
+
count: number;
|
|
310
|
+
setInboxCursor: (update: (cursor: number) => number) => void;
|
|
311
|
+
openSession: () => void;
|
|
312
|
+
}): boolean {
|
|
313
|
+
const { focusedPane, quiet, key, count } = params;
|
|
314
|
+
|
|
315
|
+
if (focusedPane !== "threads" || !quiet || count === 0) return false;
|
|
316
|
+
if (key.name === "j" || key.name === "down") {
|
|
317
|
+
params.setInboxCursor((cursor) => Math.min(cursor + 1, count - 1));
|
|
318
|
+
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
if (key.name === "k" || key.name === "up") {
|
|
322
|
+
params.setInboxCursor((cursor) => Math.max(cursor - 1, 0));
|
|
323
|
+
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
if (key.name === "return" || key.name === "enter") {
|
|
327
|
+
params.openSession();
|
|
328
|
+
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function appLeaderHandled(params: {
|
|
336
|
+
focusedPane: FocusPane;
|
|
337
|
+
key: { name: string; shift?: boolean; ctrl?: boolean; meta?: boolean; super?: boolean };
|
|
338
|
+
leaderCombos: readonly string[];
|
|
339
|
+
pending: { current: boolean };
|
|
340
|
+
runLeaderCommand: (key: { name: string; shift?: boolean }) => void;
|
|
341
|
+
}): boolean {
|
|
342
|
+
const { focusedPane, key, leaderCombos, pending, runLeaderCommand } = params;
|
|
343
|
+
|
|
344
|
+
if (focusedPane !== "threads" && focusedPane !== "project") return false;
|
|
345
|
+
if (pending.current) {
|
|
346
|
+
pending.current = false;
|
|
347
|
+
if (key.name !== "escape") runLeaderCommand(key);
|
|
348
|
+
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
if (matchesLeader(key, leaderCombos)) {
|
|
352
|
+
pending.current = true;
|
|
353
|
+
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function visiblePanes(
|
|
361
|
+
sidebarOpen: boolean,
|
|
362
|
+
threadShown: boolean,
|
|
363
|
+
changesOpen: boolean,
|
|
364
|
+
projectOpen: boolean,
|
|
365
|
+
): FocusPane[] {
|
|
366
|
+
const panes: FocusPane[] = [];
|
|
367
|
+
|
|
368
|
+
if (sidebarOpen) panes.push("threads");
|
|
369
|
+
if (threadShown) panes.push("thread");
|
|
370
|
+
if (changesOpen) panes.push("changes");
|
|
371
|
+
if (projectOpen) panes.push("project");
|
|
372
|
+
|
|
373
|
+
return panes;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function nextFocusPane(
|
|
377
|
+
current: FocusPane,
|
|
378
|
+
panes: FocusPane[],
|
|
379
|
+
backward: boolean,
|
|
380
|
+
): FocusPane {
|
|
381
|
+
if (panes.length === 0) return current;
|
|
382
|
+
const index = panes.indexOf(current);
|
|
383
|
+
const step = backward ? -1 : 1;
|
|
384
|
+
|
|
385
|
+
return panes[(index + step + panes.length) % panes.length] ?? current;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The panes that can own the keyboard: a diff's Thread pane is an inert placeholder, so it is out. */
|
|
389
|
+
function navigableFocusPanes(
|
|
390
|
+
sidebarOpen: boolean,
|
|
391
|
+
zoomed: boolean,
|
|
392
|
+
isDiff: boolean,
|
|
393
|
+
changesOpen: boolean,
|
|
394
|
+
projectOpen: boolean,
|
|
395
|
+
): FocusPane[] {
|
|
396
|
+
return visiblePanes(sidebarOpen, !zoomed && !isDiff, changesOpen, projectOpen);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** The layout hid or inert-ed the focused pane: fall back to the first navigable one, else stay put. */
|
|
400
|
+
export function reconciledFocus(navigable: FocusPane[], focusedPane: FocusPane): FocusPane | null {
|
|
401
|
+
if (navigable.length === 0 || navigable.includes(focusedPane)) return null;
|
|
402
|
+
|
|
403
|
+
return navigable[0] ?? null;
|
|
404
|
+
}
|
|
405
|
+
|
|
235
406
|
export function App({
|
|
236
407
|
home,
|
|
237
408
|
sessionId,
|
|
@@ -318,6 +489,9 @@ export function App({
|
|
|
318
489
|
const [sidebarOpen, setSidebarOpen] = useState(() => initialThreadsOpen(layout, sessionId));
|
|
319
490
|
// the Changes + Project right region and its editor grid (tabs, splits, zoom)
|
|
320
491
|
const workbench = useChangesWorkbench({ layout });
|
|
492
|
+
const [focusedPane, setFocusedPane] = useState<FocusPane>(() =>
|
|
493
|
+
initialFocusedPane(sessionId, workbench.zoomed),
|
|
494
|
+
);
|
|
321
495
|
// only the session view persists from here; the bare shell owns its own (NoThreadShell), so this
|
|
322
496
|
// workbench stays inactive with no session and never clobbers what the shell saved
|
|
323
497
|
useRememberLayout(
|
|
@@ -570,12 +744,65 @@ export function App({
|
|
|
570
744
|
|
|
571
745
|
const overlay = resolveOverlay(mode, completion.phase, walking);
|
|
572
746
|
|
|
573
|
-
const
|
|
747
|
+
const menuControl = useMenuControlState();
|
|
748
|
+
const menuOwnsKeyboard = keyboardHeldByMenu(menuDialog, menuControl.openMenuId);
|
|
574
749
|
// an overlay (submit, walk, prompt, confirm) or the menu takes the keyboard
|
|
575
750
|
// from the thread view; the view suspends its own grammar meanwhile
|
|
576
751
|
const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay);
|
|
577
752
|
|
|
753
|
+
const leaderCombos = leaderCombosFor(keysRef.current.leader);
|
|
754
|
+
const leaderPending = useRef(false);
|
|
755
|
+
const navigablePanes = navigableFocusPanes(
|
|
756
|
+
sidebarOpen,
|
|
757
|
+
workbench.zoomed,
|
|
758
|
+
isDiff,
|
|
759
|
+
workbench.changesOpen,
|
|
760
|
+
workbench.projectOpen,
|
|
761
|
+
);
|
|
762
|
+
useEffect(() => {
|
|
763
|
+
// the bare shell drives its panes from its own workbench, so only reconcile against this one
|
|
764
|
+
if (!session) return;
|
|
765
|
+
const next = reconciledFocus(navigablePanes, focusedPane);
|
|
766
|
+
|
|
767
|
+
if (next) setFocusedPane(next);
|
|
768
|
+
}, [session, navigablePanes, focusedPane]);
|
|
769
|
+
const cyclePanes = (backward: boolean): void =>
|
|
770
|
+
setFocusedPane((current) => nextFocusPane(current, navigablePanes, backward));
|
|
771
|
+
const runLeaderCommand = (key: { name: string; shift?: boolean }): void => {
|
|
772
|
+
if (key.name === "tab") return cyclePanes(Boolean(key.shift));
|
|
773
|
+
|
|
774
|
+
dispatchLeaderCommand(
|
|
775
|
+
key,
|
|
776
|
+
{ composing: threadComposing, isOwner, resolved, treeActive: railTab === "tree", isDiff },
|
|
777
|
+
dispatch,
|
|
778
|
+
);
|
|
779
|
+
};
|
|
780
|
+
// clicking a sidebar thread moves the cursor onto it too, so the row shows its selected backdrop at once
|
|
781
|
+
const openThread = (id: string): void => {
|
|
782
|
+
const index = grouped.ordered.findIndex((thread) => thread.id === id);
|
|
783
|
+
|
|
784
|
+
if (index >= 0) setInboxCursor(() => index);
|
|
785
|
+
controller.open(id);
|
|
786
|
+
};
|
|
787
|
+
|
|
578
788
|
useKeyboard((key) => {
|
|
789
|
+
if (quitKeyHandled(key, onExit)) return;
|
|
790
|
+
if (menuModalHandled(menuControl, key)) return;
|
|
791
|
+
if (
|
|
792
|
+
appLeaderHandled({ focusedPane, key, leaderCombos, pending: leaderPending, runLeaderCommand })
|
|
793
|
+
)
|
|
794
|
+
return;
|
|
795
|
+
if (
|
|
796
|
+
threadsNavHandled({
|
|
797
|
+
focusedPane,
|
|
798
|
+
quiet: overlay === "none" && !menuOwnsKeyboard && !threadComposing,
|
|
799
|
+
key,
|
|
800
|
+
count: grouped.ordered.length,
|
|
801
|
+
setInboxCursor,
|
|
802
|
+
openSession: () => dispatch({ type: "openSession" }),
|
|
803
|
+
})
|
|
804
|
+
)
|
|
805
|
+
return;
|
|
579
806
|
// The thread view owns the document grammar while active (its own
|
|
580
807
|
// useKeyboard handles marks, comments, and ctrl+q); the session chords
|
|
581
808
|
// (submit, share, edit, walk, the rail) resolve here, and the keymap only
|
|
@@ -600,8 +827,9 @@ export function App({
|
|
|
600
827
|
if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
|
|
601
828
|
// the toast is non-modal: escape only dismisses it when nothing else owns
|
|
602
829
|
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
603
|
-
if (toast
|
|
830
|
+
if (toastDismissRequested(toast !== null, key, overlay, mode.type === "span"))
|
|
604
831
|
return controller.dismissToast();
|
|
832
|
+
if (bareShellDefersKeys(session === null, overlay === "none", focusedPane)) return;
|
|
605
833
|
const state = buildKeyState({
|
|
606
834
|
keys: keysRef.current,
|
|
607
835
|
observer,
|
|
@@ -646,12 +874,13 @@ export function App({
|
|
|
646
874
|
<MenuChrome
|
|
647
875
|
menuDialog={menuDialog}
|
|
648
876
|
theme={theme}
|
|
649
|
-
keybindsSections={cheatsheetFor(keyBindings, threadViewActive)}
|
|
877
|
+
keybindsSections={cheatsheetFor(keyBindings, threadViewActive, leaderHint(leaderCombos))}
|
|
650
878
|
settingsCategories={settingsCategories}
|
|
651
879
|
settingsValues={settingsValues}
|
|
652
880
|
settingsNav={settingsNav}
|
|
653
881
|
onCategorySelect={onCategorySelect}
|
|
654
882
|
cycleSetting={cycleSetting}
|
|
883
|
+
onClose={() => setMenuDialog(null)}
|
|
655
884
|
/>
|
|
656
885
|
);
|
|
657
886
|
|
|
@@ -664,6 +893,7 @@ export function App({
|
|
|
664
893
|
<NoThreadShell
|
|
665
894
|
rows={grouped.rows}
|
|
666
895
|
inboxCursor={inboxCursor}
|
|
896
|
+
onOpenThread={openThread}
|
|
667
897
|
mode={mode}
|
|
668
898
|
theme={theme}
|
|
669
899
|
controller={controller}
|
|
@@ -672,6 +902,9 @@ export function App({
|
|
|
672
902
|
onOpenMenu={() => setMenuDialog("settings")}
|
|
673
903
|
sidebarOpen={sidebarOpen}
|
|
674
904
|
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
905
|
+
focusedPane={focusedPane}
|
|
906
|
+
onFocusPane={setFocusedPane}
|
|
907
|
+
menuControl={menuControl}
|
|
675
908
|
pinnedIds={pinnedIds}
|
|
676
909
|
onPin={togglePin}
|
|
677
910
|
onRename={(id, title) => setMode({ type: "renameThread", sessionId: id, text: title })}
|
|
@@ -763,221 +996,244 @@ export function App({
|
|
|
763
996
|
<SlashSkillsContext.Provider value={skills}>
|
|
764
997
|
<PaletteNamesContext.Provider value={paletteNames}>
|
|
765
998
|
<ThemeProvider theme={theme}>
|
|
766
|
-
<
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
999
|
+
<MenuControlProvider value={menuControl}>
|
|
1000
|
+
<AppShell
|
|
1001
|
+
sidebarOpen={sidebarOpen}
|
|
1002
|
+
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
1003
|
+
onOpenMenu={() => setMenuDialog("settings")}
|
|
1004
|
+
onFocusPane={setFocusedPane}
|
|
1005
|
+
threadsPanel={
|
|
1006
|
+
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
1007
|
+
<ThreadTree
|
|
1008
|
+
rows={grouped.rows}
|
|
1009
|
+
cursor={inboxCursor}
|
|
1010
|
+
activeId={activeSession.id}
|
|
1011
|
+
focused={focusedPane === "threads"}
|
|
1012
|
+
pinnedIds={pinnedIds}
|
|
1013
|
+
width={30}
|
|
1014
|
+
onSelect={openThread}
|
|
1015
|
+
onPin={togglePin}
|
|
1016
|
+
onRename={(id, title) =>
|
|
1017
|
+
setMode({ type: "renameThread", sessionId: id, text: title })
|
|
1018
|
+
}
|
|
1019
|
+
theme={theme}
|
|
1020
|
+
/>
|
|
1021
|
+
</scrollbox>
|
|
1022
|
+
}
|
|
1023
|
+
threadTitle={threadTitle(activeSession)}
|
|
1024
|
+
threadActions={
|
|
1025
|
+
showOwnerActions ? (
|
|
1026
|
+
<Toolbar>
|
|
1027
|
+
<Button onPress={onEditRequest} theme={theme}>
|
|
1028
|
+
{" edit "}
|
|
1029
|
+
</Button>
|
|
1030
|
+
<Button onPress={onShareRequest} theme={theme}>
|
|
1031
|
+
{" share "}
|
|
1032
|
+
</Button>
|
|
1033
|
+
</Toolbar>
|
|
1034
|
+
) : undefined
|
|
1035
|
+
}
|
|
1036
|
+
threadPanel={
|
|
1037
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
1038
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
1039
|
+
{isPixelPrototype ? (
|
|
1040
|
+
<PrototypePixels
|
|
1041
|
+
prototypePath={prototypePath}
|
|
1042
|
+
canComment={prototypeCanComment}
|
|
1043
|
+
onCommentElement={onCommentPrototype}
|
|
1044
|
+
onComposingChange={setPrototypeComposing}
|
|
1045
|
+
hidden={chromeHidden}
|
|
1046
|
+
/>
|
|
1047
|
+
) : isDiff ? (
|
|
1048
|
+
<box
|
|
1049
|
+
style={{
|
|
1050
|
+
flexGrow: 1,
|
|
1051
|
+
flexDirection: "column",
|
|
1052
|
+
alignItems: "center",
|
|
1053
|
+
justifyContent: "center",
|
|
1054
|
+
}}
|
|
1055
|
+
>
|
|
1056
|
+
<text fg={theme.textDim}>Select a thread</text>
|
|
1057
|
+
</box>
|
|
1058
|
+
) : (
|
|
1059
|
+
<ThreadView
|
|
1060
|
+
session={activeSession}
|
|
1061
|
+
suspended={surfaceSuspended(threadViewSuspended, focusedPane, "thread")}
|
|
1062
|
+
editOrphanCount={editOrphanCount}
|
|
1063
|
+
onComposingChange={setThreadComposing}
|
|
1064
|
+
leaderCombos={leaderCombos}
|
|
1065
|
+
onLeaderCommand={runLeaderCommand}
|
|
1066
|
+
resolved={resolved}
|
|
1067
|
+
onObserverBlocked={(reason) =>
|
|
1068
|
+
controller.setStatus(
|
|
1069
|
+
reason === "observer"
|
|
1070
|
+
? "observer - read-only"
|
|
1071
|
+
: "review submitted - read-only",
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
onCursorChange={setCursor}
|
|
1075
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
1076
|
+
onFocusAnnotation={setFocusedAnnotationId}
|
|
1077
|
+
display={display}
|
|
1078
|
+
marks={marks}
|
|
1079
|
+
quickActions={quickActions}
|
|
1080
|
+
observer={observer}
|
|
1081
|
+
onAnnotate={(span, body) =>
|
|
1082
|
+
void controller.annotate(
|
|
1083
|
+
"comment",
|
|
1084
|
+
span.start.blockIndex,
|
|
1085
|
+
span.start.char,
|
|
1086
|
+
span.end.char,
|
|
1087
|
+
body,
|
|
1088
|
+
span.end.blockIndex,
|
|
1089
|
+
)
|
|
1090
|
+
}
|
|
1091
|
+
onReply={(rootAnnotationId, body) =>
|
|
1092
|
+
void controller.reply(rootAnnotationId, body)
|
|
1093
|
+
}
|
|
1094
|
+
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
1095
|
+
onExit={() => onExit?.(0)}
|
|
1096
|
+
/>
|
|
1097
|
+
)}
|
|
1098
|
+
</box>
|
|
1099
|
+
{threadFooter}
|
|
1100
|
+
</box>
|
|
1101
|
+
}
|
|
1102
|
+
changesOpen={workbench.changesOpen}
|
|
1103
|
+
projectOpen={workbench.projectOpen}
|
|
1104
|
+
onToggleChanges={workbench.toggleChanges}
|
|
1105
|
+
onToggleProject={workbench.toggleProject}
|
|
1106
|
+
onToggleRight={workbench.toggleRight}
|
|
1107
|
+
projectMode={workbench.projectMode}
|
|
1108
|
+
zoomHideThread={workbench.zoomed}
|
|
1109
|
+
changesFooter={threadFooter}
|
|
1110
|
+
changesPanel={
|
|
1111
|
+
<EditorGrid
|
|
1112
|
+
tree={workbench.grid}
|
|
1113
|
+
focusedGroupId={workbench.activeGroup}
|
|
1114
|
+
commentCounts={diffCommentCounts}
|
|
1115
|
+
onFocusGroup={workbench.focusGroup}
|
|
1116
|
+
onActivateTab={workbench.activate}
|
|
1117
|
+
onCloseTab={workbench.close}
|
|
1118
|
+
onSplit={workbench.split}
|
|
1119
|
+
onZoom={workbench.toggleZoom}
|
|
1120
|
+
zoomed={workbench.zoomed}
|
|
1121
|
+
renderTab={(tab, groupFocused) => (
|
|
1122
|
+
<GridTabContent
|
|
1123
|
+
tab={tab}
|
|
1124
|
+
rows={rows}
|
|
1125
|
+
surface={{
|
|
1126
|
+
session: activeSession,
|
|
1127
|
+
quickActions,
|
|
1128
|
+
observer,
|
|
1129
|
+
commentsEnabled: true,
|
|
1130
|
+
resolved,
|
|
1131
|
+
suspended: surfaceSuspended(
|
|
1132
|
+
threadViewSuspended || !groupFocused,
|
|
1133
|
+
focusedPane,
|
|
1134
|
+
"changes",
|
|
1135
|
+
),
|
|
1136
|
+
onComposingChange: setThreadComposing,
|
|
1137
|
+
onObserverBlocked: (reason) =>
|
|
1138
|
+
controller.setStatus(
|
|
1139
|
+
reason === "observer"
|
|
1140
|
+
? "observer - read-only"
|
|
1141
|
+
: "review submitted - read-only",
|
|
1142
|
+
),
|
|
1143
|
+
onCursorChange: setCursor,
|
|
1144
|
+
focusedAnnotationId,
|
|
1145
|
+
onFocusAnnotation: setFocusedAnnotationId,
|
|
1146
|
+
// a diff thread's Changes view is the artifact itself; any other thread's is the
|
|
1147
|
+
// live working-tree diff, so its notes carry a file target and anchor there
|
|
1148
|
+
onAnnotate: (span, body) =>
|
|
1149
|
+
void controller.annotate(
|
|
1150
|
+
"comment",
|
|
1151
|
+
span.start.blockIndex,
|
|
1152
|
+
span.start.char,
|
|
1153
|
+
span.end.char,
|
|
1154
|
+
body,
|
|
1155
|
+
span.end.blockIndex,
|
|
1156
|
+
isDiff
|
|
1157
|
+
? { kind: "artifact" }
|
|
1158
|
+
: { kind: "file", path: "", rev: "worktree" },
|
|
1159
|
+
),
|
|
1160
|
+
onReply: (rootAnnotationId, body) =>
|
|
1161
|
+
void controller.reply(rootAnnotationId, body),
|
|
1162
|
+
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1163
|
+
leaderCombos,
|
|
1164
|
+
onLeaderCommand: runLeaderCommand,
|
|
1165
|
+
onExit: () => onExit?.(0),
|
|
1166
|
+
}}
|
|
1167
|
+
rejectedRows={rejectedRows}
|
|
1168
|
+
fold={diffFold}
|
|
1169
|
+
fileStats={controller.fileStats()}
|
|
1170
|
+
split={diffView === "split" && workbench.zoomed}
|
|
1171
|
+
dimmed={walking}
|
|
1172
|
+
readFile={(path) => controller.repoReadFile(path)}
|
|
1173
|
+
onAddFileComment={(path, anchor, body) =>
|
|
1174
|
+
void controller.addComment(
|
|
1175
|
+
anchor,
|
|
1176
|
+
{ kind: "file", path, rev: "worktree" },
|
|
842
1177
|
body,
|
|
843
|
-
span.end.blockIndex,
|
|
844
1178
|
)
|
|
845
1179
|
}
|
|
846
|
-
|
|
847
|
-
void controller.reply(rootAnnotationId, body)
|
|
848
|
-
}
|
|
849
|
-
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
850
|
-
onExit={() => onExit?.(0)}
|
|
1180
|
+
theme={theme}
|
|
851
1181
|
/>
|
|
852
1182
|
)}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
surface={{
|
|
881
|
-
session: activeSession,
|
|
882
|
-
quickActions,
|
|
883
|
-
observer,
|
|
884
|
-
commentsEnabled: true,
|
|
885
|
-
resolved,
|
|
886
|
-
suspended: threadViewSuspended || !groupFocused,
|
|
887
|
-
onComposingChange: setThreadComposing,
|
|
888
|
-
onObserverBlocked: (reason) =>
|
|
889
|
-
controller.setStatus(
|
|
890
|
-
reason === "observer"
|
|
891
|
-
? "observer - read-only"
|
|
892
|
-
: "review submitted - read-only",
|
|
893
|
-
),
|
|
894
|
-
onCursorChange: setCursor,
|
|
895
|
-
focusedAnnotationId,
|
|
896
|
-
onFocusAnnotation: setFocusedAnnotationId,
|
|
897
|
-
// a diff thread's Changes view is the artifact itself; any other thread's is the
|
|
898
|
-
// live working-tree diff, so its notes carry a file target and anchor there
|
|
899
|
-
onAnnotate: (span, body) =>
|
|
900
|
-
void controller.annotate(
|
|
901
|
-
"comment",
|
|
902
|
-
span.start.blockIndex,
|
|
903
|
-
span.start.char,
|
|
904
|
-
span.end.char,
|
|
905
|
-
body,
|
|
906
|
-
span.end.blockIndex,
|
|
907
|
-
isDiff
|
|
908
|
-
? { kind: "artifact" }
|
|
909
|
-
: { kind: "file", path: "", rev: "worktree" },
|
|
910
|
-
),
|
|
911
|
-
onReply: (rootAnnotationId, body) =>
|
|
912
|
-
void controller.reply(rootAnnotationId, body),
|
|
913
|
-
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
914
|
-
onExit: () => onExit?.(0),
|
|
915
|
-
}}
|
|
916
|
-
rejectedRows={rejectedRows}
|
|
917
|
-
fold={diffFold}
|
|
918
|
-
fileStats={controller.fileStats()}
|
|
919
|
-
split={diffView === "split" && workbench.zoomed}
|
|
920
|
-
dimmed={walking}
|
|
921
|
-
readFile={(path) => controller.repoReadFile(path)}
|
|
922
|
-
onAddFileComment={(path, anchor, body) =>
|
|
923
|
-
void controller.addComment(
|
|
924
|
-
anchor,
|
|
925
|
-
{ kind: "file", path, rev: "worktree" },
|
|
926
|
-
body,
|
|
927
|
-
)
|
|
928
|
-
}
|
|
929
|
-
theme={theme}
|
|
930
|
-
/>
|
|
931
|
-
)}
|
|
932
|
-
theme={theme}
|
|
933
|
-
/>
|
|
934
|
-
}
|
|
935
|
-
projectPanel={
|
|
936
|
-
<ProjectPanelBody
|
|
937
|
-
mode={workbench.projectMode}
|
|
938
|
-
loadChanges={() => controller.repoChanges()}
|
|
939
|
-
loadProjectFiles={() => controller.repoFiles()}
|
|
940
|
-
reloadKey={activeSession.id}
|
|
941
|
-
// the Changes navigator always opens a changed file as a diff - a diff review shows its
|
|
942
|
-
// captured snapshot, every other thread the live working-tree diff
|
|
943
|
-
onOpenChangedFile={(path) => workbench.openFile(path, "diff")}
|
|
944
|
-
onOpenProjectFile={(path) => workbench.openFile(path, "contents")}
|
|
945
|
-
commentCounts={diffCommentCounts}
|
|
1183
|
+
theme={theme}
|
|
1184
|
+
/>
|
|
1185
|
+
}
|
|
1186
|
+
projectPanel={
|
|
1187
|
+
<ProjectPanelBody
|
|
1188
|
+
mode={workbench.projectMode}
|
|
1189
|
+
loadChanges={() => controller.repoChanges()}
|
|
1190
|
+
loadProjectFiles={() => controller.repoFiles()}
|
|
1191
|
+
reloadKey={activeSession.id}
|
|
1192
|
+
// the Changes navigator always opens a changed file as a diff - a diff review shows its
|
|
1193
|
+
// captured snapshot, every other thread the live working-tree diff
|
|
1194
|
+
onOpenChangedFile={(path) => workbench.openFile(path, "diff")}
|
|
1195
|
+
onOpenProjectFile={(path) => workbench.openFile(path, "contents")}
|
|
1196
|
+
commentCounts={diffCommentCounts}
|
|
1197
|
+
focused={focusedPane === "project"}
|
|
1198
|
+
theme={theme}
|
|
1199
|
+
/>
|
|
1200
|
+
}
|
|
1201
|
+
theme={theme}
|
|
1202
|
+
>
|
|
1203
|
+
<TrailingOverlays
|
|
1204
|
+
walking={walking}
|
|
1205
|
+
walk={walk}
|
|
1206
|
+
walkFileList={walkFileList}
|
|
1207
|
+
viewedPaths={viewedPaths}
|
|
1208
|
+
session={activeSession}
|
|
1209
|
+
terminalWidth={terminalWidth}
|
|
946
1210
|
theme={theme}
|
|
1211
|
+
mode={mode}
|
|
1212
|
+
toast={toast}
|
|
1213
|
+
setMode={setMode}
|
|
1214
|
+
dispatch={dispatch}
|
|
947
1215
|
/>
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
top: 0,
|
|
970
|
-
width: "100%",
|
|
971
|
-
height: "100%",
|
|
972
|
-
justifyContent: "center",
|
|
973
|
-
alignItems: "center",
|
|
974
|
-
}}
|
|
975
|
-
>
|
|
976
|
-
<ConfirmCard {...submitConfirmState} theme={theme} />
|
|
977
|
-
</box>
|
|
978
|
-
) : null}
|
|
979
|
-
{menuChrome}
|
|
980
|
-
</AppShell>
|
|
1216
|
+
{submitConfirmState !== null ? (
|
|
1217
|
+
<box
|
|
1218
|
+
onMouseUp={submitConfirmState.onCancel}
|
|
1219
|
+
style={{
|
|
1220
|
+
position: "absolute",
|
|
1221
|
+
left: 0,
|
|
1222
|
+
top: 0,
|
|
1223
|
+
width: "100%",
|
|
1224
|
+
height: "100%",
|
|
1225
|
+
justifyContent: "center",
|
|
1226
|
+
alignItems: "center",
|
|
1227
|
+
}}
|
|
1228
|
+
>
|
|
1229
|
+
<box onMouseUp={(event) => event.stopPropagation()}>
|
|
1230
|
+
<ConfirmCard {...submitConfirmState} theme={theme} />
|
|
1231
|
+
</box>
|
|
1232
|
+
</box>
|
|
1233
|
+
) : null}
|
|
1234
|
+
{menuChrome}
|
|
1235
|
+
</AppShell>
|
|
1236
|
+
</MenuControlProvider>
|
|
981
1237
|
</ThemeProvider>
|
|
982
1238
|
</PaletteNamesContext.Provider>
|
|
983
1239
|
</SlashSkillsContext.Provider>
|