@cueloop/client 0.1.0-alpha.75 → 0.1.0-alpha.77
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.identity.test.tsx +78 -0
- package/src/App.regression.test.tsx +94 -0
- package/src/App.share-choice.test.tsx +105 -0
- package/src/App.test.tsx +11 -8
- package/src/App.toast.test.tsx +3 -1
- package/src/App.tsx +648 -221
- package/src/App.visual.test.tsx +57 -0
- package/src/App.workbench.test.tsx +41 -22
- package/src/app-screens.tsx +181 -123
- package/src/app-view-model.ts +1 -0
- package/src/components/AnnotatableFileView.tsx +9 -1
- package/src/components/AnnotationCards.tsx +22 -4
- 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 +12 -2
- package/src/components/DiscussionMarkerRail.tsx +2 -2
- package/src/components/EditorGrid.tsx +84 -26
- package/src/components/GridTabContent.tsx +12 -1
- package/src/components/ManageAccessDialog.stories.tsx +36 -0
- package/src/components/ManageAccessDialog.test.tsx +94 -0
- package/src/components/ManageAccessDialog.tsx +120 -0
- 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/ShareChoiceDialog.stories.tsx +34 -0
- package/src/components/ShareChoiceDialog.test.tsx +103 -0
- package/src/components/ShareChoiceDialog.tsx +82 -0
- 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 +12 -2
- package/src/components/WelcomePlayground.tsx +2 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +147 -52
- 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-identity.test.ts +78 -0
- package/src/config.ts +45 -1
- package/src/diff-intraline.ts +26 -5
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/intent-dispatch.test.ts +17 -2
- package/src/intent-dispatch.ts +7 -1
- package/src/keymap.ts +1 -0
- 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/share.ts +10 -2
- package/src/thread-chords.test.ts +33 -1
- package/src/thread-chords.ts +114 -29
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.live-diff.test.ts +1 -0
- package/src/thread-controller.share.test.ts +36 -2
- package/src/thread-controller.tree.test.ts +1 -0
- package/src/thread-controller.ts +18 -3
- package/src/use-annotation-surface.tsx +32 -2
- package/src/use-frame-measure.ts +4 -1
- package/src/use-settings-dialog.tsx +33 -7
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
package/src/App.tsx
CHANGED
|
@@ -15,11 +15,15 @@ import {
|
|
|
15
15
|
loadConfig,
|
|
16
16
|
persistAuthorName,
|
|
17
17
|
persistDiffView,
|
|
18
|
+
persistIdentity,
|
|
18
19
|
persistPins,
|
|
19
20
|
type AutoClose,
|
|
20
21
|
type DiffViewMode,
|
|
22
|
+
type IdentityConfig,
|
|
21
23
|
type QuickAction,
|
|
22
24
|
} from "./config";
|
|
25
|
+
import { resolveDisplayName } from "./attribution";
|
|
26
|
+
import { resolveGithubIdentity } from "./github-identity";
|
|
23
27
|
import { loadSkills, PaletteNamesContext, SlashSkillsContext } from "./skills";
|
|
24
28
|
import { mergeSlashItems, slashItemsFrom, type SlashItem } from "./slash-palette";
|
|
25
29
|
import {
|
|
@@ -38,12 +42,14 @@ import { KeyBindings, type CheatsheetSection } from "./key-bindings";
|
|
|
38
42
|
import { useReadySignal } from "./ready-signal";
|
|
39
43
|
import { ThemeProvider } from "./components/theme-context";
|
|
40
44
|
import { Button } from "./components/primitives/Button";
|
|
45
|
+
import { ShareChoiceDialog, SHARE_CHOICES } from "./components/ShareChoiceDialog";
|
|
41
46
|
import { Toolbar } from "./components/primitives/Toolbar";
|
|
42
47
|
import { groupInbox, projectName, threadTitle } from "./components/session-tree";
|
|
43
48
|
import { ThreadTree } from "./components/ThreadTree";
|
|
44
49
|
import { ChangesFileTree } from "./components/ChangesColumn";
|
|
50
|
+
import { MenuControlProvider, useMenuControlState } from "./components/menu-control";
|
|
45
51
|
import { ProjectTreeView } from "./components/ProjectTreeView";
|
|
46
|
-
import { AppShell, type ProjectPanelMode } from "./components/AppShell";
|
|
52
|
+
import { AppShell, type FocusPane, type ProjectPanelMode } from "./components/AppShell";
|
|
47
53
|
import { EditorGrid } from "./components/EditorGrid";
|
|
48
54
|
import { GridTabContent } from "./components/GridTabContent";
|
|
49
55
|
import { useChangesWorkbench } from "./use-changes-workbench";
|
|
@@ -51,18 +57,23 @@ import { useRememberLayout } from "./use-remember-layout";
|
|
|
51
57
|
import type { LaunchLayout } from "./launch-layout";
|
|
52
58
|
import { ThreadFooter } from "./components/ThreadFooter";
|
|
53
59
|
import { ConfirmCard } from "./components/ConfirmCard";
|
|
60
|
+
import { ManageAccessDialog } from "./components/ManageAccessDialog";
|
|
54
61
|
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
|
|
55
62
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
diffChordEntries,
|
|
64
|
+
dispatchLeaderCommand,
|
|
65
|
+
leaderCombosFor,
|
|
66
|
+
leaderHint,
|
|
67
|
+
matchesLeader,
|
|
68
|
+
railChordEntries,
|
|
58
69
|
resolveThreadChord,
|
|
59
70
|
THREAD_CHORD_ENTRIES,
|
|
60
|
-
|
|
71
|
+
treeChordEntries,
|
|
61
72
|
} from "./thread-chords";
|
|
62
73
|
import { type DiffFoldControls } from "./components/DiffContentView";
|
|
63
74
|
import { commentCountsByFile } from "./view-diff";
|
|
64
75
|
import { annotationTarget } from "@cueloop/schema";
|
|
65
|
-
import type { Artifact, DiffFileContents, Thread } from "@cueloop/schema";
|
|
76
|
+
import type { Annotation, Artifact, DiffFileContents, Identity, Thread } from "@cueloop/schema";
|
|
66
77
|
import { PrototypePixels } from "./prototype-pixels";
|
|
67
78
|
import type { PrototypeElement } from "./prototype-browser";
|
|
68
79
|
import {
|
|
@@ -86,7 +97,7 @@ import {
|
|
|
86
97
|
TrailingOverlays,
|
|
87
98
|
} from "./app-screens";
|
|
88
99
|
/** A toast clears itself after this idle; esc dismisses it sooner. */
|
|
89
|
-
const TOAST_DISMISS_MS =
|
|
100
|
+
const TOAST_DISMISS_MS = 2000;
|
|
90
101
|
|
|
91
102
|
export interface AppProps {
|
|
92
103
|
home?: string;
|
|
@@ -134,16 +145,139 @@ function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
|
|
|
134
145
|
return menuDialog !== null;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
/** The private-share allowlist for a session, or an empty list for a public one. */
|
|
149
|
+
function allowedGithubLogins(session: Thread | null): string[] {
|
|
150
|
+
return session?.access?.githubLogins ?? [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The drop-up chrome or a floating popover menu (thread actions, editor split) holds the keyboard. */
|
|
154
|
+
function keyboardHeldByMenu(
|
|
155
|
+
menuDialog: "keybinds" | "settings" | null,
|
|
156
|
+
openMenuId: string | null,
|
|
157
|
+
): boolean {
|
|
158
|
+
return menuChromeOpen(menuDialog) || openMenuId !== null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A non-modal toast: escape dismisses it only when nothing else owns escape (no overlay, not span mode). */
|
|
162
|
+
export function toastDismissRequested(
|
|
163
|
+
toastOpen: boolean,
|
|
164
|
+
key: { name: string },
|
|
165
|
+
overlay: KeyState["overlay"],
|
|
166
|
+
spanMode: boolean,
|
|
167
|
+
): boolean {
|
|
168
|
+
return toastOpen && key.name === "escape" && overlay === "none" && !spanMode;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** ctrl+q quits from anywhere, ahead of every focus/menu/overlay gate, with no confirmation. */
|
|
172
|
+
export function quitKeyHandled(
|
|
173
|
+
key: { name: string; ctrl?: boolean },
|
|
174
|
+
onExit?: (code: number) => void,
|
|
175
|
+
): boolean {
|
|
176
|
+
if (!key.ctrl || key.name !== "q") return false;
|
|
177
|
+
onExit?.(0);
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A popover menu is open, so the shell keyboard is modal: escape closes it, every other key is swallowed. */
|
|
183
|
+
function menuModalHandled(
|
|
184
|
+
menuControl: { openMenuId: string | null; closeMenu: () => void },
|
|
185
|
+
key: { name: string },
|
|
186
|
+
): boolean {
|
|
187
|
+
if (menuControl.openMenuId === null) return false;
|
|
188
|
+
if (key.name === "escape") menuControl.closeMenu();
|
|
189
|
+
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
137
193
|
/** True while a menu or an overlay owns the keyboard instead of the thread view. */
|
|
138
194
|
function keyboardOwnedElsewhere(menuOwnsKeyboard: boolean, overlay: KeyState["overlay"]): boolean {
|
|
139
195
|
return menuOwnsKeyboard || overlay !== "none";
|
|
140
196
|
}
|
|
141
197
|
|
|
198
|
+
/** The highlighted share option, or the public default when the choice is closed. */
|
|
199
|
+
function shareChoiceIndex(mode: Mode): number {
|
|
200
|
+
return mode.type === "shareChoice" ? mode.index : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface ShareChoiceActions {
|
|
204
|
+
setMode: (mode: Mode) => void;
|
|
205
|
+
isOwner: boolean;
|
|
206
|
+
publish: () => void;
|
|
207
|
+
openManageAccess: () => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Move the share-choice highlight by one row, clamped to the list. */
|
|
211
|
+
function movedShareChoice(index: number, delta: number): Mode {
|
|
212
|
+
const next = Math.min(SHARE_CHOICES.length - 1, Math.max(0, index + delta));
|
|
213
|
+
|
|
214
|
+
return { type: "shareChoice", index: next };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Commit the highlighted share option: public publishes a link, private opens manage-access. */
|
|
218
|
+
function commitShareChoice(index: number, actions: ShareChoiceActions): void {
|
|
219
|
+
const picked = SHARE_CHOICES[index]?.choice;
|
|
220
|
+
|
|
221
|
+
actions.setMode({ type: "normal" });
|
|
222
|
+
|
|
223
|
+
if (!actions.isOwner) return;
|
|
224
|
+
|
|
225
|
+
if (picked === "private") actions.openManageAccess();
|
|
226
|
+
else actions.publish();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Route one key to the open share choice: escape cancels, arrows move, enter commits. */
|
|
230
|
+
function handleShareChoiceKey(
|
|
231
|
+
index: number,
|
|
232
|
+
key: { name: string },
|
|
233
|
+
actions: ShareChoiceActions,
|
|
234
|
+
): void {
|
|
235
|
+
if (key.name === "escape") return actions.setMode({ type: "normal" });
|
|
236
|
+
|
|
237
|
+
if (key.name === "down" || key.name === "j") return actions.setMode(movedShareChoice(index, 1));
|
|
238
|
+
|
|
239
|
+
if (key.name === "up" || key.name === "k") return actions.setMode(movedShareChoice(index, -1));
|
|
240
|
+
|
|
241
|
+
if (key.name === "return" || key.name === "enter") commitShareChoice(index, actions);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* An app-level dialog owns the keyboard while open: the manage-access dialog takes its own keys,
|
|
246
|
+
* and the share choice moves the highlight, commits on enter, and cancels on escape. Returns true
|
|
247
|
+
* when a dialog consumed the key so the caller stops the grammar.
|
|
248
|
+
*/
|
|
249
|
+
function appDialogOwnsKey(
|
|
250
|
+
accessDialogOpen: boolean,
|
|
251
|
+
mode: Mode,
|
|
252
|
+
key: { name: string },
|
|
253
|
+
actions: ShareChoiceActions,
|
|
254
|
+
): boolean {
|
|
255
|
+
if (accessDialogOpen) return true;
|
|
256
|
+
|
|
257
|
+
if (mode.type !== "shareChoice") return false;
|
|
258
|
+
|
|
259
|
+
handleShareChoiceKey(mode.index, key, actions);
|
|
260
|
+
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
142
264
|
/** The footer submit fires only for the owner of an unresolved review, never an observer. */
|
|
143
265
|
function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
|
|
144
266
|
return isOwner && !resolved && !observer;
|
|
145
267
|
}
|
|
146
268
|
|
|
269
|
+
/** The author display name for a comment's hover tooltip: the reviewer's own name for their notes, else the resolved collaborator name. */
|
|
270
|
+
function authorLabelResolver(
|
|
271
|
+
ownName: string | undefined,
|
|
272
|
+
participants: Identity[] | undefined,
|
|
273
|
+
overrides: Record<string, string>,
|
|
274
|
+
): (annotation: Annotation) => string | undefined {
|
|
275
|
+
return (annotation) =>
|
|
276
|
+
annotation.author === undefined
|
|
277
|
+
? (ownName ?? "you")
|
|
278
|
+
: resolveDisplayName(annotation.author, participants, overrides);
|
|
279
|
+
}
|
|
280
|
+
|
|
147
281
|
/** The Project pane body: the resolved repo's changed files in changes mode, its full tree otherwise. */
|
|
148
282
|
function ProjectPanelBody(props: {
|
|
149
283
|
mode: ProjectPanelMode;
|
|
@@ -153,6 +287,7 @@ function ProjectPanelBody(props: {
|
|
|
153
287
|
onOpenChangedFile: (path: string) => void;
|
|
154
288
|
onOpenProjectFile: (path: string) => void;
|
|
155
289
|
commentCounts?: ReadonlyMap<string, number>;
|
|
290
|
+
focused?: boolean;
|
|
156
291
|
theme: Theme;
|
|
157
292
|
}): React.ReactNode {
|
|
158
293
|
const [changes, setChanges] = useState<readonly DiffFileContents[]>([]);
|
|
@@ -183,6 +318,7 @@ function ProjectPanelBody(props: {
|
|
|
183
318
|
files={changes}
|
|
184
319
|
onSelectFile={props.onOpenChangedFile}
|
|
185
320
|
commentCounts={props.commentCounts}
|
|
321
|
+
focused={props.focused}
|
|
186
322
|
theme={props.theme}
|
|
187
323
|
/>
|
|
188
324
|
);
|
|
@@ -192,6 +328,7 @@ function ProjectPanelBody(props: {
|
|
|
192
328
|
key={props.reloadKey}
|
|
193
329
|
loadFiles={props.loadProjectFiles}
|
|
194
330
|
onSelectFile={props.onOpenProjectFile}
|
|
331
|
+
focused={props.focused}
|
|
195
332
|
theme={props.theme}
|
|
196
333
|
/>
|
|
197
334
|
);
|
|
@@ -207,7 +344,11 @@ function usableScreenReached(
|
|
|
207
344
|
}
|
|
208
345
|
|
|
209
346
|
/** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
|
|
210
|
-
function cheatsheetFor(
|
|
347
|
+
function cheatsheetFor(
|
|
348
|
+
keyBindings: KeyBindings,
|
|
349
|
+
threadViewActive: boolean,
|
|
350
|
+
hint: string,
|
|
351
|
+
): CheatsheetSection[] {
|
|
211
352
|
const base = keyBindings.cheatsheet();
|
|
212
353
|
|
|
213
354
|
if (!threadViewActive) {
|
|
@@ -217,9 +358,9 @@ function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): Che
|
|
|
217
358
|
return [
|
|
218
359
|
...THREAD_VIEW_CHEATSHEET,
|
|
219
360
|
{ title: "Session", entries: [...THREAD_CHORD_ENTRIES] },
|
|
220
|
-
{ title: "Diff", entries:
|
|
221
|
-
{ title: "Rail", entries:
|
|
222
|
-
{ title: "Tree", entries:
|
|
361
|
+
{ title: "Diff", entries: diffChordEntries(hint) },
|
|
362
|
+
{ title: "Rail", entries: railChordEntries(hint) },
|
|
363
|
+
{ title: "Tree", entries: treeChordEntries(hint) },
|
|
223
364
|
...base.filter((section) => section.title === "Agent terminal"),
|
|
224
365
|
];
|
|
225
366
|
}
|
|
@@ -232,6 +373,125 @@ function initialThreadsOpen(
|
|
|
232
373
|
return layout ? layout.threads : sessionId === undefined;
|
|
233
374
|
}
|
|
234
375
|
|
|
376
|
+
function initialFocusedPane(sessionId: string | undefined, zoomed: boolean): FocusPane {
|
|
377
|
+
if (sessionId === undefined) return "threads";
|
|
378
|
+
|
|
379
|
+
return zoomed ? "changes" : "thread";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function bareShellDefersKeys(
|
|
383
|
+
sessionIsNull: boolean,
|
|
384
|
+
overlayIsNone: boolean,
|
|
385
|
+
focusedPane: FocusPane,
|
|
386
|
+
): boolean {
|
|
387
|
+
return sessionIsNull && overlayIsNone && focusedPane !== "threads";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function surfaceSuspended(base: boolean, focusedPane: FocusPane, pane: FocusPane): boolean {
|
|
391
|
+
return base || focusedPane !== pane;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function threadsNavHandled(params: {
|
|
395
|
+
focusedPane: FocusPane;
|
|
396
|
+
quiet: boolean;
|
|
397
|
+
key: { name: string };
|
|
398
|
+
count: number;
|
|
399
|
+
setInboxCursor: (update: (cursor: number) => number) => void;
|
|
400
|
+
openSession: () => void;
|
|
401
|
+
}): boolean {
|
|
402
|
+
const { focusedPane, quiet, key, count } = params;
|
|
403
|
+
|
|
404
|
+
if (focusedPane !== "threads" || !quiet || count === 0) return false;
|
|
405
|
+
if (key.name === "j" || key.name === "down") {
|
|
406
|
+
params.setInboxCursor((cursor) => Math.min(cursor + 1, count - 1));
|
|
407
|
+
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
if (key.name === "k" || key.name === "up") {
|
|
411
|
+
params.setInboxCursor((cursor) => Math.max(cursor - 1, 0));
|
|
412
|
+
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
if (key.name === "return" || key.name === "enter") {
|
|
416
|
+
params.openSession();
|
|
417
|
+
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function appLeaderHandled(params: {
|
|
425
|
+
focusedPane: FocusPane;
|
|
426
|
+
key: { name: string; shift?: boolean; ctrl?: boolean; meta?: boolean; super?: boolean };
|
|
427
|
+
leaderCombos: readonly string[];
|
|
428
|
+
pending: { current: boolean };
|
|
429
|
+
runLeaderCommand: (key: { name: string; shift?: boolean }) => void;
|
|
430
|
+
}): boolean {
|
|
431
|
+
const { focusedPane, key, leaderCombos, pending, runLeaderCommand } = params;
|
|
432
|
+
|
|
433
|
+
if (focusedPane !== "threads" && focusedPane !== "project") return false;
|
|
434
|
+
if (pending.current) {
|
|
435
|
+
pending.current = false;
|
|
436
|
+
if (key.name !== "escape") runLeaderCommand(key);
|
|
437
|
+
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
if (matchesLeader(key, leaderCombos)) {
|
|
441
|
+
pending.current = true;
|
|
442
|
+
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function visiblePanes(
|
|
450
|
+
sidebarOpen: boolean,
|
|
451
|
+
threadShown: boolean,
|
|
452
|
+
changesOpen: boolean,
|
|
453
|
+
projectOpen: boolean,
|
|
454
|
+
): FocusPane[] {
|
|
455
|
+
const panes: FocusPane[] = [];
|
|
456
|
+
|
|
457
|
+
if (sidebarOpen) panes.push("threads");
|
|
458
|
+
if (threadShown) panes.push("thread");
|
|
459
|
+
if (changesOpen) panes.push("changes");
|
|
460
|
+
if (projectOpen) panes.push("project");
|
|
461
|
+
|
|
462
|
+
return panes;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function nextFocusPane(
|
|
466
|
+
current: FocusPane,
|
|
467
|
+
panes: FocusPane[],
|
|
468
|
+
backward: boolean,
|
|
469
|
+
): FocusPane {
|
|
470
|
+
if (panes.length === 0) return current;
|
|
471
|
+
const index = panes.indexOf(current);
|
|
472
|
+
const step = backward ? -1 : 1;
|
|
473
|
+
|
|
474
|
+
return panes[(index + step + panes.length) % panes.length] ?? current;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** The panes that can own the keyboard: a diff's Thread pane is an inert placeholder, so it is out. */
|
|
478
|
+
function navigableFocusPanes(
|
|
479
|
+
sidebarOpen: boolean,
|
|
480
|
+
zoomed: boolean,
|
|
481
|
+
isDiff: boolean,
|
|
482
|
+
changesOpen: boolean,
|
|
483
|
+
projectOpen: boolean,
|
|
484
|
+
): FocusPane[] {
|
|
485
|
+
return visiblePanes(sidebarOpen, !zoomed && !isDiff, changesOpen, projectOpen);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** The layout hid or inert-ed the focused pane: fall back to the first navigable one, else stay put. */
|
|
489
|
+
export function reconciledFocus(navigable: FocusPane[], focusedPane: FocusPane): FocusPane | null {
|
|
490
|
+
if (navigable.length === 0 || navigable.includes(focusedPane)) return null;
|
|
491
|
+
|
|
492
|
+
return navigable[0] ?? null;
|
|
493
|
+
}
|
|
494
|
+
|
|
235
495
|
export function App({
|
|
236
496
|
home,
|
|
237
497
|
sessionId,
|
|
@@ -318,6 +578,9 @@ export function App({
|
|
|
318
578
|
const [sidebarOpen, setSidebarOpen] = useState(() => initialThreadsOpen(layout, sessionId));
|
|
319
579
|
// the Changes + Project right region and its editor grid (tabs, splits, zoom)
|
|
320
580
|
const workbench = useChangesWorkbench({ layout });
|
|
581
|
+
const [focusedPane, setFocusedPane] = useState<FocusPane>(() =>
|
|
582
|
+
initialFocusedPane(sessionId, workbench.zoomed),
|
|
583
|
+
);
|
|
321
584
|
// only the session view persists from here; the bare shell owns its own (NoThreadShell), so this
|
|
322
585
|
// workbench stays inactive with no session and never clobbers what the shell saved
|
|
323
586
|
useRememberLayout(
|
|
@@ -331,6 +594,7 @@ export function App({
|
|
|
331
594
|
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
332
595
|
// the top-left settings gear drop-down and the centered dialog it opens
|
|
333
596
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
597
|
+
const [accessDialogOpen, setAccessDialogOpen] = useState(false);
|
|
334
598
|
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
335
599
|
// unified or side-by-side diff; split only lays out when the Changes pane is zoomed
|
|
336
600
|
const [diffView, setDiffView] = useState<DiffViewMode>("split");
|
|
@@ -352,6 +616,7 @@ export function App({
|
|
|
352
616
|
const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
|
|
353
617
|
const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
|
|
354
618
|
const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
|
|
619
|
+
const [identity, setIdentity] = useState<IdentityConfig>({ provider: "typed" });
|
|
355
620
|
const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
|
|
356
621
|
const [skills, setSkills] = useState<SlashItem[]>([]);
|
|
357
622
|
const paletteNames = useMemo(
|
|
@@ -371,6 +636,7 @@ export function App({
|
|
|
371
636
|
setThemeName(config.ui.theme);
|
|
372
637
|
setThemeOverrides(config.themeOverrides);
|
|
373
638
|
setAuthorNames(config.authors);
|
|
639
|
+
setIdentity(config.identity);
|
|
374
640
|
setQuickActions(config.actions);
|
|
375
641
|
setSkills(loadSkills(config.skillsPath));
|
|
376
642
|
setAutoClose(config.ui.autoClose);
|
|
@@ -404,6 +670,29 @@ export function App({
|
|
|
404
670
|
if (!known) setMode({ type: "nameSelf", text: "" });
|
|
405
671
|
}, [role, selfAuthor, session]);
|
|
406
672
|
|
|
673
|
+
// Bumped on every identity change so a slow GitHub sync that lands after a newer rename is discarded.
|
|
674
|
+
const identityGenerationRef = useRef(0);
|
|
675
|
+
const applyIdentity = (next: IdentityConfig): void => {
|
|
676
|
+
identityGenerationRef.current += 1;
|
|
677
|
+
setIdentity(next);
|
|
678
|
+
try {
|
|
679
|
+
persistIdentity(next);
|
|
680
|
+
} catch {
|
|
681
|
+
controller.setStatus("could not save identity to config");
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const syncGithubIdentity = (): void => {
|
|
685
|
+
const generation = (identityGenerationRef.current += 1);
|
|
686
|
+
void resolveGithubIdentity().then((github) => {
|
|
687
|
+
if (identityGenerationRef.current !== generation) return;
|
|
688
|
+
if (!github) return controller.setStatus("GitHub not connected - run gh auth login");
|
|
689
|
+
const name = github.name?.trim() || github.login;
|
|
690
|
+
|
|
691
|
+
applyIdentity({ name, provider: "github" });
|
|
692
|
+
controller.setStatus(`identity synced from GitHub - ${name}`);
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
|
|
407
696
|
// ── settings dialog: config-backed model, navigation, persistence ──
|
|
408
697
|
const {
|
|
409
698
|
settingsNav,
|
|
@@ -426,6 +715,13 @@ export function App({
|
|
|
426
715
|
quickActions,
|
|
427
716
|
setQuickActions,
|
|
428
717
|
setMenuDialog,
|
|
718
|
+
identityName: identity.name,
|
|
719
|
+
identityProvider: identity.provider,
|
|
720
|
+
onSyncGithubIdentity: syncGithubIdentity,
|
|
721
|
+
onRenameDisplayName: () => {
|
|
722
|
+
setMenuDialog(null);
|
|
723
|
+
setMode({ type: "renameSelf", text: identity.name ?? "" });
|
|
724
|
+
},
|
|
429
725
|
});
|
|
430
726
|
|
|
431
727
|
// ── derived view model ──────────────────────
|
|
@@ -544,6 +840,9 @@ export function App({
|
|
|
544
840
|
setAuthorNames((prev) => ({ ...prev, [id]: name }));
|
|
545
841
|
},
|
|
546
842
|
renameThread: (id: string, title: string) => controller.renameSession(id, title),
|
|
843
|
+
setLocalIdentityName: (name: string) => {
|
|
844
|
+
applyIdentity(name ? { name, provider: "typed" } : { provider: "typed" });
|
|
845
|
+
},
|
|
547
846
|
liveInput,
|
|
548
847
|
setCursor,
|
|
549
848
|
setInboxCursor,
|
|
@@ -570,12 +869,75 @@ export function App({
|
|
|
570
869
|
|
|
571
870
|
const overlay = resolveOverlay(mode, completion.phase, walking);
|
|
572
871
|
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
//
|
|
872
|
+
const menuControl = useMenuControlState();
|
|
873
|
+
const menuOwnsKeyboard = keyboardHeldByMenu(menuDialog, menuControl.openMenuId);
|
|
874
|
+
// an overlay (submit, walk, prompt, confirm, share choice) or the menu takes the
|
|
875
|
+
// keyboard from the thread view; the view suspends its own grammar meanwhile
|
|
576
876
|
const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay);
|
|
577
877
|
|
|
878
|
+
const leaderCombos = leaderCombosFor(keysRef.current.leader);
|
|
879
|
+
const leaderPending = useRef(false);
|
|
880
|
+
const navigablePanes = navigableFocusPanes(
|
|
881
|
+
sidebarOpen,
|
|
882
|
+
workbench.zoomed,
|
|
883
|
+
isDiff,
|
|
884
|
+
workbench.changesOpen,
|
|
885
|
+
workbench.projectOpen,
|
|
886
|
+
);
|
|
887
|
+
useEffect(() => {
|
|
888
|
+
// the bare shell drives its panes from its own workbench, so only reconcile against this one
|
|
889
|
+
if (!session) return;
|
|
890
|
+
const next = reconciledFocus(navigablePanes, focusedPane);
|
|
891
|
+
|
|
892
|
+
if (next) setFocusedPane(next);
|
|
893
|
+
}, [session, navigablePanes, focusedPane]);
|
|
894
|
+
const cyclePanes = (backward: boolean): void =>
|
|
895
|
+
setFocusedPane((current) => nextFocusPane(current, navigablePanes, backward));
|
|
896
|
+
const runLeaderCommand = (key: { name: string; shift?: boolean }): void => {
|
|
897
|
+
if (key.name === "tab") return cyclePanes(Boolean(key.shift));
|
|
898
|
+
|
|
899
|
+
dispatchLeaderCommand(
|
|
900
|
+
key,
|
|
901
|
+
{ composing: threadComposing, isOwner, resolved, treeActive: railTab === "tree", isDiff },
|
|
902
|
+
dispatch,
|
|
903
|
+
);
|
|
904
|
+
};
|
|
905
|
+
// clicking a sidebar thread moves the cursor onto it too, so the row shows its selected backdrop at once
|
|
906
|
+
const openThread = (id: string): void => {
|
|
907
|
+
const index = grouped.ordered.findIndex((thread) => thread.id === id);
|
|
908
|
+
|
|
909
|
+
if (index >= 0) setInboxCursor(() => index);
|
|
910
|
+
controller.open(id);
|
|
911
|
+
};
|
|
912
|
+
|
|
578
913
|
useKeyboard((key) => {
|
|
914
|
+
if (quitKeyHandled(key, onExit)) return;
|
|
915
|
+
// an app-level dialog (manage-access, share choice) owns the keyboard while open
|
|
916
|
+
if (
|
|
917
|
+
appDialogOwnsKey(accessDialogOpen, mode, key, {
|
|
918
|
+
setMode,
|
|
919
|
+
isOwner,
|
|
920
|
+
publish: () => controller.share(),
|
|
921
|
+
openManageAccess: () => setAccessDialogOpen(true),
|
|
922
|
+
})
|
|
923
|
+
)
|
|
924
|
+
return;
|
|
925
|
+
if (menuModalHandled(menuControl, key)) return;
|
|
926
|
+
if (
|
|
927
|
+
appLeaderHandled({ focusedPane, key, leaderCombos, pending: leaderPending, runLeaderCommand })
|
|
928
|
+
)
|
|
929
|
+
return;
|
|
930
|
+
if (
|
|
931
|
+
threadsNavHandled({
|
|
932
|
+
focusedPane,
|
|
933
|
+
quiet: overlay === "none" && !menuOwnsKeyboard && !threadComposing,
|
|
934
|
+
key,
|
|
935
|
+
count: grouped.ordered.length,
|
|
936
|
+
setInboxCursor,
|
|
937
|
+
openSession: () => dispatch({ type: "openSession" }),
|
|
938
|
+
})
|
|
939
|
+
)
|
|
940
|
+
return;
|
|
579
941
|
// The thread view owns the document grammar while active (its own
|
|
580
942
|
// useKeyboard handles marks, comments, and ctrl+q); the session chords
|
|
581
943
|
// (submit, share, edit, walk, the rail) resolve here, and the keymap only
|
|
@@ -600,8 +962,9 @@ export function App({
|
|
|
600
962
|
if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
|
|
601
963
|
// the toast is non-modal: escape only dismisses it when nothing else owns
|
|
602
964
|
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
603
|
-
if (toast
|
|
965
|
+
if (toastDismissRequested(toast !== null, key, overlay, mode.type === "span"))
|
|
604
966
|
return controller.dismissToast();
|
|
967
|
+
if (bareShellDefersKeys(session === null, overlay === "none", focusedPane)) return;
|
|
605
968
|
const state = buildKeyState({
|
|
606
969
|
keys: keysRef.current,
|
|
607
970
|
observer,
|
|
@@ -646,12 +1009,13 @@ export function App({
|
|
|
646
1009
|
<MenuChrome
|
|
647
1010
|
menuDialog={menuDialog}
|
|
648
1011
|
theme={theme}
|
|
649
|
-
keybindsSections={cheatsheetFor(keyBindings, threadViewActive)}
|
|
1012
|
+
keybindsSections={cheatsheetFor(keyBindings, threadViewActive, leaderHint(leaderCombos))}
|
|
650
1013
|
settingsCategories={settingsCategories}
|
|
651
1014
|
settingsValues={settingsValues}
|
|
652
1015
|
settingsNav={settingsNav}
|
|
653
1016
|
onCategorySelect={onCategorySelect}
|
|
654
1017
|
cycleSetting={cycleSetting}
|
|
1018
|
+
onClose={() => setMenuDialog(null)}
|
|
655
1019
|
/>
|
|
656
1020
|
);
|
|
657
1021
|
|
|
@@ -664,6 +1028,7 @@ export function App({
|
|
|
664
1028
|
<NoThreadShell
|
|
665
1029
|
rows={grouped.rows}
|
|
666
1030
|
inboxCursor={inboxCursor}
|
|
1031
|
+
onOpenThread={openThread}
|
|
667
1032
|
mode={mode}
|
|
668
1033
|
theme={theme}
|
|
669
1034
|
controller={controller}
|
|
@@ -672,6 +1037,9 @@ export function App({
|
|
|
672
1037
|
onOpenMenu={() => setMenuDialog("settings")}
|
|
673
1038
|
sidebarOpen={sidebarOpen}
|
|
674
1039
|
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
1040
|
+
focusedPane={focusedPane}
|
|
1041
|
+
onFocusPane={setFocusedPane}
|
|
1042
|
+
menuControl={menuControl}
|
|
675
1043
|
pinnedIds={pinnedIds}
|
|
676
1044
|
onPin={togglePin}
|
|
677
1045
|
onRename={(id, title) => setMode({ type: "renameThread", sessionId: id, text: title })}
|
|
@@ -686,6 +1054,11 @@ export function App({
|
|
|
686
1054
|
);
|
|
687
1055
|
|
|
688
1056
|
const activeSession = session;
|
|
1057
|
+
const resolveAuthorLabel = authorLabelResolver(
|
|
1058
|
+
identity.name,
|
|
1059
|
+
activeSession.participants,
|
|
1060
|
+
authorNames,
|
|
1061
|
+
);
|
|
689
1062
|
|
|
690
1063
|
if (isCompletionOverlayPhase(completion) && activeSession.verdict)
|
|
691
1064
|
return (
|
|
@@ -735,12 +1108,19 @@ export function App({
|
|
|
735
1108
|
runEditorHandOff();
|
|
736
1109
|
};
|
|
737
1110
|
|
|
738
|
-
//
|
|
1111
|
+
// the share choice's public option: publish the plan, copy the ssh line
|
|
739
1112
|
const onShareRequest = (): void => {
|
|
740
1113
|
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
741
1114
|
controller.share();
|
|
742
1115
|
};
|
|
743
1116
|
|
|
1117
|
+
// the share choice's private option opens the manage-access allowlist surface
|
|
1118
|
+
const onPrivateShareRequest = (): void => {
|
|
1119
|
+
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
1120
|
+
setAccessDialogOpen(true);
|
|
1121
|
+
};
|
|
1122
|
+
const allowedLogins = allowedGithubLogins(session);
|
|
1123
|
+
|
|
744
1124
|
// clicking the rail Submit button: same read-only answer as the submit key
|
|
745
1125
|
const onSubmitRequest = (): void => {
|
|
746
1126
|
if (observer) return controller.setStatus("observer - read-only");
|
|
@@ -763,221 +1143,268 @@ export function App({
|
|
|
763
1143
|
<SlashSkillsContext.Provider value={skills}>
|
|
764
1144
|
<PaletteNamesContext.Provider value={paletteNames}>
|
|
765
1145
|
<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
|
-
|
|
1146
|
+
<MenuControlProvider value={menuControl}>
|
|
1147
|
+
<AppShell
|
|
1148
|
+
sidebarOpen={sidebarOpen}
|
|
1149
|
+
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
1150
|
+
onOpenMenu={() => setMenuDialog("settings")}
|
|
1151
|
+
onFocusPane={setFocusedPane}
|
|
1152
|
+
threadsPanel={
|
|
1153
|
+
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
1154
|
+
<ThreadTree
|
|
1155
|
+
rows={grouped.rows}
|
|
1156
|
+
cursor={inboxCursor}
|
|
1157
|
+
activeId={activeSession.id}
|
|
1158
|
+
focused={focusedPane === "threads"}
|
|
1159
|
+
pinnedIds={pinnedIds}
|
|
1160
|
+
width={30}
|
|
1161
|
+
onSelect={openThread}
|
|
1162
|
+
onPin={togglePin}
|
|
1163
|
+
onRename={(id, title) =>
|
|
1164
|
+
setMode({ type: "renameThread", sessionId: id, text: title })
|
|
1165
|
+
}
|
|
1166
|
+
theme={theme}
|
|
1167
|
+
/>
|
|
1168
|
+
</scrollbox>
|
|
1169
|
+
}
|
|
1170
|
+
threadTitle={threadTitle(activeSession)}
|
|
1171
|
+
threadActions={
|
|
1172
|
+
showOwnerActions ? (
|
|
1173
|
+
<Toolbar>
|
|
1174
|
+
<Button onPress={onEditRequest} theme={theme}>
|
|
1175
|
+
{" edit "}
|
|
1176
|
+
</Button>
|
|
1177
|
+
<Button onPress={() => dispatch({ type: "share" })} theme={theme}>
|
|
1178
|
+
{" share "}
|
|
1179
|
+
</Button>
|
|
1180
|
+
</Toolbar>
|
|
1181
|
+
) : undefined
|
|
1182
|
+
}
|
|
1183
|
+
threadPanel={
|
|
1184
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
1185
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
1186
|
+
{isPixelPrototype ? (
|
|
1187
|
+
<PrototypePixels
|
|
1188
|
+
prototypePath={prototypePath}
|
|
1189
|
+
canComment={prototypeCanComment}
|
|
1190
|
+
onCommentElement={onCommentPrototype}
|
|
1191
|
+
onComposingChange={setPrototypeComposing}
|
|
1192
|
+
hidden={chromeHidden}
|
|
1193
|
+
/>
|
|
1194
|
+
) : isDiff ? (
|
|
1195
|
+
<box
|
|
1196
|
+
style={{
|
|
1197
|
+
flexGrow: 1,
|
|
1198
|
+
flexDirection: "column",
|
|
1199
|
+
alignItems: "center",
|
|
1200
|
+
justifyContent: "center",
|
|
1201
|
+
}}
|
|
1202
|
+
>
|
|
1203
|
+
<text fg={theme.textDim}>Select a thread</text>
|
|
1204
|
+
</box>
|
|
1205
|
+
) : (
|
|
1206
|
+
<ThreadView
|
|
1207
|
+
session={activeSession}
|
|
1208
|
+
suspended={surfaceSuspended(threadViewSuspended, focusedPane, "thread")}
|
|
1209
|
+
editOrphanCount={editOrphanCount}
|
|
1210
|
+
onComposingChange={setThreadComposing}
|
|
1211
|
+
leaderCombos={leaderCombos}
|
|
1212
|
+
onLeaderCommand={runLeaderCommand}
|
|
1213
|
+
resolved={resolved}
|
|
1214
|
+
onObserverBlocked={(reason) =>
|
|
1215
|
+
controller.setStatus(
|
|
1216
|
+
reason === "observer"
|
|
1217
|
+
? "observer - read-only"
|
|
1218
|
+
: "review submitted - read-only",
|
|
1219
|
+
)
|
|
1220
|
+
}
|
|
1221
|
+
onCursorChange={setCursor}
|
|
1222
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
1223
|
+
onFocusAnnotation={setFocusedAnnotationId}
|
|
1224
|
+
display={display}
|
|
1225
|
+
marks={marks}
|
|
1226
|
+
quickActions={quickActions}
|
|
1227
|
+
observer={observer}
|
|
1228
|
+
onAnnotate={(span, body) =>
|
|
1229
|
+
void controller.annotate(
|
|
1230
|
+
"comment",
|
|
1231
|
+
span.start.blockIndex,
|
|
1232
|
+
span.start.char,
|
|
1233
|
+
span.end.char,
|
|
1234
|
+
body,
|
|
1235
|
+
span.end.blockIndex,
|
|
1236
|
+
)
|
|
1237
|
+
}
|
|
1238
|
+
onReply={(rootAnnotationId, body) =>
|
|
1239
|
+
void controller.reply(rootAnnotationId, body)
|
|
1240
|
+
}
|
|
1241
|
+
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
1242
|
+
resolveAuthorLabel={resolveAuthorLabel}
|
|
1243
|
+
onExit={() => onExit?.(0)}
|
|
1244
|
+
/>
|
|
1245
|
+
)}
|
|
1246
|
+
</box>
|
|
1247
|
+
{threadFooter}
|
|
1248
|
+
</box>
|
|
1249
|
+
}
|
|
1250
|
+
changesOpen={workbench.changesOpen}
|
|
1251
|
+
projectOpen={workbench.projectOpen}
|
|
1252
|
+
onToggleChanges={workbench.toggleChanges}
|
|
1253
|
+
onToggleProject={workbench.toggleProject}
|
|
1254
|
+
onToggleRight={workbench.toggleRight}
|
|
1255
|
+
projectMode={workbench.projectMode}
|
|
1256
|
+
zoomHideThread={workbench.zoomed}
|
|
1257
|
+
changesFooter={threadFooter}
|
|
1258
|
+
changesPanel={
|
|
1259
|
+
<EditorGrid
|
|
1260
|
+
tree={workbench.grid}
|
|
1261
|
+
focusedGroupId={workbench.activeGroup}
|
|
1262
|
+
commentCounts={diffCommentCounts}
|
|
1263
|
+
onFocusGroup={workbench.focusGroup}
|
|
1264
|
+
onActivateTab={workbench.activate}
|
|
1265
|
+
onCloseTab={workbench.close}
|
|
1266
|
+
onSplit={workbench.split}
|
|
1267
|
+
onZoom={workbench.toggleZoom}
|
|
1268
|
+
zoomed={workbench.zoomed}
|
|
1269
|
+
renderTab={(tab, groupFocused) => (
|
|
1270
|
+
<GridTabContent
|
|
1271
|
+
tab={tab}
|
|
1272
|
+
rows={rows}
|
|
1273
|
+
surface={{
|
|
1274
|
+
session: activeSession,
|
|
1275
|
+
quickActions,
|
|
1276
|
+
observer,
|
|
1277
|
+
commentsEnabled: true,
|
|
1278
|
+
resolved,
|
|
1279
|
+
suspended: surfaceSuspended(
|
|
1280
|
+
threadViewSuspended || !groupFocused,
|
|
1281
|
+
focusedPane,
|
|
1282
|
+
"changes",
|
|
1283
|
+
),
|
|
1284
|
+
onComposingChange: setThreadComposing,
|
|
1285
|
+
onObserverBlocked: (reason) =>
|
|
1286
|
+
controller.setStatus(
|
|
1287
|
+
reason === "observer"
|
|
1288
|
+
? "observer - read-only"
|
|
1289
|
+
: "review submitted - read-only",
|
|
1290
|
+
),
|
|
1291
|
+
onCursorChange: setCursor,
|
|
1292
|
+
focusedAnnotationId,
|
|
1293
|
+
onFocusAnnotation: setFocusedAnnotationId,
|
|
1294
|
+
// a diff thread's Changes view is the artifact itself; any other thread's is the
|
|
1295
|
+
// live working-tree diff, so its notes carry a file target and anchor there
|
|
1296
|
+
onAnnotate: (span, body) =>
|
|
1297
|
+
void controller.annotate(
|
|
1298
|
+
"comment",
|
|
1299
|
+
span.start.blockIndex,
|
|
1300
|
+
span.start.char,
|
|
1301
|
+
span.end.char,
|
|
1302
|
+
body,
|
|
1303
|
+
span.end.blockIndex,
|
|
1304
|
+
isDiff
|
|
1305
|
+
? { kind: "artifact" }
|
|
1306
|
+
: { kind: "file", path: "", rev: "worktree" },
|
|
1307
|
+
),
|
|
1308
|
+
onReply: (rootAnnotationId, body) =>
|
|
1309
|
+
void controller.reply(rootAnnotationId, body),
|
|
1310
|
+
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1311
|
+
resolveAuthorLabel,
|
|
1312
|
+
leaderCombos,
|
|
1313
|
+
onLeaderCommand: runLeaderCommand,
|
|
1314
|
+
onExit: () => onExit?.(0),
|
|
1315
|
+
}}
|
|
1316
|
+
rejectedRows={rejectedRows}
|
|
1317
|
+
fold={diffFold}
|
|
1318
|
+
fileStats={controller.fileStats()}
|
|
1319
|
+
split={diffView === "split" && workbench.zoomed}
|
|
1320
|
+
dimmed={walking}
|
|
1321
|
+
readFile={(path) => controller.repoReadFile(path)}
|
|
1322
|
+
onAddFileComment={(path, anchor, body) =>
|
|
1323
|
+
void controller.addComment(
|
|
1324
|
+
anchor,
|
|
1325
|
+
{ kind: "file", path, rev: "worktree" },
|
|
842
1326
|
body,
|
|
843
|
-
span.end.blockIndex,
|
|
844
1327
|
)
|
|
845
1328
|
}
|
|
846
|
-
|
|
847
|
-
void controller.reply(rootAnnotationId, body)
|
|
848
|
-
}
|
|
849
|
-
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
850
|
-
onExit={() => onExit?.(0)}
|
|
1329
|
+
theme={theme}
|
|
851
1330
|
/>
|
|
852
1331
|
)}
|
|
1332
|
+
theme={theme}
|
|
1333
|
+
/>
|
|
1334
|
+
}
|
|
1335
|
+
projectPanel={
|
|
1336
|
+
<ProjectPanelBody
|
|
1337
|
+
mode={workbench.projectMode}
|
|
1338
|
+
loadChanges={() => controller.repoChanges()}
|
|
1339
|
+
loadProjectFiles={() => controller.repoFiles()}
|
|
1340
|
+
reloadKey={activeSession.id}
|
|
1341
|
+
// the Changes navigator always opens a changed file as a diff - a diff review shows its
|
|
1342
|
+
// captured snapshot, every other thread the live working-tree diff
|
|
1343
|
+
onOpenChangedFile={(path) => workbench.openFile(path, "diff")}
|
|
1344
|
+
onOpenProjectFile={(path) => workbench.openFile(path, "contents")}
|
|
1345
|
+
commentCounts={diffCommentCounts}
|
|
1346
|
+
focused={focusedPane === "project"}
|
|
1347
|
+
theme={theme}
|
|
1348
|
+
/>
|
|
1349
|
+
}
|
|
1350
|
+
theme={theme}
|
|
1351
|
+
>
|
|
1352
|
+
<TrailingOverlays
|
|
1353
|
+
walking={walking}
|
|
1354
|
+
walk={walk}
|
|
1355
|
+
walkFileList={walkFileList}
|
|
1356
|
+
viewedPaths={viewedPaths}
|
|
1357
|
+
session={activeSession}
|
|
1358
|
+
terminalWidth={terminalWidth}
|
|
1359
|
+
theme={theme}
|
|
1360
|
+
mode={mode}
|
|
1361
|
+
toast={toast}
|
|
1362
|
+
setMode={setMode}
|
|
1363
|
+
dispatch={dispatch}
|
|
1364
|
+
/>
|
|
1365
|
+
{submitConfirmState !== null ? (
|
|
1366
|
+
<box
|
|
1367
|
+
onMouseUp={submitConfirmState.onCancel}
|
|
1368
|
+
style={{
|
|
1369
|
+
position: "absolute",
|
|
1370
|
+
left: 0,
|
|
1371
|
+
top: 0,
|
|
1372
|
+
width: "100%",
|
|
1373
|
+
height: "100%",
|
|
1374
|
+
justifyContent: "center",
|
|
1375
|
+
alignItems: "center",
|
|
1376
|
+
}}
|
|
1377
|
+
>
|
|
1378
|
+
<box onMouseUp={(event) => event.stopPropagation()}>
|
|
1379
|
+
<ConfirmCard {...submitConfirmState} theme={theme} />
|
|
1380
|
+
</box>
|
|
853
1381
|
</box>
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
tree={workbench.grid}
|
|
868
|
-
focusedGroupId={workbench.activeGroup}
|
|
869
|
-
commentCounts={diffCommentCounts}
|
|
870
|
-
onFocusGroup={workbench.focusGroup}
|
|
871
|
-
onActivateTab={workbench.activate}
|
|
872
|
-
onCloseTab={workbench.close}
|
|
873
|
-
onSplit={workbench.split}
|
|
874
|
-
onZoom={workbench.toggleZoom}
|
|
875
|
-
zoomed={workbench.zoomed}
|
|
876
|
-
renderTab={(tab, groupFocused) => (
|
|
877
|
-
<GridTabContent
|
|
878
|
-
tab={tab}
|
|
879
|
-
rows={rows}
|
|
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
|
-
)}
|
|
1382
|
+
) : null}
|
|
1383
|
+
<ManageAccessDialog
|
|
1384
|
+
isOpen={accessDialogOpen}
|
|
1385
|
+
logins={allowedLogins}
|
|
1386
|
+
onAdd={(login) => controller.setShareAccess([...allowedLogins, login])}
|
|
1387
|
+
onRemove={(login) =>
|
|
1388
|
+
controller.setShareAccess(allowedLogins.filter((entry) => entry !== login))
|
|
1389
|
+
}
|
|
1390
|
+
onCreateLink={() => {
|
|
1391
|
+
setAccessDialogOpen(false);
|
|
1392
|
+
controller.share();
|
|
1393
|
+
}}
|
|
1394
|
+
onClose={() => setAccessDialogOpen(false)}
|
|
932
1395
|
theme={theme}
|
|
933
1396
|
/>
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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}
|
|
1397
|
+
<ShareChoiceDialog
|
|
1398
|
+
isOpen={mode.type === "shareChoice"}
|
|
1399
|
+
selectedIndex={shareChoiceIndex(mode)}
|
|
1400
|
+
onPublicShare={onShareRequest}
|
|
1401
|
+
onPrivateShare={onPrivateShareRequest}
|
|
1402
|
+
onClose={() => setMode({ type: "normal" })}
|
|
946
1403
|
theme={theme}
|
|
947
1404
|
/>
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
>
|
|
951
|
-
<TrailingOverlays
|
|
952
|
-
walking={walking}
|
|
953
|
-
walk={walk}
|
|
954
|
-
walkFileList={walkFileList}
|
|
955
|
-
viewedPaths={viewedPaths}
|
|
956
|
-
session={activeSession}
|
|
957
|
-
terminalWidth={terminalWidth}
|
|
958
|
-
theme={theme}
|
|
959
|
-
mode={mode}
|
|
960
|
-
toast={toast}
|
|
961
|
-
setMode={setMode}
|
|
962
|
-
dispatch={dispatch}
|
|
963
|
-
/>
|
|
964
|
-
{submitConfirmState !== null ? (
|
|
965
|
-
<box
|
|
966
|
-
style={{
|
|
967
|
-
position: "absolute",
|
|
968
|
-
left: 0,
|
|
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>
|
|
1405
|
+
{menuChrome}
|
|
1406
|
+
</AppShell>
|
|
1407
|
+
</MenuControlProvider>
|
|
981
1408
|
</ThemeProvider>
|
|
982
1409
|
</PaletteNamesContext.Provider>
|
|
983
1410
|
</SlashSkillsContext.Provider>
|