@cueloop/client 0.1.0-alpha.101 → 0.1.0-alpha.103
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/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +14 -1
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +154 -19
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +72 -6
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +1 -0
- package/src/app/app-view-model.ts +7 -0
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +3 -2
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +28 -3
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +4 -0
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +16 -3
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +41 -0
- package/src/settings/config.ts +44 -0
- package/src/settings/use-settings-dialog.test.tsx +98 -0
- package/src/settings/use-settings-dialog.tsx +38 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +18 -10
- package/src/thread/components/AgentConfigControls.tsx +1 -0
- package/src/thread/components/AgentThreadPane.test.tsx +257 -1
- package/src/thread/components/AgentThreadPane.tsx +48 -16
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.ts +123 -0
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- package/src/workbench/use-repo-changes.ts +2 -0
|
@@ -36,6 +36,7 @@ export function projectAgentTranscript(thread: Thread, state: ThreadAgentState)
|
|
|
36
36
|
|
|
37
37
|
display.push(...blocks);
|
|
38
38
|
sources.push(...blocks.map((_, blockIndex) => ({ messageId: message.id, blockIndex })));
|
|
39
|
+
|
|
39
40
|
if (message.role === "user") {
|
|
40
41
|
const tools = state.tools.filter((tool) => tool.turnId === message.id);
|
|
41
42
|
|
|
@@ -176,6 +177,7 @@ export function projectThreadConversation(
|
|
|
176
177
|
kind: "p",
|
|
177
178
|
work: { kind: "p", text: "", lineStart: 0, lineEnd: 0 },
|
|
178
179
|
});
|
|
180
|
+
|
|
179
181
|
const offset = display.length;
|
|
180
182
|
const annotation = {
|
|
181
183
|
id: `mirror:${id}`,
|
|
@@ -195,6 +197,8 @@ export function projectThreadConversation(
|
|
|
195
197
|
display.push(...blocks);
|
|
196
198
|
sources.push(...blocks.map(() => ({ kind: "mirror" as const, commentId })));
|
|
197
199
|
annotations.push(annotation);
|
|
200
|
+
const mirrorAnnotations = [annotation];
|
|
201
|
+
|
|
198
202
|
if (commentId) {
|
|
199
203
|
const replies = [...thread.annotations, ...state.comments].filter(
|
|
200
204
|
(entry) => entry.replyTo === commentId,
|
|
@@ -210,23 +214,23 @@ export function projectThreadConversation(
|
|
|
210
214
|
};
|
|
211
215
|
|
|
212
216
|
annotations.push(replica);
|
|
217
|
+
mirrorAnnotations.push(replica);
|
|
213
218
|
mirrors.set(replica.id, { commentId: reply.id });
|
|
214
219
|
}
|
|
215
220
|
}
|
|
221
|
+
|
|
216
222
|
const localMarks = quote.trim()
|
|
217
|
-
? marksByDisplay(
|
|
223
|
+
? marksByDisplay(mirrorAnnotations, blocks)
|
|
218
224
|
: new Map<number, Mark[]>([
|
|
219
225
|
[
|
|
220
226
|
0,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
},
|
|
229
|
-
],
|
|
227
|
+
mirrorAnnotations.map((entry) => ({
|
|
228
|
+
start: 0,
|
|
229
|
+
end: 0,
|
|
230
|
+
role: "mark-comment",
|
|
231
|
+
annotationId: entry.id,
|
|
232
|
+
span: { start: { blockIndex: 0, char: 0 }, end: { blockIndex: 0, char: 0 } },
|
|
233
|
+
})),
|
|
230
234
|
],
|
|
231
235
|
]);
|
|
232
236
|
|
|
@@ -245,6 +249,7 @@ export function projectThreadConversation(
|
|
|
245
249
|
);
|
|
246
250
|
}
|
|
247
251
|
mirrors.set(annotation.id, { commentId, submissionId });
|
|
252
|
+
|
|
248
253
|
if (submissionId) destinations.set(submissionId, offset);
|
|
249
254
|
};
|
|
250
255
|
const message = (entry: ThreadAgentState["messages"][number]): void => {
|
|
@@ -298,12 +303,14 @@ export function projectThreadConversation(
|
|
|
298
303
|
revision: 1,
|
|
299
304
|
});
|
|
300
305
|
}
|
|
306
|
+
|
|
301
307
|
for (const entry of state.messages.filter(
|
|
302
308
|
(entry) => entry.role === "agent" && entry.submissionId === submission.id,
|
|
303
309
|
))
|
|
304
310
|
message(entry);
|
|
305
311
|
}
|
|
306
312
|
} else for (const entry of state.messages) message(entry);
|
|
313
|
+
|
|
307
314
|
const activityIndex = showActivity ? display.length : -1;
|
|
308
315
|
|
|
309
316
|
if (showActivity) {
|
|
@@ -314,6 +321,7 @@ export function projectThreadConversation(
|
|
|
314
321
|
});
|
|
315
322
|
sources.push({ kind: "activity" });
|
|
316
323
|
}
|
|
324
|
+
|
|
317
325
|
const tailIndex = display.length;
|
|
318
326
|
|
|
319
327
|
display.push({
|
|
@@ -43,6 +43,33 @@ const empty: ThreadAgentState = {
|
|
|
43
43
|
comments: [],
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
test("a failed harness turn displays its error below the conversation", async () => {
|
|
47
|
+
const failure = "OAuth auth derivation failed: missing provider module";
|
|
48
|
+
const { client } = createTestAgentClient({ ...empty, phase: { kind: "failed", error: failure } });
|
|
49
|
+
const setup = await testRender(
|
|
50
|
+
<AgentThreadPane
|
|
51
|
+
thread={thread}
|
|
52
|
+
client={client}
|
|
53
|
+
focused
|
|
54
|
+
theme={DARK}
|
|
55
|
+
onActiveChange={noop}
|
|
56
|
+
onOpenFile={noop}
|
|
57
|
+
>
|
|
58
|
+
{artifactView(thread)}
|
|
59
|
+
</AgentThreadPane>,
|
|
60
|
+
{ width: 100, height: 24 },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await waitForText(setup, "Original artifact");
|
|
65
|
+
await settle(setup);
|
|
66
|
+
expect(setup.captureCharFrame()).toContain(failure);
|
|
67
|
+
expect(setup.captureCharFrame()).not.toContain("Thinking");
|
|
68
|
+
} finally {
|
|
69
|
+
setup.renderer.destroy();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
46
73
|
function createTestAgentClient(initial: ThreadAgentState) {
|
|
47
74
|
let state = initial;
|
|
48
75
|
const prompts: AgentPromptRequest[] = [];
|
|
@@ -56,6 +83,7 @@ function createTestAgentClient(initial: ThreadAgentState) {
|
|
|
56
83
|
agentGet: async () => state,
|
|
57
84
|
agentPrompt: async (params) => {
|
|
58
85
|
prompts.push(params);
|
|
86
|
+
|
|
59
87
|
if (params.text)
|
|
60
88
|
state = {
|
|
61
89
|
...state,
|
|
@@ -287,6 +315,7 @@ test("three bottom prompts survive delayed acceptance and remain separate submis
|
|
|
287
315
|
|
|
288
316
|
client.agentPrompt = async (params) => {
|
|
289
317
|
prompts.push(params.text);
|
|
318
|
+
|
|
290
319
|
if (prompts.length === 1)
|
|
291
320
|
return new Promise((resolve) => {
|
|
292
321
|
release = resolve;
|
|
@@ -374,6 +403,227 @@ test("typing on an accepted plain prompt cannot edit it or create an unmarked co
|
|
|
374
403
|
}
|
|
375
404
|
});
|
|
376
405
|
|
|
406
|
+
for (const editingReply of [false, true]) {
|
|
407
|
+
test(`Option+Enter preserves the exact ${editingReply ? "edited" : "new"} reply for Ctrl+Enter`, async () => {
|
|
408
|
+
const submitted: string[] = [];
|
|
409
|
+
const root = {
|
|
410
|
+
id: "root",
|
|
411
|
+
kind: "comment" as const,
|
|
412
|
+
body: "Submitted question",
|
|
413
|
+
createdAt: "2026-10-04",
|
|
414
|
+
anchor: { quote: "Original", prefix: "", suffix: " artifact" },
|
|
415
|
+
};
|
|
416
|
+
const display = buildDisplay(thread.artifact.content);
|
|
417
|
+
|
|
418
|
+
function ControlledDiscussion(): React.ReactNode {
|
|
419
|
+
const [value, setValue] = React.useState<Thread>({
|
|
420
|
+
...thread,
|
|
421
|
+
annotations: editingReply
|
|
422
|
+
? [root, { ...root, id: "reply", replyTo: "root", body: "Editable followup" }]
|
|
423
|
+
: [root],
|
|
424
|
+
});
|
|
425
|
+
const [focused, setFocused] = React.useState<string>();
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<ThreadView
|
|
429
|
+
session={value}
|
|
430
|
+
display={display}
|
|
431
|
+
marks={marksByDisplay(value.annotations, display)}
|
|
432
|
+
quickActions={[]}
|
|
433
|
+
observer={false}
|
|
434
|
+
focusedAnnotationId={focused}
|
|
435
|
+
onFocusAnnotation={setFocused}
|
|
436
|
+
isAnnotationReadOnly={(id) => id === "root"}
|
|
437
|
+
onAnnotate={noop}
|
|
438
|
+
onReply={(id, body) => {
|
|
439
|
+
setValue((current) => ({
|
|
440
|
+
...current,
|
|
441
|
+
annotations: [...current.annotations, { ...root, id: "reply", replyTo: id, body }],
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
return "reply";
|
|
445
|
+
}}
|
|
446
|
+
onUpdateAnnotation={(id, body) =>
|
|
447
|
+
setValue((current) => ({
|
|
448
|
+
...current,
|
|
449
|
+
annotations: current.annotations.map((note) =>
|
|
450
|
+
note.id === id ? { ...note, body } : note,
|
|
451
|
+
),
|
|
452
|
+
}))
|
|
453
|
+
}
|
|
454
|
+
onInvoke={(id) => {
|
|
455
|
+
if (id) submitted.push(id);
|
|
456
|
+
}}
|
|
457
|
+
onExit={noop}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const setup = await testRender(<ControlledDiscussion />, {
|
|
462
|
+
width: 100,
|
|
463
|
+
height: 24,
|
|
464
|
+
kittyKeyboard: true,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
await waitForText(setup, editingReply ? "Editable followup" : "Submitted question");
|
|
469
|
+
const comment = locateText(setup, editingReply ? "Editable followup" : "Submitted question");
|
|
470
|
+
|
|
471
|
+
await setup.mockMouse.click(comment.column, comment.row);
|
|
472
|
+
await typeText(setup, " Why?");
|
|
473
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
474
|
+
expect(submitted).toEqual([]);
|
|
475
|
+
const saved = locateText(setup, editingReply ? "Editable followup Why?" : "Why?");
|
|
476
|
+
|
|
477
|
+
await setup.mockMouse.click(saved.column, saved.row);
|
|
478
|
+
await pressKey(setup, "RETURN", { ctrl: true });
|
|
479
|
+
expect(submitted).toEqual(["reply"]);
|
|
480
|
+
} finally {
|
|
481
|
+
setup.renderer.destroy();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
test("Option-saved Changes mirror replies send their stored ID with Ctrl+Enter", async () => {
|
|
487
|
+
const root = {
|
|
488
|
+
id: "changes-note",
|
|
489
|
+
kind: "comment" as const,
|
|
490
|
+
body: "Question on Changes",
|
|
491
|
+
createdAt: thread.createdAt,
|
|
492
|
+
anchor: { quote: "Changed code", prefix: "", suffix: "" },
|
|
493
|
+
};
|
|
494
|
+
const { client, prompts } = createTestAgentClient({
|
|
495
|
+
...empty,
|
|
496
|
+
submissions: [
|
|
497
|
+
{
|
|
498
|
+
id: "accepted",
|
|
499
|
+
commentId: root.id,
|
|
500
|
+
prompt: root.body,
|
|
501
|
+
quote: root.anchor.quote,
|
|
502
|
+
status: "completed",
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
function ChangesDiscussion(): React.ReactNode {
|
|
508
|
+
const [value, setValue] = React.useState<Thread>({ ...thread, annotations: [root] });
|
|
509
|
+
const [focused, setFocused] = React.useState<string>();
|
|
510
|
+
const display = buildDisplay(value.artifact.content);
|
|
511
|
+
const saveReply = (id: string, body: string): string => {
|
|
512
|
+
setValue((current) => ({
|
|
513
|
+
...current,
|
|
514
|
+
annotations: [...current.annotations, { ...root, id: "followup", replyTo: id, body }],
|
|
515
|
+
}));
|
|
516
|
+
|
|
517
|
+
return "followup";
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<AgentThreadPane
|
|
522
|
+
thread={value}
|
|
523
|
+
client={client}
|
|
524
|
+
focused
|
|
525
|
+
theme={DARK}
|
|
526
|
+
onActiveChange={noop}
|
|
527
|
+
onOpenFile={noop}
|
|
528
|
+
>
|
|
529
|
+
<ThreadView
|
|
530
|
+
session={value}
|
|
531
|
+
display={display}
|
|
532
|
+
marks={marksByDisplay(value.annotations, display)}
|
|
533
|
+
quickActions={[]}
|
|
534
|
+
observer={false}
|
|
535
|
+
focusedAnnotationId={focused}
|
|
536
|
+
onFocusAnnotation={setFocused}
|
|
537
|
+
onAnnotate={noop}
|
|
538
|
+
onReply={saveReply}
|
|
539
|
+
onUpdateAnnotation={noop}
|
|
540
|
+
onExit={noop}
|
|
541
|
+
/>
|
|
542
|
+
</AgentThreadPane>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
const setup = await testRender(<ChangesDiscussion />, {
|
|
546
|
+
width: 100,
|
|
547
|
+
height: 24,
|
|
548
|
+
kittyKeyboard: true,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
await waitForText(setup, root.body);
|
|
553
|
+
const mirror = locateText(setup, root.body);
|
|
554
|
+
|
|
555
|
+
await setup.mockMouse.click(mirror.column, mirror.row);
|
|
556
|
+
await typeText(setup, "Follow up on this change");
|
|
557
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
558
|
+
await waitForText(setup, "Follow up on this change");
|
|
559
|
+
expect(prompts).toHaveLength(0);
|
|
560
|
+
await pressKey(setup, "RETURN", { ctrl: true });
|
|
561
|
+
expect(prompts.map((prompt) => prompt.commentId)).toEqual(["followup"]);
|
|
562
|
+
} finally {
|
|
563
|
+
setup.renderer.destroy();
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("Ctrl+Enter submits only the comment just saved, leaving older notes editable", async () => {
|
|
568
|
+
const value: Thread = {
|
|
569
|
+
...thread,
|
|
570
|
+
annotations: [
|
|
571
|
+
{
|
|
572
|
+
id: "older",
|
|
573
|
+
kind: "comment",
|
|
574
|
+
body: "Older unsent note",
|
|
575
|
+
createdAt: "2026-10-04",
|
|
576
|
+
anchor: { quote: "Original", prefix: "", suffix: " artifact" },
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
};
|
|
580
|
+
const { client, prompts } = createTestAgentClient(empty);
|
|
581
|
+
const display = buildDisplay(value.artifact.content);
|
|
582
|
+
const setup = await testRender(
|
|
583
|
+
<AgentThreadPane
|
|
584
|
+
thread={value}
|
|
585
|
+
theme={DARK}
|
|
586
|
+
focused
|
|
587
|
+
onActiveChange={noop}
|
|
588
|
+
onOpenFile={noop}
|
|
589
|
+
client={client}
|
|
590
|
+
>
|
|
591
|
+
<ThreadView
|
|
592
|
+
session={value}
|
|
593
|
+
display={display}
|
|
594
|
+
marks={marksByDisplay(value.annotations, display)}
|
|
595
|
+
quickActions={[]}
|
|
596
|
+
observer={false}
|
|
597
|
+
onAnnotate={() => "selected"}
|
|
598
|
+
onReply={noop}
|
|
599
|
+
onUpdateAnnotation={noop}
|
|
600
|
+
onExit={noop}
|
|
601
|
+
/>
|
|
602
|
+
</AgentThreadPane>,
|
|
603
|
+
{ width: 100, height: 24, kittyKeyboard: true },
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
await waitForText(setup, "Original artifact");
|
|
608
|
+
const artifact = locateText(setup, "Original artifact");
|
|
609
|
+
|
|
610
|
+
await setup.mockMouse.drag(
|
|
611
|
+
artifact.column + 9,
|
|
612
|
+
artifact.row,
|
|
613
|
+
artifact.column + 17,
|
|
614
|
+
artifact.row,
|
|
615
|
+
);
|
|
616
|
+
await typeText(setup, "Only this question");
|
|
617
|
+
await pressKey(setup, "RETURN", { ctrl: true });
|
|
618
|
+
await waitForState(setup, () => prompts.length === 1, "selected comment invocation");
|
|
619
|
+
expect(prompts[0]?.commentId).toBe("selected");
|
|
620
|
+
expect(prompts[0]?.id).toBe(value.id);
|
|
621
|
+
expect(value.annotations[0]?.body).toBe("Older unsent note");
|
|
622
|
+
} finally {
|
|
623
|
+
setup.renderer.destroy();
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
377
627
|
test("Option+Enter saves a comment, Command+Enter leaves its draft, and Ctrl+Enter invokes", async () => {
|
|
378
628
|
const saved: string[] = [];
|
|
379
629
|
let invocations = 0;
|
|
@@ -385,7 +635,11 @@ test("Option+Enter saves a comment, Command+Enter leaves its draft, and Ctrl+Ent
|
|
|
385
635
|
marks={marksByDisplay([], display)}
|
|
386
636
|
quickActions={[]}
|
|
387
637
|
observer={false}
|
|
388
|
-
onAnnotate={(_span, body) =>
|
|
638
|
+
onAnnotate={(_span, body) => {
|
|
639
|
+
saved.push(body);
|
|
640
|
+
|
|
641
|
+
return `saved-${saved.length}`;
|
|
642
|
+
}}
|
|
389
643
|
onReply={noop}
|
|
390
644
|
onUpdateAnnotation={noop}
|
|
391
645
|
onExit={noop}
|
|
@@ -643,6 +897,7 @@ test("an arriving reply preserves a bottom draft and submits it as the next prom
|
|
|
643
897
|
|
|
644
898
|
client.agentPrompt = async (params) => {
|
|
645
899
|
prompts.push(params);
|
|
900
|
+
|
|
646
901
|
if (prompts.length === 1)
|
|
647
902
|
return new Promise<ThreadAgentState>((resolve) => {
|
|
648
903
|
finish = resolve;
|
|
@@ -813,6 +1068,7 @@ test("a failed mutation flush restores the prompt alongside a newer visible draf
|
|
|
813
1068
|
onOpenFile={noop}
|
|
814
1069
|
flushMutations={() => {
|
|
815
1070
|
if (flushStarted) return Promise.resolve();
|
|
1071
|
+
|
|
816
1072
|
flushStarted = true;
|
|
817
1073
|
|
|
818
1074
|
return failedFlush;
|
|
@@ -29,7 +29,7 @@ export interface AgentThreadPaneProps {
|
|
|
29
29
|
action: (id: string) => { label: string; run: () => void } | undefined,
|
|
30
30
|
) => void;
|
|
31
31
|
onRevealReply?: () => void;
|
|
32
|
-
onInvokeChange?: (invoke: (() => void) | undefined) => void;
|
|
32
|
+
onInvokeChange?: (invoke: ((commentId?: string) => void) | undefined) => void;
|
|
33
33
|
onStateChange?: (state: ThreadAgentState) => void;
|
|
34
34
|
flushMutations?: () => Promise<void>;
|
|
35
35
|
client?: ThreadAgentClient;
|
|
@@ -68,7 +68,9 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
68
68
|
useState<ThreadViewProps["promptRestoreRequest"]>();
|
|
69
69
|
const restoreSequence = useRef(0);
|
|
70
70
|
const invoking = useRef(false);
|
|
71
|
-
const invocations = useRef<{ text: string; writes: Promise<boolean>[] }[]>(
|
|
71
|
+
const invocations = useRef<{ text: string; commentId?: string; writes: Promise<boolean>[] }[]>(
|
|
72
|
+
[],
|
|
73
|
+
);
|
|
72
74
|
const pendingWrites = useRef<Promise<boolean>[]>([]);
|
|
73
75
|
const state = agent.state;
|
|
74
76
|
const continuation = agentContinuation(state);
|
|
@@ -78,6 +80,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
78
80
|
|
|
79
81
|
useEffect(() => {
|
|
80
82
|
if (!busy) return;
|
|
83
|
+
|
|
81
84
|
const timer = setInterval(() => setPulse((value) => !value), 600);
|
|
82
85
|
|
|
83
86
|
return () => clearInterval(timer);
|
|
@@ -115,11 +118,22 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
115
118
|
notifyControls();
|
|
116
119
|
}, [state, theme]);
|
|
117
120
|
|
|
118
|
-
const invoke = async (): Promise<void> => {
|
|
121
|
+
const invoke = async (commentId?: string): Promise<void> => {
|
|
119
122
|
if (thread.status !== "pending") return;
|
|
120
|
-
|
|
123
|
+
|
|
124
|
+
if (!commentId && !draft.current.trim()) return;
|
|
125
|
+
|
|
126
|
+
invocations.current.push({
|
|
127
|
+
text: draft.current,
|
|
128
|
+
commentId: commentId
|
|
129
|
+
? (projection.mirrors.get(commentId)?.commentId ?? commentId)
|
|
130
|
+
: undefined,
|
|
131
|
+
writes: pendingWrites.current.splice(0),
|
|
132
|
+
});
|
|
121
133
|
draft.current = "";
|
|
134
|
+
|
|
122
135
|
if (invoking.current) return;
|
|
136
|
+
|
|
123
137
|
invoking.current = true;
|
|
124
138
|
|
|
125
139
|
const rejected: string[] = [];
|
|
@@ -135,11 +149,13 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
135
149
|
rejected.push(input.text);
|
|
136
150
|
continue;
|
|
137
151
|
}
|
|
152
|
+
|
|
138
153
|
await props.flushMutations?.();
|
|
139
154
|
const accepted = await agent.act((client) =>
|
|
140
155
|
client.agentPrompt({
|
|
141
156
|
id: thread.id,
|
|
142
157
|
text: input.text,
|
|
158
|
+
commentId: input.commentId,
|
|
143
159
|
inputOnly: Boolean(input.text.trim()),
|
|
144
160
|
operationId: newAnnotationId(),
|
|
145
161
|
}),
|
|
@@ -158,32 +174,36 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
158
174
|
if (text) setPromptRestoreRequest({ id: ++restoreSequence.current, text });
|
|
159
175
|
}
|
|
160
176
|
};
|
|
161
|
-
const invokeRef = useRef<() => Promise<void>>(async () => {});
|
|
177
|
+
const invokeRef = useRef<(commentId?: string) => Promise<void>>(async () => {});
|
|
162
178
|
|
|
163
179
|
useEffect(() => {
|
|
164
180
|
invokeRef.current = invoke;
|
|
165
181
|
});
|
|
166
|
-
const notifyInvoke = useEffectEvent((invoke: (() => void) | undefined) =>
|
|
182
|
+
const notifyInvoke = useEffectEvent((invoke: ((commentId?: string) => void) | undefined) =>
|
|
167
183
|
props.onInvokeChange?.(invoke),
|
|
168
184
|
);
|
|
169
185
|
|
|
170
186
|
useEffect(() => {
|
|
171
|
-
notifyInvoke(() => void invokeRef.current());
|
|
187
|
+
notifyInvoke((commentId) => void invokeRef.current(commentId));
|
|
172
188
|
|
|
173
189
|
return () => notifyInvoke(undefined);
|
|
174
190
|
}, []);
|
|
175
|
-
const reply = (id: string, body: string): void => {
|
|
191
|
+
const reply = (id: string, body: string): string | void => {
|
|
176
192
|
const origin = projection.mirrors.get(id)?.commentId ?? id;
|
|
177
193
|
const root = agentCommentRoot(state, origin);
|
|
194
|
+
const replyFocusId = (commentId: string | void): string | void =>
|
|
195
|
+
commentId && projection.mirrors.has(id) ? `${id}:${commentId}` : commentId;
|
|
196
|
+
|
|
197
|
+
if (root) {
|
|
198
|
+
const commentId = newAnnotationId();
|
|
178
199
|
|
|
179
|
-
if (root)
|
|
180
200
|
pendingWrites.current.push(
|
|
181
201
|
agent.act((client) =>
|
|
182
202
|
client.agentComment({
|
|
183
203
|
id: thread.id,
|
|
184
204
|
comment: {
|
|
185
205
|
...root,
|
|
186
|
-
id:
|
|
206
|
+
id: commentId,
|
|
187
207
|
body,
|
|
188
208
|
sent: false,
|
|
189
209
|
author: undefined,
|
|
@@ -192,7 +212,11 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
192
212
|
}),
|
|
193
213
|
),
|
|
194
214
|
);
|
|
195
|
-
|
|
215
|
+
|
|
216
|
+
return replyFocusId(commentId);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return replyFocusId(child?.onReply(origin, body));
|
|
196
220
|
};
|
|
197
221
|
const actionFor = (id: string) => {
|
|
198
222
|
const mirror = projection.mirrors.get(id);
|
|
@@ -201,6 +225,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
201
225
|
);
|
|
202
226
|
|
|
203
227
|
if (!submission) return undefined;
|
|
228
|
+
|
|
204
229
|
if (submission.status === "failed" && props.client?.canControlAgent !== false)
|
|
205
230
|
return {
|
|
206
231
|
label: "Retry",
|
|
@@ -245,7 +270,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
245
270
|
Boolean(state.comments.find((entry) => entry.id === id)?.sent)
|
|
246
271
|
}
|
|
247
272
|
annotationAction={actionFor}
|
|
248
|
-
onInvoke={() => void invoke()}
|
|
273
|
+
onInvoke={(commentId) => void invoke(commentId)}
|
|
249
274
|
isPromptBlock={(index) => index === projection.tailIndex}
|
|
250
275
|
promptRestoreRequest={promptRestoreRequest}
|
|
251
276
|
promptFocusRequest={
|
|
@@ -275,22 +300,26 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
275
300
|
|
|
276
301
|
return;
|
|
277
302
|
}
|
|
303
|
+
|
|
278
304
|
if (source?.kind === "mirror") {
|
|
279
|
-
|
|
305
|
+
const mirrorId = projection.marks.get(span.start.blockIndex)?.[0]?.annotationId;
|
|
306
|
+
|
|
307
|
+
if (mirrorId) return reply(mirrorId, body);
|
|
280
308
|
|
|
281
309
|
return;
|
|
282
310
|
}
|
|
283
|
-
if (source?.kind === "artifact" && end?.kind === "artifact") {
|
|
284
|
-
child?.onAnnotate(span, body);
|
|
285
311
|
|
|
286
|
-
|
|
312
|
+
if (source?.kind === "artifact" && end?.kind === "artifact") {
|
|
313
|
+
return child?.onAnnotate(span, body);
|
|
287
314
|
}
|
|
315
|
+
|
|
288
316
|
if (
|
|
289
317
|
source?.kind !== "message" ||
|
|
290
318
|
end?.kind !== "message" ||
|
|
291
319
|
source.messageId !== end.messageId
|
|
292
320
|
)
|
|
293
321
|
return;
|
|
322
|
+
|
|
294
323
|
const message = state.messages.find((entry) => entry.id === source.messageId)!;
|
|
295
324
|
const localState = { ...state, messages: [message] };
|
|
296
325
|
const comment = commentOnAgentSpan(
|
|
@@ -307,6 +336,8 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
307
336
|
pendingWrites.current.push(
|
|
308
337
|
agent.act((client) => client.agentComment({ id: thread.id, comment })),
|
|
309
338
|
);
|
|
339
|
+
|
|
340
|
+
return comment.id;
|
|
310
341
|
}}
|
|
311
342
|
onReply={reply}
|
|
312
343
|
onUpdateAnnotation={(id, body) => {
|
|
@@ -327,6 +358,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
|
|
|
327
358
|
{state.phase.kind === "offline" ? (
|
|
328
359
|
<text fg={theme.textMuted}>Owner offline</text>
|
|
329
360
|
) : null}
|
|
361
|
+
{state.phase.kind === "failed" ? <text fg={theme.red}>{state.phase.error}</text> : null}
|
|
330
362
|
{busy ? <text fg={pulse ? theme.textDim : theme.textMuted}>Thinking…</text> : null}
|
|
331
363
|
{state.phase.kind === "permission" && props.client?.canControlAgent !== false ? (
|
|
332
364
|
<>
|
|
@@ -6,6 +6,7 @@ function session(id: string, title: string, rootCommit?: string, remote?: string
|
|
|
6
6
|
const workspace: WorkspaceKey = { repoRoot: `/home/dev/${id}-checkout`, branch: "main" };
|
|
7
7
|
|
|
8
8
|
if (rootCommit !== undefined) workspace.rootCommit = rootCommit;
|
|
9
|
+
|
|
9
10
|
if (remote !== undefined) workspace.remote = remote;
|
|
10
11
|
|
|
11
12
|
return {
|
|
@@ -99,6 +99,7 @@ function fakeClient(initial: Thread, sink: WorkingCopySink): ThreadClient {
|
|
|
99
99
|
|
|
100
100
|
sink.workingCopy = content;
|
|
101
101
|
session = { ...session, workingCopy: content };
|
|
102
|
+
|
|
102
103
|
if (content === undefined) delete session.workingCopy;
|
|
103
104
|
|
|
104
105
|
return session;
|
|
@@ -111,6 +112,7 @@ function fakeClient(initial: Thread, sink: WorkingCopySink): ThreadClient {
|
|
|
111
112
|
|
|
112
113
|
sink.workingCopy = content;
|
|
113
114
|
session = { ...session, workingCopy: content };
|
|
115
|
+
|
|
114
116
|
if (rejections.length) session.curation = rejections;
|
|
115
117
|
else delete session.curation;
|
|
116
118
|
|