@cueloop/client 0.1.0-alpha.32 → 0.1.0-alpha.34
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 +27 -26
- package/src/App.autoclose.test.tsx +7 -4
- package/src/App.diff.test.tsx +131 -5
- package/src/App.export.test.tsx +23 -5
- package/src/App.plan-review.test.tsx +133 -32
- package/src/App.readonly.test.tsx +35 -13
- package/src/App.rendering.test.tsx +14 -4
- package/src/App.review-panel.test.tsx +27 -15
- package/src/App.submit-confirm.test.tsx +14 -13
- package/src/App.test.tsx +50 -13
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +505 -101
- package/src/App.walk.test.tsx +31 -11
- package/src/attribution.ts +8 -2
- package/src/clipboard.ts +5 -1
- package/src/components/AnnotationCard.stories.tsx +29 -10
- package/src/components/AnnotationCard.tsx +57 -37
- package/src/components/Breadcrumb.tsx +7 -1
- package/src/components/CodeBlock.stories.tsx +19 -2
- package/src/components/CodeBlock.tsx +24 -9
- package/src/components/CompletionOverlay.tsx +13 -3
- package/src/components/ConfirmCard.stories.tsx +22 -3
- package/src/components/ConfirmCard.tsx +23 -5
- package/src/components/ConfirmDialog.tsx +10 -2
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +35 -1
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +213 -101
- package/src/components/InboxList.stories.tsx +16 -3
- package/src/components/InboxList.tsx +17 -10
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +58 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +102 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +35 -3
- package/src/components/PlanSheet.tsx +160 -100
- package/src/components/PromptDialog.tsx +19 -3
- package/src/components/ReviewPanel.stories.tsx +17 -19
- package/src/components/ReviewPanel.tsx +17 -32
- package/src/components/ReviewRail.stories.tsx +159 -47
- package/src/components/ReviewRail.tsx +206 -62
- package/src/components/SettingsDialog.stories.tsx +51 -3
- package/src/components/SettingsDialog.tsx +11 -5
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/SettingsRows.tsx +19 -3
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +13 -2
- package/src/components/WalkWizard.tsx +9 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +853 -623
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.stories.tsx +2 -2
- package/src/components/primitives/Button.tsx +5 -1
- package/src/components/primitives/Dialog.tsx +12 -1
- package/src/components/primitives/Tabs.tsx +40 -39
- package/src/components/stories-app.tsx +34 -6
- package/src/components/story-fixtures.ts +11 -3
- package/src/components/story.ts +3 -1
- package/src/components/syntax-highlight.ts +47 -12
- package/src/components/theme-context.stories.tsx +6 -1
- package/src/components/theme-context.tsx +7 -1
- package/src/config.test.ts +178 -2
- package/src/config.ts +121 -23
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +13 -4
- package/src/editor.ts +31 -5
- package/src/herdr.ts +8 -2
- package/src/index.ts +9 -1
- package/src/integrations.ts +3 -1
- package/src/intent-dispatch.test.ts +193 -9
- package/src/intent-dispatch.ts +151 -18
- package/src/key-bindings.test.ts +19 -9
- package/src/key-bindings.ts +77 -16
- package/src/keymap.test.ts +171 -26
- package/src/keymap.ts +83 -16
- package/src/review-panel.test.ts +3 -1
- package/src/review-panel.ts +9 -2
- package/src/serve.test.ts +13 -5
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +77 -16
- package/src/session-controller.ts +319 -26
- package/src/share.ts +41 -8
- package/src/test-support.ts +97 -23
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +1 -3
- package/src/version.ts +5 -0
- package/src/view-diff.ts +15 -4
- package/src/view-plan.test.ts +40 -6
- package/src/view-plan.ts +71 -28
- package/src/walk.test.ts +7 -2
- package/src/walk.ts +7 -2
- package/src/components/ComposeBar.stories.tsx +0 -18
- package/src/components/ComposeBar.tsx +0 -54
package/src/App.tsx
CHANGED
|
@@ -13,24 +13,52 @@ import React, { useEffect, useMemo, useRef, useState, useSyncExternalStore } fro
|
|
|
13
13
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
14
14
|
import type { Clock, MouseEvent } from "@opentui/core";
|
|
15
15
|
import { type VerdictKind } from "@cueloop/schema";
|
|
16
|
-
import { displayText, marksByDisplay, type Mark } from "./view-plan";
|
|
16
|
+
import { displayText, marksByDisplay, spanFromRange, type Mark } from "./view-plan";
|
|
17
17
|
import { noteForFile, viewedCount } from "./walk";
|
|
18
18
|
import { DARK, dimmedTheme } from "./theme";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_KEYS,
|
|
21
|
+
DEFAULT_QUICK_ACTIONS,
|
|
22
|
+
loadConfig,
|
|
23
|
+
persistAuthorName,
|
|
24
|
+
persistAutoClose,
|
|
25
|
+
persistTheme,
|
|
26
|
+
type AutoClose,
|
|
27
|
+
type QuickAction,
|
|
28
|
+
} from "./config";
|
|
29
|
+
import {
|
|
30
|
+
composeTheme,
|
|
31
|
+
DEFAULT_THEME_NAME,
|
|
32
|
+
THEME_LABELS,
|
|
33
|
+
THEME_NAMES,
|
|
34
|
+
type ThemeName,
|
|
35
|
+
} from "./theme-presets";
|
|
36
|
+
import type { Theme } from "./theme";
|
|
20
37
|
import { returnPaneFor } from "@cueloop/schema";
|
|
21
38
|
import { createReviewController } from "./session-controller";
|
|
22
39
|
import type { SessionClient } from "@cueloop/daemon/client";
|
|
23
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
activeSpanState,
|
|
42
|
+
createIntentDispatch,
|
|
43
|
+
reviewerAnnotations,
|
|
44
|
+
type Mode,
|
|
45
|
+
} from "./intent-dispatch";
|
|
24
46
|
import { reduceKey, type KeyState } from "./keymap";
|
|
25
|
-
import { KeyBindings
|
|
47
|
+
import { KeyBindings } from "./key-bindings";
|
|
26
48
|
import { ThemeProvider } from "./components/theme-context";
|
|
27
|
-
import {
|
|
49
|
+
import { Button } from "./components/primitives/Button";
|
|
50
|
+
import { Toolbar } from "./components/primitives/Toolbar";
|
|
51
|
+
import { MenuBar } from "./components/MenuBar";
|
|
52
|
+
import { KeybindsDialog } from "./components/KeybindsDialog";
|
|
53
|
+
import { SettingsDialog, type SettingsCategory } from "./components/SettingsDialog";
|
|
54
|
+
import { CLIENT_VERSION } from "./version";
|
|
28
55
|
import { Breadcrumb, type BreadcrumbItem } from "./components/Breadcrumb";
|
|
29
56
|
import { PlanSheet, type PlanSheetHandle } from "./components/PlanSheet";
|
|
30
57
|
import { DiffSheet } from "./components/DiffSheet";
|
|
31
58
|
import { annotationBlocking, type ReviewRailHandle } from "./components/ReviewRail";
|
|
32
59
|
import { ReviewPanel } from "./components/ReviewPanel";
|
|
33
60
|
import {
|
|
61
|
+
REVIEW_COMPACT_WIDTH,
|
|
34
62
|
REVIEW_DEFAULT_WIDTH,
|
|
35
63
|
resolveReviewWidth,
|
|
36
64
|
toggleReviewPanelMode,
|
|
@@ -41,14 +69,11 @@ import { CompletionOverlay } from "./components/CompletionOverlay";
|
|
|
41
69
|
import { InboxList } from "./components/InboxList";
|
|
42
70
|
import { ConfirmDialog } from "./components/ConfirmDialog";
|
|
43
71
|
import { PromptDialog } from "./components/PromptDialog";
|
|
44
|
-
import { ComposeBar } from "./components/ComposeBar";
|
|
45
72
|
import { WalkWizard } from "./components/WalkWizard";
|
|
73
|
+
import { Toast } from "./components/Toast";
|
|
46
74
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
* review layout (plan column, divider, rail) gets the rows that remain.
|
|
50
|
-
*/
|
|
51
|
-
const CHROME_ROWS = 2;
|
|
75
|
+
/** A toast clears itself after this idle; esc dismisses it sooner. */
|
|
76
|
+
const TOAST_DISMISS_MS = 4000;
|
|
52
77
|
|
|
53
78
|
export interface AppProps {
|
|
54
79
|
home?: string;
|
|
@@ -76,7 +101,16 @@ export interface AppProps {
|
|
|
76
101
|
selfAuthor?: string;
|
|
77
102
|
}
|
|
78
103
|
|
|
79
|
-
export function App({
|
|
104
|
+
export function App({
|
|
105
|
+
home,
|
|
106
|
+
sessionId,
|
|
107
|
+
readOnly = false,
|
|
108
|
+
onExit,
|
|
109
|
+
clock,
|
|
110
|
+
openClient,
|
|
111
|
+
role = "owner",
|
|
112
|
+
selfAuthor,
|
|
113
|
+
}: AppProps): React.ReactNode {
|
|
80
114
|
// Observer stays fully read-only; a collaborator writes annotations but not
|
|
81
115
|
// the plan or a verdict. `observer` is what the controller and every write
|
|
82
116
|
// gate key off; the two capability flags carve out the collaborator's middle.
|
|
@@ -86,7 +120,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
86
120
|
// feeds all three; split it the day a collaborator earns one of them.
|
|
87
121
|
const isOwner = !observer && role === "owner";
|
|
88
122
|
const controller = useMemo(
|
|
89
|
-
() =>
|
|
123
|
+
() =>
|
|
124
|
+
createReviewController({ home, sessionId, readOnly: observer, onExit, clock, openClient }),
|
|
90
125
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
126
|
[home, sessionId],
|
|
92
127
|
);
|
|
@@ -94,11 +129,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
94
129
|
controller.connect();
|
|
95
130
|
return () => controller.close();
|
|
96
131
|
}, [controller]);
|
|
97
|
-
const { session, inbox, status, error, completion, editOrphanCount, walk } =
|
|
98
|
-
controller.subscribe,
|
|
99
|
-
controller.getSnapshot,
|
|
100
|
-
controller.getSnapshot,
|
|
101
|
-
);
|
|
132
|
+
const { session, inbox, status, toast, error, completion, editOrphanCount, walk } =
|
|
133
|
+
useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
102
134
|
// A shared plan you own polls for collaborator notes while it is open; the
|
|
103
135
|
// merge refreshes through the normal event path. Stops on leave.
|
|
104
136
|
useEffect(() => {
|
|
@@ -106,13 +138,27 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
106
138
|
return controller.startSharePoll();
|
|
107
139
|
}, [isOwner, session?.id, session?.shareId, controller]);
|
|
108
140
|
const renderer = useRenderer();
|
|
109
|
-
const { width: terminalWidth
|
|
141
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
110
142
|
|
|
111
143
|
// ── view state ──────────────────────────────
|
|
112
144
|
const [cursor, setCursor] = useState(0);
|
|
113
145
|
const [inboxCursor, setInboxCursor] = useState(0);
|
|
114
146
|
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
147
|
+
// the bottom-left menu drop-up and the centered dialog it opens
|
|
148
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
149
|
+
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
150
|
+
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
151
|
+
const [settingsNav, setSettingsNav] = useState<{
|
|
152
|
+
categoryId: string;
|
|
153
|
+
rowIndex: number;
|
|
154
|
+
zone: "nav" | "body";
|
|
155
|
+
}>({
|
|
156
|
+
categoryId: "general",
|
|
157
|
+
rowIndex: 0,
|
|
158
|
+
zone: "body",
|
|
159
|
+
});
|
|
115
160
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
161
|
+
const [selectedCurationId, setSelectedCurationId] = useState<string | undefined>(undefined);
|
|
116
162
|
const [railTab, setRailTab] = useState<"review" | "agent">("review");
|
|
117
163
|
// review panel layout: mode + expanded width are client view state, loaded
|
|
118
164
|
// from and persisted to the user config so they survive a restart. The ref
|
|
@@ -133,16 +179,23 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
133
179
|
const keysRef = useRef(DEFAULT_KEYS);
|
|
134
180
|
const keyBindings = useMemo(() => new KeyBindings(DEFAULT_KEYS), []);
|
|
135
181
|
const [theme, setTheme] = useState(DARK);
|
|
182
|
+
const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
|
|
183
|
+
const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
|
|
136
184
|
const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
|
|
185
|
+
const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
|
|
137
186
|
useEffect(() => {
|
|
138
187
|
const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
139
188
|
keysRef.current = config.keys;
|
|
140
189
|
keyBindings.setKeys(config.keys);
|
|
141
190
|
setTheme(config.theme);
|
|
191
|
+
setThemeName(config.ui.theme);
|
|
192
|
+
setThemeOverrides(config.themeOverrides);
|
|
142
193
|
setReviewMode(config.ui.reviewState);
|
|
143
194
|
setReviewWidth(config.ui.reviewWidth);
|
|
144
195
|
reviewWidthRef.current = config.ui.reviewWidth;
|
|
145
196
|
setAuthorNames(config.authors);
|
|
197
|
+
setQuickActions(config.actions);
|
|
198
|
+
setAutoClose(config.ui.autoClose);
|
|
146
199
|
controller.applyConfig(config);
|
|
147
200
|
}, [session?.workspace.repoRoot, controller, keyBindings]);
|
|
148
201
|
useEffect(
|
|
@@ -152,6 +205,12 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
152
205
|
[],
|
|
153
206
|
);
|
|
154
207
|
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!toast) return;
|
|
210
|
+
const timer = setTimeout(() => controller.dismissToast(), TOAST_DISMISS_MS);
|
|
211
|
+
return () => clearTimeout(timer);
|
|
212
|
+
}, [toast, controller]);
|
|
213
|
+
|
|
155
214
|
// First open of a share: ask the collaborator for a display name once, unless
|
|
156
215
|
// a past visit already recorded one. esc skips and their notes read anonymous.
|
|
157
216
|
const promptedSelfRef = useRef(false);
|
|
@@ -165,8 +224,13 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
165
224
|
// ── derived view model ──────────────────────
|
|
166
225
|
const display = controller.display();
|
|
167
226
|
const rows = controller.rows();
|
|
227
|
+
const rejectedRows = controller.rejectedRows();
|
|
228
|
+
const curationItems = controller.curationItems();
|
|
168
229
|
const marks = useMemo(
|
|
169
|
-
() =>
|
|
230
|
+
() =>
|
|
231
|
+
session
|
|
232
|
+
? marksByDisplay(session.annotations, display, pulsedAnnotationId ?? undefined)
|
|
233
|
+
: new Map<number, Mark[]>(),
|
|
170
234
|
[session, display, pulsedAnnotationId],
|
|
171
235
|
);
|
|
172
236
|
/** Annotation ids whose anchor resolved against the working copy. */
|
|
@@ -179,6 +243,25 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
179
243
|
}, [marks]);
|
|
180
244
|
const resolved = session?.status === "resolved";
|
|
181
245
|
const isDiff = session?.artifact.type === "diff";
|
|
246
|
+
// sort position per annotation so the rail interleaves annotation and removal
|
|
247
|
+
// cards in one line-ordered stack: a diff row carries its blockIndex; a plan
|
|
248
|
+
// annotation resolves to the display index it marked
|
|
249
|
+
const annotationPositions = useMemo(() => {
|
|
250
|
+
const positions = new Map<string, number>();
|
|
251
|
+
if (!session) return positions;
|
|
252
|
+
if (isDiff) {
|
|
253
|
+
for (const annotation of session.annotations) {
|
|
254
|
+
const blockIndex = (annotation.anchor as { blockIndex?: number }).blockIndex;
|
|
255
|
+
if (blockIndex !== undefined) positions.set(annotation.id, blockIndex);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
for (const [displayIndex, blockMarks] of marks) {
|
|
259
|
+
for (const mark of blockMarks)
|
|
260
|
+
if (mark.annotationId) positions.set(mark.annotationId, displayIndex);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return positions;
|
|
264
|
+
}, [session, isDiff, marks]);
|
|
182
265
|
|
|
183
266
|
// ── the guided walk's view model ────────────
|
|
184
267
|
const walkFileList = controller.files();
|
|
@@ -189,8 +272,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
189
272
|
// out of span mode clears the renderer selection (compose paints its own
|
|
190
273
|
// mark, and a mouse drag never changes the mode, so it survives)
|
|
191
274
|
useEffect(() => {
|
|
192
|
-
|
|
193
|
-
|
|
275
|
+
// one marker at a time: clear any prior selection, then paint the current
|
|
276
|
+
// span (span and its quick-actions sub-mode both keep it painted)
|
|
277
|
+
const span = activeSpanState(mode);
|
|
278
|
+
planSheetRef.current?.clearSelection();
|
|
279
|
+
if (span) planSheetRef.current?.driveSpanSelection(span);
|
|
194
280
|
}, [mode]);
|
|
195
281
|
|
|
196
282
|
// ── selection symmetry: one selected id, both sides ──
|
|
@@ -221,6 +307,25 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
221
307
|
revealAnchor(annotationId);
|
|
222
308
|
};
|
|
223
309
|
|
|
310
|
+
// selecting a removal reveals its source line, so the reader sees what the
|
|
311
|
+
// pending undo would bring back: the diff sheet follows the cursor; the plan
|
|
312
|
+
// sheet also needs an explicit scroll to the cut block
|
|
313
|
+
const selectCurationFromRail = (curationId: string): void => {
|
|
314
|
+
setSelectedCurationId(curationId);
|
|
315
|
+
const item = curationItems.find((candidate) => candidate.id === curationId);
|
|
316
|
+
if (!item) return;
|
|
317
|
+
setCursor(item.revealIndex);
|
|
318
|
+
if (item.source === "plan") planSheetRef.current?.revealBlock(item.revealIndex);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// the selected removal card's undo button: same restore path as the u key,
|
|
322
|
+
// including the observer read-only guard the keyboard path gets via reduceKey
|
|
323
|
+
const undoCurationFromRail = (curationId: string): void => {
|
|
324
|
+
if (observer) return controller.setStatus("observer - read-only");
|
|
325
|
+
controller.restoreCuration(curationId);
|
|
326
|
+
setSelectedCurationId(undefined);
|
|
327
|
+
};
|
|
328
|
+
|
|
224
329
|
const openCardEdit = (annotationId: string): void => {
|
|
225
330
|
if (observer) return controller.setStatus("observer - read-only");
|
|
226
331
|
const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
|
|
@@ -229,7 +334,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
229
334
|
// the author rather than editing the body the planner does not own
|
|
230
335
|
if (annotation.author) {
|
|
231
336
|
setFocusedAnnotationId(annotationId);
|
|
232
|
-
return void setMode({
|
|
337
|
+
return void setMode({
|
|
338
|
+
type: "rename",
|
|
339
|
+
authorId: annotation.author,
|
|
340
|
+
text: authorNames[annotation.author] ?? "",
|
|
341
|
+
});
|
|
233
342
|
}
|
|
234
343
|
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
235
344
|
liveInput.current = annotation.body;
|
|
@@ -262,7 +371,9 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
262
371
|
reviewWidth,
|
|
263
372
|
terminalWidth,
|
|
264
373
|
focusedAnnotationId,
|
|
374
|
+
selectedCurationId,
|
|
265
375
|
authorNames,
|
|
376
|
+
quickActions,
|
|
266
377
|
renameAuthor: (id: string, name: string) => {
|
|
267
378
|
persistAuthorName(id, name);
|
|
268
379
|
setAuthorNames((prev) => ({ ...prev, [id]: name }));
|
|
@@ -277,6 +388,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
277
388
|
setReviewWidth,
|
|
278
389
|
setRailTab,
|
|
279
390
|
setFocusedAnnotationId,
|
|
391
|
+
setSelectedCurationId,
|
|
280
392
|
setPulsedAnnotationId,
|
|
281
393
|
selectCardFromDocument,
|
|
282
394
|
runEditorHandOff,
|
|
@@ -290,17 +402,126 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
290
402
|
? "submit"
|
|
291
403
|
: mode.type === "confirmDelete"
|
|
292
404
|
? "confirm"
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
405
|
+
: mode.type === "rename" || mode.type === "nameSelf"
|
|
406
|
+
? "prompt"
|
|
407
|
+
: mode.type === "spanActions"
|
|
408
|
+
? "spanActions"
|
|
409
|
+
: completion.phase === "prompt"
|
|
410
|
+
? "completion-prompt"
|
|
411
|
+
: completion.phase === "counting"
|
|
412
|
+
? "completion-counting"
|
|
413
|
+
: walking
|
|
414
|
+
? "walk"
|
|
415
|
+
: "none";
|
|
416
|
+
|
|
417
|
+
// ── settings dialog: config-backed model, navigation, persistence ──
|
|
418
|
+
const settingsCategories: SettingsCategory[] = [
|
|
419
|
+
{
|
|
420
|
+
id: "general",
|
|
421
|
+
name: "General",
|
|
422
|
+
description: "submission behaviour",
|
|
423
|
+
rows: [
|
|
424
|
+
{
|
|
425
|
+
key: "autoClose",
|
|
426
|
+
label: "Auto-close on submit",
|
|
427
|
+
kind: "cycle",
|
|
428
|
+
options: ["off", "3s", "10s"],
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
id: "display",
|
|
434
|
+
name: "Display",
|
|
435
|
+
description: "the review panel",
|
|
436
|
+
rows: [
|
|
437
|
+
{
|
|
438
|
+
key: "reviewPanel",
|
|
439
|
+
label: "Review panel",
|
|
440
|
+
kind: "cycle",
|
|
441
|
+
options: ["expanded", "compact", "hidden"],
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: "appearance",
|
|
447
|
+
name: "Appearance",
|
|
448
|
+
description: "the color theme",
|
|
449
|
+
rows: [
|
|
450
|
+
{
|
|
451
|
+
key: "theme",
|
|
452
|
+
label: "Theme",
|
|
453
|
+
kind: "cycle",
|
|
454
|
+
options: THEME_NAMES.map((name) => THEME_LABELS[name]),
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
const settingsValues = {
|
|
460
|
+
autoClose: autoClose === "off" ? "off" : `${autoClose}s`,
|
|
461
|
+
reviewPanel: reviewMode,
|
|
462
|
+
theme: THEME_LABELS[themeName],
|
|
463
|
+
};
|
|
464
|
+
const cycleSetting = (rowKey: string): void => {
|
|
465
|
+
if (rowKey === "autoClose") {
|
|
466
|
+
const next: AutoClose = autoClose === "off" ? 3 : autoClose === 3 ? 10 : "off";
|
|
467
|
+
setAutoClose(next);
|
|
468
|
+
persistAutoClose(next);
|
|
469
|
+
} else if (rowKey === "reviewPanel") {
|
|
470
|
+
const order: ReviewPanelMode[] = ["expanded", "compact", "hidden"];
|
|
471
|
+
const next = order[(order.indexOf(reviewMode) + 1) % order.length]!;
|
|
472
|
+
setReviewMode(next);
|
|
473
|
+
controller.saveReviewPanel({ mode: next });
|
|
474
|
+
} else if (rowKey === "theme") {
|
|
475
|
+
const next = THEME_NAMES[(THEME_NAMES.indexOf(themeName) + 1) % THEME_NAMES.length]!;
|
|
476
|
+
setThemeName(next);
|
|
477
|
+
setTheme(composeTheme(next, themeOverrides));
|
|
478
|
+
persistTheme(next);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
const handleSettingsKey = (name: string): void => {
|
|
482
|
+
if (name === "escape") return void setMenuDialog(null);
|
|
483
|
+
const categoryIndex = settingsCategories.findIndex(
|
|
484
|
+
(category) => category.id === settingsNav.categoryId,
|
|
485
|
+
);
|
|
486
|
+
const category = settingsCategories[categoryIndex]!;
|
|
487
|
+
if (settingsNav.zone === "nav") {
|
|
488
|
+
if (name === "j" || name === "down")
|
|
489
|
+
setSettingsNav({
|
|
490
|
+
categoryId:
|
|
491
|
+
settingsCategories[Math.min(settingsCategories.length - 1, categoryIndex + 1)]!.id,
|
|
492
|
+
rowIndex: 0,
|
|
493
|
+
zone: "nav",
|
|
494
|
+
});
|
|
495
|
+
else if (name === "k" || name === "up")
|
|
496
|
+
setSettingsNav({
|
|
497
|
+
categoryId: settingsCategories[Math.max(0, categoryIndex - 1)]!.id,
|
|
498
|
+
rowIndex: 0,
|
|
499
|
+
zone: "nav",
|
|
500
|
+
});
|
|
501
|
+
else if (name === "l" || name === "tab" || name === "return")
|
|
502
|
+
setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: 0 }));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (name === "j" || name === "down")
|
|
506
|
+
setSettingsNav((state) => ({
|
|
507
|
+
...state,
|
|
508
|
+
rowIndex: Math.min(category.rows.length - 1, state.rowIndex + 1),
|
|
509
|
+
}));
|
|
510
|
+
else if (name === "k" || name === "up")
|
|
511
|
+
setSettingsNav((state) => ({ ...state, rowIndex: Math.max(0, state.rowIndex - 1) }));
|
|
512
|
+
else if (name === "h" || name === "tab") setSettingsNav((state) => ({ ...state, zone: "nav" }));
|
|
513
|
+
else if (name === "return" || name === "space")
|
|
514
|
+
cycleSetting(category.rows[settingsNav.rowIndex]!.key);
|
|
515
|
+
};
|
|
302
516
|
|
|
303
517
|
useKeyboard((key) => {
|
|
518
|
+
if (menuDialog === "settings") return void handleSettingsKey(key.name);
|
|
519
|
+
if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
|
|
520
|
+
if (menuOpen) return void (key.name === "escape" && setMenuOpen(false));
|
|
521
|
+
// the toast is non-modal: escape only dismisses it when nothing else owns
|
|
522
|
+
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
523
|
+
if (toast && key.name === "escape" && overlay === "none" && mode.type !== "span")
|
|
524
|
+
return controller.dismissToast();
|
|
304
525
|
const state: KeyState = {
|
|
305
526
|
keys: keysRef.current,
|
|
306
527
|
readOnly: observer,
|
|
@@ -316,15 +537,59 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
316
537
|
hasFocusedAnnotation: focusedAnnotationId !== undefined,
|
|
317
538
|
walkAtEnd: walk !== null && walk.index >= walkFileList.length,
|
|
318
539
|
cursorAnnotatable: isDiff
|
|
319
|
-
? rows[cursor] !== undefined &&
|
|
540
|
+
? rows[cursor] !== undefined &&
|
|
541
|
+
rows[cursor]!.kind !== "file" &&
|
|
542
|
+
rows[cursor]!.kind !== "hunk"
|
|
320
543
|
: !!display[cursor]?.work,
|
|
321
544
|
};
|
|
322
545
|
keyBindings.setContext({ overlay: state.overlay, spanMode: state.spanMode });
|
|
323
546
|
const action = keyBindings.resolveAction({ name: key.name, shift: !!key.shift });
|
|
324
|
-
for (const intent of reduceKey(
|
|
547
|
+
for (const intent of reduceKey(
|
|
548
|
+
state,
|
|
549
|
+
{ name: key.name, shift: !!key.shift, meta: !!key.meta },
|
|
550
|
+
action,
|
|
551
|
+
))
|
|
325
552
|
dispatch(intent);
|
|
326
553
|
});
|
|
327
554
|
|
|
555
|
+
// ── shared bottom chrome: the menu bar and its drop-up dialogs, one render
|
|
556
|
+
// reused by the inbox and by plan/diff review so the two never drift ──
|
|
557
|
+
const menuChrome = (
|
|
558
|
+
<>
|
|
559
|
+
<MenuBar
|
|
560
|
+
open={menuOpen}
|
|
561
|
+
version={CLIENT_VERSION}
|
|
562
|
+
status={status}
|
|
563
|
+
onToggle={() => setMenuOpen((isOpen) => !isOpen)}
|
|
564
|
+
onSettings={() => {
|
|
565
|
+
setMenuOpen(false);
|
|
566
|
+
setMenuDialog("settings");
|
|
567
|
+
}}
|
|
568
|
+
onKeybinds={() => {
|
|
569
|
+
setMenuOpen(false);
|
|
570
|
+
setMenuDialog("keybinds");
|
|
571
|
+
}}
|
|
572
|
+
theme={theme}
|
|
573
|
+
/>
|
|
574
|
+
{menuDialog === "keybinds" ? (
|
|
575
|
+
<KeybindsDialog sections={keyBindings.cheatsheet()} theme={theme} />
|
|
576
|
+
) : null}
|
|
577
|
+
{menuDialog === "settings" ? (
|
|
578
|
+
<SettingsDialog
|
|
579
|
+
isOpen
|
|
580
|
+
categories={settingsCategories}
|
|
581
|
+
values={settingsValues}
|
|
582
|
+
activeCategoryId={settingsNav.categoryId}
|
|
583
|
+
activeRowIndex={settingsNav.rowIndex}
|
|
584
|
+
activeZone={settingsNav.zone}
|
|
585
|
+
onCategorySelect={(id) => setSettingsNav({ categoryId: id, rowIndex: 0, zone: "body" })}
|
|
586
|
+
onRowActivate={(row) => cycleSetting(row.key)}
|
|
587
|
+
theme={theme}
|
|
588
|
+
/>
|
|
589
|
+
) : null}
|
|
590
|
+
</>
|
|
591
|
+
);
|
|
592
|
+
|
|
328
593
|
// ── render ──────────────────────────────────
|
|
329
594
|
if (error) {
|
|
330
595
|
return (
|
|
@@ -344,28 +609,65 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
344
609
|
const confirming = mode.type === "confirmDelete" ? mode : null;
|
|
345
610
|
return (
|
|
346
611
|
<ThemeProvider theme={theme}>
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
isOpen={confirming !== null}
|
|
354
|
-
title=" Delete plan "
|
|
355
|
-
message={confirming ? `Delete "${confirming.title}"? This removes the plan and its review.` : ""}
|
|
356
|
-
onConfirm={() => {
|
|
357
|
-
if (confirming) controller.deleteSession(confirming.sessionId);
|
|
358
|
-
setMode({ type: "normal" });
|
|
612
|
+
<box
|
|
613
|
+
style={{
|
|
614
|
+
flexDirection: "column",
|
|
615
|
+
width: "100%",
|
|
616
|
+
height: "100%",
|
|
617
|
+
backgroundColor: theme.background,
|
|
359
618
|
}}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
619
|
+
>
|
|
620
|
+
{/* mirrors the review header row: same box, position, and accent product
|
|
621
|
+
word, with a " · resume" separator and no Edit/Share toolbar */}
|
|
622
|
+
<box
|
|
623
|
+
style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme.panel }}
|
|
624
|
+
>
|
|
625
|
+
<box
|
|
626
|
+
style={{
|
|
627
|
+
height: 1,
|
|
628
|
+
backgroundColor: theme.panel,
|
|
629
|
+
paddingLeft: 1,
|
|
630
|
+
flexDirection: "row",
|
|
631
|
+
}}
|
|
632
|
+
>
|
|
633
|
+
<text>
|
|
634
|
+
<span fg={theme.accent}>cueloop</span>
|
|
635
|
+
<span fg={theme.textDim}> · resume</span>
|
|
636
|
+
</text>
|
|
637
|
+
</box>
|
|
638
|
+
<box style={{ flexGrow: 1 }} />
|
|
639
|
+
</box>
|
|
640
|
+
<InboxList
|
|
641
|
+
inbox={inbox}
|
|
642
|
+
cursor={inboxCursor}
|
|
643
|
+
onRequestDelete={(id, title) =>
|
|
644
|
+
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
645
|
+
}
|
|
646
|
+
/>
|
|
647
|
+
{menuChrome}
|
|
648
|
+
<ConfirmDialog
|
|
649
|
+
isOpen={confirming !== null}
|
|
650
|
+
title=" Delete plan "
|
|
651
|
+
message={
|
|
652
|
+
confirming
|
|
653
|
+
? `Delete "${confirming.title}"? This removes the plan and its review.`
|
|
654
|
+
: ""
|
|
655
|
+
}
|
|
656
|
+
onConfirm={() => {
|
|
657
|
+
if (confirming) controller.deleteSession(confirming.sessionId);
|
|
658
|
+
setMode({ type: "normal" });
|
|
659
|
+
}}
|
|
660
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
661
|
+
theme={theme}
|
|
662
|
+
/>
|
|
663
|
+
</box>
|
|
363
664
|
</ThemeProvider>
|
|
364
665
|
);
|
|
365
666
|
}
|
|
366
667
|
|
|
367
668
|
const activeSession = session!;
|
|
368
|
-
const pendingCount =
|
|
669
|
+
const pendingCount =
|
|
670
|
+
reviewerAnnotations(activeSession).length + (activeSession.workingCopy !== undefined ? 1 : 0);
|
|
369
671
|
|
|
370
672
|
if ((completion.phase === "prompt" || completion.phase === "counting") && activeSession.verdict) {
|
|
371
673
|
return (
|
|
@@ -374,7 +676,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
374
676
|
verdict={activeSession.verdict.kind}
|
|
375
677
|
completion={completion}
|
|
376
678
|
status={status}
|
|
377
|
-
returnsTo={
|
|
679
|
+
returnsTo={
|
|
680
|
+
returnPaneFor(activeSession.artifact.meta.herdrPane)
|
|
681
|
+
? (activeSession.artifact.meta.agent ?? "the agent")
|
|
682
|
+
: undefined
|
|
683
|
+
}
|
|
378
684
|
/>
|
|
379
685
|
</ThemeProvider>
|
|
380
686
|
);
|
|
@@ -398,17 +704,93 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
398
704
|
}
|
|
399
705
|
: null;
|
|
400
706
|
|
|
401
|
-
const
|
|
402
|
-
mode.type === "
|
|
403
|
-
? {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
707
|
+
const diffComposeState =
|
|
708
|
+
mode.type === "compose" && isDiff
|
|
709
|
+
? {
|
|
710
|
+
kind: mode.kind,
|
|
711
|
+
rowIndex: mode.displayIndex,
|
|
712
|
+
quote: rows[mode.displayIndex]?.text.replace(/\n$/, "") ?? "",
|
|
713
|
+
draft: {
|
|
714
|
+
text: mode.text,
|
|
715
|
+
onInput: (text: string) => {
|
|
716
|
+
liveInput.current = text;
|
|
717
|
+
setMode({ ...mode, text });
|
|
718
|
+
},
|
|
719
|
+
onSave: () => dispatch({ type: "saveCompose" }),
|
|
720
|
+
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
721
|
+
},
|
|
722
|
+
}
|
|
723
|
+
: null;
|
|
724
|
+
|
|
725
|
+
const markedSpan = activeSpanState(mode);
|
|
726
|
+
const activeSpan = markedSpan
|
|
727
|
+
? { displayIndex: markedSpan.displayIndex, start: markedSpan.start, end: markedSpan.end }
|
|
728
|
+
: mode.type === "compose" && !isDiff
|
|
729
|
+
? // the compose anchor stays painted selection-style while the box is open
|
|
730
|
+
{ displayIndex: mode.displayIndex, start: mode.start, end: mode.end }
|
|
731
|
+
: null;
|
|
732
|
+
|
|
733
|
+
// mouse mutations bypass reduceKey, so they replay its gates: an observer or a
|
|
734
|
+
// resolved review is read-only. Returns the status to answer with, or null.
|
|
735
|
+
const spanMutationBlock = (): string | null =>
|
|
736
|
+
observer ? "observer - read-only" : resolved ? "review submitted - read-only" : null;
|
|
737
|
+
|
|
738
|
+
// the marker-actions popover is span mode made visible: an inline toolbar at
|
|
739
|
+
// the marked block, or its quick-actions list. Cut is owner-only (hidden for a
|
|
740
|
+
// collaborator, like every other plan-edit affordance).
|
|
741
|
+
const popoverState =
|
|
742
|
+
markedSpan && !isDiff
|
|
743
|
+
? {
|
|
744
|
+
displayIndex: markedSpan.displayIndex,
|
|
745
|
+
view: mode.type === "spanActions" ? ("actions" as const) : ("toolbar" as const),
|
|
746
|
+
actions: quickActions,
|
|
747
|
+
actionIndex: mode.type === "spanActions" ? mode.index : 0,
|
|
748
|
+
canCut: isOwner,
|
|
749
|
+
onComment: () => {
|
|
750
|
+
const blocked = spanMutationBlock();
|
|
751
|
+
if (blocked) return controller.setStatus(blocked);
|
|
752
|
+
dispatch({ type: "openCompose", kind: "comment", from: "span" });
|
|
753
|
+
},
|
|
754
|
+
onCut: () => {
|
|
755
|
+
const blocked = spanMutationBlock();
|
|
756
|
+
if (blocked) return controller.setStatus(blocked);
|
|
757
|
+
if (!isOwner) return;
|
|
758
|
+
dispatch({ type: "spanCut" });
|
|
759
|
+
},
|
|
760
|
+
onOpenActions: () => {
|
|
761
|
+
const blocked = spanMutationBlock();
|
|
762
|
+
if (blocked) return controller.setStatus(blocked);
|
|
763
|
+
dispatch({ type: "openSpanActions" });
|
|
764
|
+
},
|
|
765
|
+
onClose: () => dispatch({ type: "closeOverlay" }),
|
|
766
|
+
onPickAction: (index: number) => {
|
|
767
|
+
const blocked = spanMutationBlock();
|
|
768
|
+
if (blocked) return controller.setStatus(blocked);
|
|
769
|
+
dispatch({ type: "pickSpanAction", index });
|
|
770
|
+
},
|
|
771
|
+
onBack: () => dispatch({ type: "closeSpanActions" }),
|
|
772
|
+
}
|
|
773
|
+
: null;
|
|
408
774
|
|
|
409
775
|
const onLineActivate = (displayIndex: number): void => {
|
|
410
|
-
//
|
|
411
|
-
|
|
776
|
+
// a mouse drag leaves a native selection: turn it into a word span so the
|
|
777
|
+
// marker popover opens at the dragged range, mirroring the `v` grammar
|
|
778
|
+
if (renderer?.hasSelection) {
|
|
779
|
+
// the release block re-anchors the span, so each drag replaces the last
|
|
780
|
+
const selection = planSheetRef.current?.readSelection(displayIndex);
|
|
781
|
+
const block = selection ? display[selection.displayIndex] : undefined;
|
|
782
|
+
const span =
|
|
783
|
+
selection && block
|
|
784
|
+
? spanFromRange(
|
|
785
|
+
selection.displayIndex,
|
|
786
|
+
displayText(block),
|
|
787
|
+
selection.start,
|
|
788
|
+
selection.end,
|
|
789
|
+
)
|
|
790
|
+
: null;
|
|
791
|
+
if (span) setMode({ type: "span", span });
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
412
794
|
setCursor(displayIndex);
|
|
413
795
|
const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
|
|
414
796
|
if (annotationId) selectCardFromDocument(annotationId);
|
|
@@ -484,36 +866,48 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
484
866
|
}
|
|
485
867
|
: null;
|
|
486
868
|
|
|
869
|
+
// width the review panel occupies (divider + rail), so the header can reserve
|
|
870
|
+
// it and keep Edit/Share pinned to the plan sheet's right edge, not the rail's
|
|
871
|
+
const railFootprint =
|
|
872
|
+
reviewMode === "hidden"
|
|
873
|
+
? 0
|
|
874
|
+
: 1 +
|
|
875
|
+
(reviewMode === "compact"
|
|
876
|
+
? REVIEW_COMPACT_WIDTH
|
|
877
|
+
: resolveReviewWidth(reviewWidth, terminalWidth));
|
|
878
|
+
|
|
879
|
+
// status badges sit right after the product word so they survive a header
|
|
880
|
+
// that is too narrow for the whole trail (the rail can eat the width)
|
|
487
881
|
const headerItems: BreadcrumbItem[] = [
|
|
488
882
|
{ label: "cueloop", tone: "accent" },
|
|
489
|
-
|
|
490
|
-
|
|
883
|
+
...(resolved
|
|
884
|
+
? [
|
|
885
|
+
{
|
|
886
|
+
label: `resolved: ${activeSession.verdict!.kind.replace("_", " ")}`,
|
|
887
|
+
tone: "green" as const,
|
|
888
|
+
},
|
|
889
|
+
]
|
|
890
|
+
: []),
|
|
491
891
|
...(observer ? [{ label: "observer", tone: "dim" as const }] : []),
|
|
492
|
-
...(role === "collaborator"
|
|
493
|
-
|
|
892
|
+
...(role === "collaborator"
|
|
893
|
+
? [{ label: "shared · your notes save as you go", tone: "dim" as const }]
|
|
894
|
+
: []),
|
|
895
|
+
{
|
|
896
|
+
label: `${activeSession.artifact.meta.title ?? activeSession.artifact.meta.planPath ?? activeSession.id} · rev ${activeSession.revisions.length}`,
|
|
897
|
+
tone: "dim",
|
|
898
|
+
},
|
|
899
|
+
{ label: `submitted by ${activeSession.artifact.meta.agent ?? "unknown"}`, tone: "dim" },
|
|
494
900
|
];
|
|
495
901
|
|
|
496
|
-
keyBindings.setContext({ overlay, spanMode: mode.type === "span" });
|
|
497
|
-
const hintMode: HintMode = observer
|
|
498
|
-
? "read-only"
|
|
499
|
-
: mode.type === "submit"
|
|
500
|
-
? "submit"
|
|
501
|
-
: mode.type === "span"
|
|
502
|
-
? "span"
|
|
503
|
-
: mode.type === "compose" || mode.type === "railEdit"
|
|
504
|
-
? "compose"
|
|
505
|
-
: walking
|
|
506
|
-
? "walk"
|
|
507
|
-
: focusedAnnotationId !== undefined
|
|
508
|
-
? "card"
|
|
509
|
-
: role === "collaborator"
|
|
510
|
-
? "collaborator"
|
|
511
|
-
: "normal";
|
|
512
|
-
|
|
513
902
|
return (
|
|
514
903
|
<ThemeProvider theme={theme}>
|
|
515
904
|
<box
|
|
516
|
-
style={{
|
|
905
|
+
style={{
|
|
906
|
+
flexDirection: "column",
|
|
907
|
+
width: "100%",
|
|
908
|
+
height: "100%",
|
|
909
|
+
backgroundColor: theme.background,
|
|
910
|
+
}}
|
|
517
911
|
onMouseDrag={(event: MouseEvent) => {
|
|
518
912
|
if (!dividerDragging || reviewMode !== "expanded") return;
|
|
519
913
|
const next = widthFromMouseColumn(event.x, terminalWidth);
|
|
@@ -526,7 +920,25 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
526
920
|
controller.saveReviewPanel({ width: reviewWidthRef.current });
|
|
527
921
|
}}
|
|
528
922
|
>
|
|
529
|
-
<
|
|
923
|
+
<box
|
|
924
|
+
style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme.panel }}
|
|
925
|
+
>
|
|
926
|
+
<box style={{ flexGrow: 1, flexDirection: "row", paddingRight: 1 }}>
|
|
927
|
+
<Breadcrumb items={headerItems} />
|
|
928
|
+
<box style={{ flexGrow: 1 }} />
|
|
929
|
+
{isOwner && !isDiff && !resolved ? (
|
|
930
|
+
<Toolbar>
|
|
931
|
+
<Button onPress={onEditRequest} theme={theme}>
|
|
932
|
+
{" Edit "}
|
|
933
|
+
</Button>
|
|
934
|
+
<Button onPress={onShareRequest} theme={theme}>
|
|
935
|
+
{" Share "}
|
|
936
|
+
</Button>
|
|
937
|
+
</Toolbar>
|
|
938
|
+
) : null}
|
|
939
|
+
</box>
|
|
940
|
+
<box style={{ width: railFootprint }} />
|
|
941
|
+
</box>
|
|
530
942
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
531
943
|
{isDiff ? (
|
|
532
944
|
// the sheet dims to reading-quiet colors while the wizard has focus
|
|
@@ -535,6 +947,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
535
947
|
cursor={cursor}
|
|
536
948
|
annotations={activeSession.annotations}
|
|
537
949
|
focusedAnnotationId={focusedAnnotationId}
|
|
950
|
+
rejectedRows={rejectedRows}
|
|
951
|
+
compose={diffComposeState}
|
|
538
952
|
theme={walking ? dimmedTheme(theme) : undefined}
|
|
539
953
|
/>
|
|
540
954
|
) : (
|
|
@@ -546,18 +960,14 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
546
960
|
cursor={cursor}
|
|
547
961
|
activeSpan={activeSpan}
|
|
548
962
|
compose={composeState}
|
|
963
|
+
popover={popoverState}
|
|
549
964
|
editOrphanCount={editOrphanCount}
|
|
550
965
|
onLineActivate={onLineActivate}
|
|
551
|
-
onEditRequest={onEditRequest}
|
|
552
|
-
onShareRequest={onShareRequest}
|
|
553
|
-
canEdit={isOwner}
|
|
554
966
|
/>
|
|
555
967
|
)}
|
|
556
968
|
<ReviewPanel
|
|
557
969
|
mode={reviewMode}
|
|
558
970
|
width={resolveReviewWidth(reviewWidth, terminalWidth)}
|
|
559
|
-
height={terminalHeight - CHROME_ROWS}
|
|
560
|
-
dragging={dividerDragging}
|
|
561
971
|
onDividerGrab={onDividerGrab}
|
|
562
972
|
onToggle={onToggleReviewPanel}
|
|
563
973
|
railRef={railRef}
|
|
@@ -566,6 +976,9 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
566
976
|
authorNames,
|
|
567
977
|
selectedId: focusedAnnotationId,
|
|
568
978
|
resolvedIds: isDiff ? null : resolvedIds,
|
|
979
|
+
curationItems,
|
|
980
|
+
selectedCurationId,
|
|
981
|
+
annotationPositions,
|
|
569
982
|
railTab,
|
|
570
983
|
pendingCount,
|
|
571
984
|
cardEdit: cardEditState,
|
|
@@ -573,23 +986,12 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
573
986
|
onTabChange: setRailTab,
|
|
574
987
|
onSelectCard: selectCardFromRail,
|
|
575
988
|
onActivateCard: openCardEdit,
|
|
989
|
+
onSelectCuration: selectCurationFromRail,
|
|
990
|
+
onUndoCuration: undoCurationFromRail,
|
|
576
991
|
onSubmitRequest,
|
|
577
992
|
}}
|
|
578
993
|
/>
|
|
579
994
|
</box>
|
|
580
|
-
{mode.type === "compose" && isDiff ? (
|
|
581
|
-
<ComposeBar
|
|
582
|
-
kind={mode.kind}
|
|
583
|
-
quote={rows[mode.displayIndex]?.text ?? ""}
|
|
584
|
-
text={mode.text}
|
|
585
|
-
onInput={(text) => {
|
|
586
|
-
liveInput.current = text;
|
|
587
|
-
setMode({ ...mode, text });
|
|
588
|
-
}}
|
|
589
|
-
/>
|
|
590
|
-
) : (
|
|
591
|
-
<StatusBar>{keyBindings.statusHint(hintMode)}</StatusBar>
|
|
592
|
-
)}
|
|
593
995
|
{walking && walk !== null ? (
|
|
594
996
|
<WalkWizard
|
|
595
997
|
files={walkFileList}
|
|
@@ -630,6 +1032,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
630
1032
|
theme={theme}
|
|
631
1033
|
/>
|
|
632
1034
|
) : null}
|
|
1035
|
+
{toast ? <Toast title={toast.title} body={toast.body} theme={theme} /> : null}
|
|
1036
|
+
{menuChrome}
|
|
633
1037
|
</box>
|
|
634
1038
|
</ThemeProvider>
|
|
635
1039
|
);
|