@cueloop/client 0.1.0-alpha.64 → 0.1.0-alpha.66
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.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +8 -73
- package/src/App.export.test.tsx +2 -2
- package/src/App.plan-review.test.tsx +60 -412
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +85 -62
- package/src/App.rendering.test.tsx +5 -6
- package/src/App.submit-confirm.test.tsx +32 -70
- package/src/App.test.tsx +136 -66
- package/src/App.toast.test.tsx +17 -8
- package/src/App.tsx +243 -284
- package/src/App.walk.test.tsx +1 -1
- package/src/app-screens.tsx +67 -52
- package/src/app-view-model.ts +6 -100
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +2 -2
- package/src/components/AppShell.stories.tsx +84 -0
- package/src/components/AppShell.tsx +127 -0
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +147 -0
- package/src/components/CodeBlock.stories.tsx +1 -1
- package/src/components/CompletionOverlay.stories.tsx +1 -1
- package/src/components/ConfirmCard.stories.tsx +1 -1
- package/src/components/ConfirmCard.tsx +3 -4
- package/src/components/ConfirmDialog.stories.tsx +1 -1
- package/src/components/DiffSheet.stories.tsx +1 -1
- package/src/components/InboxList.stories.tsx +23 -8
- package/src/components/InboxList.tsx +45 -19
- package/src/components/KeybindsDialog.stories.tsx +1 -1
- package/src/components/KeybindsDialog.tsx +61 -17
- package/src/components/MarkerPopover.stories.tsx +1 -1
- package/src/components/MenuBar.stories.tsx +3 -12
- package/src/components/MenuBar.tsx +25 -56
- package/src/components/PromptDialog.stories.tsx +1 -1
- package/src/components/PrototypeSheet.stories.tsx +1 -1
- package/src/components/ReviewPanel.stories.tsx +15 -1
- package/src/components/ReviewRail.stories.tsx +15 -1
- package/src/components/ReviewRail.tsx +13 -6
- package/src/components/SettingsDialog.stories.tsx +7 -1
- package/src/components/SettingsDialog.tsx +112 -66
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.tsx +49 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +1708 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +57 -0
- package/src/components/Toast.stories.tsx +1 -1
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +156 -0
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +463 -305
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/file-tree.test.ts +45 -0
- package/src/components/file-tree.ts +57 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +1 -1
- package/src/components/primitives/Card.stories.tsx +1 -1
- package/src/components/primitives/Dialog.stories.tsx +1 -1
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +43 -0
- package/src/components/primitives/StatusBar.stories.tsx +1 -1
- package/src/components/primitives/Tabs.stories.tsx +1 -1
- package/src/components/primitives/Toolbar.stories.tsx +1 -1
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +87 -0
- package/src/components/primitives/icons.ts +38 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +1 -1
- package/src/components/session-tree.test.ts +114 -0
- package/src/components/session-tree.ts +94 -0
- package/src/components/stories-app.tsx +142 -82
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +55 -0
- package/src/components/theme-context.stories.tsx +1 -1
- package/src/diff-hunk-curate.ts +4 -104
- package/src/index.ts +1 -0
- package/src/intent-dispatch.test.ts +136 -3
- package/src/intent-dispatch.ts +92 -24
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +4 -4
- package/src/keymap.ts +14 -7
- package/src/session-controller.curate.test.ts +89 -2
- package/src/session-controller.share.test.ts +195 -55
- package/src/session-controller.tree.test.ts +228 -0
- package/src/session-controller.ts +331 -72
- package/src/share.ts +114 -16
- package/src/test-support.test.ts +193 -1
- package/src/test-support.ts +64 -0
- package/src/theme.ts +4 -4
- package/src/thread-chords.test.ts +154 -0
- package/src/thread-chords.ts +144 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-settings-dialog.tsx +9 -0
- package/src/view-plan.ts +29 -12
- package/src/App.review-panel.test.tsx +0 -143
- package/src/components/PlanSheet.stories.tsx +0 -113
- package/src/components/PlanSheet.tsx +0 -419
- package/src/components/plan-sheet-run-style.ts +0 -88
package/src/App.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The cueloop TUI: state wiring, keymap dispatch, and layout composition -
|
|
3
3
|
* nothing else. Rendering lives in components/, daemon IO and the mutation
|
|
4
|
-
*
|
|
4
|
+
* primitives in session-controller.ts, the keyboard grammar in keymap.ts with
|
|
5
5
|
* binding resolution and status hints from key-bindings.ts, and theming in
|
|
6
6
|
* the ThemeProvider. Selection is the entry primitive (mouse drag or keyboard
|
|
7
7
|
* span on one native renderer selection); annotation text lives in the rail
|
|
@@ -18,8 +18,8 @@ import React, {
|
|
|
18
18
|
useSyncExternalStore,
|
|
19
19
|
} from "react";
|
|
20
20
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
21
|
-
import type { Clock
|
|
22
|
-
import {
|
|
21
|
+
import type { Clock } from "@opentui/core";
|
|
22
|
+
import { marksByDisplay, type Mark } from "./view-plan";
|
|
23
23
|
import { dimmedTheme } from "./theme";
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_KEYS,
|
|
@@ -39,39 +39,37 @@ import {
|
|
|
39
39
|
import type { Theme } from "./theme";
|
|
40
40
|
import { createReviewController, type ShareTransport } from "./session-controller";
|
|
41
41
|
import type { SessionClient } from "@cueloop/daemon/client";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import { KeyBindings } from "./key-bindings";
|
|
42
|
+
import { createIntentDispatch, type Mode } from "./intent-dispatch";
|
|
43
|
+
import { reduceKey, type KeyState } from "./keymap";
|
|
44
|
+
import { KeyBindings, type CheatsheetSection } from "./key-bindings";
|
|
46
45
|
import { ThemeProvider } from "./components/theme-context";
|
|
47
46
|
import { Button } from "./components/primitives/Button";
|
|
48
47
|
import { Toolbar } from "./components/primitives/Toolbar";
|
|
48
|
+
import { NERD } from "./components/primitives/icons";
|
|
49
|
+
import { groupInbox, projectName } from "./components/session-tree";
|
|
50
|
+
import { ThreadsSidebar } from "./components/ThreadsSidebar";
|
|
51
|
+
import { ChangesColumn, DiffChangesToggle, useDiffColumns } from "./components/ChangesColumn";
|
|
52
|
+
import { IconButton } from "./components/primitives/IconButton";
|
|
53
|
+
import { ThreadFooter } from "./components/ThreadFooter";
|
|
54
|
+
import { ConfirmCard } from "./components/ConfirmCard";
|
|
49
55
|
import { Breadcrumb } from "./components/Breadcrumb";
|
|
50
|
-
import {
|
|
56
|
+
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
|
|
57
|
+
import {
|
|
58
|
+
RAIL_CHORD_ENTRIES,
|
|
59
|
+
resolveThreadChord,
|
|
60
|
+
THREAD_CHORD_ENTRIES,
|
|
61
|
+
TREE_CHORD_ENTRIES,
|
|
62
|
+
} from "./thread-chords";
|
|
51
63
|
import { DiffSheet } from "./components/DiffSheet";
|
|
52
64
|
import { PrototypeSheet } from "./components/PrototypeSheet";
|
|
53
65
|
import type { PrototypeElement } from "./prototype-browser";
|
|
54
|
-
import { type ReviewRailHandle } from "./components/ReviewRail";
|
|
55
|
-
import type
|
|
56
|
-
import { ReviewPanel } from "./components/ReviewPanel";
|
|
57
|
-
import {
|
|
58
|
-
REVIEW_DEFAULT_WIDTH,
|
|
59
|
-
resolveReviewWidth,
|
|
60
|
-
toggleReviewPanelMode,
|
|
61
|
-
widthFromMouseColumn,
|
|
62
|
-
type ReviewPanelMode,
|
|
63
|
-
} from "./review-panel";
|
|
66
|
+
import { type RailTab, type ReviewRailHandle } from "./components/ReviewRail";
|
|
67
|
+
import { REVIEW_DEFAULT_WIDTH, type ReviewPanelMode } from "./review-panel";
|
|
64
68
|
import {
|
|
65
|
-
buildActiveSpan,
|
|
66
|
-
buildCardEditState,
|
|
67
|
-
buildComposeState,
|
|
68
69
|
buildDiffComposeState,
|
|
69
70
|
buildHeaderItems,
|
|
70
|
-
buildPopoverState,
|
|
71
71
|
buildRenderFlags,
|
|
72
72
|
buildSubmitConfirmState,
|
|
73
|
-
computePendingCount,
|
|
74
|
-
computeRailFootprint,
|
|
75
73
|
computeRoleCapabilities,
|
|
76
74
|
deriveReviewFlags,
|
|
77
75
|
isCompletionOverlayPhase,
|
|
@@ -96,7 +94,7 @@ export interface AppProps {
|
|
|
96
94
|
home?: string;
|
|
97
95
|
sessionId?: string;
|
|
98
96
|
/**
|
|
99
|
-
* Observer mode (SSH-served connections): every mutating
|
|
97
|
+
* Observer mode (SSH-served connections): every mutating primitive is ignored and
|
|
100
98
|
* answers "observer - read-only" in the status line; navigation still works.
|
|
101
99
|
*/
|
|
102
100
|
readOnly?: boolean;
|
|
@@ -125,6 +123,43 @@ export interface AppProps {
|
|
|
125
123
|
appearance?: Appearance;
|
|
126
124
|
}
|
|
127
125
|
|
|
126
|
+
/** True while the drop-up or one of its dialogs is open and owns the keyboard. */
|
|
127
|
+
function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
|
|
128
|
+
return menuDialog !== null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** True while a menu or an overlay owns the keyboard instead of the thread view. */
|
|
132
|
+
function keyboardOwnedElsewhere(menuOwnsKeyboard: boolean, overlay: KeyState["overlay"]): boolean {
|
|
133
|
+
return menuOwnsKeyboard || overlay !== "none";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The footer submit fires only for the owner of an unresolved review, never an observer. */
|
|
137
|
+
function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
|
|
138
|
+
return isOwner && !resolved && !observer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The left panel toggle glyph: the mirrored sidebar icon, filled when the column is open. */
|
|
142
|
+
function sidebarToggleGlyph(open: boolean): string {
|
|
143
|
+
return open ? NERD.sidebarLeft : NERD.sidebarLeftOff;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
|
|
147
|
+
function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): CheatsheetSection[] {
|
|
148
|
+
const base = keyBindings.cheatsheet();
|
|
149
|
+
|
|
150
|
+
if (!threadViewActive) {
|
|
151
|
+
return base;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return [
|
|
155
|
+
...THREAD_VIEW_CHEATSHEET,
|
|
156
|
+
{ title: "Session", entries: [...THREAD_CHORD_ENTRIES] },
|
|
157
|
+
{ title: "Rail", entries: [...RAIL_CHORD_ENTRIES] },
|
|
158
|
+
{ title: "Tree", entries: [...TREE_CHORD_ENTRIES] },
|
|
159
|
+
...base.filter((section) => section.title === "Agent terminal"),
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
128
163
|
export function App({
|
|
129
164
|
home,
|
|
130
165
|
sessionId,
|
|
@@ -173,7 +208,7 @@ export function App({
|
|
|
173
208
|
useEffect(() => {
|
|
174
209
|
if (!isOwner || !session?.shareId) return;
|
|
175
210
|
|
|
176
|
-
return controller.
|
|
211
|
+
return controller.startShareSync();
|
|
177
212
|
}, [isOwner, session?.id, session?.shareId, controller]);
|
|
178
213
|
const renderer = useRenderer();
|
|
179
214
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
@@ -181,22 +216,25 @@ export function App({
|
|
|
181
216
|
// ── view state ──────────────────────────────
|
|
182
217
|
const [cursor, setCursor] = useState(0);
|
|
183
218
|
const [inboxCursor, setInboxCursor] = useState(0);
|
|
219
|
+
// Projects and Threads grouping; ordered is the flat sequence the inbox cursor walks
|
|
220
|
+
const grouped = useMemo(() => groupInbox(inbox ?? []), [inbox]);
|
|
221
|
+
// the left Projects and Threads column; collapsed by default per the context
|
|
222
|
+
// matrix (the thread owns the width), toggled open to jump between threads
|
|
223
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
184
224
|
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
185
|
-
// the
|
|
186
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
225
|
+
// the top-left settings gear drop-down and the centered dialog it opens
|
|
187
226
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
188
227
|
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
189
228
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
190
229
|
const [selectedCurationId, setSelectedCurationId] = useState<string | undefined>(undefined);
|
|
191
|
-
const [railTab, setRailTab] = useState<
|
|
192
|
-
//
|
|
193
|
-
const [
|
|
230
|
+
const [railTab, setRailTab] = useState<RailTab>("review");
|
|
231
|
+
// the tree row the reviewer stands on in the rail's Tree tab
|
|
232
|
+
const [selectedEntryId, setSelectedEntryId] = useState<string | undefined>(undefined);
|
|
194
233
|
// review panel layout: mode + expanded width are client view state, loaded
|
|
195
234
|
// from and persisted to the user config so they survive a restart. The ref
|
|
196
235
|
// mirrors the width so the drag-end persist reads the latest value.
|
|
197
236
|
const [reviewMode, setReviewMode] = useState<ReviewPanelMode>("expanded");
|
|
198
237
|
const [reviewWidth, setReviewWidth] = useState(REVIEW_DEFAULT_WIDTH);
|
|
199
|
-
const [dividerDragging, setDividerDragging] = useState(false);
|
|
200
238
|
const reviewWidthRef = useRef(REVIEW_DEFAULT_WIDTH);
|
|
201
239
|
// ~2s focus pulse on the document highlight when a rail card is activated
|
|
202
240
|
const [pulsedAnnotationId, setPulsedAnnotationId] = useState<string | null>(null);
|
|
@@ -204,7 +242,6 @@ export function App({
|
|
|
204
242
|
// live mirror of overlay input text: refs commit synchronously, so the
|
|
205
243
|
// RETURN handler never reads a stale value mid-typing
|
|
206
244
|
const liveInput = useRef("");
|
|
207
|
-
const planSheetRef = useRef<PlanSheetHandle | null>(null);
|
|
208
245
|
const railRef = useRef<ReviewRailHandle | null>(null);
|
|
209
246
|
// keymap from layered config; the loaded theme swaps the provider value
|
|
210
247
|
const keysRef = useRef(DEFAULT_KEYS);
|
|
@@ -285,8 +322,8 @@ export function App({
|
|
|
285
322
|
// ── derived view model ──────────────────────
|
|
286
323
|
const display = controller.display();
|
|
287
324
|
const rows = controller.rows();
|
|
325
|
+
const diffColumns = useDiffColumns({ session, rows, cursor, setCursor });
|
|
288
326
|
const rejectedRows = controller.rejectedRows();
|
|
289
|
-
const curationItems = controller.curationItems();
|
|
290
327
|
const marks = useMemo(
|
|
291
328
|
() =>
|
|
292
329
|
session
|
|
@@ -305,97 +342,24 @@ export function App({
|
|
|
305
342
|
return ids;
|
|
306
343
|
}, [marks]);
|
|
307
344
|
const { isDiff, isPrototype, resolved } = deriveReviewFlags(session);
|
|
345
|
+
// plans and replies open in the thread view; diffs and prototypes keep their sheets
|
|
346
|
+
const threadViewActive = session !== null && !isDiff && !isPrototype;
|
|
347
|
+
const [threadComposing, setThreadComposing] = useState(false);
|
|
308
348
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
309
349
|
// sort position per annotation so the rail interleaves annotation and removal
|
|
310
350
|
// cards in one line-ordered stack: a diff row carries its blockIndex; a plan
|
|
311
351
|
// annotation resolves to the display index it marked
|
|
312
|
-
const annotationPositions = useMemo(() => {
|
|
313
|
-
const positions = new Map<string, number>();
|
|
314
|
-
|
|
315
|
-
if (!session) return positions;
|
|
316
|
-
if (isDiff) {
|
|
317
|
-
for (const annotation of session.annotations) {
|
|
318
|
-
const blockIndex =
|
|
319
|
-
"blockIndex" in annotation.anchor ? annotation.anchor.blockIndex : undefined;
|
|
320
|
-
|
|
321
|
-
if (blockIndex !== undefined) positions.set(annotation.id, blockIndex);
|
|
322
|
-
}
|
|
323
|
-
} else {
|
|
324
|
-
for (const [displayIndex, blockMarks] of marks) {
|
|
325
|
-
for (const mark of blockMarks)
|
|
326
|
-
if (mark.annotationId) positions.set(mark.annotationId, displayIndex);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return positions;
|
|
331
|
-
}, [session, isDiff, marks]);
|
|
332
|
-
|
|
333
352
|
// ── the guided walk's view model ────────────
|
|
334
353
|
const walkFileList = controller.files();
|
|
335
354
|
const walking = isWalking(isDiff, walk);
|
|
336
355
|
const viewedPaths = useMemo(() => new Set(session?.viewedPaths ?? []), [session]);
|
|
337
356
|
|
|
338
|
-
// driving needs committed layout, so it runs after render; any transition
|
|
339
|
-
// out of span mode clears the renderer selection (compose paints its own
|
|
340
|
-
// mark, and a mouse drag never changes the mode, so it survives)
|
|
341
|
-
useEffect(() => {
|
|
342
|
-
// one marker at a time: clear any prior selection, then paint the current
|
|
343
|
-
// span (span and its quick-actions sub-mode both keep it painted)
|
|
344
|
-
const span = activeSpanState(mode);
|
|
345
|
-
|
|
346
|
-
planSheetRef.current?.clearSelection();
|
|
347
|
-
if (span) planSheetRef.current?.driveSpanSelection(span);
|
|
348
|
-
}, [mode]);
|
|
349
|
-
|
|
350
357
|
// ── selection symmetry: one selected id, both sides ──
|
|
351
|
-
const pulse = (id: string): void => {
|
|
352
|
-
setPulsedAnnotationId(id);
|
|
353
|
-
if (pulseTimer.current) clearTimeout(pulseTimer.current);
|
|
354
|
-
pulseTimer.current = setTimeout(() => setPulsedAnnotationId(null), 2000);
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
/** Card activation scrolls the document to the anchor and pulses it. */
|
|
358
|
-
const revealAnchor = (annotationId: string): void => {
|
|
359
|
-
for (const [displayIndex, blockMarks] of marks) {
|
|
360
|
-
if (!blockMarks.some((mark) => mark.annotationId === annotationId)) continue;
|
|
361
|
-
setCursor(displayIndex);
|
|
362
|
-
planSheetRef.current?.revealBlock(displayIndex);
|
|
363
|
-
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
|
|
368
358
|
const selectCardFromDocument = (annotationId: string): void => {
|
|
369
359
|
setFocusedAnnotationId(annotationId);
|
|
370
360
|
railRef.current?.revealCard(annotationId);
|
|
371
361
|
};
|
|
372
362
|
|
|
373
|
-
const selectCardFromRail = (annotationId: string): void => {
|
|
374
|
-
setFocusedAnnotationId(annotationId);
|
|
375
|
-
pulse(annotationId);
|
|
376
|
-
revealAnchor(annotationId);
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
// selecting a removal reveals its source line, so the reader sees what the
|
|
380
|
-
// pending undo would bring back: the diff sheet follows the cursor; the plan
|
|
381
|
-
// sheet also needs an explicit scroll to the cut block
|
|
382
|
-
const selectCurationFromRail = (curationId: string): void => {
|
|
383
|
-
setSelectedCurationId(curationId);
|
|
384
|
-
const item = curationItems.find((candidate) => candidate.id === curationId);
|
|
385
|
-
|
|
386
|
-
if (!item) return;
|
|
387
|
-
setCursor(item.revealIndex);
|
|
388
|
-
if (item.source === "plan") planSheetRef.current?.revealBlock(item.revealIndex);
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
// the selected removal card's undo button: same restore path as the u key,
|
|
392
|
-
// including the observer read-only guard the keyboard path gets via reduceKey
|
|
393
|
-
const undoCurationFromRail = (curationId: string): void => {
|
|
394
|
-
if (observer) return controller.setStatus("observer - read-only");
|
|
395
|
-
controller.restoreCuration(curationId);
|
|
396
|
-
setSelectedCurationId(undefined);
|
|
397
|
-
};
|
|
398
|
-
|
|
399
363
|
const openCardEdit = (annotationId: string): void => {
|
|
400
364
|
if (observer) return controller.setStatus("observer - read-only");
|
|
401
365
|
const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
|
|
@@ -435,7 +399,7 @@ export function App({
|
|
|
435
399
|
display,
|
|
436
400
|
rows,
|
|
437
401
|
cursor,
|
|
438
|
-
inbox,
|
|
402
|
+
inbox: inbox === null ? null : grouped.ordered,
|
|
439
403
|
inboxCursor,
|
|
440
404
|
mode,
|
|
441
405
|
session,
|
|
@@ -444,6 +408,8 @@ export function App({
|
|
|
444
408
|
terminalWidth,
|
|
445
409
|
focusedAnnotationId,
|
|
446
410
|
selectedCurationId,
|
|
411
|
+
railTab,
|
|
412
|
+
selectedEntryId,
|
|
447
413
|
authorNames,
|
|
448
414
|
quickActions,
|
|
449
415
|
renameAuthor: (id: string, name: string) => {
|
|
@@ -452,13 +418,13 @@ export function App({
|
|
|
452
418
|
},
|
|
453
419
|
liveInput,
|
|
454
420
|
reviewWidthRef,
|
|
455
|
-
planSheetRef,
|
|
456
421
|
setCursor,
|
|
457
422
|
setInboxCursor,
|
|
458
423
|
setMode,
|
|
459
424
|
setReviewMode,
|
|
460
425
|
setReviewWidth,
|
|
461
426
|
setRailTab,
|
|
427
|
+
setSelectedEntryId,
|
|
462
428
|
setFocusedAnnotationId,
|
|
463
429
|
setSelectedCurationId,
|
|
464
430
|
setPulsedAnnotationId,
|
|
@@ -469,20 +435,33 @@ export function App({
|
|
|
469
435
|
|
|
470
436
|
const overlay = resolveOverlay(mode, completion.phase, walking);
|
|
471
437
|
|
|
438
|
+
const menuOwnsKeyboard = menuChromeOpen(menuDialog);
|
|
439
|
+
// an overlay (submit, walk, prompt, confirm) or the menu takes the keyboard
|
|
440
|
+
// from the thread view; the view suspends its own grammar meanwhile
|
|
441
|
+
const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay);
|
|
442
|
+
|
|
472
443
|
useKeyboard((key) => {
|
|
444
|
+
// The thread view owns the document grammar while active (its own
|
|
445
|
+
// useKeyboard handles marks, comments, and ctrl+q); the session chords
|
|
446
|
+
// (submit, share, edit, walk, the rail) resolve here, and the keymap only
|
|
447
|
+
// sees keys while an overlay or the menu owns them.
|
|
448
|
+
if (threadViewActive && !threadViewSuspended) {
|
|
449
|
+
const chord = resolveThreadChord(key, {
|
|
450
|
+
composing: threadComposing,
|
|
451
|
+
isOwner,
|
|
452
|
+
resolved,
|
|
453
|
+
treeActive: railTab === "tree",
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (chord) dispatch(chord);
|
|
457
|
+
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
473
460
|
// The prototype compose textarea owns the keyboard while open: let it receive
|
|
474
461
|
// the typed note instead of the global keymap acting on each letter.
|
|
475
462
|
if (prototypeComposing) return;
|
|
476
|
-
// A running in-tab agent terminal owns the keyboard: forward every key to it,
|
|
477
|
-
// with ctrl+] as the detach chord back to the review.
|
|
478
|
-
if (agentTerminal) {
|
|
479
|
-
if (key.ctrl && key.name === "]") return void agentTerminal.detach();
|
|
480
|
-
|
|
481
|
-
return void agentTerminal.write(key.sequence);
|
|
482
|
-
}
|
|
483
463
|
if (menuDialog === "settings") return void handleSettingsKey(key.name);
|
|
484
464
|
if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
|
|
485
|
-
if (menuOpen) return void (key.name === "escape" && setMenuOpen(false));
|
|
486
465
|
// the toast is non-modal: escape only dismisses it when nothing else owns
|
|
487
466
|
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
488
467
|
if (toast && key.name === "escape" && overlay === "none" && mode.type !== "span")
|
|
@@ -505,8 +484,14 @@ export function App({
|
|
|
505
484
|
display,
|
|
506
485
|
});
|
|
507
486
|
|
|
508
|
-
keyBindings.setContext({
|
|
509
|
-
|
|
487
|
+
keyBindings.setContext({
|
|
488
|
+
overlay: state.overlay,
|
|
489
|
+
spanMode: state.spanMode,
|
|
490
|
+
});
|
|
491
|
+
const action = keyBindings.resolveAction({
|
|
492
|
+
name: key.name,
|
|
493
|
+
shift: !!key.shift,
|
|
494
|
+
});
|
|
510
495
|
|
|
511
496
|
for (const intent of reduceKey(
|
|
512
497
|
state,
|
|
@@ -520,13 +505,9 @@ export function App({
|
|
|
520
505
|
// reused by the inbox and by plan/diff review so the two never drift ──
|
|
521
506
|
const menuChrome = (
|
|
522
507
|
<MenuChrome
|
|
523
|
-
menuOpen={menuOpen}
|
|
524
508
|
menuDialog={menuDialog}
|
|
525
|
-
status={status}
|
|
526
509
|
theme={theme}
|
|
527
|
-
|
|
528
|
-
setMenuDialog={setMenuDialog}
|
|
529
|
-
keybindsSections={keyBindings.cheatsheet()}
|
|
510
|
+
keybindsSections={cheatsheetFor(keyBindings, threadViewActive)}
|
|
530
511
|
settingsCategories={settingsCategories}
|
|
531
512
|
settingsValues={settingsValues}
|
|
532
513
|
settingsNav={settingsNav}
|
|
@@ -540,13 +521,14 @@ export function App({
|
|
|
540
521
|
if (!session)
|
|
541
522
|
return inbox ? (
|
|
542
523
|
<InboxScreen
|
|
543
|
-
|
|
524
|
+
rows={grouped.rows}
|
|
544
525
|
inboxCursor={inboxCursor}
|
|
545
526
|
mode={mode}
|
|
546
527
|
theme={theme}
|
|
547
528
|
controller={controller}
|
|
548
529
|
setMode={setMode}
|
|
549
530
|
menuChrome={menuChrome}
|
|
531
|
+
onOpenMenu={() => setMenuDialog("settings")}
|
|
550
532
|
/>
|
|
551
533
|
) : (
|
|
552
534
|
<ConnectingScreen theme={theme} />
|
|
@@ -565,7 +547,6 @@ export function App({
|
|
|
565
547
|
/>
|
|
566
548
|
);
|
|
567
549
|
|
|
568
|
-
const composeState = buildComposeState({ mode, isDiff, display, liveInput, setMode, dispatch });
|
|
569
550
|
const diffComposeState = buildDiffComposeState({
|
|
570
551
|
mode,
|
|
571
552
|
isDiff,
|
|
@@ -574,17 +555,6 @@ export function App({
|
|
|
574
555
|
setMode,
|
|
575
556
|
dispatch,
|
|
576
557
|
});
|
|
577
|
-
const activeSpan = buildActiveSpan(mode, isDiff);
|
|
578
|
-
const popoverState = buildPopoverState({
|
|
579
|
-
mode,
|
|
580
|
-
isDiff,
|
|
581
|
-
quickActions,
|
|
582
|
-
isOwner,
|
|
583
|
-
observer,
|
|
584
|
-
resolved,
|
|
585
|
-
controller,
|
|
586
|
-
dispatch,
|
|
587
|
-
});
|
|
588
558
|
const submitConfirmState = buildSubmitConfirmState({
|
|
589
559
|
mode,
|
|
590
560
|
isDiff,
|
|
@@ -595,54 +565,21 @@ export function App({
|
|
|
595
565
|
setMode,
|
|
596
566
|
dispatch,
|
|
597
567
|
});
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
// a mouse drag leaves a native selection: turn it into a word span so the
|
|
615
|
-
// marker popover opens at the dragged range, mirroring the `v` grammar.
|
|
616
|
-
// Returns whether a native selection existed (the release ended a drag).
|
|
617
|
-
const activateSpanFromSelection = (preferredIndex?: number): boolean => {
|
|
618
|
-
if (!renderer?.hasSelection) return false;
|
|
619
|
-
// the release block re-anchors the span, so each drag replaces the last
|
|
620
|
-
const selection = planSheetRef.current?.readSelection(preferredIndex);
|
|
621
|
-
const block = selection ? display[selection.displayIndex] : undefined;
|
|
622
|
-
const span =
|
|
623
|
-
selection && block
|
|
624
|
-
? spanFromRange(selection.displayIndex, displayText(block), selection.start, selection.end)
|
|
625
|
-
: null;
|
|
626
|
-
|
|
627
|
-
if (span) setMode({ type: "span", span });
|
|
628
|
-
|
|
629
|
-
return true;
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
const onLineActivate = (displayIndex: number): void => {
|
|
633
|
-
if (activateSpanFromSelection(displayIndex)) return;
|
|
634
|
-
setCursor(displayIndex);
|
|
635
|
-
const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
|
|
636
|
-
|
|
637
|
-
if (annotationId) selectCardFromDocument(annotationId);
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
// a drag released outside any block's text (the gutter, past a line end, a
|
|
641
|
-
// gap between blocks) never reaches a block handler; the sheet-level release
|
|
642
|
-
// still turns the finished drag into a span
|
|
643
|
-
const onSelectionRelease = (): void => {
|
|
644
|
-
activateSpanFromSelection();
|
|
645
|
-
};
|
|
568
|
+
const headerItems = buildHeaderItems({
|
|
569
|
+
session: activeSession,
|
|
570
|
+
resolved,
|
|
571
|
+
observer,
|
|
572
|
+
role,
|
|
573
|
+
});
|
|
574
|
+
const { showOwnerActions, prototypeCanComment, chromeHidden, prototypePath } = buildRenderFlags({
|
|
575
|
+
session: activeSession,
|
|
576
|
+
isOwner,
|
|
577
|
+
isDiff,
|
|
578
|
+
isPrototype,
|
|
579
|
+
resolved,
|
|
580
|
+
menuDialog,
|
|
581
|
+
resolvedIds,
|
|
582
|
+
});
|
|
646
583
|
|
|
647
584
|
const onEditRequest = (): void => {
|
|
648
585
|
// A share viewer/observer has no Edit affordance (the button is hidden), so
|
|
@@ -666,19 +603,6 @@ export function App({
|
|
|
666
603
|
dispatch({ type: "openSubmit" });
|
|
667
604
|
};
|
|
668
605
|
|
|
669
|
-
// the clickable chevron toggles expanded <-> compact; the keybinding (b)
|
|
670
|
-
// cycles all three including hidden
|
|
671
|
-
const onToggleReviewPanel = (): void => {
|
|
672
|
-
const next = toggleReviewPanelMode(reviewMode);
|
|
673
|
-
|
|
674
|
-
setReviewMode(next);
|
|
675
|
-
controller.saveReviewPanel({ mode: next });
|
|
676
|
-
};
|
|
677
|
-
// grabbing the divider only arms a drag when there is a width to drag
|
|
678
|
-
const onDividerGrab = (): void => {
|
|
679
|
-
if (reviewMode === "expanded") setDividerDragging(true);
|
|
680
|
-
};
|
|
681
|
-
|
|
682
606
|
return (
|
|
683
607
|
<ThemeProvider theme={theme}>
|
|
684
608
|
<box
|
|
@@ -688,26 +612,25 @@ export function App({
|
|
|
688
612
|
height: "100%",
|
|
689
613
|
backgroundColor: theme.background,
|
|
690
614
|
}}
|
|
691
|
-
onMouseDrag={(event: MouseEvent) => {
|
|
692
|
-
if (!dividerDragging || reviewMode !== "expanded") return;
|
|
693
|
-
const next = widthFromMouseColumn(event.x, terminalWidth);
|
|
694
|
-
|
|
695
|
-
// many raw mouse-moves land in the same column: only re-render when the
|
|
696
|
-
// width actually changes, so the drag does not reconcile per pixel
|
|
697
|
-
if (next === reviewWidthRef.current) return;
|
|
698
|
-
reviewWidthRef.current = next;
|
|
699
|
-
setReviewWidth(next);
|
|
700
|
-
}}
|
|
701
|
-
onMouseUp={() => {
|
|
702
|
-
if (!dividerDragging) return;
|
|
703
|
-
setDividerDragging(false);
|
|
704
|
-
controller.saveReviewPanel({ width: reviewWidthRef.current });
|
|
705
|
-
}}
|
|
706
615
|
>
|
|
707
616
|
<box
|
|
708
|
-
style={{
|
|
617
|
+
style={{
|
|
618
|
+
flexDirection: "row",
|
|
619
|
+
height: 2,
|
|
620
|
+
paddingTop: 1,
|
|
621
|
+
backgroundColor: theme.panel,
|
|
622
|
+
}}
|
|
709
623
|
>
|
|
710
624
|
<box style={{ flexGrow: 1, flexDirection: "row", paddingRight: 1 }}>
|
|
625
|
+
<box onMouseUp={() => setMenuDialog("settings")} style={{ paddingRight: 2 }}>
|
|
626
|
+
<text fg={theme.textMuted}>{NERD.settings}</text>
|
|
627
|
+
</box>
|
|
628
|
+
<IconButton
|
|
629
|
+
glyph={sidebarToggleGlyph(sidebarOpen)}
|
|
630
|
+
onPress={() => setSidebarOpen((open) => !open)}
|
|
631
|
+
marginRight={2}
|
|
632
|
+
theme={theme}
|
|
633
|
+
/>
|
|
711
634
|
<Breadcrumb items={headerItems} />
|
|
712
635
|
<box style={{ flexGrow: 1 }} />
|
|
713
636
|
{showOwnerActions ? (
|
|
@@ -720,77 +643,98 @@ export function App({
|
|
|
720
643
|
</Button>
|
|
721
644
|
</Toolbar>
|
|
722
645
|
) : null}
|
|
646
|
+
<DiffChangesToggle
|
|
647
|
+
isDiff={isDiff}
|
|
648
|
+
open={diffColumns.changesOpen}
|
|
649
|
+
onToggle={diffColumns.toggleChanges}
|
|
650
|
+
theme={theme}
|
|
651
|
+
/>
|
|
723
652
|
</box>
|
|
724
|
-
<box style={{ width: railFootprint }} />
|
|
725
653
|
</box>
|
|
726
654
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
655
|
+
<ThreadsSidebar
|
|
656
|
+
open={sidebarOpen}
|
|
657
|
+
rows={grouped.rows}
|
|
658
|
+
cursor={inboxCursor}
|
|
659
|
+
activeId={session.id}
|
|
660
|
+
onSelect={(id) => controller.open(id)}
|
|
661
|
+
theme={theme}
|
|
662
|
+
/>
|
|
663
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
664
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
665
|
+
{isPrototype ? (
|
|
666
|
+
<PrototypeSheet
|
|
667
|
+
prototypePath={prototypePath}
|
|
668
|
+
quickActions={quickActions}
|
|
669
|
+
canComment={prototypeCanComment}
|
|
670
|
+
onCommentElement={onCommentPrototype}
|
|
671
|
+
onComposingChange={setPrototypeComposing}
|
|
672
|
+
hidden={chromeHidden}
|
|
673
|
+
/>
|
|
674
|
+
) : isDiff ? (
|
|
675
|
+
// the sheet dims to reading-quiet colors while the wizard has focus
|
|
676
|
+
<DiffSheet
|
|
677
|
+
rows={rows}
|
|
678
|
+
cursor={cursor}
|
|
679
|
+
annotations={activeSession.annotations}
|
|
680
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
681
|
+
rejectedRows={rejectedRows}
|
|
682
|
+
compose={diffComposeState}
|
|
683
|
+
theme={walking ? dimmedTheme(theme) : undefined}
|
|
684
|
+
/>
|
|
685
|
+
) : (
|
|
686
|
+
<ThreadView
|
|
687
|
+
session={activeSession}
|
|
688
|
+
suspended={threadViewSuspended}
|
|
689
|
+
editOrphanCount={editOrphanCount}
|
|
690
|
+
onComposingChange={setThreadComposing}
|
|
691
|
+
resolved={resolved}
|
|
692
|
+
onObserverBlocked={(reason) =>
|
|
693
|
+
controller.setStatus(
|
|
694
|
+
reason === "observer"
|
|
695
|
+
? "observer - read-only"
|
|
696
|
+
: "review submitted - read-only",
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
onCursorChange={setCursor}
|
|
700
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
701
|
+
onFocusAnnotation={setFocusedAnnotationId}
|
|
702
|
+
display={display}
|
|
703
|
+
marks={marks}
|
|
704
|
+
quickActions={quickActions}
|
|
705
|
+
observer={observer}
|
|
706
|
+
onAnnotate={(span, body) =>
|
|
707
|
+
void controller.annotate(
|
|
708
|
+
"comment",
|
|
709
|
+
span.start.blockIndex,
|
|
710
|
+
span.start.char,
|
|
711
|
+
span.end.char,
|
|
712
|
+
body,
|
|
713
|
+
span.end.blockIndex,
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
onReply={(rootAnnotationId, body) =>
|
|
717
|
+
void controller.reply(rootAnnotationId, body)
|
|
718
|
+
}
|
|
719
|
+
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
720
|
+
onExit={() => onExit?.(0)}
|
|
721
|
+
/>
|
|
722
|
+
)}
|
|
723
|
+
</box>
|
|
724
|
+
<ThreadFooter
|
|
725
|
+
repo={projectName(activeSession.workspace)}
|
|
726
|
+
branch={activeSession.workspace.branch}
|
|
727
|
+
onSubmit={onSubmitRequest}
|
|
728
|
+
canSubmit={canSubmitReview(isOwner, resolved, observer)}
|
|
729
|
+
theme={theme}
|
|
760
730
|
/>
|
|
761
|
-
|
|
762
|
-
<
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
rail={{
|
|
769
|
-
session: activeSession,
|
|
770
|
-
authorNames,
|
|
771
|
-
selectedId: focusedAnnotationId,
|
|
772
|
-
resolvedIds: railResolvedIds,
|
|
773
|
-
curationItems,
|
|
774
|
-
selectedCurationId,
|
|
775
|
-
annotationPositions,
|
|
776
|
-
railTab,
|
|
777
|
-
pendingCount,
|
|
778
|
-
cardEdit: cardEditState,
|
|
779
|
-
submitConfirm: submitConfirmState,
|
|
780
|
-
onTabChange: setRailTab,
|
|
781
|
-
onSelectCard: selectCardFromRail,
|
|
782
|
-
onActivateCard: openCardEdit,
|
|
783
|
-
onSelectCuration: selectCurationFromRail,
|
|
784
|
-
onUndoCuration: undoCurationFromRail,
|
|
785
|
-
onSubmitRequest,
|
|
786
|
-
onLaunchHarness: (command, seedText) =>
|
|
787
|
-
launchHarnessInSplit({
|
|
788
|
-
command,
|
|
789
|
-
cwd: activeSession.artifact.meta.cwd ?? process.cwd(),
|
|
790
|
-
seedText,
|
|
791
|
-
}),
|
|
792
|
-
onAgentTerminal: setAgentTerminal,
|
|
793
|
-
}}
|
|
731
|
+
</box>
|
|
732
|
+
<ChangesColumn
|
|
733
|
+
open={diffColumns.changesOpen}
|
|
734
|
+
files={activeSession.artifact.files}
|
|
735
|
+
selectedPath={diffColumns.currentFilePath}
|
|
736
|
+
onSelectFile={diffColumns.scrollToFile}
|
|
737
|
+
theme={theme}
|
|
794
738
|
/>
|
|
795
739
|
</box>
|
|
796
740
|
<TrailingOverlays
|
|
@@ -806,6 +750,21 @@ export function App({
|
|
|
806
750
|
setMode={setMode}
|
|
807
751
|
dispatch={dispatch}
|
|
808
752
|
/>
|
|
753
|
+
{submitConfirmState !== null ? (
|
|
754
|
+
<box
|
|
755
|
+
style={{
|
|
756
|
+
position: "absolute",
|
|
757
|
+
left: 0,
|
|
758
|
+
top: 0,
|
|
759
|
+
width: "100%",
|
|
760
|
+
height: "100%",
|
|
761
|
+
justifyContent: "center",
|
|
762
|
+
alignItems: "center",
|
|
763
|
+
}}
|
|
764
|
+
>
|
|
765
|
+
<ConfirmCard {...submitConfirmState} theme={theme} />
|
|
766
|
+
</box>
|
|
767
|
+
) : null}
|
|
809
768
|
{menuChrome}
|
|
810
769
|
</box>
|
|
811
770
|
</ThemeProvider>
|