@cueloop/client 0.1.0-alpha.58 → 0.1.0-alpha.59
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.tsx +174 -574
- package/src/app-key-state.ts +60 -0
- package/src/app-screens.tsx +266 -0
- package/src/app-view-model.ts +286 -0
- package/src/components/PlanSheet.tsx +2 -62
- package/src/components/plan-sheet-run-style.ts +86 -0
- package/src/config.ts +61 -47
- package/src/intent-dispatch.ts +427 -307
- package/src/keymap.ts +81 -59
- package/src/use-settings-dialog.tsx +263 -0
package/src/App.tsx
CHANGED
|
@@ -19,52 +19,34 @@ import React, {
|
|
|
19
19
|
} from "react";
|
|
20
20
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
21
21
|
import type { Clock, MouseEvent } from "@opentui/core";
|
|
22
|
-
import { type VerdictKind } from "@cueloop/schema";
|
|
23
22
|
import { displayText, marksByDisplay, spanFromRange, type Mark } from "./view-plan";
|
|
24
|
-
import { noteForFile, viewedCount } from "./walk";
|
|
25
23
|
import { dimmedTheme } from "./theme";
|
|
26
24
|
import {
|
|
27
25
|
DEFAULT_KEYS,
|
|
28
26
|
DEFAULT_QUICK_ACTIONS,
|
|
29
27
|
loadConfig,
|
|
30
|
-
persistActions,
|
|
31
28
|
persistAuthorName,
|
|
32
|
-
persistAutoClose,
|
|
33
|
-
persistTheme,
|
|
34
29
|
type AutoClose,
|
|
35
30
|
type QuickAction,
|
|
36
31
|
} from "./config";
|
|
37
32
|
import {
|
|
38
33
|
composeTheme,
|
|
39
34
|
DEFAULT_THEME_NAME,
|
|
40
|
-
THEME_LABELS,
|
|
41
|
-
THEME_NAMES,
|
|
42
35
|
themeForName,
|
|
43
36
|
type Appearance,
|
|
44
37
|
type ThemeName,
|
|
45
38
|
} from "./theme-presets";
|
|
46
39
|
import type { Theme } from "./theme";
|
|
47
|
-
import { returnPaneFor } from "@cueloop/schema";
|
|
48
40
|
import { createReviewController } from "./session-controller";
|
|
49
41
|
import type { SessionClient } from "@cueloop/daemon/client";
|
|
50
42
|
import { launchHarnessInSplit } from "@cueloop/daemon/herdr-split";
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
createIntentDispatch,
|
|
54
|
-
reviewerAnnotations,
|
|
55
|
-
type Mode,
|
|
56
|
-
} from "./intent-dispatch";
|
|
57
|
-
import { reduceKey, type KeyState } from "./keymap";
|
|
43
|
+
import { activeSpanState, createIntentDispatch, type Mode } from "./intent-dispatch";
|
|
44
|
+
import { reduceKey } from "./keymap";
|
|
58
45
|
import { KeyBindings } from "./key-bindings";
|
|
59
46
|
import { ThemeProvider } from "./components/theme-context";
|
|
60
47
|
import { Button } from "./components/primitives/Button";
|
|
61
48
|
import { Toolbar } from "./components/primitives/Toolbar";
|
|
62
|
-
import {
|
|
63
|
-
import { KeybindsDialog } from "./components/KeybindsDialog";
|
|
64
|
-
import { SettingsDialog, type SettingsCategory } from "./components/SettingsDialog";
|
|
65
|
-
import { QuickActionsEditor } from "./components/quick-actions-editor";
|
|
66
|
-
import { CLIENT_VERSION } from "./version";
|
|
67
|
-
import { Breadcrumb, type BreadcrumbItem } from "./components/Breadcrumb";
|
|
49
|
+
import { Breadcrumb } from "./components/Breadcrumb";
|
|
68
50
|
import { PlanSheet, type PlanSheetHandle } from "./components/PlanSheet";
|
|
69
51
|
import { DiffSheet } from "./components/DiffSheet";
|
|
70
52
|
import { PrototypeSheet } from "./components/PrototypeSheet";
|
|
@@ -73,19 +55,39 @@ import { type ReviewRailHandle } from "./components/ReviewRail";
|
|
|
73
55
|
import type { AgentTerminalHandle } from "./components/agent-launcher";
|
|
74
56
|
import { ReviewPanel } from "./components/ReviewPanel";
|
|
75
57
|
import {
|
|
76
|
-
REVIEW_COMPACT_WIDTH,
|
|
77
58
|
REVIEW_DEFAULT_WIDTH,
|
|
78
59
|
resolveReviewWidth,
|
|
79
60
|
toggleReviewPanelMode,
|
|
80
61
|
widthFromMouseColumn,
|
|
81
62
|
type ReviewPanelMode,
|
|
82
63
|
} from "./review-panel";
|
|
83
|
-
import {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
64
|
+
import {
|
|
65
|
+
buildActiveSpan,
|
|
66
|
+
buildCardEditState,
|
|
67
|
+
buildComposeState,
|
|
68
|
+
buildDiffComposeState,
|
|
69
|
+
buildHeaderItems,
|
|
70
|
+
buildPopoverState,
|
|
71
|
+
buildRenderFlags,
|
|
72
|
+
buildSubmitConfirmState,
|
|
73
|
+
computePendingCount,
|
|
74
|
+
computeRailFootprint,
|
|
75
|
+
computeRoleCapabilities,
|
|
76
|
+
deriveReviewFlags,
|
|
77
|
+
isCompletionOverlayPhase,
|
|
78
|
+
isWalking,
|
|
79
|
+
resolveOverlay,
|
|
80
|
+
} from "./app-view-model";
|
|
81
|
+
import { buildKeyState } from "./app-key-state";
|
|
82
|
+
import { useSettingsDialog } from "./use-settings-dialog";
|
|
83
|
+
import {
|
|
84
|
+
CompletionScreen,
|
|
85
|
+
ConnectingScreen,
|
|
86
|
+
ErrorScreen,
|
|
87
|
+
InboxScreen,
|
|
88
|
+
MenuChrome,
|
|
89
|
+
TrailingOverlays,
|
|
90
|
+
} from "./app-screens";
|
|
89
91
|
|
|
90
92
|
/** A toast clears itself after this idle; esc dismisses it sooner. */
|
|
91
93
|
const TOAST_DISMISS_MS = 4000;
|
|
@@ -133,14 +135,7 @@ export function App({
|
|
|
133
135
|
selfAuthor,
|
|
134
136
|
appearance = "dark",
|
|
135
137
|
}: AppProps): React.ReactNode {
|
|
136
|
-
|
|
137
|
-
// the plan or a verdict. `observer` is what the controller and every write
|
|
138
|
-
// gate key off; the two capability flags carve out the collaborator's middle.
|
|
139
|
-
const observer = readOnly || role === "observer";
|
|
140
|
-
// Editing the plan, submitting a verdict, and sharing are all the owner's
|
|
141
|
-
// alone - a collaborator annotates, an observer only reads. One predicate
|
|
142
|
-
// feeds all three; split it the day a collaborator earns one of them.
|
|
143
|
-
const isOwner = !observer && role === "owner";
|
|
138
|
+
const { observer, isOwner } = computeRoleCapabilities(readOnly, role);
|
|
144
139
|
const controller = useMemo(
|
|
145
140
|
() =>
|
|
146
141
|
createReviewController({ home, sessionId, readOnly: observer, onExit, clock, openClient }),
|
|
@@ -177,15 +172,6 @@ export function App({
|
|
|
177
172
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
178
173
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
179
174
|
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
180
|
-
const [settingsNav, setSettingsNav] = useState<{
|
|
181
|
-
categoryId: string;
|
|
182
|
-
rowIndex: number;
|
|
183
|
-
zone: "nav" | "body";
|
|
184
|
-
}>({
|
|
185
|
-
categoryId: "general",
|
|
186
|
-
rowIndex: 0,
|
|
187
|
-
zone: "body",
|
|
188
|
-
});
|
|
189
175
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
190
176
|
const [selectedCurationId, setSelectedCurationId] = useState<string | undefined>(undefined);
|
|
191
177
|
const [railTab, setRailTab] = useState<"review" | "agent">("review");
|
|
@@ -214,8 +200,6 @@ export function App({
|
|
|
214
200
|
const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
|
|
215
201
|
const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
|
|
216
202
|
const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
|
|
217
|
-
// the quick-action row whose system-prompt input is open in Settings, or null
|
|
218
|
-
const [actionsExpandedIndex, setActionsExpandedIndex] = useState<number | null>(null);
|
|
219
203
|
useEffect(() => {
|
|
220
204
|
const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
221
205
|
keysRef.current = config.keys;
|
|
@@ -254,6 +238,31 @@ export function App({
|
|
|
254
238
|
if (!known) setMode({ type: "nameSelf", text: "" });
|
|
255
239
|
}, [role, selfAuthor, session]);
|
|
256
240
|
|
|
241
|
+
// ── settings dialog: config-backed model, navigation, persistence ──
|
|
242
|
+
const {
|
|
243
|
+
settingsNav,
|
|
244
|
+
settingsCategories,
|
|
245
|
+
settingsValues,
|
|
246
|
+
cycleSetting,
|
|
247
|
+
handleSettingsKey,
|
|
248
|
+
onCategorySelect,
|
|
249
|
+
} = useSettingsDialog({
|
|
250
|
+
theme,
|
|
251
|
+
appearance,
|
|
252
|
+
autoClose,
|
|
253
|
+
setAutoClose,
|
|
254
|
+
reviewMode,
|
|
255
|
+
setReviewMode,
|
|
256
|
+
themeName,
|
|
257
|
+
setThemeName,
|
|
258
|
+
themeOverrides,
|
|
259
|
+
setTheme,
|
|
260
|
+
quickActions,
|
|
261
|
+
setQuickActions,
|
|
262
|
+
controller,
|
|
263
|
+
setMenuDialog,
|
|
264
|
+
});
|
|
265
|
+
|
|
257
266
|
// ── derived view model ──────────────────────
|
|
258
267
|
const display = controller.display();
|
|
259
268
|
const rows = controller.rows();
|
|
@@ -274,9 +283,7 @@ export function App({
|
|
|
274
283
|
}
|
|
275
284
|
return ids;
|
|
276
285
|
}, [marks]);
|
|
277
|
-
const resolved = session
|
|
278
|
-
const isDiff = session?.artifact.type === "diff";
|
|
279
|
-
const isPrototype = session?.artifact.type === "prototype";
|
|
286
|
+
const { isDiff, isPrototype, resolved } = deriveReviewFlags(session);
|
|
280
287
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
281
288
|
// sort position per annotation so the rail interleaves annotation and removal
|
|
282
289
|
// cards in one line-ordered stack: a diff row carries its blockIndex; a plan
|
|
@@ -300,7 +307,7 @@ export function App({
|
|
|
300
307
|
|
|
301
308
|
// ── the guided walk's view model ────────────
|
|
302
309
|
const walkFileList = controller.files();
|
|
303
|
-
const walking = isDiff
|
|
310
|
+
const walking = isWalking(isDiff, walk);
|
|
304
311
|
const viewedPaths = useMemo(() => new Set(session?.viewedPaths ?? []), [session]);
|
|
305
312
|
|
|
306
313
|
// driving needs committed layout, so it runs after render; any transition
|
|
@@ -430,188 +437,7 @@ export function App({
|
|
|
430
437
|
openCardEdit,
|
|
431
438
|
});
|
|
432
439
|
|
|
433
|
-
const overlay
|
|
434
|
-
mode.type === "compose" || mode.type === "railEdit"
|
|
435
|
-
? "compose"
|
|
436
|
-
: mode.type === "submit"
|
|
437
|
-
? "submit"
|
|
438
|
-
: mode.type === "confirmDelete"
|
|
439
|
-
? "confirm"
|
|
440
|
-
: mode.type === "rename" || mode.type === "nameSelf"
|
|
441
|
-
? "prompt"
|
|
442
|
-
: mode.type === "spanActions"
|
|
443
|
-
? "spanActions"
|
|
444
|
-
: completion.phase === "prompt"
|
|
445
|
-
? "completion-prompt"
|
|
446
|
-
: completion.phase === "counting"
|
|
447
|
-
? "completion-counting"
|
|
448
|
-
: walking
|
|
449
|
-
? "walk"
|
|
450
|
-
: "none";
|
|
451
|
-
|
|
452
|
-
// ── settings dialog: config-backed model, navigation, persistence ──
|
|
453
|
-
const commitActions = (next: QuickAction[]): void => {
|
|
454
|
-
setQuickActions(next);
|
|
455
|
-
persistActions(next);
|
|
456
|
-
};
|
|
457
|
-
const editActionMetadata = (index: number, metadata: string): void =>
|
|
458
|
-
commitActions(
|
|
459
|
-
quickActions.map((action, actionIndex) =>
|
|
460
|
-
actionIndex === index
|
|
461
|
-
? { ...action, metadata: metadata.trim() ? metadata : undefined }
|
|
462
|
-
: action,
|
|
463
|
-
),
|
|
464
|
-
);
|
|
465
|
-
const resetActions = (): void => {
|
|
466
|
-
setActionsExpandedIndex(null);
|
|
467
|
-
commitActions(DEFAULT_QUICK_ACTIONS.map((action) => ({ ...action })));
|
|
468
|
-
};
|
|
469
|
-
const addAction = (): void => {
|
|
470
|
-
setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: quickActions.length }));
|
|
471
|
-
commitActions([...quickActions, { prompt: "New action" }]);
|
|
472
|
-
};
|
|
473
|
-
const settingsCategories: SettingsCategory[] = [
|
|
474
|
-
{
|
|
475
|
-
id: "general",
|
|
476
|
-
name: "General",
|
|
477
|
-
description: "submission behaviour",
|
|
478
|
-
rows: [
|
|
479
|
-
{
|
|
480
|
-
key: "autoClose",
|
|
481
|
-
label: "Auto-close on submit",
|
|
482
|
-
kind: "cycle",
|
|
483
|
-
options: ["off", "3s", "10s"],
|
|
484
|
-
},
|
|
485
|
-
],
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
id: "display",
|
|
489
|
-
name: "Display",
|
|
490
|
-
description: "the review panel",
|
|
491
|
-
rows: [
|
|
492
|
-
{
|
|
493
|
-
key: "reviewPanel",
|
|
494
|
-
label: "Review panel",
|
|
495
|
-
kind: "cycle",
|
|
496
|
-
options: ["expanded", "compact", "hidden"],
|
|
497
|
-
},
|
|
498
|
-
],
|
|
499
|
-
},
|
|
500
|
-
{
|
|
501
|
-
id: "appearance",
|
|
502
|
-
name: "Appearance",
|
|
503
|
-
description: "the color theme",
|
|
504
|
-
rows: [
|
|
505
|
-
{
|
|
506
|
-
key: "theme",
|
|
507
|
-
label: "Theme",
|
|
508
|
-
kind: "cycle",
|
|
509
|
-
options: THEME_NAMES.map((name) => THEME_LABELS[name]),
|
|
510
|
-
},
|
|
511
|
-
],
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
id: "actions",
|
|
515
|
-
name: "Actions",
|
|
516
|
-
description: "quick-action comments",
|
|
517
|
-
rows: [],
|
|
518
|
-
customBody: (
|
|
519
|
-
<QuickActionsEditor
|
|
520
|
-
actions={quickActions}
|
|
521
|
-
selectedIndex={settingsNav.categoryId === "actions" ? settingsNav.rowIndex : -1}
|
|
522
|
-
expandedIndex={actionsExpandedIndex}
|
|
523
|
-
onToggleExpand={(index) => {
|
|
524
|
-
setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: index }));
|
|
525
|
-
setActionsExpandedIndex((current) => (current === index ? null : index));
|
|
526
|
-
}}
|
|
527
|
-
onEditMetadata={editActionMetadata}
|
|
528
|
-
onReset={resetActions}
|
|
529
|
-
onAdd={addAction}
|
|
530
|
-
theme={theme}
|
|
531
|
-
/>
|
|
532
|
-
),
|
|
533
|
-
},
|
|
534
|
-
];
|
|
535
|
-
const settingsValues = {
|
|
536
|
-
autoClose: autoClose === "off" ? "off" : `${autoClose}s`,
|
|
537
|
-
reviewPanel: reviewMode,
|
|
538
|
-
theme: THEME_LABELS[themeName],
|
|
539
|
-
};
|
|
540
|
-
const cycleSetting = (rowKey: string): void => {
|
|
541
|
-
if (rowKey === "autoClose") {
|
|
542
|
-
const next: AutoClose = autoClose === "off" ? 3 : autoClose === 3 ? 10 : "off";
|
|
543
|
-
setAutoClose(next);
|
|
544
|
-
persistAutoClose(next);
|
|
545
|
-
} else if (rowKey === "reviewPanel") {
|
|
546
|
-
const order: ReviewPanelMode[] = ["expanded", "compact", "hidden"];
|
|
547
|
-
const next = order[(order.indexOf(reviewMode) + 1) % order.length]!;
|
|
548
|
-
setReviewMode(next);
|
|
549
|
-
controller.saveReviewPanel({ mode: next });
|
|
550
|
-
} else if (rowKey === "theme") {
|
|
551
|
-
const next = THEME_NAMES[(THEME_NAMES.indexOf(themeName) + 1) % THEME_NAMES.length]!;
|
|
552
|
-
setThemeName(next);
|
|
553
|
-
setTheme(composeTheme(next, themeOverrides, appearance));
|
|
554
|
-
persistTheme(next);
|
|
555
|
-
}
|
|
556
|
-
};
|
|
557
|
-
const handleSettingsKey = (name: string): void => {
|
|
558
|
-
// an open system-prompt input owns typing; only esc (close it) escapes here
|
|
559
|
-
if (actionsExpandedIndex !== null) {
|
|
560
|
-
if (name === "escape") setActionsExpandedIndex(null);
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
if (name === "escape") return void setMenuDialog(null);
|
|
564
|
-
const categoryIndex = settingsCategories.findIndex(
|
|
565
|
-
(category) => category.id === settingsNav.categoryId,
|
|
566
|
-
);
|
|
567
|
-
const category = settingsCategories[categoryIndex]!;
|
|
568
|
-
if (settingsNav.zone === "nav") {
|
|
569
|
-
if (name === "j" || name === "down")
|
|
570
|
-
setSettingsNav({
|
|
571
|
-
categoryId:
|
|
572
|
-
settingsCategories[Math.min(settingsCategories.length - 1, categoryIndex + 1)]!.id,
|
|
573
|
-
rowIndex: 0,
|
|
574
|
-
zone: "nav",
|
|
575
|
-
});
|
|
576
|
-
else if (name === "k" || name === "up")
|
|
577
|
-
setSettingsNav({
|
|
578
|
-
categoryId: settingsCategories[Math.max(0, categoryIndex - 1)]!.id,
|
|
579
|
-
rowIndex: 0,
|
|
580
|
-
zone: "nav",
|
|
581
|
-
});
|
|
582
|
-
else if (name === "l" || name === "tab" || name === "return")
|
|
583
|
-
setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: 0 }));
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
// the Actions category is a list of quick actions plus a trailing "add" row
|
|
587
|
-
if (category.id === "actions") {
|
|
588
|
-
const rowCount = quickActions.length + 1;
|
|
589
|
-
if (name === "j" || name === "down")
|
|
590
|
-
setSettingsNav((state) => ({
|
|
591
|
-
...state,
|
|
592
|
-
rowIndex: Math.min(rowCount - 1, state.rowIndex + 1),
|
|
593
|
-
}));
|
|
594
|
-
else if (name === "k" || name === "up")
|
|
595
|
-
setSettingsNav((state) => ({ ...state, rowIndex: Math.max(0, state.rowIndex - 1) }));
|
|
596
|
-
else if (name === "h" || name === "tab")
|
|
597
|
-
setSettingsNav((state) => ({ ...state, zone: "nav" }));
|
|
598
|
-
else if (name === "return" || name === "space" || name === "l") {
|
|
599
|
-
if (settingsNav.rowIndex === quickActions.length) addAction();
|
|
600
|
-
else setActionsExpandedIndex(settingsNav.rowIndex);
|
|
601
|
-
}
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
if (name === "j" || name === "down")
|
|
605
|
-
setSettingsNav((state) => ({
|
|
606
|
-
...state,
|
|
607
|
-
rowIndex: Math.min(category.rows.length - 1, state.rowIndex + 1),
|
|
608
|
-
}));
|
|
609
|
-
else if (name === "k" || name === "up")
|
|
610
|
-
setSettingsNav((state) => ({ ...state, rowIndex: Math.max(0, state.rowIndex - 1) }));
|
|
611
|
-
else if (name === "h" || name === "tab") setSettingsNav((state) => ({ ...state, zone: "nav" }));
|
|
612
|
-
else if (name === "return" || name === "space")
|
|
613
|
-
cycleSetting(category.rows[settingsNav.rowIndex]!.key);
|
|
614
|
-
};
|
|
440
|
+
const overlay = resolveOverlay(mode, completion.phase, walking);
|
|
615
441
|
|
|
616
442
|
useKeyboard((key) => {
|
|
617
443
|
// The prototype compose textarea owns the keyboard while open: let it receive
|
|
@@ -630,26 +456,23 @@ export function App({
|
|
|
630
456
|
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
631
457
|
if (toast && key.name === "escape" && overlay === "none" && mode.type !== "span")
|
|
632
458
|
return controller.dismissToast();
|
|
633
|
-
const state
|
|
459
|
+
const state = buildKeyState({
|
|
634
460
|
keys: keysRef.current,
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
canSubmitVerdict: isOwner,
|
|
638
|
-
canShare: isOwner,
|
|
461
|
+
observer,
|
|
462
|
+
isOwner,
|
|
639
463
|
overlay,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
};
|
|
464
|
+
session,
|
|
465
|
+
isDiff,
|
|
466
|
+
mode,
|
|
467
|
+
resolved,
|
|
468
|
+
inbox,
|
|
469
|
+
focusedAnnotationId,
|
|
470
|
+
walk,
|
|
471
|
+
walkFileList,
|
|
472
|
+
rows,
|
|
473
|
+
cursor,
|
|
474
|
+
display,
|
|
475
|
+
});
|
|
653
476
|
keyBindings.setContext({ overlay: state.overlay, spanMode: state.spanMode });
|
|
654
477
|
const action = keyBindings.resolveAction({ name: key.name, shift: !!key.shift });
|
|
655
478
|
for (const intent of reduceKey(
|
|
@@ -663,225 +486,97 @@ export function App({
|
|
|
663
486
|
// ── shared bottom chrome: the menu bar and its drop-up dialogs, one render
|
|
664
487
|
// reused by the inbox and by plan/diff review so the two never drift ──
|
|
665
488
|
const menuChrome = (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
theme={theme}
|
|
681
|
-
/>
|
|
682
|
-
{menuDialog === "keybinds" ? (
|
|
683
|
-
<KeybindsDialog sections={keyBindings.cheatsheet()} theme={theme} />
|
|
684
|
-
) : null}
|
|
685
|
-
{menuDialog === "settings" ? (
|
|
686
|
-
<SettingsDialog
|
|
687
|
-
isOpen
|
|
688
|
-
categories={settingsCategories}
|
|
689
|
-
values={settingsValues}
|
|
690
|
-
activeCategoryId={settingsNav.categoryId}
|
|
691
|
-
activeRowIndex={settingsNav.rowIndex}
|
|
692
|
-
activeZone={settingsNav.zone}
|
|
693
|
-
onCategorySelect={(id) => {
|
|
694
|
-
setActionsExpandedIndex(null);
|
|
695
|
-
setSettingsNav({ categoryId: id, rowIndex: 0, zone: "body" });
|
|
696
|
-
}}
|
|
697
|
-
onRowActivate={(row) => cycleSetting(row.key)}
|
|
698
|
-
theme={theme}
|
|
699
|
-
/>
|
|
700
|
-
) : null}
|
|
701
|
-
</>
|
|
489
|
+
<MenuChrome
|
|
490
|
+
menuOpen={menuOpen}
|
|
491
|
+
menuDialog={menuDialog}
|
|
492
|
+
status={status}
|
|
493
|
+
theme={theme}
|
|
494
|
+
setMenuOpen={setMenuOpen}
|
|
495
|
+
setMenuDialog={setMenuDialog}
|
|
496
|
+
keybindsSections={keyBindings.cheatsheet()}
|
|
497
|
+
settingsCategories={settingsCategories}
|
|
498
|
+
settingsValues={settingsValues}
|
|
499
|
+
settingsNav={settingsNav}
|
|
500
|
+
onCategorySelect={onCategorySelect}
|
|
501
|
+
cycleSetting={cycleSetting}
|
|
502
|
+
/>
|
|
702
503
|
);
|
|
703
504
|
|
|
704
505
|
// ── render ──────────────────────────────────
|
|
705
|
-
if (error) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
if (!session && inbox) {
|
|
720
|
-
const confirming = mode.type === "confirmDelete" ? mode : null;
|
|
721
|
-
return (
|
|
722
|
-
<ThemeProvider theme={theme}>
|
|
723
|
-
<box
|
|
724
|
-
style={{
|
|
725
|
-
flexDirection: "column",
|
|
726
|
-
width: "100%",
|
|
727
|
-
height: "100%",
|
|
728
|
-
backgroundColor: theme.background,
|
|
729
|
-
}}
|
|
730
|
-
>
|
|
731
|
-
{/* mirrors the review header row: same box, position, and accent product
|
|
732
|
-
word, with a " · resume" separator and no Edit/Share toolbar */}
|
|
733
|
-
<box
|
|
734
|
-
style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme.panel }}
|
|
735
|
-
>
|
|
736
|
-
<box
|
|
737
|
-
style={{
|
|
738
|
-
height: 1,
|
|
739
|
-
backgroundColor: theme.panel,
|
|
740
|
-
paddingLeft: 1,
|
|
741
|
-
flexDirection: "row",
|
|
742
|
-
}}
|
|
743
|
-
>
|
|
744
|
-
<text>
|
|
745
|
-
<span fg={theme.accent}>cueloop</span>
|
|
746
|
-
<span fg={theme.textDim}> · resume</span>
|
|
747
|
-
</text>
|
|
748
|
-
</box>
|
|
749
|
-
<box style={{ flexGrow: 1 }} />
|
|
750
|
-
</box>
|
|
751
|
-
<InboxList
|
|
752
|
-
inbox={inbox}
|
|
753
|
-
cursor={inboxCursor}
|
|
754
|
-
onRequestDelete={(id, title) =>
|
|
755
|
-
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
756
|
-
}
|
|
757
|
-
/>
|
|
758
|
-
{menuChrome}
|
|
759
|
-
<ConfirmDialog
|
|
760
|
-
isOpen={confirming !== null}
|
|
761
|
-
title=" Delete plan "
|
|
762
|
-
message={
|
|
763
|
-
confirming
|
|
764
|
-
? `Delete "${confirming.title}"? This removes the plan and its review.`
|
|
765
|
-
: ""
|
|
766
|
-
}
|
|
767
|
-
onConfirm={() => {
|
|
768
|
-
if (confirming) controller.deleteSession(confirming.sessionId);
|
|
769
|
-
setMode({ type: "normal" });
|
|
770
|
-
}}
|
|
771
|
-
onCancel={() => setMode({ type: "normal" })}
|
|
772
|
-
theme={theme}
|
|
773
|
-
/>
|
|
774
|
-
</box>
|
|
775
|
-
</ThemeProvider>
|
|
506
|
+
if (error) return <ErrorScreen error={error} theme={theme} />;
|
|
507
|
+
if (!session)
|
|
508
|
+
return inbox ? (
|
|
509
|
+
<InboxScreen
|
|
510
|
+
inbox={inbox}
|
|
511
|
+
inboxCursor={inboxCursor}
|
|
512
|
+
mode={mode}
|
|
513
|
+
theme={theme}
|
|
514
|
+
controller={controller}
|
|
515
|
+
setMode={setMode}
|
|
516
|
+
menuChrome={menuChrome}
|
|
517
|
+
/>
|
|
518
|
+
) : (
|
|
519
|
+
<ConnectingScreen theme={theme} />
|
|
776
520
|
);
|
|
777
|
-
}
|
|
778
521
|
|
|
779
|
-
const activeSession = session
|
|
780
|
-
const pendingCount =
|
|
781
|
-
reviewerAnnotations(activeSession).length + (activeSession.workingCopy !== undefined ? 1 : 0);
|
|
522
|
+
const activeSession = session;
|
|
782
523
|
|
|
783
|
-
if ((completion
|
|
524
|
+
if (isCompletionOverlayPhase(completion) && activeSession.verdict)
|
|
784
525
|
return (
|
|
785
|
-
<
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
? (activeSession.artifact.meta.agent ?? "the agent")
|
|
793
|
-
: undefined
|
|
794
|
-
}
|
|
795
|
-
/>
|
|
796
|
-
</ThemeProvider>
|
|
526
|
+
<CompletionScreen
|
|
527
|
+
theme={theme}
|
|
528
|
+
session={activeSession}
|
|
529
|
+
verdict={activeSession.verdict.kind}
|
|
530
|
+
completion={completion}
|
|
531
|
+
status={status}
|
|
532
|
+
/>
|
|
797
533
|
);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const composeState =
|
|
801
|
-
mode.type === "compose" && !isDiff
|
|
802
|
-
? {
|
|
803
|
-
kind: mode.kind,
|
|
804
|
-
displayIndex: mode.displayIndex,
|
|
805
|
-
quote: displayText(display[mode.displayIndex]!).slice(mode.start, mode.end),
|
|
806
|
-
draft: {
|
|
807
|
-
text: mode.text,
|
|
808
|
-
onInput: (text: string) => {
|
|
809
|
-
liveInput.current = text;
|
|
810
|
-
setMode({ ...mode, text });
|
|
811
|
-
},
|
|
812
|
-
onSave: () => dispatch({ type: "saveCompose" }),
|
|
813
|
-
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
814
|
-
},
|
|
815
|
-
}
|
|
816
|
-
: null;
|
|
817
|
-
|
|
818
|
-
const diffComposeState =
|
|
819
|
-
mode.type === "compose" && isDiff
|
|
820
|
-
? {
|
|
821
|
-
kind: mode.kind,
|
|
822
|
-
rowIndex: mode.displayIndex,
|
|
823
|
-
quote: rows[mode.displayIndex]?.text.replace(/\n$/, "") ?? "",
|
|
824
|
-
draft: {
|
|
825
|
-
text: mode.text,
|
|
826
|
-
onInput: (text: string) => {
|
|
827
|
-
liveInput.current = text;
|
|
828
|
-
setMode({ ...mode, text });
|
|
829
|
-
},
|
|
830
|
-
onSave: () => dispatch({ type: "saveCompose" }),
|
|
831
|
-
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
832
|
-
},
|
|
833
|
-
}
|
|
834
|
-
: null;
|
|
835
|
-
|
|
836
|
-
const markedSpan = activeSpanState(mode);
|
|
837
|
-
const activeSpan = markedSpan
|
|
838
|
-
? { displayIndex: markedSpan.displayIndex, start: markedSpan.start, end: markedSpan.end }
|
|
839
|
-
: mode.type === "compose" && !isDiff
|
|
840
|
-
? // the compose anchor stays painted selection-style while the box is open
|
|
841
|
-
{ displayIndex: mode.displayIndex, start: mode.start, end: mode.end }
|
|
842
|
-
: null;
|
|
843
534
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
535
|
+
const composeState = buildComposeState({ mode, isDiff, display, liveInput, setMode, dispatch });
|
|
536
|
+
const diffComposeState = buildDiffComposeState({
|
|
537
|
+
mode,
|
|
538
|
+
isDiff,
|
|
539
|
+
rows,
|
|
540
|
+
liveInput,
|
|
541
|
+
setMode,
|
|
542
|
+
dispatch,
|
|
543
|
+
});
|
|
544
|
+
const activeSpan = buildActiveSpan(mode, isDiff);
|
|
545
|
+
const popoverState = buildPopoverState({
|
|
546
|
+
mode,
|
|
547
|
+
isDiff,
|
|
548
|
+
quickActions,
|
|
549
|
+
isOwner,
|
|
550
|
+
observer,
|
|
551
|
+
resolved,
|
|
552
|
+
controller,
|
|
553
|
+
dispatch,
|
|
554
|
+
});
|
|
555
|
+
const submitConfirmState = buildSubmitConfirmState({
|
|
556
|
+
mode,
|
|
557
|
+
isDiff,
|
|
558
|
+
session: activeSession,
|
|
559
|
+
walkFileList,
|
|
560
|
+
viewedPaths,
|
|
561
|
+
liveInput,
|
|
562
|
+
setMode,
|
|
563
|
+
dispatch,
|
|
564
|
+
});
|
|
565
|
+
const cardEditState = buildCardEditState({ mode, liveInput, setMode, dispatch });
|
|
566
|
+
const pendingCount = computePendingCount(activeSession);
|
|
567
|
+
const railFootprint = computeRailFootprint(reviewMode, reviewWidth, terminalWidth);
|
|
568
|
+
const headerItems = buildHeaderItems({ session: activeSession, resolved, observer, role });
|
|
569
|
+
const { showOwnerActions, prototypeCanComment, chromeHidden, prototypePath, railResolvedIds } =
|
|
570
|
+
buildRenderFlags({
|
|
571
|
+
session: activeSession,
|
|
572
|
+
isOwner,
|
|
573
|
+
isDiff,
|
|
574
|
+
isPrototype,
|
|
575
|
+
resolved,
|
|
576
|
+
menuOpen,
|
|
577
|
+
menuDialog,
|
|
578
|
+
resolvedIds,
|
|
579
|
+
});
|
|
885
580
|
|
|
886
581
|
// a mouse drag leaves a native selection: turn it into a word span so the
|
|
887
582
|
// marker popover opens at the dragged range, mirroring the `v` grammar.
|
|
@@ -947,73 +642,6 @@ export function App({
|
|
|
947
642
|
if (reviewMode === "expanded") setDividerDragging(true);
|
|
948
643
|
};
|
|
949
644
|
|
|
950
|
-
const submitConfirmState =
|
|
951
|
-
mode.type === "submit"
|
|
952
|
-
? {
|
|
953
|
-
verdict: mode.verdict,
|
|
954
|
-
summary: mode.summary,
|
|
955
|
-
// walk coverage keeps partial passes honest at the verdict
|
|
956
|
-
viewedSummary:
|
|
957
|
-
isDiff && activeSession.viewedPaths !== undefined
|
|
958
|
-
? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
|
|
959
|
-
: undefined,
|
|
960
|
-
onInput: (summary: string) => {
|
|
961
|
-
liveInput.current = summary;
|
|
962
|
-
setMode({ ...mode, summary });
|
|
963
|
-
},
|
|
964
|
-
onSelectVerdict: (verdict: VerdictKind) => setMode({ ...mode, verdict }),
|
|
965
|
-
onSubmit: () => dispatch({ type: "submitVerdict" }),
|
|
966
|
-
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
967
|
-
}
|
|
968
|
-
: null;
|
|
969
|
-
|
|
970
|
-
const cardEditState =
|
|
971
|
-
mode.type === "railEdit"
|
|
972
|
-
? {
|
|
973
|
-
id: mode.id,
|
|
974
|
-
text: mode.text,
|
|
975
|
-
onInput: (text: string) => {
|
|
976
|
-
liveInput.current = text;
|
|
977
|
-
setMode({ type: "railEdit", id: mode.id, text });
|
|
978
|
-
},
|
|
979
|
-
onSave: () => dispatch({ type: "saveCompose" }),
|
|
980
|
-
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
981
|
-
}
|
|
982
|
-
: null;
|
|
983
|
-
|
|
984
|
-
// width the review panel occupies (divider + rail), so the header can reserve
|
|
985
|
-
// it and keep Edit/Share pinned to the plan sheet's right edge, not the rail's
|
|
986
|
-
const railFootprint =
|
|
987
|
-
reviewMode === "hidden"
|
|
988
|
-
? 0
|
|
989
|
-
: 1 +
|
|
990
|
-
(reviewMode === "compact"
|
|
991
|
-
? REVIEW_COMPACT_WIDTH
|
|
992
|
-
: resolveReviewWidth(reviewWidth, terminalWidth));
|
|
993
|
-
|
|
994
|
-
// status badges sit right after the product word so they survive a header
|
|
995
|
-
// that is too narrow for the whole trail (the rail can eat the width)
|
|
996
|
-
const headerItems: BreadcrumbItem[] = [
|
|
997
|
-
{ label: "cueloop", tone: "accent" },
|
|
998
|
-
...(resolved
|
|
999
|
-
? [
|
|
1000
|
-
{
|
|
1001
|
-
label: `resolved: ${activeSession.verdict!.kind.replace("_", " ")}`,
|
|
1002
|
-
tone: "green" as const,
|
|
1003
|
-
},
|
|
1004
|
-
]
|
|
1005
|
-
: []),
|
|
1006
|
-
...(observer ? [{ label: "observer", tone: "dim" as const }] : []),
|
|
1007
|
-
...(role === "collaborator"
|
|
1008
|
-
? [{ label: "shared · your notes save as you go", tone: "dim" as const }]
|
|
1009
|
-
: []),
|
|
1010
|
-
{
|
|
1011
|
-
label: `${activeSession.artifact.meta.title ?? activeSession.artifact.meta.planPath ?? activeSession.id} · rev ${activeSession.revisions.length}`,
|
|
1012
|
-
tone: "dim",
|
|
1013
|
-
},
|
|
1014
|
-
{ label: `submitted by ${activeSession.artifact.meta.agent ?? "unknown"}`, tone: "dim" },
|
|
1015
|
-
];
|
|
1016
|
-
|
|
1017
645
|
return (
|
|
1018
646
|
<ThemeProvider theme={theme}>
|
|
1019
647
|
<box
|
|
@@ -1044,7 +672,7 @@ export function App({
|
|
|
1044
672
|
<box style={{ flexGrow: 1, flexDirection: "row", paddingRight: 1 }}>
|
|
1045
673
|
<Breadcrumb items={headerItems} />
|
|
1046
674
|
<box style={{ flexGrow: 1 }} />
|
|
1047
|
-
{
|
|
675
|
+
{showOwnerActions ? (
|
|
1048
676
|
<Toolbar>
|
|
1049
677
|
<Button onPress={onEditRequest} theme={theme}>
|
|
1050
678
|
{" Edit "}
|
|
@@ -1060,12 +688,12 @@ export function App({
|
|
|
1060
688
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
1061
689
|
{isPrototype ? (
|
|
1062
690
|
<PrototypeSheet
|
|
1063
|
-
prototypePath={
|
|
691
|
+
prototypePath={prototypePath}
|
|
1064
692
|
quickActions={quickActions}
|
|
1065
|
-
canComment={
|
|
693
|
+
canComment={prototypeCanComment}
|
|
1066
694
|
onCommentElement={onCommentPrototype}
|
|
1067
695
|
onComposingChange={setPrototypeComposing}
|
|
1068
|
-
hidden={
|
|
696
|
+
hidden={chromeHidden}
|
|
1069
697
|
/>
|
|
1070
698
|
) : isDiff ? (
|
|
1071
699
|
// the sheet dims to reading-quiet colors while the wizard has focus
|
|
@@ -1103,7 +731,7 @@ export function App({
|
|
|
1103
731
|
session: activeSession,
|
|
1104
732
|
authorNames,
|
|
1105
733
|
selectedId: focusedAnnotationId,
|
|
1106
|
-
resolvedIds:
|
|
734
|
+
resolvedIds: railResolvedIds,
|
|
1107
735
|
curationItems,
|
|
1108
736
|
selectedCurationId,
|
|
1109
737
|
annotationPositions,
|
|
@@ -1127,47 +755,19 @@ export function App({
|
|
|
1127
755
|
}}
|
|
1128
756
|
/>
|
|
1129
757
|
</box>
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
dispatch({ type: "openSubmit" });
|
|
1144
|
-
}}
|
|
1145
|
-
onBack={() => dispatch({ type: "walkBack" })}
|
|
1146
|
-
/>
|
|
1147
|
-
) : null}
|
|
1148
|
-
{mode.type === "rename" ? (
|
|
1149
|
-
<PromptDialog
|
|
1150
|
-
isOpen
|
|
1151
|
-
title=" Rename author "
|
|
1152
|
-
label="Display name for this collaborator:"
|
|
1153
|
-
value={mode.text}
|
|
1154
|
-
placeholder="their name"
|
|
1155
|
-
onInput={(text) => setMode({ ...mode, text })}
|
|
1156
|
-
theme={theme}
|
|
1157
|
-
/>
|
|
1158
|
-
) : null}
|
|
1159
|
-
{mode.type === "nameSelf" ? (
|
|
1160
|
-
<PromptDialog
|
|
1161
|
-
isOpen
|
|
1162
|
-
title=" Welcome "
|
|
1163
|
-
label="Your name (optional) - it attributes the notes you leave:"
|
|
1164
|
-
value={mode.text}
|
|
1165
|
-
placeholder="your name"
|
|
1166
|
-
onInput={(text) => setMode({ ...mode, text })}
|
|
1167
|
-
theme={theme}
|
|
1168
|
-
/>
|
|
1169
|
-
) : null}
|
|
1170
|
-
{toast ? <Toast title={toast.title} body={toast.body} theme={theme} /> : null}
|
|
758
|
+
<TrailingOverlays
|
|
759
|
+
walking={walking}
|
|
760
|
+
walk={walk}
|
|
761
|
+
walkFileList={walkFileList}
|
|
762
|
+
viewedPaths={viewedPaths}
|
|
763
|
+
session={activeSession}
|
|
764
|
+
terminalWidth={terminalWidth}
|
|
765
|
+
theme={theme}
|
|
766
|
+
mode={mode}
|
|
767
|
+
toast={toast}
|
|
768
|
+
setMode={setMode}
|
|
769
|
+
dispatch={dispatch}
|
|
770
|
+
/>
|
|
1171
771
|
{menuChrome}
|
|
1172
772
|
</box>
|
|
1173
773
|
</ThemeProvider>
|