@cueloop/client 0.1.0-alpha.33 → 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 +130 -4
- package/src/App.export.test.tsx +22 -4
- package/src/App.plan-review.test.tsx +127 -27
- package/src/App.readonly.test.tsx +31 -8
- package/src/App.rendering.test.tsx +12 -3
- package/src/App.review-panel.test.tsx +11 -3
- package/src/App.submit-confirm.test.tsx +12 -7
- package/src/App.test.tsx +46 -9
- package/src/App.toast.test.tsx +10 -3
- package/src/App.tsx +400 -100
- package/src/App.walk.test.tsx +30 -10
- package/src/attribution.ts +8 -2
- package/src/clipboard.ts +5 -1
- package/src/components/AnnotationCard.stories.tsx +28 -9
- package/src/components/AnnotationCard.tsx +53 -38
- 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 +22 -5
- package/src/components/ConfirmDialog.tsx +9 -1
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +18 -2
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +184 -98
- package/src/components/InboxList.stories.tsx +16 -3
- package/src/components/InboxList.tsx +17 -10
- package/src/components/KeybindsDialog.tsx +19 -3
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +102 -0
- package/src/components/MenuBar.stories.tsx +5 -1
- package/src/components/MenuBar.tsx +18 -2
- package/src/components/PlanSheet.stories.tsx +34 -2
- package/src/components/PlanSheet.tsx +141 -32
- package/src/components/PromptDialog.tsx +18 -2
- package/src/components/ReviewPanel.stories.tsx +13 -3
- package/src/components/ReviewPanel.tsx +8 -4
- package/src/components/ReviewRail.stories.tsx +145 -72
- package/src/components/ReviewRail.tsx +177 -38
- package/src/components/SettingsDialog.stories.tsx +51 -3
- package/src/components/SettingsDialog.tsx +8 -3
- package/src/components/SettingsRows.tsx +19 -3
- package/src/components/Toast.tsx +17 -2
- package/src/components/WalkWizard.stories.tsx +13 -2
- package/src/components/WalkWizard.tsx +9 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +281 -83
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.tsx +5 -1
- package/src/components/primitives/Dialog.tsx +9 -1
- package/src/components/primitives/Tabs.tsx +24 -4
- 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 +36 -14
- 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 +11 -3
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +65 -17
- package/src/session-controller.ts +301 -26
- package/src/share.ts +41 -8
- package/src/test-support.ts +16 -6
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +1 -3
- 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/App.tsx
CHANGED
|
@@ -13,14 +13,36 @@ 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
47
|
import { KeyBindings } from "./key-bindings";
|
|
26
48
|
import { ThemeProvider } from "./components/theme-context";
|
|
@@ -79,7 +101,16 @@ export interface AppProps {
|
|
|
79
101
|
selfAuthor?: string;
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
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 {
|
|
83
114
|
// Observer stays fully read-only; a collaborator writes annotations but not
|
|
84
115
|
// the plan or a verdict. `observer` is what the controller and every write
|
|
85
116
|
// gate key off; the two capability flags carve out the collaborator's middle.
|
|
@@ -89,7 +120,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
89
120
|
// feeds all three; split it the day a collaborator earns one of them.
|
|
90
121
|
const isOwner = !observer && role === "owner";
|
|
91
122
|
const controller = useMemo(
|
|
92
|
-
() =>
|
|
123
|
+
() =>
|
|
124
|
+
createReviewController({ home, sessionId, readOnly: observer, onExit, clock, openClient }),
|
|
93
125
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
94
126
|
[home, sessionId],
|
|
95
127
|
);
|
|
@@ -97,11 +129,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
97
129
|
controller.connect();
|
|
98
130
|
return () => controller.close();
|
|
99
131
|
}, [controller]);
|
|
100
|
-
const { session, inbox, status, toast, error, completion, editOrphanCount, walk } =
|
|
101
|
-
controller.subscribe,
|
|
102
|
-
controller.getSnapshot,
|
|
103
|
-
controller.getSnapshot,
|
|
104
|
-
);
|
|
132
|
+
const { session, inbox, status, toast, error, completion, editOrphanCount, walk } =
|
|
133
|
+
useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
105
134
|
// A shared plan you own polls for collaborator notes while it is open; the
|
|
106
135
|
// merge refreshes through the normal event path. Stops on leave.
|
|
107
136
|
useEffect(() => {
|
|
@@ -119,12 +148,17 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
119
148
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
120
149
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
121
150
|
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
122
|
-
const [settingsNav, setSettingsNav] = useState<{
|
|
151
|
+
const [settingsNav, setSettingsNav] = useState<{
|
|
152
|
+
categoryId: string;
|
|
153
|
+
rowIndex: number;
|
|
154
|
+
zone: "nav" | "body";
|
|
155
|
+
}>({
|
|
123
156
|
categoryId: "general",
|
|
124
157
|
rowIndex: 0,
|
|
125
158
|
zone: "body",
|
|
126
159
|
});
|
|
127
160
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
161
|
+
const [selectedCurationId, setSelectedCurationId] = useState<string | undefined>(undefined);
|
|
128
162
|
const [railTab, setRailTab] = useState<"review" | "agent">("review");
|
|
129
163
|
// review panel layout: mode + expanded width are client view state, loaded
|
|
130
164
|
// from and persisted to the user config so they survive a restart. The ref
|
|
@@ -145,16 +179,22 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
145
179
|
const keysRef = useRef(DEFAULT_KEYS);
|
|
146
180
|
const keyBindings = useMemo(() => new KeyBindings(DEFAULT_KEYS), []);
|
|
147
181
|
const [theme, setTheme] = useState(DARK);
|
|
182
|
+
const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
|
|
183
|
+
const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
|
|
148
184
|
const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
|
|
185
|
+
const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
|
|
149
186
|
useEffect(() => {
|
|
150
187
|
const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
151
188
|
keysRef.current = config.keys;
|
|
152
189
|
keyBindings.setKeys(config.keys);
|
|
153
190
|
setTheme(config.theme);
|
|
191
|
+
setThemeName(config.ui.theme);
|
|
192
|
+
setThemeOverrides(config.themeOverrides);
|
|
154
193
|
setReviewMode(config.ui.reviewState);
|
|
155
194
|
setReviewWidth(config.ui.reviewWidth);
|
|
156
195
|
reviewWidthRef.current = config.ui.reviewWidth;
|
|
157
196
|
setAuthorNames(config.authors);
|
|
197
|
+
setQuickActions(config.actions);
|
|
158
198
|
setAutoClose(config.ui.autoClose);
|
|
159
199
|
controller.applyConfig(config);
|
|
160
200
|
}, [session?.workspace.repoRoot, controller, keyBindings]);
|
|
@@ -184,8 +224,13 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
184
224
|
// ── derived view model ──────────────────────
|
|
185
225
|
const display = controller.display();
|
|
186
226
|
const rows = controller.rows();
|
|
227
|
+
const rejectedRows = controller.rejectedRows();
|
|
228
|
+
const curationItems = controller.curationItems();
|
|
187
229
|
const marks = useMemo(
|
|
188
|
-
() =>
|
|
230
|
+
() =>
|
|
231
|
+
session
|
|
232
|
+
? marksByDisplay(session.annotations, display, pulsedAnnotationId ?? undefined)
|
|
233
|
+
: new Map<number, Mark[]>(),
|
|
189
234
|
[session, display, pulsedAnnotationId],
|
|
190
235
|
);
|
|
191
236
|
/** Annotation ids whose anchor resolved against the working copy. */
|
|
@@ -198,6 +243,25 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
198
243
|
}, [marks]);
|
|
199
244
|
const resolved = session?.status === "resolved";
|
|
200
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]);
|
|
201
265
|
|
|
202
266
|
// ── the guided walk's view model ────────────
|
|
203
267
|
const walkFileList = controller.files();
|
|
@@ -208,8 +272,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
208
272
|
// out of span mode clears the renderer selection (compose paints its own
|
|
209
273
|
// mark, and a mouse drag never changes the mode, so it survives)
|
|
210
274
|
useEffect(() => {
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
213
280
|
}, [mode]);
|
|
214
281
|
|
|
215
282
|
// ── selection symmetry: one selected id, both sides ──
|
|
@@ -240,6 +307,25 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
240
307
|
revealAnchor(annotationId);
|
|
241
308
|
};
|
|
242
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
|
+
|
|
243
329
|
const openCardEdit = (annotationId: string): void => {
|
|
244
330
|
if (observer) return controller.setStatus("observer - read-only");
|
|
245
331
|
const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
|
|
@@ -248,7 +334,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
248
334
|
// the author rather than editing the body the planner does not own
|
|
249
335
|
if (annotation.author) {
|
|
250
336
|
setFocusedAnnotationId(annotationId);
|
|
251
|
-
return void setMode({
|
|
337
|
+
return void setMode({
|
|
338
|
+
type: "rename",
|
|
339
|
+
authorId: annotation.author,
|
|
340
|
+
text: authorNames[annotation.author] ?? "",
|
|
341
|
+
});
|
|
252
342
|
}
|
|
253
343
|
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
254
344
|
liveInput.current = annotation.body;
|
|
@@ -281,7 +371,9 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
281
371
|
reviewWidth,
|
|
282
372
|
terminalWidth,
|
|
283
373
|
focusedAnnotationId,
|
|
374
|
+
selectedCurationId,
|
|
284
375
|
authorNames,
|
|
376
|
+
quickActions,
|
|
285
377
|
renameAuthor: (id: string, name: string) => {
|
|
286
378
|
persistAuthorName(id, name);
|
|
287
379
|
setAuthorNames((prev) => ({ ...prev, [id]: name }));
|
|
@@ -296,6 +388,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
296
388
|
setReviewWidth,
|
|
297
389
|
setRailTab,
|
|
298
390
|
setFocusedAnnotationId,
|
|
391
|
+
setSelectedCurationId,
|
|
299
392
|
setPulsedAnnotationId,
|
|
300
393
|
selectCardFromDocument,
|
|
301
394
|
runEditorHandOff,
|
|
@@ -309,15 +402,17 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
309
402
|
? "submit"
|
|
310
403
|
: mode.type === "confirmDelete"
|
|
311
404
|
? "confirm"
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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";
|
|
321
416
|
|
|
322
417
|
// ── settings dialog: config-backed model, navigation, persistence ──
|
|
323
418
|
const settingsCategories: SettingsCategory[] = [
|
|
@@ -325,16 +420,47 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
325
420
|
id: "general",
|
|
326
421
|
name: "General",
|
|
327
422
|
description: "submission behaviour",
|
|
328
|
-
rows: [
|
|
423
|
+
rows: [
|
|
424
|
+
{
|
|
425
|
+
key: "autoClose",
|
|
426
|
+
label: "Auto-close on submit",
|
|
427
|
+
kind: "cycle",
|
|
428
|
+
options: ["off", "3s", "10s"],
|
|
429
|
+
},
|
|
430
|
+
],
|
|
329
431
|
},
|
|
330
432
|
{
|
|
331
433
|
id: "display",
|
|
332
434
|
name: "Display",
|
|
333
435
|
description: "the review panel",
|
|
334
|
-
rows: [
|
|
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
|
+
],
|
|
335
457
|
},
|
|
336
458
|
];
|
|
337
|
-
const settingsValues = {
|
|
459
|
+
const settingsValues = {
|
|
460
|
+
autoClose: autoClose === "off" ? "off" : `${autoClose}s`,
|
|
461
|
+
reviewPanel: reviewMode,
|
|
462
|
+
theme: THEME_LABELS[themeName],
|
|
463
|
+
};
|
|
338
464
|
const cycleSetting = (rowKey: string): void => {
|
|
339
465
|
if (rowKey === "autoClose") {
|
|
340
466
|
const next: AutoClose = autoClose === "off" ? 3 : autoClose === 3 ? 10 : "off";
|
|
@@ -345,22 +471,47 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
345
471
|
const next = order[(order.indexOf(reviewMode) + 1) % order.length]!;
|
|
346
472
|
setReviewMode(next);
|
|
347
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);
|
|
348
479
|
}
|
|
349
480
|
};
|
|
350
481
|
const handleSettingsKey = (name: string): void => {
|
|
351
482
|
if (name === "escape") return void setMenuDialog(null);
|
|
352
|
-
const categoryIndex = settingsCategories.findIndex(
|
|
483
|
+
const categoryIndex = settingsCategories.findIndex(
|
|
484
|
+
(category) => category.id === settingsNav.categoryId,
|
|
485
|
+
);
|
|
353
486
|
const category = settingsCategories[categoryIndex]!;
|
|
354
487
|
if (settingsNav.zone === "nav") {
|
|
355
|
-
if (name === "j" || name === "down")
|
|
356
|
-
|
|
357
|
-
|
|
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 }));
|
|
358
503
|
return;
|
|
359
504
|
}
|
|
360
|
-
if (name === "j" || name === "down")
|
|
361
|
-
|
|
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) }));
|
|
362
512
|
else if (name === "h" || name === "tab") setSettingsNav((state) => ({ ...state, zone: "nav" }));
|
|
363
|
-
else if (name === "return" || name === "space")
|
|
513
|
+
else if (name === "return" || name === "space")
|
|
514
|
+
cycleSetting(category.rows[settingsNav.rowIndex]!.key);
|
|
364
515
|
};
|
|
365
516
|
|
|
366
517
|
useKeyboard((key) => {
|
|
@@ -369,7 +520,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
369
520
|
if (menuOpen) return void (key.name === "escape" && setMenuOpen(false));
|
|
370
521
|
// the toast is non-modal: escape only dismisses it when nothing else owns
|
|
371
522
|
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
372
|
-
if (toast && key.name === "escape" && overlay === "none" && mode.type !== "span")
|
|
523
|
+
if (toast && key.name === "escape" && overlay === "none" && mode.type !== "span")
|
|
524
|
+
return controller.dismissToast();
|
|
373
525
|
const state: KeyState = {
|
|
374
526
|
keys: keysRef.current,
|
|
375
527
|
readOnly: observer,
|
|
@@ -385,15 +537,59 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
385
537
|
hasFocusedAnnotation: focusedAnnotationId !== undefined,
|
|
386
538
|
walkAtEnd: walk !== null && walk.index >= walkFileList.length,
|
|
387
539
|
cursorAnnotatable: isDiff
|
|
388
|
-
? rows[cursor] !== undefined &&
|
|
540
|
+
? rows[cursor] !== undefined &&
|
|
541
|
+
rows[cursor]!.kind !== "file" &&
|
|
542
|
+
rows[cursor]!.kind !== "hunk"
|
|
389
543
|
: !!display[cursor]?.work,
|
|
390
544
|
};
|
|
391
545
|
keyBindings.setContext({ overlay: state.overlay, spanMode: state.spanMode });
|
|
392
546
|
const action = keyBindings.resolveAction({ name: key.name, shift: !!key.shift });
|
|
393
|
-
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
|
+
))
|
|
394
552
|
dispatch(intent);
|
|
395
553
|
});
|
|
396
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
|
+
|
|
397
593
|
// ── render ──────────────────────────────────
|
|
398
594
|
if (error) {
|
|
399
595
|
return (
|
|
@@ -413,28 +609,65 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
413
609
|
const confirming = mode.type === "confirmDelete" ? mode : null;
|
|
414
610
|
return (
|
|
415
611
|
<ThemeProvider theme={theme}>
|
|
416
|
-
<
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
isOpen={confirming !== null}
|
|
423
|
-
title=" Delete plan "
|
|
424
|
-
message={confirming ? `Delete "${confirming.title}"? This removes the plan and its review.` : ""}
|
|
425
|
-
onConfirm={() => {
|
|
426
|
-
if (confirming) controller.deleteSession(confirming.sessionId);
|
|
427
|
-
setMode({ type: "normal" });
|
|
612
|
+
<box
|
|
613
|
+
style={{
|
|
614
|
+
flexDirection: "column",
|
|
615
|
+
width: "100%",
|
|
616
|
+
height: "100%",
|
|
617
|
+
backgroundColor: theme.background,
|
|
428
618
|
}}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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>
|
|
432
664
|
</ThemeProvider>
|
|
433
665
|
);
|
|
434
666
|
}
|
|
435
667
|
|
|
436
668
|
const activeSession = session!;
|
|
437
|
-
const pendingCount =
|
|
669
|
+
const pendingCount =
|
|
670
|
+
reviewerAnnotations(activeSession).length + (activeSession.workingCopy !== undefined ? 1 : 0);
|
|
438
671
|
|
|
439
672
|
if ((completion.phase === "prompt" || completion.phase === "counting") && activeSession.verdict) {
|
|
440
673
|
return (
|
|
@@ -443,7 +676,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
443
676
|
verdict={activeSession.verdict.kind}
|
|
444
677
|
completion={completion}
|
|
445
678
|
status={status}
|
|
446
|
-
returnsTo={
|
|
679
|
+
returnsTo={
|
|
680
|
+
returnPaneFor(activeSession.artifact.meta.herdrPane)
|
|
681
|
+
? (activeSession.artifact.meta.agent ?? "the agent")
|
|
682
|
+
: undefined
|
|
683
|
+
}
|
|
447
684
|
/>
|
|
448
685
|
</ThemeProvider>
|
|
449
686
|
);
|
|
@@ -485,17 +722,75 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
485
722
|
}
|
|
486
723
|
: null;
|
|
487
724
|
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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;
|
|
495
774
|
|
|
496
775
|
const onLineActivate = (displayIndex: number): void => {
|
|
497
|
-
//
|
|
498
|
-
|
|
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
|
+
}
|
|
499
794
|
setCursor(displayIndex);
|
|
500
795
|
const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
|
|
501
796
|
if (annotationId) selectCardFromDocument(annotationId);
|
|
@@ -576,23 +871,43 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
576
871
|
const railFootprint =
|
|
577
872
|
reviewMode === "hidden"
|
|
578
873
|
? 0
|
|
579
|
-
: 1 +
|
|
874
|
+
: 1 +
|
|
875
|
+
(reviewMode === "compact"
|
|
876
|
+
? REVIEW_COMPACT_WIDTH
|
|
877
|
+
: resolveReviewWidth(reviewWidth, terminalWidth));
|
|
580
878
|
|
|
581
879
|
// status badges sit right after the product word so they survive a header
|
|
582
880
|
// that is too narrow for the whole trail (the rail can eat the width)
|
|
583
881
|
const headerItems: BreadcrumbItem[] = [
|
|
584
882
|
{ label: "cueloop", tone: "accent" },
|
|
585
|
-
...(resolved
|
|
883
|
+
...(resolved
|
|
884
|
+
? [
|
|
885
|
+
{
|
|
886
|
+
label: `resolved: ${activeSession.verdict!.kind.replace("_", " ")}`,
|
|
887
|
+
tone: "green" as const,
|
|
888
|
+
},
|
|
889
|
+
]
|
|
890
|
+
: []),
|
|
586
891
|
...(observer ? [{ label: "observer", tone: "dim" as const }] : []),
|
|
587
|
-
...(role === "collaborator"
|
|
588
|
-
|
|
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
|
+
},
|
|
589
899
|
{ label: `submitted by ${activeSession.artifact.meta.agent ?? "unknown"}`, tone: "dim" },
|
|
590
900
|
];
|
|
591
901
|
|
|
592
902
|
return (
|
|
593
903
|
<ThemeProvider theme={theme}>
|
|
594
904
|
<box
|
|
595
|
-
style={{
|
|
905
|
+
style={{
|
|
906
|
+
flexDirection: "column",
|
|
907
|
+
width: "100%",
|
|
908
|
+
height: "100%",
|
|
909
|
+
backgroundColor: theme.background,
|
|
910
|
+
}}
|
|
596
911
|
onMouseDrag={(event: MouseEvent) => {
|
|
597
912
|
if (!dividerDragging || reviewMode !== "expanded") return;
|
|
598
913
|
const next = widthFromMouseColumn(event.x, terminalWidth);
|
|
@@ -605,14 +920,20 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
605
920
|
controller.saveReviewPanel({ width: reviewWidthRef.current });
|
|
606
921
|
}}
|
|
607
922
|
>
|
|
608
|
-
<box
|
|
923
|
+
<box
|
|
924
|
+
style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme.panel }}
|
|
925
|
+
>
|
|
609
926
|
<box style={{ flexGrow: 1, flexDirection: "row", paddingRight: 1 }}>
|
|
610
927
|
<Breadcrumb items={headerItems} />
|
|
611
928
|
<box style={{ flexGrow: 1 }} />
|
|
612
929
|
{isOwner && !isDiff && !resolved ? (
|
|
613
930
|
<Toolbar>
|
|
614
|
-
<Button onPress={onEditRequest} theme={theme}>
|
|
615
|
-
|
|
931
|
+
<Button onPress={onEditRequest} theme={theme}>
|
|
932
|
+
{" Edit "}
|
|
933
|
+
</Button>
|
|
934
|
+
<Button onPress={onShareRequest} theme={theme}>
|
|
935
|
+
{" Share "}
|
|
936
|
+
</Button>
|
|
616
937
|
</Toolbar>
|
|
617
938
|
) : null}
|
|
618
939
|
</box>
|
|
@@ -626,6 +947,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
626
947
|
cursor={cursor}
|
|
627
948
|
annotations={activeSession.annotations}
|
|
628
949
|
focusedAnnotationId={focusedAnnotationId}
|
|
950
|
+
rejectedRows={rejectedRows}
|
|
629
951
|
compose={diffComposeState}
|
|
630
952
|
theme={walking ? dimmedTheme(theme) : undefined}
|
|
631
953
|
/>
|
|
@@ -638,6 +960,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
638
960
|
cursor={cursor}
|
|
639
961
|
activeSpan={activeSpan}
|
|
640
962
|
compose={composeState}
|
|
963
|
+
popover={popoverState}
|
|
641
964
|
editOrphanCount={editOrphanCount}
|
|
642
965
|
onLineActivate={onLineActivate}
|
|
643
966
|
/>
|
|
@@ -653,6 +976,9 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
653
976
|
authorNames,
|
|
654
977
|
selectedId: focusedAnnotationId,
|
|
655
978
|
resolvedIds: isDiff ? null : resolvedIds,
|
|
979
|
+
curationItems,
|
|
980
|
+
selectedCurationId,
|
|
981
|
+
annotationPositions,
|
|
656
982
|
railTab,
|
|
657
983
|
pendingCount,
|
|
658
984
|
cardEdit: cardEditState,
|
|
@@ -660,25 +986,12 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
660
986
|
onTabChange: setRailTab,
|
|
661
987
|
onSelectCard: selectCardFromRail,
|
|
662
988
|
onActivateCard: openCardEdit,
|
|
989
|
+
onSelectCuration: selectCurationFromRail,
|
|
990
|
+
onUndoCuration: undoCurationFromRail,
|
|
663
991
|
onSubmitRequest,
|
|
664
992
|
}}
|
|
665
993
|
/>
|
|
666
994
|
</box>
|
|
667
|
-
<MenuBar
|
|
668
|
-
open={menuOpen}
|
|
669
|
-
version={CLIENT_VERSION}
|
|
670
|
-
status={status}
|
|
671
|
-
onToggle={() => setMenuOpen((isOpen) => !isOpen)}
|
|
672
|
-
onSettings={() => {
|
|
673
|
-
setMenuOpen(false);
|
|
674
|
-
setMenuDialog("settings");
|
|
675
|
-
}}
|
|
676
|
-
onKeybinds={() => {
|
|
677
|
-
setMenuOpen(false);
|
|
678
|
-
setMenuDialog("keybinds");
|
|
679
|
-
}}
|
|
680
|
-
theme={theme}
|
|
681
|
-
/>
|
|
682
995
|
{walking && walk !== null ? (
|
|
683
996
|
<WalkWizard
|
|
684
997
|
files={walkFileList}
|
|
@@ -720,20 +1033,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
|
|
|
720
1033
|
/>
|
|
721
1034
|
) : null}
|
|
722
1035
|
{toast ? <Toast title={toast.title} body={toast.body} theme={theme} /> : null}
|
|
723
|
-
{
|
|
724
|
-
{menuDialog === "settings" ? (
|
|
725
|
-
<SettingsDialog
|
|
726
|
-
isOpen
|
|
727
|
-
categories={settingsCategories}
|
|
728
|
-
values={settingsValues}
|
|
729
|
-
activeCategoryId={settingsNav.categoryId}
|
|
730
|
-
activeRowIndex={settingsNav.rowIndex}
|
|
731
|
-
activeZone={settingsNav.zone}
|
|
732
|
-
onCategorySelect={(id) => setSettingsNav({ categoryId: id, rowIndex: 0, zone: "body" })}
|
|
733
|
-
onRowActivate={(row) => cycleSetting(row.key)}
|
|
734
|
-
theme={theme}
|
|
735
|
-
/>
|
|
736
|
-
) : null}
|
|
1036
|
+
{menuChrome}
|
|
737
1037
|
</box>
|
|
738
1038
|
</ThemeProvider>
|
|
739
1039
|
);
|