@cueloop/client 0.1.0-alpha.16 → 0.1.0-alpha.18
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 +138 -139
- package/src/App.walk.test.tsx +1 -1
- package/src/components/AnnotationCard.tsx +4 -4
- package/src/components/ComposeBar.tsx +2 -2
- 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.tsx +1 -1
- 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 +12 -12
- package/src/editor.ts +1 -1
- package/src/herdr.test.ts +3 -34
- package/src/herdr.ts +8 -21
- package/src/key-bindings.ts +1 -1
- package/src/keymap.test.ts +111 -111
- package/src/keymap.ts +64 -64
- package/src/run.ts +3 -3
- package/src/serve.test.ts +6 -6
- package/src/serve.ts +7 -7
- package/src/session-controller.ts +76 -73
- package/src/theme.ts +1 -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.ts +4 -4
- package/src/components/format.ts +0 -6
- package/src/view.ts +0 -341
package/src/App.tsx
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
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
15
|
import { isAgentNote, type ReviewSession, type VerdictKind } from "@cueloop/schema";
|
|
20
|
-
import { displayText, marksByDisplay, spanKey, startSpan, type Mark, type SpanState } from "./view";
|
|
16
|
+
import { displayText, marksByDisplay, spanKey, startSpan, type Mark, type SpanState } from "./view-plan";
|
|
21
17
|
import { noteForFile, viewedCount } from "./walk";
|
|
22
18
|
import { DARK, dimmedTheme } from "./theme";
|
|
23
19
|
import { DEFAULT_KEYS, loadConfig } from "./config";
|
|
24
|
-
import { returnPaneFor } from "
|
|
20
|
+
import { returnPaneFor } from "@cueloop/schema";
|
|
25
21
|
import { createReviewController } from "./session-controller";
|
|
26
22
|
import { reduceKey, type Intent, type KeyState } from "./keymap";
|
|
27
23
|
import { KeyBindings, type HintMode } from "./key-bindings";
|
|
@@ -51,11 +47,11 @@ export interface AppProps {
|
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
type Mode =
|
|
54
|
-
| {
|
|
55
|
-
| {
|
|
56
|
-
| {
|
|
57
|
-
| {
|
|
58
|
-
| {
|
|
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 };
|
|
59
55
|
|
|
60
56
|
/** Reviewer-authored annotations only: agent notes never count as feedback. */
|
|
61
57
|
function reviewerAnnotations(session: ReviewSession) {
|
|
@@ -87,11 +83,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
87
83
|
// ── view state ──────────────────────────────
|
|
88
84
|
const [cursor, setCursor] = useState(0);
|
|
89
85
|
const [inboxCursor, setInboxCursor] = useState(0);
|
|
90
|
-
const [mode, setMode] = useState<Mode>({
|
|
91
|
-
const [
|
|
86
|
+
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
87
|
+
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
92
88
|
const [railTab, setRailTab] = useState<"review" | "agent">("review");
|
|
93
89
|
// ~2s focus pulse on the document highlight when a rail card is activated
|
|
94
|
-
const [
|
|
90
|
+
const [pulsedAnnotationId, setPulsedAnnotationId] = useState<string | null>(null);
|
|
95
91
|
const pulseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
96
92
|
// live mirror of overlay input text: refs commit synchronously, so the
|
|
97
93
|
// RETURN handler never reads a stale value mid-typing
|
|
@@ -103,11 +99,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
103
99
|
const keyBindings = useMemo(() => new KeyBindings(DEFAULT_KEYS), []);
|
|
104
100
|
const [theme, setTheme] = useState(DARK);
|
|
105
101
|
useEffect(() => {
|
|
106
|
-
const
|
|
107
|
-
keysRef.current =
|
|
108
|
-
keyBindings.setKeys(
|
|
109
|
-
setTheme(
|
|
110
|
-
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);
|
|
111
107
|
}, [session?.workspace.repoRoot, controller, keyBindings]);
|
|
112
108
|
useEffect(
|
|
113
109
|
() => () => {
|
|
@@ -120,8 +116,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
120
116
|
const display = controller.display();
|
|
121
117
|
const rows = controller.rows();
|
|
122
118
|
const marks = useMemo(
|
|
123
|
-
() => (session ? marksByDisplay(session.annotations, display,
|
|
124
|
-
[session, display,
|
|
119
|
+
() => (session ? marksByDisplay(session.annotations, display, pulsedAnnotationId ?? undefined) : new Map<number, Mark[]>()),
|
|
120
|
+
[session, display, pulsedAnnotationId],
|
|
125
121
|
);
|
|
126
122
|
/** Annotation ids whose anchor resolved against the working copy. */
|
|
127
123
|
const resolvedIds = useMemo(() => {
|
|
@@ -143,34 +139,34 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
143
139
|
// out of span mode clears the renderer selection (compose paints its own
|
|
144
140
|
// mark, and a mouse drag never changes the mode, so it survives)
|
|
145
141
|
useEffect(() => {
|
|
146
|
-
if (mode.
|
|
142
|
+
if (mode.type === "span") planSheetRef.current?.driveSpanSelection(mode.span);
|
|
147
143
|
else planSheetRef.current?.clearSelection();
|
|
148
144
|
}, [mode]);
|
|
149
145
|
|
|
150
146
|
// ── selection symmetry: one selected id, both sides ──
|
|
151
147
|
const pulse = (id: string): void => {
|
|
152
|
-
|
|
148
|
+
setPulsedAnnotationId(id);
|
|
153
149
|
if (pulseTimer.current) clearTimeout(pulseTimer.current);
|
|
154
|
-
pulseTimer.current = setTimeout(() =>
|
|
150
|
+
pulseTimer.current = setTimeout(() => setPulsedAnnotationId(null), 2000);
|
|
155
151
|
};
|
|
156
152
|
|
|
157
153
|
/** Card activation scrolls the document to the anchor and pulses it. */
|
|
158
154
|
const revealAnchor = (annotationId: string): void => {
|
|
159
|
-
for (const [
|
|
155
|
+
for (const [displayIndex, blockMarks] of marks) {
|
|
160
156
|
if (!blockMarks.some((mark) => mark.annotationId === annotationId)) continue;
|
|
161
|
-
setCursor(
|
|
162
|
-
planSheetRef.current?.revealBlock(
|
|
157
|
+
setCursor(displayIndex);
|
|
158
|
+
planSheetRef.current?.revealBlock(displayIndex);
|
|
163
159
|
return;
|
|
164
160
|
}
|
|
165
161
|
};
|
|
166
162
|
|
|
167
163
|
const selectCardFromDocument = (annotationId: string): void => {
|
|
168
|
-
|
|
164
|
+
setFocusedAnnotationId(annotationId);
|
|
169
165
|
railRef.current?.revealCard(annotationId);
|
|
170
166
|
};
|
|
171
167
|
|
|
172
168
|
const selectCardFromRail = (annotationId: string): void => {
|
|
173
|
-
|
|
169
|
+
setFocusedAnnotationId(annotationId);
|
|
174
170
|
pulse(annotationId);
|
|
175
171
|
revealAnchor(annotationId);
|
|
176
172
|
};
|
|
@@ -181,7 +177,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
181
177
|
const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
|
|
182
178
|
if (!annotation) return;
|
|
183
179
|
liveInput.current = annotation.body;
|
|
184
|
-
setMode({
|
|
180
|
+
setMode({ type: "railEdit", id: annotation.id, text: annotation.body });
|
|
185
181
|
};
|
|
186
182
|
|
|
187
183
|
/** The $EDITOR hand-off releases the terminal: suspend, edit, resume. */
|
|
@@ -196,64 +192,64 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
196
192
|
|
|
197
193
|
// ── keyboard grammar: build state, reduce, dispatch ──
|
|
198
194
|
const dispatch = (intent: Intent): void => {
|
|
199
|
-
switch (intent.
|
|
195
|
+
switch (intent.type) {
|
|
200
196
|
case "exit":
|
|
201
197
|
return void onExit?.(0);
|
|
202
198
|
case "status":
|
|
203
|
-
return controller.setStatus(intent.
|
|
199
|
+
return controller.setStatus(intent.message);
|
|
204
200
|
case "move": {
|
|
205
|
-
const
|
|
206
|
-
if (intent.to === "down") setCursor((
|
|
207
|
-
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));
|
|
208
204
|
else if (intent.to === "top") setCursor(0);
|
|
209
|
-
else setCursor(
|
|
205
|
+
else setCursor(navigableCount - 1);
|
|
210
206
|
return;
|
|
211
207
|
}
|
|
212
208
|
case "inboxMove": {
|
|
213
|
-
const
|
|
214
|
-
setInboxCursor((
|
|
209
|
+
const navigableCount = inbox?.length ?? 0;
|
|
210
|
+
setInboxCursor((current) => (intent.to === "down" ? Math.min(navigableCount - 1, current + 1) : Math.max(0, current - 1)));
|
|
215
211
|
return;
|
|
216
212
|
}
|
|
217
213
|
case "openSession": {
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
214
|
+
const selected = inbox?.[inboxCursor];
|
|
215
|
+
if (selected) controller.open(selected.id);
|
|
220
216
|
return;
|
|
221
217
|
}
|
|
222
218
|
case "startSpan": {
|
|
223
|
-
const
|
|
224
|
-
if (!
|
|
225
|
-
const span = startSpan(cursor, displayText(
|
|
226
|
-
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 });
|
|
227
223
|
return;
|
|
228
224
|
}
|
|
229
225
|
case "spanKey":
|
|
230
|
-
if (mode.
|
|
231
|
-
const span = spanKey(mode.span, intent.name, displayText(display[mode.span.
|
|
232
|
-
setMode({
|
|
226
|
+
if (mode.type === "span") {
|
|
227
|
+
const span = spanKey(mode.span, intent.name, displayText(display[mode.span.displayIndex]!));
|
|
228
|
+
setMode({ type: "span", span });
|
|
233
229
|
}
|
|
234
230
|
return;
|
|
235
231
|
case "openCompose": {
|
|
236
232
|
liveInput.current = "";
|
|
237
|
-
if (intent.from === "span" && mode.
|
|
233
|
+
if (intent.from === "span" && mode.type === "span") {
|
|
238
234
|
setMode({
|
|
239
|
-
|
|
235
|
+
type: "compose",
|
|
240
236
|
kind: intent.kind,
|
|
241
|
-
|
|
237
|
+
displayIndex: mode.span.displayIndex,
|
|
242
238
|
start: mode.span.start,
|
|
243
239
|
end: mode.span.end,
|
|
244
240
|
text: "",
|
|
245
241
|
});
|
|
246
242
|
} else if (isDiff) {
|
|
247
243
|
const row = rows[cursor];
|
|
248
|
-
if (row) setMode({
|
|
244
|
+
if (row) setMode({ type: "compose", kind: intent.kind, displayIndex: cursor, start: 0, end: row.text.length, text: "" });
|
|
249
245
|
} else {
|
|
250
246
|
// a mouse drag leaves a native selection; it wins over the cursor block
|
|
251
247
|
const native = planSheetRef.current?.readSelection() ?? null;
|
|
252
248
|
if (native) {
|
|
253
|
-
setMode({
|
|
249
|
+
setMode({ type: "compose", kind: intent.kind, ...native, text: "" });
|
|
254
250
|
} else {
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
251
|
+
const block = display[cursor];
|
|
252
|
+
if (block) setMode({ type: "compose", kind: intent.kind, displayIndex: cursor, start: 0, end: displayText(block).length, text: "" });
|
|
257
253
|
}
|
|
258
254
|
}
|
|
259
255
|
return;
|
|
@@ -263,21 +259,24 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
263
259
|
liveInput.current = "";
|
|
264
260
|
// the confirm card lives in the review tab; opening submit reveals it
|
|
265
261
|
setRailTab("review");
|
|
266
|
-
return void setMode({
|
|
262
|
+
return void setMode({ type: "submit", verdict: defaultVerdict(session), summary: "" });
|
|
267
263
|
case "cut":
|
|
268
264
|
return controller.cut(cursor);
|
|
269
265
|
case "edit":
|
|
270
266
|
return runEditorHandOff();
|
|
271
267
|
case "editCard":
|
|
272
|
-
if (
|
|
268
|
+
if (focusedAnnotationId) openCardEdit(focusedAnnotationId);
|
|
273
269
|
return;
|
|
274
|
-
case "
|
|
275
|
-
case "
|
|
276
|
-
const
|
|
277
|
-
if (!
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
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);
|
|
281
280
|
}
|
|
282
281
|
case "walkStart":
|
|
283
282
|
return controller.walkStart();
|
|
@@ -288,38 +287,38 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
288
287
|
case "walkLeave":
|
|
289
288
|
return controller.walkLeave();
|
|
290
289
|
case "removeAnnotation":
|
|
291
|
-
if (
|
|
292
|
-
controller.removeAnnotation(
|
|
293
|
-
|
|
290
|
+
if (focusedAnnotationId) {
|
|
291
|
+
controller.removeAnnotation(focusedAnnotationId);
|
|
292
|
+
setFocusedAnnotationId(undefined);
|
|
294
293
|
}
|
|
295
294
|
return;
|
|
296
295
|
case "deselect":
|
|
297
296
|
planSheetRef.current?.clearSelection();
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
setFocusedAnnotationId(undefined);
|
|
298
|
+
setPulsedAnnotationId(null);
|
|
300
299
|
return;
|
|
301
300
|
case "closeOverlay":
|
|
302
|
-
return void setMode({
|
|
301
|
+
return void setMode({ type: "normal" });
|
|
303
302
|
case "saveCompose": {
|
|
304
303
|
const body = liveInput.current.trim();
|
|
305
|
-
if (mode.
|
|
304
|
+
if (mode.type === "railEdit") {
|
|
306
305
|
if (session && body) controller.updateAnnotation(mode.id, body);
|
|
307
|
-
return void setMode({
|
|
306
|
+
return void setMode({ type: "normal" });
|
|
308
307
|
}
|
|
309
|
-
if (mode.
|
|
308
|
+
if (mode.type !== "compose") return;
|
|
310
309
|
if (session && body) {
|
|
311
|
-
const annotationId = controller.annotate(mode.kind, mode.
|
|
312
|
-
if (annotationId)
|
|
310
|
+
const annotationId = controller.annotate(mode.kind, mode.displayIndex, mode.start, mode.end, body);
|
|
311
|
+
if (annotationId) setFocusedAnnotationId(annotationId);
|
|
313
312
|
}
|
|
314
|
-
return void setMode({
|
|
313
|
+
return void setMode({ type: "normal" });
|
|
315
314
|
}
|
|
316
315
|
case "submitVerdict":
|
|
317
|
-
if (mode.
|
|
318
|
-
return void setMode({
|
|
316
|
+
if (mode.type === "submit") controller.submit(mode.verdict, liveInput.current);
|
|
317
|
+
return void setMode({ type: "normal" });
|
|
319
318
|
case "cycleVerdict": {
|
|
320
|
-
if (mode.
|
|
321
|
-
const
|
|
322
|
-
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]! });
|
|
323
322
|
}
|
|
324
323
|
case "finishReview":
|
|
325
324
|
return controller.finishReview();
|
|
@@ -331,9 +330,9 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
331
330
|
};
|
|
332
331
|
|
|
333
332
|
const overlay: KeyState["overlay"] =
|
|
334
|
-
mode.
|
|
333
|
+
mode.type === "compose" || mode.type === "railEdit"
|
|
335
334
|
? "compose"
|
|
336
|
-
: mode.
|
|
335
|
+
: mode.type === "submit"
|
|
337
336
|
? "submit"
|
|
338
337
|
: completion.phase === "prompt"
|
|
339
338
|
? "completion-prompt"
|
|
@@ -349,14 +348,14 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
349
348
|
readOnly,
|
|
350
349
|
overlay,
|
|
351
350
|
view: !session ? "inbox" : isDiff ? "diff" : "plan",
|
|
352
|
-
spanMode: mode.
|
|
351
|
+
spanMode: mode.type === "span",
|
|
353
352
|
resolved: !!resolved,
|
|
354
353
|
hasInboxItems: !!inbox?.length,
|
|
355
354
|
annotationCount: session?.annotations.length ?? 0,
|
|
356
|
-
hasFocusedAnnotation:
|
|
355
|
+
hasFocusedAnnotation: focusedAnnotationId !== undefined,
|
|
357
356
|
walkAtEnd: walk !== null && walk.index >= walkFileList.length,
|
|
358
357
|
cursorAnnotatable: isDiff
|
|
359
|
-
? rows[cursor] !== undefined && rows[cursor]!.
|
|
358
|
+
? rows[cursor] !== undefined && rows[cursor]!.kind !== "file" && rows[cursor]!.kind !== "hunk"
|
|
360
359
|
: !!display[cursor]?.work,
|
|
361
360
|
};
|
|
362
361
|
keyBindings.setContext({ overlay: state.overlay, spanMode: state.spanMode });
|
|
@@ -387,53 +386,53 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
387
386
|
);
|
|
388
387
|
}
|
|
389
388
|
|
|
390
|
-
const
|
|
391
|
-
const pendingCount = reviewerAnnotations(
|
|
389
|
+
const activeSession = session!;
|
|
390
|
+
const pendingCount = reviewerAnnotations(activeSession).length + (activeSession.workingCopy !== undefined ? 1 : 0);
|
|
392
391
|
|
|
393
|
-
if ((completion.phase === "prompt" || completion.phase === "counting") &&
|
|
392
|
+
if ((completion.phase === "prompt" || completion.phase === "counting") && activeSession.verdict) {
|
|
394
393
|
return (
|
|
395
394
|
<ThemeProvider theme={theme}>
|
|
396
395
|
<CompletionOverlay
|
|
397
|
-
verdict={
|
|
396
|
+
verdict={activeSession.verdict.kind}
|
|
398
397
|
completion={completion}
|
|
399
398
|
status={status}
|
|
400
|
-
returnsTo={returnPaneFor(
|
|
399
|
+
returnsTo={returnPaneFor(activeSession.artifact.meta.herdrPane) ? (activeSession.artifact.meta.agent ?? "the agent") : undefined}
|
|
401
400
|
/>
|
|
402
401
|
</ThemeProvider>
|
|
403
402
|
);
|
|
404
403
|
}
|
|
405
404
|
|
|
406
405
|
const composeState =
|
|
407
|
-
mode.
|
|
406
|
+
mode.type === "compose" && !isDiff
|
|
408
407
|
? {
|
|
409
408
|
kind: mode.kind,
|
|
410
|
-
|
|
411
|
-
quote: displayText(display[mode.
|
|
409
|
+
displayIndex: mode.displayIndex,
|
|
410
|
+
quote: displayText(display[mode.displayIndex]!).slice(mode.start, mode.end),
|
|
412
411
|
draft: {
|
|
413
412
|
text: mode.text,
|
|
414
413
|
onInput: (text: string) => {
|
|
415
414
|
liveInput.current = text;
|
|
416
415
|
setMode({ ...mode, text });
|
|
417
416
|
},
|
|
418
|
-
onSave: () => dispatch({
|
|
419
|
-
onCancel: () => dispatch({
|
|
417
|
+
onSave: () => dispatch({ type: "saveCompose" }),
|
|
418
|
+
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
420
419
|
},
|
|
421
420
|
}
|
|
422
421
|
: null;
|
|
423
422
|
|
|
424
423
|
const activeSpan =
|
|
425
|
-
mode.
|
|
426
|
-
? {
|
|
427
|
-
: mode.
|
|
424
|
+
mode.type === "span"
|
|
425
|
+
? { displayIndex: mode.span.displayIndex, start: mode.span.start, end: mode.span.end }
|
|
426
|
+
: mode.type === "compose" && !isDiff
|
|
428
427
|
? // the compose anchor stays painted selection-style while the box is open
|
|
429
|
-
{
|
|
428
|
+
{ displayIndex: mode.displayIndex, start: mode.start, end: mode.end }
|
|
430
429
|
: null;
|
|
431
430
|
|
|
432
|
-
const onLineActivate = (
|
|
431
|
+
const onLineActivate = (displayIndex: number): void => {
|
|
433
432
|
// releasing a drag-selection lands here too; a live selection is not a click
|
|
434
433
|
if (renderer?.hasSelection) return;
|
|
435
|
-
setCursor(
|
|
436
|
-
const annotationId = marks.get(
|
|
434
|
+
setCursor(displayIndex);
|
|
435
|
+
const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
|
|
437
436
|
if (annotationId) selectCardFromDocument(annotationId);
|
|
438
437
|
};
|
|
439
438
|
|
|
@@ -447,19 +446,19 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
447
446
|
const onSubmitRequest = (): void => {
|
|
448
447
|
if (readOnly) return controller.setStatus("observer - read-only");
|
|
449
448
|
if (resolved) return;
|
|
450
|
-
dispatch({
|
|
449
|
+
dispatch({ type: "openSubmit" });
|
|
451
450
|
};
|
|
452
451
|
|
|
453
452
|
const submitConfirmState =
|
|
454
|
-
mode.
|
|
453
|
+
mode.type === "submit"
|
|
455
454
|
? {
|
|
456
455
|
verdict: mode.verdict,
|
|
457
456
|
summary: mode.summary,
|
|
458
|
-
annotationCount: reviewerAnnotations(
|
|
459
|
-
blockingCount: reviewerAnnotations(
|
|
457
|
+
annotationCount: reviewerAnnotations(activeSession).length,
|
|
458
|
+
blockingCount: reviewerAnnotations(activeSession).filter(annotationBlocking).length,
|
|
460
459
|
// walk coverage keeps partial passes honest at the verdict
|
|
461
460
|
viewedSummary:
|
|
462
|
-
isDiff &&
|
|
461
|
+
isDiff && activeSession.viewedPaths !== undefined
|
|
463
462
|
? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
|
|
464
463
|
: undefined,
|
|
465
464
|
onInput: (summary: string) => {
|
|
@@ -467,45 +466,45 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
467
466
|
setMode({ ...mode, summary });
|
|
468
467
|
},
|
|
469
468
|
onSelectVerdict: (verdict: VerdictKind) => setMode({ ...mode, verdict }),
|
|
470
|
-
onSubmit: () => dispatch({
|
|
471
|
-
onCancel: () => dispatch({
|
|
469
|
+
onSubmit: () => dispatch({ type: "submitVerdict" }),
|
|
470
|
+
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
472
471
|
}
|
|
473
472
|
: null;
|
|
474
473
|
|
|
475
474
|
const cardEditState =
|
|
476
|
-
mode.
|
|
475
|
+
mode.type === "railEdit"
|
|
477
476
|
? {
|
|
478
477
|
id: mode.id,
|
|
479
478
|
text: mode.text,
|
|
480
479
|
onInput: (text: string) => {
|
|
481
480
|
liveInput.current = text;
|
|
482
|
-
setMode({
|
|
481
|
+
setMode({ type: "railEdit", id: mode.id, text });
|
|
483
482
|
},
|
|
484
|
-
onSave: () => dispatch({
|
|
485
|
-
onCancel: () => dispatch({
|
|
483
|
+
onSave: () => dispatch({ type: "saveCompose" }),
|
|
484
|
+
onCancel: () => dispatch({ type: "closeOverlay" }),
|
|
486
485
|
}
|
|
487
486
|
: null;
|
|
488
487
|
|
|
489
488
|
const headerItems: BreadcrumbItem[] = [
|
|
490
489
|
{ label: "cueloop", tone: "accent" },
|
|
491
|
-
{ label: `${
|
|
492
|
-
...(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 }] : []),
|
|
493
492
|
...(readOnly ? [{ label: "observer", tone: "dim" as const }] : []),
|
|
494
493
|
...(status ? [{ label: status, tone: "accent" as const }] : []),
|
|
495
494
|
];
|
|
496
495
|
|
|
497
|
-
keyBindings.setContext({ overlay, spanMode: mode.
|
|
496
|
+
keyBindings.setContext({ overlay, spanMode: mode.type === "span" });
|
|
498
497
|
const hintMode: HintMode = readOnly
|
|
499
498
|
? "read-only"
|
|
500
|
-
: mode.
|
|
499
|
+
: mode.type === "submit"
|
|
501
500
|
? "submit"
|
|
502
|
-
: mode.
|
|
501
|
+
: mode.type === "span"
|
|
503
502
|
? "span"
|
|
504
|
-
: mode.
|
|
503
|
+
: mode.type === "compose" || mode.type === "railEdit"
|
|
505
504
|
? "compose"
|
|
506
505
|
: walking
|
|
507
506
|
? "walk"
|
|
508
|
-
:
|
|
507
|
+
: focusedAnnotationId !== undefined
|
|
509
508
|
? "card"
|
|
510
509
|
: "normal";
|
|
511
510
|
const railWidth = terminalWidth >= 100 ? 34 : 28;
|
|
@@ -520,14 +519,14 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
520
519
|
<DiffSheet
|
|
521
520
|
rows={rows}
|
|
522
521
|
cursor={cursor}
|
|
523
|
-
annotations={
|
|
524
|
-
focusedAnnotationId={
|
|
522
|
+
annotations={activeSession.annotations}
|
|
523
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
525
524
|
theme={walking ? dimmedTheme(theme) : undefined}
|
|
526
525
|
/>
|
|
527
526
|
) : (
|
|
528
527
|
<PlanSheet
|
|
529
528
|
ref={planSheetRef}
|
|
530
|
-
session={
|
|
529
|
+
session={activeSession}
|
|
531
530
|
display={display}
|
|
532
531
|
marks={marks}
|
|
533
532
|
cursor={cursor}
|
|
@@ -540,8 +539,8 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
540
539
|
)}
|
|
541
540
|
<ReviewRail
|
|
542
541
|
ref={railRef}
|
|
543
|
-
session={
|
|
544
|
-
selectedId={
|
|
542
|
+
session={activeSession}
|
|
543
|
+
selectedId={focusedAnnotationId}
|
|
545
544
|
resolvedIds={isDiff ? null : resolvedIds}
|
|
546
545
|
railTab={railTab}
|
|
547
546
|
pendingCount={pendingCount}
|
|
@@ -554,10 +553,10 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
554
553
|
width={railWidth}
|
|
555
554
|
/>
|
|
556
555
|
</box>
|
|
557
|
-
{mode.
|
|
556
|
+
{mode.type === "compose" && isDiff ? (
|
|
558
557
|
<ComposeBar
|
|
559
558
|
kind={mode.kind}
|
|
560
|
-
quote={rows[mode.
|
|
559
|
+
quote={rows[mode.displayIndex]?.text ?? ""}
|
|
561
560
|
text={mode.text}
|
|
562
561
|
onInput={(text) => {
|
|
563
562
|
liveInput.current = text;
|
|
@@ -574,15 +573,15 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
|
|
|
574
573
|
viewedPaths={viewedPaths}
|
|
575
574
|
note={
|
|
576
575
|
walkFileList[walk.index] !== undefined
|
|
577
|
-
? noteForFile(
|
|
576
|
+
? noteForFile(activeSession.annotations, walkFileList[walk.index]!.path)
|
|
578
577
|
: undefined
|
|
579
578
|
}
|
|
580
579
|
terminalWidth={terminalWidth}
|
|
581
580
|
onSubmitRequest={() => {
|
|
582
|
-
dispatch({
|
|
583
|
-
dispatch({
|
|
581
|
+
dispatch({ type: "walkLeave" });
|
|
582
|
+
dispatch({ type: "openSubmit" });
|
|
584
583
|
}}
|
|
585
|
-
onBack={() => dispatch({
|
|
584
|
+
onBack={() => dispatch({ type: "walkBack" })}
|
|
586
585
|
/>
|
|
587
586
|
) : null}
|
|
588
587
|
</box>
|
package/src/App.walk.test.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The guided walk in the virtual terminal
|
|
2
|
+
* The guided walk in the virtual terminal: the wizard steps every
|
|
3
3
|
* changed file with a plain progress title, ] marks viewed and persists with
|
|
4
4
|
* the session, esc keeps progress, w resumes at the first unviewed file
|
|
5
5
|
* (across App instances - the daemon round-trip), the agent-note block
|
|
@@ -15,7 +15,7 @@ import { useComponentTheme } from "./theme-context";
|
|
|
15
15
|
import { Card } from "./primitives/Card";
|
|
16
16
|
import { Button } from "./primitives/Button";
|
|
17
17
|
import { Toolbar } from "./primitives/Toolbar";
|
|
18
|
-
import {
|
|
18
|
+
import { truncateToSingleLine } from "./truncate-text";
|
|
19
19
|
|
|
20
20
|
export interface AnnotationDraft {
|
|
21
21
|
text: string;
|
|
@@ -101,7 +101,7 @@ export function AnnotationCard({ id, kind, quote, draft, saved, theme }: Annotat
|
|
|
101
101
|
const verb = kind === "suggestion" ? "suggest replacement for" : "comment on";
|
|
102
102
|
return (
|
|
103
103
|
<Card
|
|
104
|
-
title={` ${verb} "${
|
|
104
|
+
title={` ${verb} "${truncateToSingleLine(quote, 40)}" `}
|
|
105
105
|
contentRows={editorRows(draft.text) + 1}
|
|
106
106
|
borderColor={kindColor}
|
|
107
107
|
marginLeft={2}
|
|
@@ -125,11 +125,11 @@ export function AnnotationCard({ id, kind, quote, draft, saved, theme }: Annotat
|
|
|
125
125
|
{card.isBlocking ? <span fg={tokens.red}> · BLOCKING</span> : null}
|
|
126
126
|
<span fg={tokens.textDim}>{card.isOrphan ? " · ORPHANED" : " · pending"}</span>
|
|
127
127
|
</text>
|
|
128
|
-
<text fg={tokens.textDim}> "{
|
|
128
|
+
<text fg={tokens.textDim}> "{truncateToSingleLine(quote, 26)}"</text>
|
|
129
129
|
{card.editing ? (
|
|
130
130
|
<DraftEditor draft={card.editing} theme={theme} />
|
|
131
131
|
) : (
|
|
132
|
-
<text fg={card.isOrphan ? tokens.textDim : tokens.textMuted}> {
|
|
132
|
+
<text fg={card.isOrphan ? tokens.textDim : tokens.textMuted}> {truncateToSingleLine(card.body, 28)}</text>
|
|
133
133
|
)}
|
|
134
134
|
</box>
|
|
135
135
|
);
|
|
@@ -8,7 +8,7 @@ import React, { useRef } from "react";
|
|
|
8
8
|
import type { KeyBinding, TextareaRenderable } from "@opentui/core";
|
|
9
9
|
import type { Theme } from "../theme";
|
|
10
10
|
import { useComponentTheme } from "./theme-context";
|
|
11
|
-
import {
|
|
11
|
+
import { truncateToSingleLine } from "./truncate-text";
|
|
12
12
|
|
|
13
13
|
export interface ComposeBarProps {
|
|
14
14
|
kind: "comment" | "suggestion";
|
|
@@ -30,7 +30,7 @@ export function ComposeBar({ kind, quote, text, onInput, theme }: ComposeBarProp
|
|
|
30
30
|
return (
|
|
31
31
|
<box style={{ height: 1 + editorRows, backgroundColor: tokens.elevated, flexDirection: "column", paddingLeft: 1 }}>
|
|
32
32
|
<text fg={kind === "suggestion" ? tokens.green : tokens.accent}>
|
|
33
|
-
{kind === "suggestion" ? "SUGGEST REPLACEMENT FOR" : "COMMENT ON"} “{
|
|
33
|
+
{kind === "suggestion" ? "SUGGEST REPLACEMENT FOR" : "COMMENT ON"} “{truncateToSingleLine(quote, 60)}” · ⏎ save · esc cancel
|
|
34
34
|
</text>
|
|
35
35
|
<textarea
|
|
36
36
|
ref={editorRef}
|