@cueloop/client 0.1.0-alpha.15 → 0.1.0-alpha.17
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.plan-review.test.tsx +1 -1
- package/src/App.readonly.test.tsx +3 -3
- package/src/App.rendering.test.tsx +16 -16
- package/src/App.test.tsx +1 -1
- package/src/App.tsx +193 -139
- package/src/App.walk.test.tsx +208 -0
- package/src/components/AnnotationCard.tsx +4 -4
- package/src/components/ComposeBar.tsx +2 -2
- package/src/components/ConfirmCard.stories.tsx +15 -0
- package/src/components/ConfirmCard.tsx +8 -1
- package/src/components/DiffSheet.tsx +8 -8
- package/src/components/PlanSheet.stories.tsx +3 -3
- package/src/components/PlanSheet.tsx +31 -31
- package/src/components/WalkWizard.stories.tsx +89 -0
- package/src/components/WalkWizard.tsx +146 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +130 -0
- package/src/components/stories.test.tsx +1 -1
- package/src/components/story-fixtures.ts +1 -1
- package/src/components/story.ts +1 -1
- package/src/components/syntax-highlight.ts +1 -1
- package/src/components/theme-context.tsx +1 -1
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +29 -29
- package/src/config.ts +13 -12
- package/src/editor.ts +1 -1
- package/src/herdr.ts +1 -1
- package/src/key-bindings.test.ts +18 -1
- package/src/key-bindings.ts +23 -2
- package/src/keymap.test.ts +138 -101
- package/src/keymap.ts +81 -56
- package/src/run.ts +3 -3
- package/src/serve.test.ts +6 -6
- package/src/serve.ts +7 -7
- package/src/session-controller.ts +140 -66
- package/src/theme.ts +20 -1
- package/src/view-diff.test.ts +8 -8
- package/src/view-diff.ts +22 -22
- package/src/{view.test.ts → view-plan.test.ts} +48 -48
- package/src/view-plan.ts +362 -0
- package/src/walk.test.ts +110 -0
- package/src/walk.ts +65 -0
- package/src/components/format.ts +0 -6
- package/src/view.ts +0 -341
package/src/App.tsx
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The cueloop TUI
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* keyboard span on one native renderer selection), compose happens inline
|
|
11
|
-
* under the anchor, annotation text lives in the rail while the document
|
|
12
|
-
* keeps only the kind-colored highlight, and the rail edits what the
|
|
13
|
-
* document selects - one selected id drives both sides.
|
|
2
|
+
* The cueloop TUI: state wiring, keymap dispatch, and layout composition -
|
|
3
|
+
* nothing else. Rendering lives in components/, daemon IO and the mutation
|
|
4
|
+
* verbs in session-controller.ts, the keyboard grammar in keymap.ts with
|
|
5
|
+
* binding resolution and status hints from key-bindings.ts, and theming in
|
|
6
|
+
* the ThemeProvider. Selection is the entry primitive (mouse drag or keyboard
|
|
7
|
+
* span on one native renderer selection); annotation text lives in the rail
|
|
8
|
+
* while the document keeps only the highlight, and one selected id drives
|
|
9
|
+
* both sides.
|
|
14
10
|
*/
|
|
15
11
|
|
|
16
12
|
import React, { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
17
13
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
18
14
|
import type { Clock } from "@opentui/core";
|
|
19
|
-
import { type ReviewSession, type VerdictKind } from "@cueloop/schema";
|
|
20
|
-
import { displayText, marksByDisplay, spanKey, startSpan, type Mark, type SpanState } from "./view";
|
|
21
|
-
import {
|
|
15
|
+
import { isAgentNote, type ReviewSession, type VerdictKind } from "@cueloop/schema";
|
|
16
|
+
import { displayText, marksByDisplay, spanKey, startSpan, type Mark, type SpanState } from "./view-plan";
|
|
17
|
+
import { noteForFile, viewedCount } from "./walk";
|
|
18
|
+
import { DARK, dimmedTheme } from "./theme";
|
|
22
19
|
import { DEFAULT_KEYS, loadConfig } from "./config";
|
|
23
20
|
import { returnPaneFor } from "./herdr";
|
|
24
21
|
import { createReviewController } from "./session-controller";
|
|
@@ -34,6 +31,7 @@ import { VERDICTS } from "./components/ConfirmCard";
|
|
|
34
31
|
import { CompletionOverlay } from "./components/CompletionOverlay";
|
|
35
32
|
import { InboxList } from "./components/InboxList";
|
|
36
33
|
import { ComposeBar } from "./components/ComposeBar";
|
|
34
|
+
import { WalkWizard } from "./components/WalkWizard";
|
|
37
35
|
|
|
38
36
|
export interface AppProps {
|
|
39
37
|
home?: string;
|
|
@@ -49,14 +47,19 @@ export interface AppProps {
|
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
type Mode =
|
|
52
|
-
| {
|
|
53
|
-
| {
|
|
54
|
-
| {
|
|
55
|
-
| {
|
|
56
|
-
| {
|
|
50
|
+
| { type: "normal" }
|
|
51
|
+
| { type: "span"; span: SpanState }
|
|
52
|
+
| { type: "compose"; kind: "comment" | "suggestion"; displayIndex: number; start: number; end: number; text: string }
|
|
53
|
+
| { type: "railEdit"; id: string; text: string }
|
|
54
|
+
| { type: "submit"; verdict: VerdictKind; summary: string };
|
|
55
|
+
|
|
56
|
+
/** Reviewer-authored annotations only: agent notes never count as feedback. */
|
|
57
|
+
function reviewerAnnotations(session: ReviewSession) {
|
|
58
|
+
return session.annotations.filter((annotation) => !isAgentNote(annotation));
|
|
59
|
+
}
|
|
57
60
|
|
|
58
61
|
function defaultVerdict(session: ReviewSession): VerdictKind {
|
|
59
|
-
return session.
|
|
62
|
+
return reviewerAnnotations(session).length || session.workingCopy !== undefined ? "request_changes" : "approve";
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
export function App({ home, sessionId, readOnly = false, onExit, clock }: AppProps): React.ReactNode {
|
|
@@ -69,7 +72,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
69
72
|
controller.connect();
|
|
70
73
|
return () => controller.close();
|
|
71
74
|
}, [controller]);
|
|
72
|
-
const { session, inbox, status, error, completion, editOrphanCount } = useSyncExternalStore(
|
|
75
|
+
const { session, inbox, status, error, completion, editOrphanCount, walk } = useSyncExternalStore(
|
|
73
76
|
controller.subscribe,
|
|
74
77
|
controller.getSnapshot,
|
|
75
78
|
controller.getSnapshot,
|
|
@@ -80,11 +83,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
80
83
|
// ── view state ──────────────────────────────
|
|
81
84
|
const [cursor, setCursor] = useState(0);
|
|
82
85
|
const [inboxCursor, setInboxCursor] = useState(0);
|
|
83
|
-
const [mode, setMode] = useState<Mode>({
|
|
84
|
-
const [
|
|
86
|
+
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
87
|
+
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
85
88
|
const [railTab, setRailTab] = useState<"review" | "agent">("review");
|
|
86
89
|
// ~2s focus pulse on the document highlight when a rail card is activated
|
|
87
|
-
const [
|
|
90
|
+
const [pulsedAnnotationId, setPulsedAnnotationId] = useState<string | null>(null);
|
|
88
91
|
const pulseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
89
92
|
// live mirror of overlay input text: refs commit synchronously, so the
|
|
90
93
|
// RETURN handler never reads a stale value mid-typing
|
|
@@ -96,11 +99,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
96
99
|
const keyBindings = useMemo(() => new KeyBindings(DEFAULT_KEYS), []);
|
|
97
100
|
const [theme, setTheme] = useState(DARK);
|
|
98
101
|
useEffect(() => {
|
|
99
|
-
const
|
|
100
|
-
keysRef.current =
|
|
101
|
-
keyBindings.setKeys(
|
|
102
|
-
setTheme(
|
|
103
|
-
controller.applyConfig(
|
|
102
|
+
const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
103
|
+
keysRef.current = config.keys;
|
|
104
|
+
keyBindings.setKeys(config.keys);
|
|
105
|
+
setTheme(config.theme);
|
|
106
|
+
controller.applyConfig(config);
|
|
104
107
|
}, [session?.workspace.repoRoot, controller, keyBindings]);
|
|
105
108
|
useEffect(
|
|
106
109
|
() => () => {
|
|
@@ -113,8 +116,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
113
116
|
const display = controller.display();
|
|
114
117
|
const rows = controller.rows();
|
|
115
118
|
const marks = useMemo(
|
|
116
|
-
() => (session ? marksByDisplay(session.annotations, display,
|
|
117
|
-
[session, display,
|
|
119
|
+
() => (session ? marksByDisplay(session.annotations, display, pulsedAnnotationId ?? undefined) : new Map<number, Mark[]>()),
|
|
120
|
+
[session, display, pulsedAnnotationId],
|
|
118
121
|
);
|
|
119
122
|
/** Annotation ids whose anchor resolved against the working copy. */
|
|
120
123
|
const resolvedIds = useMemo(() => {
|
|
@@ -127,38 +130,43 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
127
130
|
const resolved = session?.status === "resolved";
|
|
128
131
|
const isDiff = session?.artifact.type === "diff";
|
|
129
132
|
|
|
133
|
+
// ── the guided walk's view model ────────────
|
|
134
|
+
const walkFileList = controller.files();
|
|
135
|
+
const walking = isDiff && walk !== null;
|
|
136
|
+
const viewedPaths = useMemo(() => new Set(session?.viewedPaths ?? []), [session]);
|
|
137
|
+
|
|
130
138
|
// driving needs committed layout, so it runs after render; any transition
|
|
131
139
|
// out of span mode clears the renderer selection (compose paints its own
|
|
132
140
|
// mark, and a mouse drag never changes the mode, so it survives)
|
|
133
141
|
useEffect(() => {
|
|
134
|
-
if (mode.
|
|
142
|
+
if (mode.type === "span") planSheetRef.current?.driveSpanSelection(mode.span);
|
|
135
143
|
else planSheetRef.current?.clearSelection();
|
|
136
144
|
}, [mode]);
|
|
137
145
|
|
|
138
146
|
// ── selection symmetry: one selected id, both sides ──
|
|
139
147
|
const pulse = (id: string): void => {
|
|
140
|
-
|
|
148
|
+
setPulsedAnnotationId(id);
|
|
141
149
|
if (pulseTimer.current) clearTimeout(pulseTimer.current);
|
|
142
|
-
pulseTimer.current = setTimeout(() =>
|
|
150
|
+
pulseTimer.current = setTimeout(() => setPulsedAnnotationId(null), 2000);
|
|
143
151
|
};
|
|
144
152
|
|
|
145
153
|
/** Card activation scrolls the document to the anchor and pulses it. */
|
|
146
154
|
const revealAnchor = (annotationId: string): void => {
|
|
147
|
-
for (const [
|
|
155
|
+
for (const [displayIndex, blockMarks] of marks) {
|
|
148
156
|
if (!blockMarks.some((mark) => mark.annotationId === annotationId)) continue;
|
|
149
|
-
setCursor(
|
|
150
|
-
planSheetRef.current?.revealBlock(
|
|
157
|
+
setCursor(displayIndex);
|
|
158
|
+
planSheetRef.current?.revealBlock(displayIndex);
|
|
151
159
|
return;
|
|
152
160
|
}
|
|
153
161
|
};
|
|
154
162
|
|
|
155
163
|
const selectCardFromDocument = (annotationId: string): void => {
|
|
156
|
-
|
|
164
|
+
setFocusedAnnotationId(annotationId);
|
|
157
165
|
railRef.current?.revealCard(annotationId);
|
|
158
166
|
};
|
|
159
167
|
|
|
160
168
|
const selectCardFromRail = (annotationId: string): void => {
|
|
161
|
-
|
|
169
|
+
setFocusedAnnotationId(annotationId);
|
|
162
170
|
pulse(annotationId);
|
|
163
171
|
revealAnchor(annotationId);
|
|
164
172
|
};
|
|
@@ -169,7 +177,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
169
177
|
const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
|
|
170
178
|
if (!annotation) return;
|
|
171
179
|
liveInput.current = annotation.body;
|
|
172
|
-
setMode({
|
|
180
|
+
setMode({ type: "railEdit", id: annotation.id, text: annotation.body });
|
|
173
181
|
};
|
|
174
182
|
|
|
175
183
|
/** The $EDITOR hand-off releases the terminal: suspend, edit, resume. */
|
|
@@ -184,64 +192,64 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
184
192
|
|
|
185
193
|
// ── keyboard grammar: build state, reduce, dispatch ──
|
|
186
194
|
const dispatch = (intent: Intent): void => {
|
|
187
|
-
switch (intent.
|
|
195
|
+
switch (intent.type) {
|
|
188
196
|
case "exit":
|
|
189
197
|
return void onExit?.(0);
|
|
190
198
|
case "status":
|
|
191
|
-
return controller.setStatus(intent.
|
|
199
|
+
return controller.setStatus(intent.message);
|
|
192
200
|
case "move": {
|
|
193
|
-
const
|
|
194
|
-
if (intent.to === "down") setCursor((
|
|
195
|
-
else if (intent.to === "up") setCursor((
|
|
201
|
+
const navigableCount = isDiff ? rows.length : display.length;
|
|
202
|
+
if (intent.to === "down") setCursor((current) => Math.min(navigableCount - 1, current + 1));
|
|
203
|
+
else if (intent.to === "up") setCursor((current) => Math.max(0, current - 1));
|
|
196
204
|
else if (intent.to === "top") setCursor(0);
|
|
197
|
-
else setCursor(
|
|
205
|
+
else setCursor(navigableCount - 1);
|
|
198
206
|
return;
|
|
199
207
|
}
|
|
200
208
|
case "inboxMove": {
|
|
201
|
-
const
|
|
202
|
-
setInboxCursor((
|
|
209
|
+
const navigableCount = inbox?.length ?? 0;
|
|
210
|
+
setInboxCursor((current) => (intent.to === "down" ? Math.min(navigableCount - 1, current + 1) : Math.max(0, current - 1)));
|
|
203
211
|
return;
|
|
204
212
|
}
|
|
205
213
|
case "openSession": {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
214
|
+
const selected = inbox?.[inboxCursor];
|
|
215
|
+
if (selected) controller.open(selected.id);
|
|
208
216
|
return;
|
|
209
217
|
}
|
|
210
218
|
case "startSpan": {
|
|
211
|
-
const
|
|
212
|
-
if (!
|
|
213
|
-
const span = startSpan(cursor, displayText(
|
|
214
|
-
if (span) setMode({
|
|
219
|
+
const block = display[cursor];
|
|
220
|
+
if (!block?.work) return;
|
|
221
|
+
const span = startSpan(cursor, displayText(block));
|
|
222
|
+
if (span) setMode({ type: "span", span });
|
|
215
223
|
return;
|
|
216
224
|
}
|
|
217
225
|
case "spanKey":
|
|
218
|
-
if (mode.
|
|
219
|
-
const span = spanKey(mode.span, intent.name, displayText(display[mode.span.
|
|
220
|
-
setMode({
|
|
226
|
+
if (mode.type === "span") {
|
|
227
|
+
const span = spanKey(mode.span, intent.name, displayText(display[mode.span.displayIndex]!));
|
|
228
|
+
setMode({ type: "span", span });
|
|
221
229
|
}
|
|
222
230
|
return;
|
|
223
231
|
case "openCompose": {
|
|
224
232
|
liveInput.current = "";
|
|
225
|
-
if (intent.from === "span" && mode.
|
|
233
|
+
if (intent.from === "span" && mode.type === "span") {
|
|
226
234
|
setMode({
|
|
227
|
-
|
|
235
|
+
type: "compose",
|
|
228
236
|
kind: intent.kind,
|
|
229
|
-
|
|
237
|
+
displayIndex: mode.span.displayIndex,
|
|
230
238
|
start: mode.span.start,
|
|
231
239
|
end: mode.span.end,
|
|
232
240
|
text: "",
|
|
233
241
|
});
|
|
234
242
|
} else if (isDiff) {
|
|
235
243
|
const row = rows[cursor];
|
|
236
|
-
if (row) setMode({
|
|
244
|
+
if (row) setMode({ type: "compose", kind: intent.kind, displayIndex: cursor, start: 0, end: row.text.length, text: "" });
|
|
237
245
|
} else {
|
|
238
246
|
// a mouse drag leaves a native selection; it wins over the cursor block
|
|
239
247
|
const native = planSheetRef.current?.readSelection() ?? null;
|
|
240
248
|
if (native) {
|
|
241
|
-
setMode({
|
|
249
|
+
setMode({ type: "compose", kind: intent.kind, ...native, text: "" });
|
|
242
250
|
} else {
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
251
|
+
const block = display[cursor];
|
|
252
|
+
if (block) setMode({ type: "compose", kind: intent.kind, displayIndex: cursor, start: 0, end: displayText(block).length, text: "" });
|
|
245
253
|
}
|
|
246
254
|
}
|
|
247
255
|
return;
|
|
@@ -251,55 +259,66 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
251
259
|
liveInput.current = "";
|
|
252
260
|
// the confirm card lives in the review tab; opening submit reveals it
|
|
253
261
|
setRailTab("review");
|
|
254
|
-
return void setMode({
|
|
262
|
+
return void setMode({ type: "submit", verdict: defaultVerdict(session), summary: "" });
|
|
255
263
|
case "cut":
|
|
256
264
|
return controller.cut(cursor);
|
|
257
265
|
case "edit":
|
|
258
266
|
return runEditorHandOff();
|
|
259
267
|
case "editCard":
|
|
260
|
-
if (
|
|
268
|
+
if (focusedAnnotationId) openCardEdit(focusedAnnotationId);
|
|
261
269
|
return;
|
|
262
|
-
case "
|
|
263
|
-
case "
|
|
264
|
-
const
|
|
265
|
-
if (!
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
270
|
+
case "nextAnnotation":
|
|
271
|
+
case "prevAnnotation": {
|
|
272
|
+
const annotations = session?.annotations ?? [];
|
|
273
|
+
if (!annotations.length) return;
|
|
274
|
+
const focusedIndex = annotations.findIndex((annotation) => annotation.id === focusedAnnotationId);
|
|
275
|
+
const nextIndex =
|
|
276
|
+
focusedIndex === -1
|
|
277
|
+
? 0
|
|
278
|
+
: (focusedIndex + (intent.type === "nextAnnotation" ? 1 : -1) + annotations.length) % annotations.length;
|
|
279
|
+
return void selectCardFromDocument(annotations[nextIndex]!.id);
|
|
269
280
|
}
|
|
281
|
+
case "walkStart":
|
|
282
|
+
return controller.walkStart();
|
|
283
|
+
case "walkForward":
|
|
284
|
+
return controller.walkForward();
|
|
285
|
+
case "walkBack":
|
|
286
|
+
return controller.walkBack();
|
|
287
|
+
case "walkLeave":
|
|
288
|
+
return controller.walkLeave();
|
|
270
289
|
case "removeAnnotation":
|
|
271
|
-
if (
|
|
272
|
-
controller.removeAnnotation(
|
|
273
|
-
|
|
290
|
+
if (focusedAnnotationId) {
|
|
291
|
+
controller.removeAnnotation(focusedAnnotationId);
|
|
292
|
+
setFocusedAnnotationId(undefined);
|
|
274
293
|
}
|
|
275
294
|
return;
|
|
276
295
|
case "deselect":
|
|
277
296
|
planSheetRef.current?.clearSelection();
|
|
278
|
-
|
|
279
|
-
|
|
297
|
+
setFocusedAnnotationId(undefined);
|
|
298
|
+
setPulsedAnnotationId(null);
|
|
280
299
|
return;
|
|
281
300
|
case "closeOverlay":
|
|
282
|
-
return void setMode({
|
|
301
|
+
return void setMode({ type: "normal" });
|
|
283
302
|
case "saveCompose": {
|
|
284
303
|
const body = liveInput.current.trim();
|
|
285
|
-
if (mode.
|
|
304
|
+
if (mode.type === "railEdit") {
|
|
286
305
|
if (session && body) controller.updateAnnotation(mode.id, body);
|
|
287
|
-
return void setMode({
|
|
306
|
+
return void setMode({ type: "normal" });
|
|
288
307
|
}
|
|
289
|
-
if (mode.
|
|
308
|
+
if (mode.type !== "compose") return;
|
|
290
309
|
if (session && body) {
|
|
291
|
-
const annotationId = controller.annotate(mode.kind, mode.
|
|
292
|
-
if (annotationId)
|
|
310
|
+
const annotationId = controller.annotate(mode.kind, mode.displayIndex, mode.start, mode.end, body);
|
|
311
|
+
if (annotationId) setFocusedAnnotationId(annotationId);
|
|
293
312
|
}
|
|
294
|
-
return void setMode({
|
|
313
|
+
return void setMode({ type: "normal" });
|
|
295
314
|
}
|
|
296
315
|
case "submitVerdict":
|
|
297
|
-
if (mode.
|
|
298
|
-
return void setMode({
|
|
316
|
+
if (mode.type === "submit") controller.submit(mode.verdict, liveInput.current);
|
|
317
|
+
return void setMode({ type: "normal" });
|
|
299
318
|
case "cycleVerdict": {
|
|
300
|
-
if (mode.
|
|
301
|
-
const
|
|
302
|
-
return void setMode({ ...mode, verdict: VERDICTS[
|
|
319
|
+
if (mode.type !== "submit") return;
|
|
320
|
+
const verdictIndex = (VERDICTS.indexOf(mode.verdict) + intent.dir + VERDICTS.length) % VERDICTS.length;
|
|
321
|
+
return void setMode({ ...mode, verdict: VERDICTS[verdictIndex]! });
|
|
303
322
|
}
|
|
304
323
|
case "finishReview":
|
|
305
324
|
return controller.finishReview();
|
|
@@ -311,15 +330,17 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
311
330
|
};
|
|
312
331
|
|
|
313
332
|
const overlay: KeyState["overlay"] =
|
|
314
|
-
mode.
|
|
333
|
+
mode.type === "compose" || mode.type === "railEdit"
|
|
315
334
|
? "compose"
|
|
316
|
-
: mode.
|
|
335
|
+
: mode.type === "submit"
|
|
317
336
|
? "submit"
|
|
318
337
|
: completion.phase === "prompt"
|
|
319
338
|
? "completion-prompt"
|
|
320
339
|
: completion.phase === "counting"
|
|
321
340
|
? "completion-counting"
|
|
322
|
-
:
|
|
341
|
+
: walking
|
|
342
|
+
? "walk"
|
|
343
|
+
: "none";
|
|
323
344
|
|
|
324
345
|
useKeyboard((key) => {
|
|
325
346
|
const state: KeyState = {
|
|
@@ -327,13 +348,14 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
327
348
|
readOnly,
|
|
328
349
|
overlay,
|
|
329
350
|
view: !session ? "inbox" : isDiff ? "diff" : "plan",
|
|
330
|
-
spanMode: mode.
|
|
351
|
+
spanMode: mode.type === "span",
|
|
331
352
|
resolved: !!resolved,
|
|
332
353
|
hasInboxItems: !!inbox?.length,
|
|
333
354
|
annotationCount: session?.annotations.length ?? 0,
|
|
334
|
-
hasFocusedAnnotation:
|
|
355
|
+
hasFocusedAnnotation: focusedAnnotationId !== undefined,
|
|
356
|
+
walkAtEnd: walk !== null && walk.index >= walkFileList.length,
|
|
335
357
|
cursorAnnotatable: isDiff
|
|
336
|
-
? rows[cursor] !== undefined && rows[cursor]!.
|
|
358
|
+
? rows[cursor] !== undefined && rows[cursor]!.kind !== "file" && rows[cursor]!.kind !== "hunk"
|
|
337
359
|
: !!display[cursor]?.work,
|
|
338
360
|
};
|
|
339
361
|
keyBindings.setContext({ overlay: state.overlay, spanMode: state.spanMode });
|
|
@@ -364,53 +386,53 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
364
386
|
);
|
|
365
387
|
}
|
|
366
388
|
|
|
367
|
-
const
|
|
368
|
-
const pendingCount =
|
|
389
|
+
const activeSession = session!;
|
|
390
|
+
const pendingCount = reviewerAnnotations(activeSession).length + (activeSession.workingCopy !== undefined ? 1 : 0);
|
|
369
391
|
|
|
370
|
-
if ((completion.phase === "prompt" || completion.phase === "counting") &&
|
|
392
|
+
if ((completion.phase === "prompt" || completion.phase === "counting") && activeSession.verdict) {
|
|
371
393
|
return (
|
|
372
394
|
<ThemeProvider theme={theme}>
|
|
373
395
|
<CompletionOverlay
|
|
374
|
-
verdict={
|
|
396
|
+
verdict={activeSession.verdict.kind}
|
|
375
397
|
completion={completion}
|
|
376
398
|
status={status}
|
|
377
|
-
returnsTo={returnPaneFor(
|
|
399
|
+
returnsTo={returnPaneFor(activeSession.artifact.meta.herdrPane) ? (activeSession.artifact.meta.agent ?? "the agent") : undefined}
|
|
378
400
|
/>
|
|
379
401
|
</ThemeProvider>
|
|
380
402
|
);
|
|
381
403
|
}
|
|
382
404
|
|
|
383
405
|
const composeState =
|
|
384
|
-
mode.
|
|
406
|
+
mode.type === "compose" && !isDiff
|
|
385
407
|
? {
|
|
386
408
|
kind: mode.kind,
|
|
387
|
-
|
|
388
|
-
quote: displayText(display[mode.
|
|
409
|
+
displayIndex: mode.displayIndex,
|
|
410
|
+
quote: displayText(display[mode.displayIndex]!).slice(mode.start, mode.end),
|
|
389
411
|
draft: {
|
|
390
412
|
text: mode.text,
|
|
391
413
|
onInput: (text: string) => {
|
|
392
414
|
liveInput.current = text;
|
|
393
415
|
setMode({ ...mode, text });
|
|
394
416
|
},
|
|
395
|
-
onSave: () => dispatch({
|
|
396
|
-
onCancel: () => dispatch({
|
|
417
|
+
onSave: () => dispatch({ type: "saveCompose" }),
|
|
418
|
+
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
397
419
|
},
|
|
398
420
|
}
|
|
399
421
|
: null;
|
|
400
422
|
|
|
401
423
|
const activeSpan =
|
|
402
|
-
mode.
|
|
403
|
-
? {
|
|
404
|
-
: mode.
|
|
424
|
+
mode.type === "span"
|
|
425
|
+
? { displayIndex: mode.span.displayIndex, start: mode.span.start, end: mode.span.end }
|
|
426
|
+
: mode.type === "compose" && !isDiff
|
|
405
427
|
? // the compose anchor stays painted selection-style while the box is open
|
|
406
|
-
{
|
|
428
|
+
{ displayIndex: mode.displayIndex, start: mode.start, end: mode.end }
|
|
407
429
|
: null;
|
|
408
430
|
|
|
409
|
-
const onLineActivate = (
|
|
431
|
+
const onLineActivate = (displayIndex: number): void => {
|
|
410
432
|
// releasing a drag-selection lands here too; a live selection is not a click
|
|
411
433
|
if (renderer?.hasSelection) return;
|
|
412
|
-
setCursor(
|
|
413
|
-
const annotationId = marks.get(
|
|
434
|
+
setCursor(displayIndex);
|
|
435
|
+
const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
|
|
414
436
|
if (annotationId) selectCardFromDocument(annotationId);
|
|
415
437
|
};
|
|
416
438
|
|
|
@@ -424,60 +446,67 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
424
446
|
const onSubmitRequest = (): void => {
|
|
425
447
|
if (readOnly) return controller.setStatus("observer - read-only");
|
|
426
448
|
if (resolved) return;
|
|
427
|
-
dispatch({
|
|
449
|
+
dispatch({ type: "openSubmit" });
|
|
428
450
|
};
|
|
429
451
|
|
|
430
452
|
const submitConfirmState =
|
|
431
|
-
mode.
|
|
453
|
+
mode.type === "submit"
|
|
432
454
|
? {
|
|
433
455
|
verdict: mode.verdict,
|
|
434
456
|
summary: mode.summary,
|
|
435
|
-
annotationCount:
|
|
436
|
-
blockingCount:
|
|
457
|
+
annotationCount: reviewerAnnotations(activeSession).length,
|
|
458
|
+
blockingCount: reviewerAnnotations(activeSession).filter(annotationBlocking).length,
|
|
459
|
+
// walk coverage keeps partial passes honest at the verdict
|
|
460
|
+
viewedSummary:
|
|
461
|
+
isDiff && activeSession.viewedPaths !== undefined
|
|
462
|
+
? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
|
|
463
|
+
: undefined,
|
|
437
464
|
onInput: (summary: string) => {
|
|
438
465
|
liveInput.current = summary;
|
|
439
466
|
setMode({ ...mode, summary });
|
|
440
467
|
},
|
|
441
468
|
onSelectVerdict: (verdict: VerdictKind) => setMode({ ...mode, verdict }),
|
|
442
|
-
onSubmit: () => dispatch({
|
|
443
|
-
onCancel: () => dispatch({
|
|
469
|
+
onSubmit: () => dispatch({ type: "submitVerdict" }),
|
|
470
|
+
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
444
471
|
}
|
|
445
472
|
: null;
|
|
446
473
|
|
|
447
474
|
const cardEditState =
|
|
448
|
-
mode.
|
|
475
|
+
mode.type === "railEdit"
|
|
449
476
|
? {
|
|
450
477
|
id: mode.id,
|
|
451
478
|
text: mode.text,
|
|
452
479
|
onInput: (text: string) => {
|
|
453
480
|
liveInput.current = text;
|
|
454
|
-
setMode({
|
|
481
|
+
setMode({ type: "railEdit", id: mode.id, text });
|
|
455
482
|
},
|
|
456
|
-
onSave: () => dispatch({
|
|
457
|
-
onCancel: () => dispatch({
|
|
483
|
+
onSave: () => dispatch({ type: "saveCompose" }),
|
|
484
|
+
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
458
485
|
}
|
|
459
486
|
: null;
|
|
460
487
|
|
|
461
488
|
const headerItems: BreadcrumbItem[] = [
|
|
462
489
|
{ label: "cueloop", tone: "accent" },
|
|
463
|
-
{ label: `${
|
|
464
|
-
...(resolved ? [{ label: `resolved: ${
|
|
490
|
+
{ label: `${activeSession.artifact.meta.title ?? activeSession.artifact.meta.planPath ?? activeSession.id} · rev ${activeSession.revisions.length}`, tone: "dim" },
|
|
491
|
+
...(resolved ? [{ label: `resolved: ${activeSession.verdict!.kind.replace("_", " ")}`, tone: "green" as const }] : []),
|
|
465
492
|
...(readOnly ? [{ label: "observer", tone: "dim" as const }] : []),
|
|
466
493
|
...(status ? [{ label: status, tone: "accent" as const }] : []),
|
|
467
494
|
];
|
|
468
495
|
|
|
469
|
-
keyBindings.setContext({ overlay, spanMode: mode.
|
|
496
|
+
keyBindings.setContext({ overlay, spanMode: mode.type === "span" });
|
|
470
497
|
const hintMode: HintMode = readOnly
|
|
471
498
|
? "read-only"
|
|
472
|
-
: mode.
|
|
499
|
+
: mode.type === "submit"
|
|
473
500
|
? "submit"
|
|
474
|
-
: mode.
|
|
501
|
+
: mode.type === "span"
|
|
475
502
|
? "span"
|
|
476
|
-
: mode.
|
|
503
|
+
: mode.type === "compose" || mode.type === "railEdit"
|
|
477
504
|
? "compose"
|
|
478
|
-
:
|
|
479
|
-
? "
|
|
480
|
-
:
|
|
505
|
+
: walking
|
|
506
|
+
? "walk"
|
|
507
|
+
: focusedAnnotationId !== undefined
|
|
508
|
+
? "card"
|
|
509
|
+
: "normal";
|
|
481
510
|
const railWidth = terminalWidth >= 100 ? 34 : 28;
|
|
482
511
|
|
|
483
512
|
return (
|
|
@@ -486,11 +515,18 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
486
515
|
<Breadcrumb items={headerItems} />
|
|
487
516
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
488
517
|
{isDiff ? (
|
|
489
|
-
|
|
518
|
+
// the sheet dims to reading-quiet colors while the wizard has focus
|
|
519
|
+
<DiffSheet
|
|
520
|
+
rows={rows}
|
|
521
|
+
cursor={cursor}
|
|
522
|
+
annotations={activeSession.annotations}
|
|
523
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
524
|
+
theme={walking ? dimmedTheme(theme) : undefined}
|
|
525
|
+
/>
|
|
490
526
|
) : (
|
|
491
527
|
<PlanSheet
|
|
492
528
|
ref={planSheetRef}
|
|
493
|
-
session={
|
|
529
|
+
session={activeSession}
|
|
494
530
|
display={display}
|
|
495
531
|
marks={marks}
|
|
496
532
|
cursor={cursor}
|
|
@@ -503,8 +539,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
503
539
|
)}
|
|
504
540
|
<ReviewRail
|
|
505
541
|
ref={railRef}
|
|
506
|
-
session={
|
|
507
|
-
selectedId={
|
|
542
|
+
session={activeSession}
|
|
543
|
+
selectedId={focusedAnnotationId}
|
|
508
544
|
resolvedIds={isDiff ? null : resolvedIds}
|
|
509
545
|
railTab={railTab}
|
|
510
546
|
pendingCount={pendingCount}
|
|
@@ -517,10 +553,10 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
517
553
|
width={railWidth}
|
|
518
554
|
/>
|
|
519
555
|
</box>
|
|
520
|
-
{mode.
|
|
556
|
+
{mode.type === "compose" && isDiff ? (
|
|
521
557
|
<ComposeBar
|
|
522
558
|
kind={mode.kind}
|
|
523
|
-
quote={rows[mode.
|
|
559
|
+
quote={rows[mode.displayIndex]?.text ?? ""}
|
|
524
560
|
text={mode.text}
|
|
525
561
|
onInput={(text) => {
|
|
526
562
|
liveInput.current = text;
|
|
@@ -530,6 +566,24 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
530
566
|
) : (
|
|
531
567
|
<StatusBar>{keyBindings.statusHint(hintMode)}</StatusBar>
|
|
532
568
|
)}
|
|
569
|
+
{walking && walk !== null ? (
|
|
570
|
+
<WalkWizard
|
|
571
|
+
files={walkFileList}
|
|
572
|
+
index={walk.index}
|
|
573
|
+
viewedPaths={viewedPaths}
|
|
574
|
+
note={
|
|
575
|
+
walkFileList[walk.index] !== undefined
|
|
576
|
+
? noteForFile(activeSession.annotations, walkFileList[walk.index]!.path)
|
|
577
|
+
: undefined
|
|
578
|
+
}
|
|
579
|
+
terminalWidth={terminalWidth}
|
|
580
|
+
onSubmitRequest={() => {
|
|
581
|
+
dispatch({ type: "walkLeave" });
|
|
582
|
+
dispatch({ type: "openSubmit" });
|
|
583
|
+
}}
|
|
584
|
+
onBack={() => dispatch({ type: "walkBack" })}
|
|
585
|
+
/>
|
|
586
|
+
) : null}
|
|
533
587
|
</box>
|
|
534
588
|
</ThemeProvider>
|
|
535
589
|
);
|