@cueloop/client 0.1.0-alpha.78 → 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.diff.test.tsx +9 -4
- 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 +75 -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 +6 -4
- package/src/App.test.tsx +31 -28
- package/src/App.toast.test.tsx +11 -4
- package/src/App.tsx +259 -274
- package/src/App.walk.test.tsx +9 -8
- package/src/App.workbench.test.tsx +86 -0
- package/src/annotation-palette.ts +1 -0
- package/src/app-screens.tsx +12 -6
- package/src/app-view-model.ts +0 -1
- package/src/components/ConfirmDialog.stories.tsx +3 -3
- package/src/components/DiffContentView.tsx +14 -7
- package/src/components/DiscussionMarkerRail.tsx +9 -4
- 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/SettingsDialog.tsx +1 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ThreadFooter.tsx +4 -1
- package/src/components/ThreadTree.tsx +1 -1
- package/src/components/ThreadView.slash.test.tsx +0 -35
- package/src/components/ThreadView.tsx +142 -68
- package/src/components/Toast.stories.tsx +7 -0
- package/src/components/Toast.tsx +15 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +258 -134
- package/src/components/primitives/Button.tsx +6 -2
- package/src/components/primitives/Card.tsx +2 -2
- package/src/components/primitives/Dialog.tsx +2 -2
- 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/share-dialog-store.ts +58 -0
- package/src/components/stories.test.tsx +4 -3
- package/src/config.test.ts +18 -0
- package/src/config.ts +8 -1
- package/src/diff-fold.ts +1 -1
- 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.ts +0 -1
- 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/prototype-browser.test.ts +9 -1
- package/src/prototype-browser.ts +22 -0
- package/src/run.ts +8 -6
- package/src/share.ts +15 -1
- 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.live-diff.test.ts +3 -0
- package/src/thread-controller.share.test.ts +60 -5
- package/src/thread-controller.switch-cache.test.ts +30 -0
- package/src/thread-controller.tree.test.ts +6 -1
- package/src/thread-controller.ts +229 -54
- package/src/use-annotation-surface.tsx +45 -36
- 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 -122
- package/src/components/ShareChoiceDialog.stories.tsx +0 -34
- package/src/components/ShareChoiceDialog.test.tsx +0 -103
- package/src/components/ShareChoiceDialog.tsx +0 -80
- 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}
|
|
@@ -1109,22 +1106,9 @@ export function App({
|
|
|
1109
1106
|
// this is owner-only; stay silent rather than nag if it is ever reached.
|
|
1110
1107
|
if (!isOwner) return;
|
|
1111
1108
|
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
1112
|
-
|
|
1113
|
-
};
|
|
1114
|
-
|
|
1115
|
-
// the share choice's public option: publish the plan, copy the ssh line
|
|
1116
|
-
const onShareRequest = (): void => {
|
|
1117
|
-
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
1118
|
-
controller.share();
|
|
1109
|
+
openBodyEditor();
|
|
1119
1110
|
};
|
|
1120
1111
|
|
|
1121
|
-
// the share choice's private option opens the manage-access allowlist surface
|
|
1122
|
-
const onPrivateShareRequest = (): void => {
|
|
1123
|
-
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
1124
|
-
setAccessDialogOpen(true);
|
|
1125
|
-
};
|
|
1126
|
-
const allowedLogins = allowedGithubLogins(session);
|
|
1127
|
-
|
|
1128
1112
|
// clicking the rail Submit button: same read-only answer as the submit key
|
|
1129
1113
|
const onSubmitRequest = (): void => {
|
|
1130
1114
|
if (observer) return controller.setStatus("observer - read-only");
|
|
@@ -1151,7 +1135,7 @@ export function App({
|
|
|
1151
1135
|
<AppShell
|
|
1152
1136
|
sidebarOpen={sidebarOpen}
|
|
1153
1137
|
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
1154
|
-
onOpenMenu={
|
|
1138
|
+
onOpenMenu={openSettings}
|
|
1155
1139
|
onFocusPane={setFocusedPane}
|
|
1156
1140
|
threadsPanel={
|
|
1157
1141
|
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
@@ -1167,86 +1151,95 @@ export function App({
|
|
|
1167
1151
|
onRename={(id, title) =>
|
|
1168
1152
|
setMode({ type: "renameThread", sessionId: id, text: title })
|
|
1169
1153
|
}
|
|
1154
|
+
onRequestDelete={(id, title) =>
|
|
1155
|
+
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
1156
|
+
}
|
|
1170
1157
|
theme={theme}
|
|
1171
1158
|
/>
|
|
1172
1159
|
</scrollbox>
|
|
1173
1160
|
}
|
|
1174
1161
|
threadTitle={threadTitle(activeSession)}
|
|
1175
1162
|
threadActions={
|
|
1176
|
-
showOwnerActions
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
) : 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
|
|
1186
1172
|
}
|
|
1187
1173
|
threadPanel={
|
|
1188
1174
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
1189
1175
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
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
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
+
})}
|
|
1250
1243
|
</box>
|
|
1251
1244
|
{threadFooter}
|
|
1252
1245
|
</box>
|
|
@@ -1313,8 +1306,7 @@ export function App({
|
|
|
1313
1306
|
void controller.reply(rootAnnotationId, body),
|
|
1314
1307
|
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1315
1308
|
resolveAuthorLabel,
|
|
1316
|
-
|
|
1317
|
-
onLeaderCommand: runLeaderCommand,
|
|
1309
|
+
onNavCommand: runNavCommand,
|
|
1318
1310
|
onExit: () => onExit?.(0),
|
|
1319
1311
|
}}
|
|
1320
1312
|
rejectedRows={rejectedRows}
|
|
@@ -1330,6 +1322,8 @@ export function App({
|
|
|
1330
1322
|
body,
|
|
1331
1323
|
)
|
|
1332
1324
|
}
|
|
1325
|
+
// zoomed, the footer already rides the Changes pane, so no extra offset is needed
|
|
1326
|
+
emptyBottomPadding={workbench.zoomed ? 0 : THREAD_FOOTER_HEIGHT}
|
|
1333
1327
|
theme={theme}
|
|
1334
1328
|
/>
|
|
1335
1329
|
)}
|
|
@@ -1384,26 +1378,17 @@ export function App({
|
|
|
1384
1378
|
</box>
|
|
1385
1379
|
</box>
|
|
1386
1380
|
) : null}
|
|
1387
|
-
<
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1398
|
-
onClose={() => setAccessDialogOpen(false)}
|
|
1399
|
-
theme={theme}
|
|
1400
|
-
/>
|
|
1401
|
-
<ShareChoiceDialog
|
|
1402
|
-
isOpen={mode.type === "shareChoice"}
|
|
1403
|
-
selectedIndex={shareChoiceIndex(mode)}
|
|
1404
|
-
onPublicShare={onShareRequest}
|
|
1405
|
-
onPrivateShare={onPrivateShareRequest}
|
|
1406
|
-
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)}
|
|
1407
1392
|
theme={theme}
|
|
1408
1393
|
/>
|
|
1409
1394
|
{menuChrome}
|