@cueloop/client 0.1.0-alpha.77 → 0.1.0-alpha.79
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 +14 -13
- package/src/App.autoclose.test.tsx +3 -3
- package/src/App.diff.test.tsx +22 -5
- package/src/App.export.test.tsx +1 -2
- package/src/App.focus.test.ts +1 -59
- package/src/App.identity.test.tsx +3 -3
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +91 -26
- package/src/App.readonly.test.tsx +6 -5
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +39 -8
- package/src/App.test.tsx +33 -30
- package/src/App.toast.test.tsx +11 -4
- package/src/App.tsx +263 -274
- package/src/App.walk.test.tsx +11 -10
- package/src/App.workbench.test.tsx +86 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +1 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +19 -9
- package/src/app-view-model.ts +3 -1
- package/src/components/AnnotatableFileView.tsx +1 -1
- package/src/components/AnnotationCards.tsx +18 -2
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/CompletionOverlay.stories.tsx +15 -1
- package/src/components/CompletionOverlay.tsx +22 -5
- package/src/components/ConfirmCard.stories.tsx +2 -0
- package/src/components/ConfirmCard.tsx +45 -18
- package/src/components/ConfirmDialog.stories.tsx +3 -3
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +18 -11
- package/src/components/DiscussionMarkerRail.tsx +9 -4
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/GridTabContent.tsx +8 -2
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/SettingsDialog.tsx +1 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.tsx +4 -1
- package/src/components/ThreadTree.tsx +12 -7
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -36
- package/src/components/ThreadView.tsx +200 -67
- package/src/components/Toast.stories.tsx +7 -0
- package/src/components/Toast.tsx +15 -8
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +436 -182
- package/src/components/primitives/Button.tsx +6 -2
- package/src/components/primitives/Card.tsx +5 -2
- package/src/components/primitives/Dialog.tsx +2 -2
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tree.tsx +5 -1
- package/src/components/primitives/frame.ts +5 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +12 -4
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/config.test.ts +18 -0
- package/src/config.ts +8 -1
- package/src/diff-fold.ts +1 -1
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/intent-dispatch.test.ts +12 -3
- package/src/intent-dispatch.ts +10 -25
- package/src/key-bindings.ts +0 -6
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +0 -2
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/prototype-browser.test.ts +9 -1
- package/src/prototype-browser.ts +22 -0
- package/src/run.ts +39 -25
- package/src/serve.ts +1 -1
- package/src/share.ts +16 -2
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/test-support.ts +14 -0
- package/src/thread-chords.test.ts +55 -178
- package/src/thread-chords.ts +78 -169
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +3 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +60 -5
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +6 -1
- package/src/thread-controller.ts +386 -78
- package/src/use-annotation-surface.tsx +88 -63
- package/src/use-settings-dialog.tsx +14 -3
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/view-plan.test.ts +56 -6
- package/src/view-plan.ts +154 -5
- package/src/App.share-choice.test.tsx +0 -105
- package/src/components/ManageAccessDialog.stories.tsx +0 -36
- package/src/components/ManageAccessDialog.test.tsx +0 -94
- package/src/components/ManageAccessDialog.tsx +0 -120
- package/src/components/ShareChoiceDialog.stories.tsx +0 -34
- package/src/components/ShareChoiceDialog.test.tsx +0 -103
- package/src/components/ShareChoiceDialog.tsx +0 -82
- package/src/components/ThreadsSidebar.stories.tsx +0 -36
- package/src/components/ThreadsSidebar.tsx +0 -71
- package/src/components/agent-launcher.stories.tsx +0 -26
- package/src/components/agent-launcher.test.tsx +0 -50
- package/src/components/agent-launcher.tsx +0 -149
package/src/App.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import React, {
|
|
|
7
7
|
useSyncExternalStore,
|
|
8
8
|
} from "react";
|
|
9
9
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
10
|
-
import type { Clock } from "@opentui/core";
|
|
10
|
+
import type { Clock, KeyEvent } from "@opentui/core";
|
|
11
11
|
import { marksByDisplay, type Mark } from "./view-plan";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_KEYS,
|
|
@@ -42,7 +42,8 @@ import { KeyBindings, type CheatsheetSection } from "./key-bindings";
|
|
|
42
42
|
import { useReadySignal } from "./ready-signal";
|
|
43
43
|
import { ThemeProvider } from "./components/theme-context";
|
|
44
44
|
import { Button } from "./components/primitives/Button";
|
|
45
|
-
import {
|
|
45
|
+
import { ShareDialog } from "./components/ShareDialog";
|
|
46
|
+
import { shareDialogStore } from "./components/share-dialog-store";
|
|
46
47
|
import { Toolbar } from "./components/primitives/Toolbar";
|
|
47
48
|
import { groupInbox, projectName, threadTitle } from "./components/session-tree";
|
|
48
49
|
import { ThreadTree } from "./components/ThreadTree";
|
|
@@ -53,27 +54,32 @@ import { AppShell, type FocusPane, type ProjectPanelMode } from "./components/Ap
|
|
|
53
54
|
import { EditorGrid } from "./components/EditorGrid";
|
|
54
55
|
import { GridTabContent } from "./components/GridTabContent";
|
|
55
56
|
import { useChangesWorkbench } from "./use-changes-workbench";
|
|
57
|
+
import { useThreadBodyEditing } from "./use-thread-body-editing";
|
|
56
58
|
import { useRememberLayout } from "./use-remember-layout";
|
|
57
59
|
import type { LaunchLayout } from "./launch-layout";
|
|
58
|
-
import { ThreadFooter } from "./components/ThreadFooter";
|
|
60
|
+
import { ThreadFooter, THREAD_FOOTER_HEIGHT } from "./components/ThreadFooter";
|
|
59
61
|
import { ConfirmCard } from "./components/ConfirmCard";
|
|
60
|
-
import { ManageAccessDialog } from "./components/ManageAccessDialog";
|
|
61
62
|
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
|
|
62
63
|
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
resolveThreadChord,
|
|
70
|
-
THREAD_CHORD_ENTRIES,
|
|
71
|
-
treeChordEntries,
|
|
64
|
+
curationCommandEntries,
|
|
65
|
+
diffCommandEntries,
|
|
66
|
+
resolveNavKey,
|
|
67
|
+
resolveSessionChord,
|
|
68
|
+
sessionCommandEntries,
|
|
69
|
+
treeCommandEntries,
|
|
72
70
|
} from "./thread-chords";
|
|
73
71
|
import { type DiffFoldControls } from "./components/DiffContentView";
|
|
74
72
|
import { commentCountsByFile } from "./view-diff";
|
|
75
|
-
import { annotationTarget } from "@cueloop/schema";
|
|
76
|
-
import type {
|
|
73
|
+
import { annotationTarget, threadShareLinks } from "@cueloop/schema";
|
|
74
|
+
import type {
|
|
75
|
+
Annotation,
|
|
76
|
+
Artifact,
|
|
77
|
+
DiffFileContents,
|
|
78
|
+
Identity,
|
|
79
|
+
ShareLink,
|
|
80
|
+
Thread,
|
|
81
|
+
VerdictKind,
|
|
82
|
+
} from "@cueloop/schema";
|
|
77
83
|
import { PrototypePixels } from "./prototype-pixels";
|
|
78
84
|
import type { PrototypeElement } from "./prototype-browser";
|
|
79
85
|
import {
|
|
@@ -145,9 +151,19 @@ function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
|
|
|
145
151
|
return menuDialog !== null;
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
/**
|
|
149
|
-
function
|
|
150
|
-
return session?.
|
|
154
|
+
/** Whether a thread already has at least one live share link. */
|
|
155
|
+
function isSharedThread(session: Thread | null): boolean {
|
|
156
|
+
return session !== null && (threadShareLinks(session)?.length ?? 0) > 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A thread's live share links for the share dialog, or an empty list with no session. */
|
|
160
|
+
function shareLinksFor(session: Thread | null): ShareLink[] {
|
|
161
|
+
return session ? (threadShareLinks(session) ?? []) : [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The title the share dialog shows and prefills a new link with, or empty with no session. */
|
|
165
|
+
function shareThreadName(session: Thread | null): string {
|
|
166
|
+
return session ? threadTitle(session) : "";
|
|
151
167
|
}
|
|
152
168
|
|
|
153
169
|
/** The drop-up chrome or a floating popover menu (thread actions, editor split) holds the keyboard. */
|
|
@@ -190,80 +206,67 @@ function menuModalHandled(
|
|
|
190
206
|
return true;
|
|
191
207
|
}
|
|
192
208
|
|
|
193
|
-
/** True while a menu
|
|
194
|
-
function keyboardOwnedElsewhere(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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;
|
|
209
|
+
/** True while a menu, an overlay, or the share dialog owns the keyboard instead of the thread view. */
|
|
210
|
+
function keyboardOwnedElsewhere(
|
|
211
|
+
menuOwnsKeyboard: boolean,
|
|
212
|
+
overlay: KeyState["overlay"],
|
|
213
|
+
shareDialogOpen: boolean,
|
|
214
|
+
): boolean {
|
|
215
|
+
return menuOwnsKeyboard || overlay !== "none" || shareDialogOpen;
|
|
208
216
|
}
|
|
209
217
|
|
|
210
|
-
/**
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return { type: "shareChoice", index: next };
|
|
218
|
+
/** The footer submit fires only for the owner of an unresolved review, never an observer. */
|
|
219
|
+
function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
|
|
220
|
+
return isOwner && !resolved && !observer;
|
|
215
221
|
}
|
|
216
222
|
|
|
217
|
-
/**
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (picked === "private") actions.openManageAccess();
|
|
226
|
-
else actions.publish();
|
|
223
|
+
/** The thread body edits inline only for its owner, on an unresolved markdown thread (never a diff). */
|
|
224
|
+
function canEditThreadBody(
|
|
225
|
+
threadViewActive: boolean,
|
|
226
|
+
isDiff: boolean,
|
|
227
|
+
isOwner: boolean,
|
|
228
|
+
resolved: boolean,
|
|
229
|
+
): boolean {
|
|
230
|
+
return threadViewActive && !isDiff && isOwner && !resolved;
|
|
227
231
|
}
|
|
228
232
|
|
|
229
|
-
/**
|
|
230
|
-
function
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
233
|
+
/** The owner's header actions: an edit/normal toggle that opens or leaves the inline editor, plus share when not editing. */
|
|
234
|
+
function ownerThreadActions(actions: {
|
|
235
|
+
editing: boolean;
|
|
236
|
+
onEdit: () => void;
|
|
237
|
+
onExitEdit: () => void;
|
|
238
|
+
onShare: () => void;
|
|
239
|
+
theme: Theme;
|
|
240
|
+
}): React.ReactNode {
|
|
241
|
+
return (
|
|
242
|
+
<Toolbar>
|
|
243
|
+
<Button onPress={actions.editing ? actions.onExitEdit : actions.onEdit} theme={actions.theme}>
|
|
244
|
+
{actions.editing ? " normal " : " edit "}
|
|
245
|
+
</Button>
|
|
246
|
+
{actions.editing ? null : (
|
|
247
|
+
<Button onPress={actions.onShare} theme={actions.theme}>
|
|
248
|
+
{" share "}
|
|
249
|
+
</Button>
|
|
250
|
+
)}
|
|
251
|
+
</Toolbar>
|
|
252
|
+
);
|
|
242
253
|
}
|
|
243
254
|
|
|
244
|
-
/**
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
if (mode.type !== "shareChoice") return false;
|
|
258
|
-
|
|
259
|
-
handleShareChoiceKey(mode.index, key, actions);
|
|
260
|
-
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
255
|
+
/** Pick the thread pane's body: the pixel prototype, the diff placeholder, the inline editor, or the read-only view. */
|
|
256
|
+
function chooseThreadBody(choice: {
|
|
257
|
+
isPixelPrototype: boolean;
|
|
258
|
+
isDiff: boolean;
|
|
259
|
+
editingBody: boolean;
|
|
260
|
+
prototype: React.ReactNode;
|
|
261
|
+
diffPlaceholder: React.ReactNode;
|
|
262
|
+
editor: React.ReactNode;
|
|
263
|
+
threadView: React.ReactNode;
|
|
264
|
+
}): React.ReactNode {
|
|
265
|
+
if (choice.isPixelPrototype) return choice.prototype;
|
|
266
|
+
if (choice.isDiff) return choice.diffPlaceholder;
|
|
267
|
+
if (choice.editingBody) return choice.editor;
|
|
263
268
|
|
|
264
|
-
|
|
265
|
-
function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
|
|
266
|
-
return isOwner && !resolved && !observer;
|
|
269
|
+
return choice.threadView;
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
/** The author display name for a comment's hover tooltip: the reviewer's own name for their notes, else the resolved collaborator name. */
|
|
@@ -344,11 +347,7 @@ function usableScreenReached(
|
|
|
344
347
|
}
|
|
345
348
|
|
|
346
349
|
/** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
|
|
347
|
-
function cheatsheetFor(
|
|
348
|
-
keyBindings: KeyBindings,
|
|
349
|
-
threadViewActive: boolean,
|
|
350
|
-
hint: string,
|
|
351
|
-
): CheatsheetSection[] {
|
|
350
|
+
function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): CheatsheetSection[] {
|
|
352
351
|
const base = keyBindings.cheatsheet();
|
|
353
352
|
|
|
354
353
|
if (!threadViewActive) {
|
|
@@ -357,11 +356,10 @@ function cheatsheetFor(
|
|
|
357
356
|
|
|
358
357
|
return [
|
|
359
358
|
...THREAD_VIEW_CHEATSHEET,
|
|
360
|
-
{ title: "
|
|
361
|
-
{ title: "
|
|
362
|
-
{ title: "
|
|
363
|
-
{ title: "
|
|
364
|
-
...base.filter((section) => section.title === "Agent terminal"),
|
|
359
|
+
{ title: "Nav mode · session", entries: sessionCommandEntries() },
|
|
360
|
+
{ title: "Nav mode · diff", entries: diffCommandEntries() },
|
|
361
|
+
{ title: "Nav mode · discussion", entries: curationCommandEntries() },
|
|
362
|
+
{ title: "Nav mode · history", entries: treeCommandEntries() },
|
|
365
363
|
];
|
|
366
364
|
}
|
|
367
365
|
|
|
@@ -421,29 +419,13 @@ export function threadsNavHandled(params: {
|
|
|
421
419
|
return false;
|
|
422
420
|
}
|
|
423
421
|
|
|
424
|
-
export function
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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;
|
|
422
|
+
export function paneCycleRequested(
|
|
423
|
+
key: { name: string },
|
|
424
|
+
overlay: string,
|
|
425
|
+
menuOwnsKeyboard: boolean,
|
|
426
|
+
threadComposing: boolean,
|
|
427
|
+
): boolean {
|
|
428
|
+
return key.name === "tab" && overlay === "none" && !menuOwnsKeyboard && !threadComposing;
|
|
447
429
|
}
|
|
448
430
|
|
|
449
431
|
export function visiblePanes(
|
|
@@ -543,11 +525,12 @@ export function App({
|
|
|
543
525
|
|
|
544
526
|
// A shared plan you own polls for collaborator notes while it is open; the
|
|
545
527
|
// merge refreshes through the normal event path. Stops on leave.
|
|
528
|
+
const threadShared = isSharedThread(session);
|
|
546
529
|
useEffect(() => {
|
|
547
|
-
if (!isOwner || !
|
|
530
|
+
if (!isOwner || !threadShared) return;
|
|
548
531
|
|
|
549
532
|
return controller.startShareSync();
|
|
550
|
-
}, [isOwner, session?.id,
|
|
533
|
+
}, [isOwner, session?.id, threadShared, controller]);
|
|
551
534
|
const renderer = useRenderer();
|
|
552
535
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
553
536
|
|
|
@@ -594,10 +577,11 @@ export function App({
|
|
|
594
577
|
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
595
578
|
// the top-left settings gear drop-down and the centered dialog it opens
|
|
596
579
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
597
|
-
const [
|
|
580
|
+
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
|
598
581
|
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
599
582
|
// unified or side-by-side diff; split only lays out when the Changes pane is zoomed
|
|
600
583
|
const [diffView, setDiffView] = useState<DiffViewMode>("split");
|
|
584
|
+
const [defaultVerdict, setDefaultVerdict] = useState<VerdictKind>("approve");
|
|
601
585
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
602
586
|
const [selectedCurationId, setSelectedCurationId] = useState<string | undefined>(undefined);
|
|
603
587
|
const [railTab, setRailTab] = useState<RailTab>("review");
|
|
@@ -641,6 +625,7 @@ export function App({
|
|
|
641
625
|
setSkills(loadSkills(config.skillsPath));
|
|
642
626
|
setAutoClose(config.ui.autoClose);
|
|
643
627
|
setDiffView(config.ui.diffView);
|
|
628
|
+
setDefaultVerdict(config.ui.defaultVerdict);
|
|
644
629
|
setPinnedIds(new Set(config.ui.pins));
|
|
645
630
|
controller.applyConfig(config);
|
|
646
631
|
}, [session?.workspace.repoRoot, controller, keyBindings, appearance]);
|
|
@@ -701,6 +686,7 @@ export function App({
|
|
|
701
686
|
cycleSetting,
|
|
702
687
|
handleSettingsKey,
|
|
703
688
|
onCategorySelect,
|
|
689
|
+
openSettings,
|
|
704
690
|
} = useSettingsDialog({
|
|
705
691
|
theme,
|
|
706
692
|
appearance,
|
|
@@ -773,6 +759,13 @@ export function App({
|
|
|
773
759
|
const [threadComposing, setThreadComposing] = useState(false);
|
|
774
760
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
775
761
|
const [welcomeComposing, setWelcomeComposing] = useState(false);
|
|
762
|
+
// inline body edit: the markdown editor owns the thread pane and all keys while open
|
|
763
|
+
const canEditBody = canEditThreadBody(threadViewActive, isDiff, isOwner, resolved);
|
|
764
|
+
const bodyEditing = useThreadBodyEditing({
|
|
765
|
+
controller,
|
|
766
|
+
sessionId: session?.id,
|
|
767
|
+
canEdit: canEditBody,
|
|
768
|
+
});
|
|
776
769
|
// sort position per annotation so the rail interleaves annotation and removal
|
|
777
770
|
// cards in one line-ordered stack: a diff row carries its blockIndex; a plan
|
|
778
771
|
// annotation resolves to the display index it marked
|
|
@@ -807,8 +800,13 @@ export function App({
|
|
|
807
800
|
setMode({ type: "railEdit", id: annotation.id, text: annotation.body });
|
|
808
801
|
};
|
|
809
802
|
|
|
810
|
-
|
|
811
|
-
const
|
|
803
|
+
// a markdown thread edits inline; a diff is not markdown, so it keeps the external-editor hand-off
|
|
804
|
+
const openBodyEditor = (): void => {
|
|
805
|
+
if (canEditBody) {
|
|
806
|
+
bodyEditing.openEditor();
|
|
807
|
+
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
812
810
|
renderer?.suspend();
|
|
813
811
|
try {
|
|
814
812
|
controller.edit();
|
|
@@ -829,6 +827,7 @@ export function App({
|
|
|
829
827
|
inboxCursor,
|
|
830
828
|
mode,
|
|
831
829
|
session,
|
|
830
|
+
defaultVerdict,
|
|
832
831
|
focusedAnnotationId,
|
|
833
832
|
selectedCurationId,
|
|
834
833
|
railTab,
|
|
@@ -853,7 +852,7 @@ export function App({
|
|
|
853
852
|
setSelectedCurationId,
|
|
854
853
|
setPulsedAnnotationId,
|
|
855
854
|
selectCardFromDocument,
|
|
856
|
-
|
|
855
|
+
openBodyEditor,
|
|
857
856
|
openCardEdit,
|
|
858
857
|
toggleDiffView: () => {
|
|
859
858
|
const next: DiffViewMode = diffView === "stacked" ? "split" : "stacked";
|
|
@@ -865,18 +864,22 @@ export function App({
|
|
|
865
864
|
else if (workbench.zoomed) controller.setStatus("split diff");
|
|
866
865
|
else controller.setStatus("split diff shows when zoomed");
|
|
867
866
|
},
|
|
867
|
+
openShareDialog: () => {
|
|
868
|
+
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
869
|
+
shareDialogStore.getState().reset();
|
|
870
|
+
setShareDialogOpen(true);
|
|
871
|
+
},
|
|
868
872
|
});
|
|
869
873
|
|
|
870
874
|
const overlay = resolveOverlay(mode, completion.phase, walking);
|
|
871
875
|
|
|
872
876
|
const menuControl = useMenuControlState();
|
|
873
877
|
const menuOwnsKeyboard = keyboardHeldByMenu(menuDialog, menuControl.openMenuId);
|
|
874
|
-
// an overlay (submit, walk, prompt, confirm, share choice)
|
|
875
|
-
// keyboard from the thread view; the view suspends
|
|
876
|
-
|
|
878
|
+
// an overlay (submit, walk, prompt, confirm, share choice), the manage-access
|
|
879
|
+
// dialog, or the menu takes the keyboard from the thread view; the view suspends
|
|
880
|
+
// its own grammar meanwhile so typing lands in the dialog, not the thread
|
|
881
|
+
const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay, shareDialogOpen);
|
|
877
882
|
|
|
878
|
-
const leaderCombos = leaderCombosFor(keysRef.current.leader);
|
|
879
|
-
const leaderPending = useRef(false);
|
|
880
883
|
const navigablePanes = navigableFocusPanes(
|
|
881
884
|
sidebarOpen,
|
|
882
885
|
workbench.zoomed,
|
|
@@ -893,40 +896,50 @@ export function App({
|
|
|
893
896
|
}, [session, navigablePanes, focusedPane]);
|
|
894
897
|
const cyclePanes = (backward: boolean): void =>
|
|
895
898
|
setFocusedPane((current) => nextFocusPane(current, navigablePanes, backward));
|
|
896
|
-
const
|
|
897
|
-
|
|
899
|
+
const runNavCommand = (key: { name: string; shift?: boolean }): boolean => {
|
|
900
|
+
const intent = resolveNavKey(key, {
|
|
901
|
+
isOwner,
|
|
902
|
+
resolved,
|
|
903
|
+
treeActive: railTab === "tree",
|
|
904
|
+
isDiff,
|
|
905
|
+
});
|
|
898
906
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
);
|
|
907
|
+
if (!intent) return false;
|
|
908
|
+
dispatch(intent);
|
|
909
|
+
|
|
910
|
+
return true;
|
|
904
911
|
};
|
|
905
912
|
// clicking a sidebar thread moves the cursor onto it too, so the row shows its selected backdrop at once
|
|
906
913
|
const openThread = (id: string): void => {
|
|
914
|
+
// save the open editor into the leaving thread's working copy before the switch, so a click away never drops edits
|
|
915
|
+
if (bodyEditing.editing) bodyEditing.requestExit();
|
|
907
916
|
const index = grouped.ordered.findIndex((thread) => thread.id === id);
|
|
908
917
|
|
|
909
918
|
if (index >= 0) setInboxCursor(() => index);
|
|
910
919
|
controller.open(id);
|
|
911
920
|
};
|
|
912
921
|
|
|
922
|
+
const threadSurfaceHandledKey = (key: KeyEvent): boolean => {
|
|
923
|
+
if (!threadViewActive || threadViewSuspended) return false;
|
|
924
|
+
if (!threadComposing) {
|
|
925
|
+
const chord = resolveSessionChord(key, { isOwner, resolved });
|
|
926
|
+
|
|
927
|
+
if (chord) dispatch(chord);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return true;
|
|
931
|
+
};
|
|
932
|
+
|
|
913
933
|
useKeyboard((key) => {
|
|
914
934
|
if (quitKeyHandled(key, onExit)) return;
|
|
915
|
-
//
|
|
916
|
-
if (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
isOwner,
|
|
920
|
-
publish: () => controller.share(),
|
|
921
|
-
openManageAccess: () => setAccessDialogOpen(true),
|
|
922
|
-
})
|
|
923
|
-
)
|
|
924
|
-
return;
|
|
935
|
+
// the inline body editor owns the pane and every key while open
|
|
936
|
+
if (bodyEditing.editing) return;
|
|
937
|
+
// the share dialog owns its own keys while open; the shell grammar stands down
|
|
938
|
+
if (shareDialogOpen) return;
|
|
925
939
|
if (menuModalHandled(menuControl, key)) return;
|
|
926
|
-
if (
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
return;
|
|
940
|
+
if (paneCycleRequested(key, overlay, menuOwnsKeyboard, threadComposing)) {
|
|
941
|
+
return cyclePanes(Boolean(key.shift));
|
|
942
|
+
}
|
|
930
943
|
if (
|
|
931
944
|
threadsNavHandled({
|
|
932
945
|
focusedPane,
|
|
@@ -938,23 +951,7 @@ export function App({
|
|
|
938
951
|
})
|
|
939
952
|
)
|
|
940
953
|
return;
|
|
941
|
-
|
|
942
|
-
// useKeyboard handles marks, comments, and ctrl+q); the session chords
|
|
943
|
-
// (submit, share, edit, walk, the rail) resolve here, and the keymap only
|
|
944
|
-
// sees keys while an overlay or the menu owns them.
|
|
945
|
-
if (threadViewActive && !threadViewSuspended) {
|
|
946
|
-
const chord = resolveThreadChord(key, {
|
|
947
|
-
composing: threadComposing,
|
|
948
|
-
isOwner,
|
|
949
|
-
resolved,
|
|
950
|
-
treeActive: railTab === "tree",
|
|
951
|
-
isDiff,
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
if (chord) dispatch(chord);
|
|
955
|
-
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
954
|
+
if (threadSurfaceHandledKey(key)) return;
|
|
958
955
|
// A compose textarea owns the keyboard while open: let it receive the typed note instead of the
|
|
959
956
|
// global keymap acting on each letter (the prototype, and the bare-shell welcome playground).
|
|
960
957
|
if (prototypeComposing || welcomeComposing) return;
|
|
@@ -1009,7 +1006,7 @@ export function App({
|
|
|
1009
1006
|
<MenuChrome
|
|
1010
1007
|
menuDialog={menuDialog}
|
|
1011
1008
|
theme={theme}
|
|
1012
|
-
keybindsSections={cheatsheetFor(keyBindings, threadViewActive
|
|
1009
|
+
keybindsSections={cheatsheetFor(keyBindings, threadViewActive)}
|
|
1013
1010
|
settingsCategories={settingsCategories}
|
|
1014
1011
|
settingsValues={settingsValues}
|
|
1015
1012
|
settingsNav={settingsNav}
|
|
@@ -1034,7 +1031,7 @@ export function App({
|
|
|
1034
1031
|
controller={controller}
|
|
1035
1032
|
setMode={setMode}
|
|
1036
1033
|
menuChrome={menuChrome}
|
|
1037
|
-
onOpenMenu={
|
|
1034
|
+
onOpenMenu={openSettings}
|
|
1038
1035
|
sidebarOpen={sidebarOpen}
|
|
1039
1036
|
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
1040
1037
|
focusedPane={focusedPane}
|
|
@@ -1068,6 +1065,9 @@ export function App({
|
|
|
1068
1065
|
verdict={activeSession.verdict.kind}
|
|
1069
1066
|
completion={completion}
|
|
1070
1067
|
status={status}
|
|
1068
|
+
onClose={() => dispatch({ type: "finishReview" })}
|
|
1069
|
+
onBackToPlan={() => dispatch({ type: "dismissCompletion" })}
|
|
1070
|
+
onAlways={() => dispatch({ type: "optInAutoClose" })}
|
|
1071
1071
|
/>
|
|
1072
1072
|
);
|
|
1073
1073
|
|
|
@@ -1086,6 +1086,7 @@ export function App({
|
|
|
1086
1086
|
session: activeSession,
|
|
1087
1087
|
walkFileList,
|
|
1088
1088
|
viewedPaths,
|
|
1089
|
+
quickActions,
|
|
1089
1090
|
liveInput,
|
|
1090
1091
|
setMode,
|
|
1091
1092
|
dispatch,
|
|
@@ -1105,22 +1106,9 @@ export function App({
|
|
|
1105
1106
|
// this is owner-only; stay silent rather than nag if it is ever reached.
|
|
1106
1107
|
if (!isOwner) return;
|
|
1107
1108
|
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
1108
|
-
|
|
1109
|
-
};
|
|
1110
|
-
|
|
1111
|
-
// the share choice's public option: publish the plan, copy the ssh line
|
|
1112
|
-
const onShareRequest = (): void => {
|
|
1113
|
-
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
1114
|
-
controller.share();
|
|
1109
|
+
openBodyEditor();
|
|
1115
1110
|
};
|
|
1116
1111
|
|
|
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
|
-
|
|
1124
1112
|
// clicking the rail Submit button: same read-only answer as the submit key
|
|
1125
1113
|
const onSubmitRequest = (): void => {
|
|
1126
1114
|
if (observer) return controller.setStatus("observer - read-only");
|
|
@@ -1147,7 +1135,7 @@ export function App({
|
|
|
1147
1135
|
<AppShell
|
|
1148
1136
|
sidebarOpen={sidebarOpen}
|
|
1149
1137
|
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
1150
|
-
onOpenMenu={
|
|
1138
|
+
onOpenMenu={openSettings}
|
|
1151
1139
|
onFocusPane={setFocusedPane}
|
|
1152
1140
|
threadsPanel={
|
|
1153
1141
|
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
@@ -1163,86 +1151,95 @@ export function App({
|
|
|
1163
1151
|
onRename={(id, title) =>
|
|
1164
1152
|
setMode({ type: "renameThread", sessionId: id, text: title })
|
|
1165
1153
|
}
|
|
1154
|
+
onRequestDelete={(id, title) =>
|
|
1155
|
+
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
1156
|
+
}
|
|
1166
1157
|
theme={theme}
|
|
1167
1158
|
/>
|
|
1168
1159
|
</scrollbox>
|
|
1169
1160
|
}
|
|
1170
1161
|
threadTitle={threadTitle(activeSession)}
|
|
1171
1162
|
threadActions={
|
|
1172
|
-
showOwnerActions
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
) : undefined
|
|
1163
|
+
showOwnerActions
|
|
1164
|
+
? ownerThreadActions({
|
|
1165
|
+
editing: bodyEditing.editing,
|
|
1166
|
+
onEdit: onEditRequest,
|
|
1167
|
+
onExitEdit: bodyEditing.requestExit,
|
|
1168
|
+
onShare: () => dispatch({ type: "share" }),
|
|
1169
|
+
theme,
|
|
1170
|
+
})
|
|
1171
|
+
: undefined
|
|
1182
1172
|
}
|
|
1183
1173
|
threadPanel={
|
|
1184
1174
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
1185
1175
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
1186
|
-
{
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1176
|
+
{chooseThreadBody({
|
|
1177
|
+
isPixelPrototype,
|
|
1178
|
+
isDiff,
|
|
1179
|
+
editingBody: bodyEditing.editing,
|
|
1180
|
+
prototype: (
|
|
1181
|
+
<PrototypePixels
|
|
1182
|
+
prototypePath={prototypePath}
|
|
1183
|
+
canComment={prototypeCanComment}
|
|
1184
|
+
onCommentElement={onCommentPrototype}
|
|
1185
|
+
onComposingChange={setPrototypeComposing}
|
|
1186
|
+
hidden={chromeHidden}
|
|
1187
|
+
/>
|
|
1188
|
+
),
|
|
1189
|
+
diffPlaceholder: (
|
|
1190
|
+
<box
|
|
1191
|
+
style={{
|
|
1192
|
+
flexGrow: 1,
|
|
1193
|
+
flexDirection: "column",
|
|
1194
|
+
alignItems: "center",
|
|
1195
|
+
justifyContent: "center",
|
|
1196
|
+
}}
|
|
1197
|
+
>
|
|
1198
|
+
<text fg={theme.textDim}>Select a thread</text>
|
|
1199
|
+
</box>
|
|
1200
|
+
),
|
|
1201
|
+
editor: bodyEditing.renderEditor(theme),
|
|
1202
|
+
threadView: (
|
|
1203
|
+
<ThreadView
|
|
1204
|
+
session={activeSession}
|
|
1205
|
+
suspended={surfaceSuspended(threadViewSuspended, focusedPane, "thread")}
|
|
1206
|
+
editOrphanCount={editOrphanCount}
|
|
1207
|
+
onComposingChange={setThreadComposing}
|
|
1208
|
+
onNavCommand={runNavCommand}
|
|
1209
|
+
resolved={resolved}
|
|
1210
|
+
onObserverBlocked={(reason) =>
|
|
1211
|
+
controller.setStatus(
|
|
1212
|
+
reason === "observer"
|
|
1213
|
+
? "observer - read-only"
|
|
1214
|
+
: "review submitted - read-only",
|
|
1215
|
+
)
|
|
1216
|
+
}
|
|
1217
|
+
onCursorChange={setCursor}
|
|
1218
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
1219
|
+
onFocusAnnotation={setFocusedAnnotationId}
|
|
1220
|
+
display={display}
|
|
1221
|
+
marks={marks}
|
|
1222
|
+
quickActions={quickActions}
|
|
1223
|
+
observer={observer}
|
|
1224
|
+
onAnnotate={(span, body) =>
|
|
1225
|
+
void controller.annotate(
|
|
1226
|
+
"comment",
|
|
1227
|
+
span.start.blockIndex,
|
|
1228
|
+
span.start.char,
|
|
1229
|
+
span.end.char,
|
|
1230
|
+
body,
|
|
1231
|
+
span.end.blockIndex,
|
|
1232
|
+
)
|
|
1233
|
+
}
|
|
1234
|
+
onReply={(rootAnnotationId, body) =>
|
|
1235
|
+
void controller.reply(rootAnnotationId, body)
|
|
1236
|
+
}
|
|
1237
|
+
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
1238
|
+
resolveAuthorLabel={resolveAuthorLabel}
|
|
1239
|
+
onExit={() => onExit?.(0)}
|
|
1240
|
+
/>
|
|
1241
|
+
),
|
|
1242
|
+
})}
|
|
1246
1243
|
</box>
|
|
1247
1244
|
{threadFooter}
|
|
1248
1245
|
</box>
|
|
@@ -1309,8 +1306,7 @@ export function App({
|
|
|
1309
1306
|
void controller.reply(rootAnnotationId, body),
|
|
1310
1307
|
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1311
1308
|
resolveAuthorLabel,
|
|
1312
|
-
|
|
1313
|
-
onLeaderCommand: runLeaderCommand,
|
|
1309
|
+
onNavCommand: runNavCommand,
|
|
1314
1310
|
onExit: () => onExit?.(0),
|
|
1315
1311
|
}}
|
|
1316
1312
|
rejectedRows={rejectedRows}
|
|
@@ -1326,6 +1322,8 @@ export function App({
|
|
|
1326
1322
|
body,
|
|
1327
1323
|
)
|
|
1328
1324
|
}
|
|
1325
|
+
// zoomed, the footer already rides the Changes pane, so no extra offset is needed
|
|
1326
|
+
emptyBottomPadding={workbench.zoomed ? 0 : THREAD_FOOTER_HEIGHT}
|
|
1329
1327
|
theme={theme}
|
|
1330
1328
|
/>
|
|
1331
1329
|
)}
|
|
@@ -1380,26 +1378,17 @@ export function App({
|
|
|
1380
1378
|
</box>
|
|
1381
1379
|
</box>
|
|
1382
1380
|
) : null}
|
|
1383
|
-
<
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
}
|
|
1394
|
-
onClose={() => setAccessDialogOpen(false)}
|
|
1395
|
-
theme={theme}
|
|
1396
|
-
/>
|
|
1397
|
-
<ShareChoiceDialog
|
|
1398
|
-
isOpen={mode.type === "shareChoice"}
|
|
1399
|
-
selectedIndex={shareChoiceIndex(mode)}
|
|
1400
|
-
onPublicShare={onShareRequest}
|
|
1401
|
-
onPrivateShare={onPrivateShareRequest}
|
|
1402
|
-
onClose={() => setMode({ type: "normal" })}
|
|
1381
|
+
<ShareDialog
|
|
1382
|
+
key={String(shareDialogOpen)}
|
|
1383
|
+
isOpen={shareDialogOpen}
|
|
1384
|
+
threadName={shareThreadName(session)}
|
|
1385
|
+
links={shareLinksFor(session)}
|
|
1386
|
+
isOwner={isOwner}
|
|
1387
|
+
onCreateLink={(input) => controller.createShareLink(input)}
|
|
1388
|
+
onUpdateLink={(id, input) => controller.updateShareLink(id, input)}
|
|
1389
|
+
onDeleteLink={(id) => controller.deleteShareLink(id)}
|
|
1390
|
+
onCopyLink={(id) => controller.copyShareLink(id)}
|
|
1391
|
+
onClose={() => setShareDialogOpen(false)}
|
|
1403
1392
|
theme={theme}
|
|
1404
1393
|
/>
|
|
1405
1394
|
{menuChrome}
|