@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60
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.autoclose.test.tsx +7 -0
- package/src/App.diff.test.tsx +11 -0
- package/src/App.export.test.tsx +5 -0
- package/src/App.plan-review.test.tsx +42 -0
- package/src/App.prototype.test.tsx +11 -0
- package/src/App.readonly.test.tsx +10 -0
- package/src/App.rendering.test.tsx +4 -0
- package/src/App.review-panel.test.tsx +7 -0
- package/src/App.submit-confirm.test.tsx +11 -0
- package/src/App.test.tsx +13 -0
- package/src/App.toast.test.tsx +2 -0
- package/src/App.tsx +27 -0
- package/src/App.walk.test.tsx +10 -0
- package/src/app-key-state.ts +1 -0
- package/src/app-screens.tsx +4 -0
- package/src/app-view-model.ts +21 -0
- package/src/attribution.ts +4 -0
- package/src/clipboard.ts +3 -0
- package/src/components/AnnotationCard.tsx +9 -0
- package/src/components/Breadcrumb.tsx +1 -0
- package/src/components/CodeBlock.tsx +1 -0
- package/src/components/CompletionOverlay.tsx +1 -0
- package/src/components/ConfirmCard.tsx +2 -0
- package/src/components/ConfirmDialog.tsx +2 -0
- package/src/components/DiffSheet.scroll.test.tsx +12 -0
- package/src/components/DiffSheet.syntax.test.tsx +3 -0
- package/src/components/DiffSheet.tsx +19 -0
- package/src/components/InboxList.tsx +2 -0
- package/src/components/KeybindsDialog.tsx +1 -0
- package/src/components/MarkerPopover.tsx +1 -0
- package/src/components/MenuBar.tsx +1 -0
- package/src/components/PlanSheet.tsx +26 -0
- package/src/components/PromptDialog.tsx +2 -0
- package/src/components/PrototypeSheet.tsx +27 -0
- package/src/components/ReviewPanel.tsx +2 -0
- package/src/components/ReviewRail.tsx +1 -0
- package/src/components/SettingsDialog.tsx +6 -0
- package/src/components/SettingsRows.tsx +4 -0
- package/src/components/Toast.tsx +1 -0
- package/src/components/agent-launcher.test.tsx +1 -0
- package/src/components/agent-launcher.tsx +2 -0
- package/src/components/diff-sheet-layout.test.ts +1 -0
- package/src/components/diff-sheet-layout.ts +17 -0
- package/src/components/plan-sheet-run-style.ts +4 -0
- package/src/components/primitives/Button.tsx +1 -0
- package/src/components/primitives/Card.tsx +1 -0
- package/src/components/primitives/Dialog.tsx +2 -0
- package/src/components/primitives/StatusBar.tsx +1 -0
- package/src/components/primitives/Tabs.tsx +4 -0
- package/src/components/quick-actions-editor.tsx +2 -0
- package/src/components/stories-app.tsx +1 -0
- package/src/components/stories.test.tsx +4 -0
- package/src/components/story.ts +6 -0
- package/src/components/syntax-highlight.ts +10 -0
- package/src/components/terminal-pane.ts +12 -0
- package/src/components/theme-context.tsx +1 -0
- package/src/components/truncate-text.ts +1 -0
- package/src/config.test.ts +18 -0
- package/src/config.ts +27 -0
- package/src/diff-hunk-curate.ts +20 -0
- package/src/diff-intraline.test.ts +1 -0
- package/src/diff-intraline.ts +14 -0
- package/src/diff-syntax.ts +22 -0
- package/src/editor.test.ts +6 -0
- package/src/editor.ts +12 -0
- package/src/ghostty-terminal.test.ts +2 -0
- package/src/ghostty-terminal.ts +8 -0
- package/src/herdr.test.ts +2 -0
- package/src/herdr.ts +3 -0
- package/src/integrations.ts +2 -0
- package/src/intent-dispatch.test.ts +6 -0
- package/src/intent-dispatch.ts +37 -0
- package/src/key-bindings.test.ts +4 -0
- package/src/key-bindings.ts +22 -0
- package/src/keymap.test.ts +13 -0
- package/src/keymap.ts +43 -0
- package/src/kitty-image.ts +9 -0
- package/src/prototype-browser.test.ts +1 -0
- package/src/prototype-browser.ts +22 -0
- package/src/pty.test.ts +3 -0
- package/src/pty.ts +15 -0
- package/src/run.ts +1 -0
- package/src/serve.test.ts +1 -0
- package/src/serve.ts +3 -0
- package/src/session-controller.curate.test.ts +7 -0
- package/src/session-controller.share.test.ts +11 -0
- package/src/session-controller.ts +79 -0
- package/src/share.ts +7 -0
- package/src/test-support.test.ts +5 -0
- package/src/test-support.ts +10 -0
- package/src/theme-presets.ts +1 -0
- package/src/use-settings-dialog.tsx +12 -0
- package/src/view-diff.test.ts +3 -0
- package/src/view-diff.ts +5 -0
- package/src/view-plan.test.ts +7 -0
- package/src/view-plan.ts +59 -0
- package/src/walk.ts +4 -0
|
@@ -56,13 +56,16 @@ async function renderApp(options: { readOnly?: boolean } = {}) {
|
|
|
56
56
|
height: 32,
|
|
57
57
|
},
|
|
58
58
|
);
|
|
59
|
+
|
|
59
60
|
await waitForText(setup, "cueloop");
|
|
61
|
+
|
|
60
62
|
return setup;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/** Seed annotations directly through the daemon core (all on one block). */
|
|
64
66
|
function seedAnnotations(count: number): void {
|
|
65
67
|
const blocks = parseBlocks(PLAN);
|
|
68
|
+
|
|
66
69
|
for (let index = 1; index <= count; index++) {
|
|
67
70
|
server.core.sessionAnnotate(session.id, {
|
|
68
71
|
id: `a_confirm_${index}`,
|
|
@@ -87,6 +90,7 @@ describe("rail submit confirm", () => {
|
|
|
87
90
|
// Assert
|
|
88
91
|
await waitForText(setup, "submit review");
|
|
89
92
|
const frame = setup.captureCharFrame();
|
|
93
|
+
|
|
90
94
|
// the card: title, verdict selector, word-buttons
|
|
91
95
|
expect(frame).toContain("submit review");
|
|
92
96
|
expect(frame).toContain("[Approve]"); // nothing pending: approve default
|
|
@@ -97,6 +101,7 @@ describe("rail submit confirm", () => {
|
|
|
97
101
|
test("left/right cycles the verdict selector in the card", async () => {
|
|
98
102
|
// Arrange
|
|
99
103
|
const setup = await renderApp();
|
|
104
|
+
|
|
100
105
|
await press(setup, "enter");
|
|
101
106
|
|
|
102
107
|
// Assert
|
|
@@ -124,6 +129,7 @@ describe("rail submit confirm", () => {
|
|
|
124
129
|
test("esc cancels the card and restores the plain Submit button", async () => {
|
|
125
130
|
// Arrange
|
|
126
131
|
const setup = await renderApp();
|
|
132
|
+
|
|
127
133
|
await press(setup, "enter");
|
|
128
134
|
|
|
129
135
|
// Assert
|
|
@@ -135,6 +141,7 @@ describe("rail submit confirm", () => {
|
|
|
135
141
|
// Assert
|
|
136
142
|
// a bare ESC settles after the parser's escape-sequence window
|
|
137
143
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
144
|
+
|
|
138
145
|
expect(frame).not.toContain("submit review");
|
|
139
146
|
expect(frame).toContain("Submit review");
|
|
140
147
|
});
|
|
@@ -150,6 +157,7 @@ describe("rail submit confirm", () => {
|
|
|
150
157
|
// Assert
|
|
151
158
|
await waitForText(setup, "[Changes]"); // pending items: request changes default
|
|
152
159
|
const frame = setup.captureCharFrame();
|
|
160
|
+
|
|
153
161
|
expect(frame).toContain("submit review");
|
|
154
162
|
|
|
155
163
|
// Act
|
|
@@ -160,6 +168,7 @@ describe("rail submit confirm", () => {
|
|
|
160
168
|
// the completion flow after submit is unchanged
|
|
161
169
|
await waitForText(setup, "feedback sent");
|
|
162
170
|
const stored = server.core.sessionGet(session.id);
|
|
171
|
+
|
|
163
172
|
expect(stored.status).toBe("resolved");
|
|
164
173
|
expect(stored.verdict!.kind).toBe("request_changes");
|
|
165
174
|
});
|
|
@@ -194,6 +203,7 @@ describe("rail submit confirm", () => {
|
|
|
194
203
|
// Assert
|
|
195
204
|
await waitForText(setup, "note 12");
|
|
196
205
|
const scrolledWithCard = setup.captureCharFrame();
|
|
206
|
+
|
|
197
207
|
expect(scrolledWithCard).toContain("submit review");
|
|
198
208
|
expect(scrolledWithCard).not.toContain("note 01");
|
|
199
209
|
});
|
|
@@ -209,6 +219,7 @@ describe("rail submit confirm", () => {
|
|
|
209
219
|
// Assert
|
|
210
220
|
await waitForText(setup, "observer - read-only");
|
|
211
221
|
const frame = setup.captureCharFrame();
|
|
222
|
+
|
|
212
223
|
expect(frame).toContain("observer - read-only");
|
|
213
224
|
expect(frame).not.toContain("1 annotations");
|
|
214
225
|
expect(frame).not.toContain("[Changes]");
|
package/src/App.test.tsx
CHANGED
|
@@ -59,8 +59,10 @@ async function renderApp(sessionId?: string) {
|
|
|
59
59
|
width: 120,
|
|
60
60
|
height: 32,
|
|
61
61
|
});
|
|
62
|
+
|
|
62
63
|
// the async daemon connect + first fetch land within the frame wait
|
|
63
64
|
await waitForText(setup, "cueloop");
|
|
65
|
+
|
|
64
66
|
return setup;
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -71,6 +73,7 @@ describe("plan rendering", () => {
|
|
|
71
73
|
|
|
72
74
|
// Assert
|
|
73
75
|
const frame = setup.captureCharFrame();
|
|
76
|
+
|
|
74
77
|
expect(frame).toContain("Migration Plan");
|
|
75
78
|
expect(frame).toContain("Context");
|
|
76
79
|
expect(frame).toContain("persists sessions to disk atomically");
|
|
@@ -93,6 +96,7 @@ describe("keyboard grammar", () => {
|
|
|
93
96
|
// Assert
|
|
94
97
|
const lines = setup.captureCharFrame().split("\n");
|
|
95
98
|
const cursorLine = lines.find((line) => line.includes("▎"))!;
|
|
99
|
+
|
|
96
100
|
expect(cursorLine).toContain("persists sessions");
|
|
97
101
|
});
|
|
98
102
|
|
|
@@ -116,6 +120,7 @@ describe("keyboard grammar", () => {
|
|
|
116
120
|
// Assert
|
|
117
121
|
await waitForText(setup, "COMMENT");
|
|
118
122
|
const stored = server.core.sessionGet(session.id);
|
|
123
|
+
|
|
119
124
|
expect(stored.annotations.length).toBe(1);
|
|
120
125
|
expect(stored.annotations[0]!.body).toBe("Define atomically.");
|
|
121
126
|
});
|
|
@@ -136,12 +141,14 @@ describe("keyboard grammar", () => {
|
|
|
136
141
|
// Assert
|
|
137
142
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
138
143
|
const stored = server.core.sessionGet(session.id);
|
|
144
|
+
|
|
139
145
|
expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
|
|
140
146
|
});
|
|
141
147
|
|
|
142
148
|
test("x cuts a block into the working copy; x restores it", async () => {
|
|
143
149
|
// Arrange
|
|
144
150
|
const setup = await renderApp();
|
|
151
|
+
|
|
145
152
|
// move to "- move the store" (h1, h2, p, h2 = 4 steps in)
|
|
146
153
|
for (let i = 0; i < 4; i++) await press(setup, "j");
|
|
147
154
|
|
|
@@ -161,6 +168,7 @@ describe("keyboard grammar", () => {
|
|
|
161
168
|
|
|
162
169
|
test("e runs $EDITOR on the working copy and tracks the diff", async () => {
|
|
163
170
|
const script = join(home, "fake-editor.sh");
|
|
171
|
+
|
|
164
172
|
await Bun.write(script, `#!/bin/sh\nsed -i '' 's/atomically/very atomically/' "$1"\n`);
|
|
165
173
|
Bun.spawnSync(["chmod", "+x", script]);
|
|
166
174
|
process.env.CUELOOP_EDITOR = script;
|
|
@@ -184,6 +192,7 @@ describe("submit", () => {
|
|
|
184
192
|
test("⏎ opens the rail confirm card; verdict + summary resolve the session", async () => {
|
|
185
193
|
// Arrange
|
|
186
194
|
const setup = await renderApp();
|
|
195
|
+
|
|
187
196
|
await press(setup, "j");
|
|
188
197
|
await press(setup, "j");
|
|
189
198
|
await press(setup, "c");
|
|
@@ -204,6 +213,7 @@ describe("submit", () => {
|
|
|
204
213
|
// Assert
|
|
205
214
|
await waitForText(setup, "feedback sent");
|
|
206
215
|
const stored = server.core.sessionGet(session.id);
|
|
216
|
+
|
|
207
217
|
expect(stored.status).toBe("resolved");
|
|
208
218
|
expect(stored.verdict!.kind).toBe("request_changes");
|
|
209
219
|
expect(stored.verdict!.feedback).toContain("Needs a phase list.");
|
|
@@ -234,10 +244,12 @@ describe("inbox", () => {
|
|
|
234
244
|
test("inbox mode wears the review chrome (header + menu bar) and opens a session", async () => {
|
|
235
245
|
// Arrange
|
|
236
246
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
247
|
+
|
|
237
248
|
await waitForText(setup, "cueloop");
|
|
238
249
|
|
|
239
250
|
// Assert - the same header/menu chrome as review, plus the session row
|
|
240
251
|
const frame = setup.captureCharFrame();
|
|
252
|
+
|
|
241
253
|
expect(frame).toContain("cueloop · resume");
|
|
242
254
|
expect(frame).toContain("Migration Plan");
|
|
243
255
|
expect(frame).toContain("menu"); // the shared MenuBar sits at the bottom
|
|
@@ -253,6 +265,7 @@ describe("inbox", () => {
|
|
|
253
265
|
test("the menu opens from the inbox and escape is not a trap", async () => {
|
|
254
266
|
// Arrange
|
|
255
267
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
268
|
+
|
|
256
269
|
await waitForText(setup, "cueloop · resume");
|
|
257
270
|
const lines = setup.captureCharFrame().split("\n");
|
|
258
271
|
const menuRow = lines.findIndex((line) => line.includes("menu"));
|
package/src/App.toast.test.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import type { ReviewSession } from "@cueloop/schema";
|
|
|
11
11
|
|
|
12
12
|
// the controller reaches the gateway through "./share"; stub it so `S` yields a toast
|
|
13
13
|
const publishShare = mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true }));
|
|
14
|
+
|
|
14
15
|
mock.module("./share", () => ({
|
|
15
16
|
publishShare,
|
|
16
17
|
pullShare: mock(async () => null),
|
|
@@ -56,6 +57,7 @@ describe("share toast", () => {
|
|
|
56
57
|
width: 120,
|
|
57
58
|
height: 32,
|
|
58
59
|
});
|
|
60
|
+
|
|
59
61
|
await waitForText(setup, "cueloop");
|
|
60
62
|
|
|
61
63
|
// Act: share raises the toast, then open a composer under it
|
package/src/App.tsx
CHANGED
|
@@ -142,8 +142,10 @@ export function App({
|
|
|
142
142
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
143
|
[home, sessionId],
|
|
144
144
|
);
|
|
145
|
+
|
|
145
146
|
useEffect(() => {
|
|
146
147
|
controller.connect();
|
|
148
|
+
|
|
147
149
|
return () => controller.close();
|
|
148
150
|
}, [controller]);
|
|
149
151
|
// stable across renders so the memoized PrototypeSheet is not re-rendered by
|
|
@@ -155,10 +157,12 @@ export function App({
|
|
|
155
157
|
);
|
|
156
158
|
const { session, inbox, status, toast, error, completion, editOrphanCount, walk } =
|
|
157
159
|
useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
160
|
+
|
|
158
161
|
// A shared plan you own polls for collaborator notes while it is open; the
|
|
159
162
|
// merge refreshes through the normal event path. Stops on leave.
|
|
160
163
|
useEffect(() => {
|
|
161
164
|
if (!isOwner || !session?.shareId) return;
|
|
165
|
+
|
|
162
166
|
return controller.startSharePoll();
|
|
163
167
|
}, [isOwner, session?.id, session?.shareId, controller]);
|
|
164
168
|
const renderer = useRenderer();
|
|
@@ -200,8 +204,10 @@ export function App({
|
|
|
200
204
|
const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
|
|
201
205
|
const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
|
|
202
206
|
const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
|
|
207
|
+
|
|
203
208
|
useEffect(() => {
|
|
204
209
|
const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
210
|
+
|
|
205
211
|
keysRef.current = config.keys;
|
|
206
212
|
keyBindings.setKeys(config.keys);
|
|
207
213
|
setTheme(composeTheme(config.ui.theme, config.themeOverrides, appearance));
|
|
@@ -225,16 +231,19 @@ export function App({
|
|
|
225
231
|
useEffect(() => {
|
|
226
232
|
if (!toast) return;
|
|
227
233
|
const timer = setTimeout(() => controller.dismissToast(), TOAST_DISMISS_MS);
|
|
234
|
+
|
|
228
235
|
return () => clearTimeout(timer);
|
|
229
236
|
}, [toast, controller]);
|
|
230
237
|
|
|
231
238
|
// First open of a share: ask the collaborator for a display name once, unless
|
|
232
239
|
// a past visit already recorded one. esc skips and their notes read anonymous.
|
|
233
240
|
const promptedSelfRef = useRef(false);
|
|
241
|
+
|
|
234
242
|
useEffect(() => {
|
|
235
243
|
if (promptedSelfRef.current || role !== "collaborator" || !selfAuthor || !session) return;
|
|
236
244
|
promptedSelfRef.current = true;
|
|
237
245
|
const known = session.participants?.find((participant) => participant.id === selfAuthor)?.name;
|
|
246
|
+
|
|
238
247
|
if (!known) setMode({ type: "nameSelf", text: "" });
|
|
239
248
|
}, [role, selfAuthor, session]);
|
|
240
249
|
|
|
@@ -278,9 +287,11 @@ export function App({
|
|
|
278
287
|
/** Annotation ids whose anchor resolved against the working copy. */
|
|
279
288
|
const resolvedIds = useMemo(() => {
|
|
280
289
|
const ids = new Set<string>();
|
|
290
|
+
|
|
281
291
|
for (const blockMarks of marks.values()) {
|
|
282
292
|
for (const mark of blockMarks) if (mark.annotationId) ids.add(mark.annotationId);
|
|
283
293
|
}
|
|
294
|
+
|
|
284
295
|
return ids;
|
|
285
296
|
}, [marks]);
|
|
286
297
|
const { isDiff, isPrototype, resolved } = deriveReviewFlags(session);
|
|
@@ -290,10 +301,12 @@ export function App({
|
|
|
290
301
|
// annotation resolves to the display index it marked
|
|
291
302
|
const annotationPositions = useMemo(() => {
|
|
292
303
|
const positions = new Map<string, number>();
|
|
304
|
+
|
|
293
305
|
if (!session) return positions;
|
|
294
306
|
if (isDiff) {
|
|
295
307
|
for (const annotation of session.annotations) {
|
|
296
308
|
const blockIndex = (annotation.anchor as { blockIndex?: number }).blockIndex;
|
|
309
|
+
|
|
297
310
|
if (blockIndex !== undefined) positions.set(annotation.id, blockIndex);
|
|
298
311
|
}
|
|
299
312
|
} else {
|
|
@@ -302,6 +315,7 @@ export function App({
|
|
|
302
315
|
if (mark.annotationId) positions.set(mark.annotationId, displayIndex);
|
|
303
316
|
}
|
|
304
317
|
}
|
|
318
|
+
|
|
305
319
|
return positions;
|
|
306
320
|
}, [session, isDiff, marks]);
|
|
307
321
|
|
|
@@ -317,6 +331,7 @@ export function App({
|
|
|
317
331
|
// one marker at a time: clear any prior selection, then paint the current
|
|
318
332
|
// span (span and its quick-actions sub-mode both keep it painted)
|
|
319
333
|
const span = activeSpanState(mode);
|
|
334
|
+
|
|
320
335
|
planSheetRef.current?.clearSelection();
|
|
321
336
|
if (span) planSheetRef.current?.driveSpanSelection(span);
|
|
322
337
|
}, [mode]);
|
|
@@ -334,6 +349,7 @@ export function App({
|
|
|
334
349
|
if (!blockMarks.some((mark) => mark.annotationId === annotationId)) continue;
|
|
335
350
|
setCursor(displayIndex);
|
|
336
351
|
planSheetRef.current?.revealBlock(displayIndex);
|
|
352
|
+
|
|
337
353
|
return;
|
|
338
354
|
}
|
|
339
355
|
};
|
|
@@ -355,6 +371,7 @@ export function App({
|
|
|
355
371
|
const selectCurationFromRail = (curationId: string): void => {
|
|
356
372
|
setSelectedCurationId(curationId);
|
|
357
373
|
const item = curationItems.find((candidate) => candidate.id === curationId);
|
|
374
|
+
|
|
358
375
|
if (!item) return;
|
|
359
376
|
setCursor(item.revealIndex);
|
|
360
377
|
if (item.source === "plan") planSheetRef.current?.revealBlock(item.revealIndex);
|
|
@@ -371,11 +388,13 @@ export function App({
|
|
|
371
388
|
const openCardEdit = (annotationId: string): void => {
|
|
372
389
|
if (observer) return controller.setStatus("observer - read-only");
|
|
373
390
|
const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
|
|
391
|
+
|
|
374
392
|
if (!annotation) return;
|
|
375
393
|
// a collaborator's note is theirs to word: activating it (click or e) renames
|
|
376
394
|
// the author rather than editing the body the planner does not own
|
|
377
395
|
if (annotation.author) {
|
|
378
396
|
setFocusedAnnotationId(annotationId);
|
|
397
|
+
|
|
379
398
|
return void setMode({
|
|
380
399
|
type: "rename",
|
|
381
400
|
authorId: annotation.author,
|
|
@@ -447,6 +466,7 @@ export function App({
|
|
|
447
466
|
// with ctrl+] as the detach chord back to the review.
|
|
448
467
|
if (agentTerminal) {
|
|
449
468
|
if (key.ctrl && key.name === "]") return void agentTerminal.detach();
|
|
469
|
+
|
|
450
470
|
return void agentTerminal.write(key.sequence);
|
|
451
471
|
}
|
|
452
472
|
if (menuDialog === "settings") return void handleSettingsKey(key.name);
|
|
@@ -473,8 +493,10 @@ export function App({
|
|
|
473
493
|
cursor,
|
|
474
494
|
display,
|
|
475
495
|
});
|
|
496
|
+
|
|
476
497
|
keyBindings.setContext({ overlay: state.overlay, spanMode: state.spanMode });
|
|
477
498
|
const action = keyBindings.resolveAction({ name: key.name, shift: !!key.shift });
|
|
499
|
+
|
|
478
500
|
for (const intent of reduceKey(
|
|
479
501
|
state,
|
|
480
502
|
{ name: key.name, shift: !!key.shift, meta: !!key.meta },
|
|
@@ -590,7 +612,9 @@ export function App({
|
|
|
590
612
|
selection && block
|
|
591
613
|
? spanFromRange(selection.displayIndex, displayText(block), selection.start, selection.end)
|
|
592
614
|
: null;
|
|
615
|
+
|
|
593
616
|
if (span) setMode({ type: "span", span });
|
|
617
|
+
|
|
594
618
|
return true;
|
|
595
619
|
};
|
|
596
620
|
|
|
@@ -598,6 +622,7 @@ export function App({
|
|
|
598
622
|
if (activateSpanFromSelection(displayIndex)) return;
|
|
599
623
|
setCursor(displayIndex);
|
|
600
624
|
const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
|
|
625
|
+
|
|
601
626
|
if (annotationId) selectCardFromDocument(annotationId);
|
|
602
627
|
};
|
|
603
628
|
|
|
@@ -634,6 +659,7 @@ export function App({
|
|
|
634
659
|
// cycles all three including hidden
|
|
635
660
|
const onToggleReviewPanel = (): void => {
|
|
636
661
|
const next = toggleReviewPanelMode(reviewMode);
|
|
662
|
+
|
|
637
663
|
setReviewMode(next);
|
|
638
664
|
controller.saveReviewPanel({ mode: next });
|
|
639
665
|
};
|
|
@@ -654,6 +680,7 @@ export function App({
|
|
|
654
680
|
onMouseDrag={(event: MouseEvent) => {
|
|
655
681
|
if (!dividerDragging || reviewMode !== "expanded") return;
|
|
656
682
|
const next = widthFromMouseColumn(event.x, terminalWidth);
|
|
683
|
+
|
|
657
684
|
// many raw mouse-moves land in the same column: only re-render when the
|
|
658
685
|
// width actually changes, so the drag does not reconcile per pixel
|
|
659
686
|
if (next === reviewWidthRef.current) return;
|
package/src/App.walk.test.tsx
CHANGED
|
@@ -69,9 +69,11 @@ async function renderApp() {
|
|
|
69
69
|
width: 120,
|
|
70
70
|
height: 34,
|
|
71
71
|
});
|
|
72
|
+
|
|
72
73
|
// Wait for session content, not just the chrome: pressing w before the diff
|
|
73
74
|
// loads drops the walk intent (no session, no diff view) and hangs the test.
|
|
74
75
|
await waitForText(setup, "src/a.ts");
|
|
76
|
+
|
|
75
77
|
return setup;
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -80,15 +82,18 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
|
80
82
|
/** Foreground colors (hex) of every styled span containing the needle. */
|
|
81
83
|
function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
82
84
|
const foregrounds: string[] = [];
|
|
85
|
+
|
|
83
86
|
for (const line of setup.captureSpans().lines) {
|
|
84
87
|
for (const span of line.spans) {
|
|
85
88
|
if (!span.text.includes(needle)) continue;
|
|
86
89
|
const [red, green, blue] = span.fg.toInts();
|
|
90
|
+
|
|
87
91
|
foregrounds.push(
|
|
88
92
|
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
89
93
|
);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
96
|
+
|
|
92
97
|
return foregrounds;
|
|
93
98
|
}
|
|
94
99
|
|
|
@@ -165,6 +170,7 @@ describe("the guided walk", () => {
|
|
|
165
170
|
test("esc keeps progress and w resumes at the first unviewed file", async () => {
|
|
166
171
|
// Arrange
|
|
167
172
|
const setup = await renderApp();
|
|
173
|
+
|
|
168
174
|
await press(setup, "w");
|
|
169
175
|
await waitForText(setup, "file 1 of 3 · 0 viewed");
|
|
170
176
|
await press(setup, "]");
|
|
@@ -188,6 +194,7 @@ describe("the guided walk", () => {
|
|
|
188
194
|
test("a half-walked review resumes across App instances via the daemon", async () => {
|
|
189
195
|
// Arrange
|
|
190
196
|
const first = await renderApp();
|
|
197
|
+
|
|
191
198
|
await press(first, "w");
|
|
192
199
|
await waitForText(first, "file 1 of 3 · 0 viewed");
|
|
193
200
|
await press(first, "]");
|
|
@@ -202,6 +209,7 @@ describe("the guided walk", () => {
|
|
|
202
209
|
// Act
|
|
203
210
|
// a fresh client reads the viewed set back from the session record
|
|
204
211
|
const second = await renderApp();
|
|
212
|
+
|
|
205
213
|
await press(second, "w");
|
|
206
214
|
|
|
207
215
|
// Assert
|
|
@@ -249,6 +257,7 @@ describe("the guided walk", () => {
|
|
|
249
257
|
// Assert
|
|
250
258
|
await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
|
|
251
259
|
const resolved = server.core.sessionGet(session.id);
|
|
260
|
+
|
|
252
261
|
expect(resolved.verdict!.kind).toBe("approve");
|
|
253
262
|
expect(resolved.verdict!.feedback).not.toContain("Swaps the stale line");
|
|
254
263
|
});
|
|
@@ -277,6 +286,7 @@ describe("the guided walk", () => {
|
|
|
277
286
|
// Arrange
|
|
278
287
|
server.core.sessionResolve(session.id, "approve", "");
|
|
279
288
|
const setup = await renderApp();
|
|
289
|
+
|
|
280
290
|
await waitForText(setup, "resolved");
|
|
281
291
|
|
|
282
292
|
// Act
|
package/src/app-key-state.ts
CHANGED
package/src/app-screens.tsx
CHANGED
|
@@ -66,6 +66,7 @@ export function MenuChrome(props: {
|
|
|
66
66
|
onCategorySelect,
|
|
67
67
|
cycleSetting,
|
|
68
68
|
} = props;
|
|
69
|
+
|
|
69
70
|
return (
|
|
70
71
|
<>
|
|
71
72
|
<MenuBar
|
|
@@ -114,6 +115,7 @@ export function InboxScreen(props: {
|
|
|
114
115
|
}): React.ReactNode {
|
|
115
116
|
const { inbox, inboxCursor, mode, theme, controller, setMode, menuChrome } = props;
|
|
116
117
|
const confirming = mode.type === "confirmDelete" ? mode : null;
|
|
118
|
+
|
|
117
119
|
return (
|
|
118
120
|
<ThemeProvider theme={theme}>
|
|
119
121
|
<box
|
|
@@ -176,6 +178,7 @@ export function CompletionScreen(props: {
|
|
|
176
178
|
status: string;
|
|
177
179
|
}): React.ReactNode {
|
|
178
180
|
const { theme, session, verdict, completion, status } = props;
|
|
181
|
+
|
|
179
182
|
return (
|
|
180
183
|
<ThemeProvider theme={theme}>
|
|
181
184
|
<CompletionOverlay
|
|
@@ -218,6 +221,7 @@ export function TrailingOverlays(props: {
|
|
|
218
221
|
setMode,
|
|
219
222
|
dispatch,
|
|
220
223
|
} = props;
|
|
224
|
+
|
|
221
225
|
return (
|
|
222
226
|
<>
|
|
223
227
|
{walking && walk !== null ? (
|
package/src/app-view-model.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function computeRoleCapabilities(
|
|
|
20
20
|
role: "owner" | "observer" | "collaborator",
|
|
21
21
|
): { observer: boolean; isOwner: boolean } {
|
|
22
22
|
const observer = readOnly || role === "observer";
|
|
23
|
+
|
|
23
24
|
return { observer, isOwner: !observer && role === "owner" };
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -52,6 +53,7 @@ export function resolveOverlay(
|
|
|
52
53
|
if (completionPhase === "prompt") return "completion-prompt";
|
|
53
54
|
if (completionPhase === "counting") return "completion-counting";
|
|
54
55
|
if (walking) return "walk";
|
|
56
|
+
|
|
55
57
|
return "none";
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -71,6 +73,7 @@ export function computeRailFootprint(
|
|
|
71
73
|
terminalWidth: number,
|
|
72
74
|
): number {
|
|
73
75
|
if (reviewMode === "hidden") return 0;
|
|
76
|
+
|
|
74
77
|
return (
|
|
75
78
|
1 +
|
|
76
79
|
(reviewMode === "compact"
|
|
@@ -86,6 +89,7 @@ export function buildHeaderItems(params: {
|
|
|
86
89
|
role: "owner" | "observer" | "collaborator";
|
|
87
90
|
}): BreadcrumbItem[] {
|
|
88
91
|
const { session, resolved, observer, role } = params;
|
|
92
|
+
|
|
89
93
|
return [
|
|
90
94
|
{ label: "cueloop", tone: "accent" },
|
|
91
95
|
...(resolved
|
|
@@ -126,6 +130,7 @@ export function buildRenderFlags(params: {
|
|
|
126
130
|
} {
|
|
127
131
|
const { session, isOwner, isDiff, isPrototype, resolved, menuOpen, menuDialog, resolvedIds } =
|
|
128
132
|
params;
|
|
133
|
+
|
|
129
134
|
return {
|
|
130
135
|
showOwnerActions: isOwner && !isDiff && !resolved,
|
|
131
136
|
prototypeCanComment: isOwner && !resolved,
|
|
@@ -140,10 +145,12 @@ export function buildActiveSpan(
|
|
|
140
145
|
isDiff: boolean,
|
|
141
146
|
): { displayIndex: number; start: number; end: number } | null {
|
|
142
147
|
const markedSpan = activeSpanState(mode);
|
|
148
|
+
|
|
143
149
|
if (markedSpan)
|
|
144
150
|
return { displayIndex: markedSpan.displayIndex, start: markedSpan.start, end: markedSpan.end };
|
|
145
151
|
if (mode.type === "compose" && !isDiff)
|
|
146
152
|
return { displayIndex: mode.displayIndex, start: mode.start, end: mode.end };
|
|
153
|
+
|
|
147
154
|
return null;
|
|
148
155
|
}
|
|
149
156
|
|
|
@@ -157,7 +164,9 @@ export function buildComposeState(
|
|
|
157
164
|
deps: DraftHandlerDeps & { mode: Mode; isDiff: boolean; display: DisplayBlock[] },
|
|
158
165
|
): PlanComposeState | null {
|
|
159
166
|
const { mode, isDiff, display, liveInput, setMode, dispatch } = deps;
|
|
167
|
+
|
|
160
168
|
if (mode.type !== "compose" || isDiff) return null;
|
|
169
|
+
|
|
161
170
|
return {
|
|
162
171
|
kind: mode.kind,
|
|
163
172
|
displayIndex: mode.displayIndex,
|
|
@@ -178,7 +187,9 @@ export function buildDiffComposeState(
|
|
|
178
187
|
deps: DraftHandlerDeps & { mode: Mode; isDiff: boolean; rows: DiffRow[] },
|
|
179
188
|
): DiffComposeState | null {
|
|
180
189
|
const { mode, isDiff, rows, liveInput, setMode, dispatch } = deps;
|
|
190
|
+
|
|
181
191
|
if (mode.type !== "compose" || !isDiff) return null;
|
|
192
|
+
|
|
182
193
|
return {
|
|
183
194
|
kind: mode.kind,
|
|
184
195
|
rowIndex: mode.displayIndex,
|
|
@@ -207,9 +218,11 @@ export function buildPopoverState(deps: {
|
|
|
207
218
|
}): PlanPopoverState | null {
|
|
208
219
|
const { mode, isDiff, quickActions, isOwner, observer, resolved, controller, dispatch } = deps;
|
|
209
220
|
const markedSpan = activeSpanState(mode);
|
|
221
|
+
|
|
210
222
|
if (!markedSpan || isDiff) return null;
|
|
211
223
|
const spanMutationBlock = (): string | null =>
|
|
212
224
|
observer ? "observer - read-only" : resolved ? "review submitted - read-only" : null;
|
|
225
|
+
|
|
213
226
|
return {
|
|
214
227
|
displayIndex: markedSpan.displayIndex,
|
|
215
228
|
view: mode.type === "spanActions" ? ("actions" as const) : ("toolbar" as const),
|
|
@@ -218,23 +231,27 @@ export function buildPopoverState(deps: {
|
|
|
218
231
|
canCut: isOwner,
|
|
219
232
|
onComment: () => {
|
|
220
233
|
const blocked = spanMutationBlock();
|
|
234
|
+
|
|
221
235
|
if (blocked) return controller.setStatus(blocked);
|
|
222
236
|
dispatch({ type: "openCompose", kind: "comment", from: "span" });
|
|
223
237
|
},
|
|
224
238
|
onCut: () => {
|
|
225
239
|
const blocked = spanMutationBlock();
|
|
240
|
+
|
|
226
241
|
if (blocked) return controller.setStatus(blocked);
|
|
227
242
|
if (!isOwner) return;
|
|
228
243
|
dispatch({ type: "spanCut" });
|
|
229
244
|
},
|
|
230
245
|
onOpenActions: () => {
|
|
231
246
|
const blocked = spanMutationBlock();
|
|
247
|
+
|
|
232
248
|
if (blocked) return controller.setStatus(blocked);
|
|
233
249
|
dispatch({ type: "openSpanActions" });
|
|
234
250
|
},
|
|
235
251
|
onClose: () => dispatch({ type: "closeOverlay" }),
|
|
236
252
|
onPickAction: (index: number) => {
|
|
237
253
|
const blocked = spanMutationBlock();
|
|
254
|
+
|
|
238
255
|
if (blocked) return controller.setStatus(blocked);
|
|
239
256
|
dispatch({ type: "pickSpanAction", index });
|
|
240
257
|
},
|
|
@@ -252,7 +269,9 @@ export function buildSubmitConfirmState(
|
|
|
252
269
|
},
|
|
253
270
|
): Omit<ConfirmCardProps, "theme"> | null {
|
|
254
271
|
const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
|
|
272
|
+
|
|
255
273
|
if (mode.type !== "submit") return null;
|
|
274
|
+
|
|
256
275
|
return {
|
|
257
276
|
verdict: mode.verdict,
|
|
258
277
|
summary: mode.summary,
|
|
@@ -272,7 +291,9 @@ export function buildSubmitConfirmState(
|
|
|
272
291
|
|
|
273
292
|
export function buildCardEditState(deps: DraftHandlerDeps & { mode: Mode }): RailCardEdit | null {
|
|
274
293
|
const { mode, liveInput, setMode, dispatch } = deps;
|
|
294
|
+
|
|
275
295
|
if (mode.type !== "railEdit") return null;
|
|
296
|
+
|
|
276
297
|
return {
|
|
277
298
|
id: mode.id,
|
|
278
299
|
text: mode.text,
|
package/src/attribution.ts
CHANGED
|
@@ -16,6 +16,7 @@ export function shortHandle(authorId: string): string {
|
|
|
16
16
|
const base = authorId.startsWith(FINGERPRINT_PREFIX)
|
|
17
17
|
? authorId.slice(FINGERPRINT_PREFIX.length)
|
|
18
18
|
: authorId;
|
|
19
|
+
|
|
19
20
|
return base.slice(0, 8) || authorId;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -30,9 +31,12 @@ export function resolveDisplayName(
|
|
|
30
31
|
overrides: Record<string, string>,
|
|
31
32
|
): string {
|
|
32
33
|
const override = overrides[authorId];
|
|
34
|
+
|
|
33
35
|
if (override) return override;
|
|
34
36
|
const identity = participants?.find((participant) => participant.id === authorId);
|
|
37
|
+
|
|
35
38
|
if (identity?.name) return identity.name;
|
|
36
39
|
if (identity) return ANONYMOUS_LABEL;
|
|
40
|
+
|
|
37
41
|
return shortHandle(authorId);
|
|
38
42
|
}
|
package/src/clipboard.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export function clipboardCommands(platform: NodeJS.Platform = process.platform): string[][] {
|
|
10
10
|
if (platform === "darwin") return [["pbcopy"]];
|
|
11
11
|
if (platform === "win32") return [["clip.exe"]];
|
|
12
|
+
|
|
12
13
|
return [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]];
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -21,10 +22,12 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
|
|
21
22
|
stdout: "ignore",
|
|
22
23
|
stderr: "ignore",
|
|
23
24
|
});
|
|
25
|
+
|
|
24
26
|
if ((await proc.exited) === 0) return true;
|
|
25
27
|
} catch {
|
|
26
28
|
// tool not installed - try the next one
|
|
27
29
|
}
|
|
28
30
|
}
|
|
31
|
+
|
|
29
32
|
return false;
|
|
30
33
|
}
|