@cueloop/client 0.1.0-alpha.100 → 0.1.0-alpha.102

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.
@@ -1,7 +1,12 @@
1
1
  import { expect, test } from "bun:test";
2
2
  import React from "react";
3
3
  import { testRender } from "@opentui/react/test-utils";
4
- import { SCHEMA_VERSION, type Thread, type ThreadAgentState } from "@cueloop/schema";
4
+ import {
5
+ SCHEMA_VERSION,
6
+ type Thread,
7
+ type ThreadAgentState,
8
+ type AgentPromptRequest,
9
+ } from "@cueloop/schema";
5
10
  import { AgentThreadPane, AgentThreadPrototype } from "./AgentThreadPane";
6
11
  import { ThreadFooter } from "./ThreadFooter";
7
12
  import { ThreadView } from "../../markdown/components/ThreadView";
@@ -16,6 +21,7 @@ import {
16
21
  waitForState,
17
22
  } from "../../testing/test-support";
18
23
  import type { ThreadAgentClient } from "../use-thread-agent";
24
+ import type { EventFrame } from "@cueloop/daemon/client";
19
25
 
20
26
  const thread: Thread = {
21
27
  schemaVersion: SCHEMA_VERSION,
@@ -39,8 +45,14 @@ const empty: ThreadAgentState = {
39
45
 
40
46
  function createTestAgentClient(initial: ThreadAgentState) {
41
47
  let state = initial;
42
- const prompts: { id: string; text: string; retry?: string }[] = [];
48
+ const prompts: AgentPromptRequest[] = [];
49
+ const listeners = new Set<(event: EventFrame) => void>();
43
50
  const client: ThreadAgentClient = {
51
+ onEvent: (listener) => {
52
+ listeners.add(listener);
53
+
54
+ return () => listeners.delete(listener);
55
+ },
44
56
  agentGet: async () => state,
45
57
  agentPrompt: async (params) => {
46
58
  prompts.push(params);
@@ -66,7 +78,12 @@ function createTestAgentClient(initial: ThreadAgentState) {
66
78
  agentPermission: async () => ({ ...state, phase: { kind: "idle" } }),
67
79
  };
68
80
 
69
- return { client, prompts };
81
+ const publishTestAgentState = (next: ThreadAgentState): void => {
82
+ state = next;
83
+ for (const listener of listeners) listener({ event: "agent.updated", sessionId: thread.id });
84
+ };
85
+
86
+ return { client, prompts, publishTestAgentState };
70
87
  }
71
88
 
72
89
  function artifactView(
@@ -132,6 +149,7 @@ test("typing on the final blank line invokes the agent while the existing footer
132
149
  setup.mockInput.pressKey("RETURN", { ctrl: true });
133
150
  await waitForText(setup, "The timer survives cancellation.");
134
151
  expect(prompts[0]?.text).toBe("Explain retries");
152
+ expect(prompts[0]?.inputOnly).toBe(true);
135
153
  const send = locateText(setup, "Send message (0)");
136
154
 
137
155
  await setup.mockMouse.click(send.column, send.row);
@@ -356,6 +374,227 @@ test("typing on an accepted plain prompt cannot edit it or create an unmarked co
356
374
  }
357
375
  });
358
376
 
377
+ for (const editingReply of [false, true]) {
378
+ test(`Option+Enter preserves the exact ${editingReply ? "edited" : "new"} reply for Ctrl+Enter`, async () => {
379
+ const submitted: string[] = [];
380
+ const root = {
381
+ id: "root",
382
+ kind: "comment" as const,
383
+ body: "Submitted question",
384
+ createdAt: "2026-10-04",
385
+ anchor: { quote: "Original", prefix: "", suffix: " artifact" },
386
+ };
387
+ const display = buildDisplay(thread.artifact.content);
388
+
389
+ function ControlledDiscussion(): React.ReactNode {
390
+ const [value, setValue] = React.useState<Thread>({
391
+ ...thread,
392
+ annotations: editingReply
393
+ ? [root, { ...root, id: "reply", replyTo: "root", body: "Editable followup" }]
394
+ : [root],
395
+ });
396
+ const [focused, setFocused] = React.useState<string>();
397
+
398
+ return (
399
+ <ThreadView
400
+ session={value}
401
+ display={display}
402
+ marks={marksByDisplay(value.annotations, display)}
403
+ quickActions={[]}
404
+ observer={false}
405
+ focusedAnnotationId={focused}
406
+ onFocusAnnotation={setFocused}
407
+ isAnnotationReadOnly={(id) => id === "root"}
408
+ onAnnotate={noop}
409
+ onReply={(id, body) => {
410
+ setValue((current) => ({
411
+ ...current,
412
+ annotations: [...current.annotations, { ...root, id: "reply", replyTo: id, body }],
413
+ }));
414
+
415
+ return "reply";
416
+ }}
417
+ onUpdateAnnotation={(id, body) =>
418
+ setValue((current) => ({
419
+ ...current,
420
+ annotations: current.annotations.map((note) =>
421
+ note.id === id ? { ...note, body } : note,
422
+ ),
423
+ }))
424
+ }
425
+ onInvoke={(id) => {
426
+ if (id) submitted.push(id);
427
+ }}
428
+ onExit={noop}
429
+ />
430
+ );
431
+ }
432
+ const setup = await testRender(<ControlledDiscussion />, {
433
+ width: 100,
434
+ height: 24,
435
+ kittyKeyboard: true,
436
+ });
437
+
438
+ try {
439
+ await waitForText(setup, editingReply ? "Editable followup" : "Submitted question");
440
+ const comment = locateText(setup, editingReply ? "Editable followup" : "Submitted question");
441
+
442
+ await setup.mockMouse.click(comment.column, comment.row);
443
+ await typeText(setup, " Why?");
444
+ await pressKey(setup, "RETURN", { meta: true });
445
+ expect(submitted).toEqual([]);
446
+ const saved = locateText(setup, editingReply ? "Editable followup Why?" : "Why?");
447
+
448
+ await setup.mockMouse.click(saved.column, saved.row);
449
+ await pressKey(setup, "RETURN", { ctrl: true });
450
+ expect(submitted).toEqual(["reply"]);
451
+ } finally {
452
+ setup.renderer.destroy();
453
+ }
454
+ });
455
+ }
456
+
457
+ test("Option-saved Changes mirror replies send their stored ID with Ctrl+Enter", async () => {
458
+ const root = {
459
+ id: "changes-note",
460
+ kind: "comment" as const,
461
+ body: "Question on Changes",
462
+ createdAt: thread.createdAt,
463
+ anchor: { quote: "Changed code", prefix: "", suffix: "" },
464
+ };
465
+ const { client, prompts } = createTestAgentClient({
466
+ ...empty,
467
+ submissions: [
468
+ {
469
+ id: "accepted",
470
+ commentId: root.id,
471
+ prompt: root.body,
472
+ quote: root.anchor.quote,
473
+ status: "completed",
474
+ },
475
+ ],
476
+ });
477
+
478
+ function ChangesDiscussion(): React.ReactNode {
479
+ const [value, setValue] = React.useState<Thread>({ ...thread, annotations: [root] });
480
+ const [focused, setFocused] = React.useState<string>();
481
+ const display = buildDisplay(value.artifact.content);
482
+ const saveReply = (id: string, body: string): string => {
483
+ setValue((current) => ({
484
+ ...current,
485
+ annotations: [...current.annotations, { ...root, id: "followup", replyTo: id, body }],
486
+ }));
487
+
488
+ return "followup";
489
+ };
490
+
491
+ return (
492
+ <AgentThreadPane
493
+ thread={value}
494
+ client={client}
495
+ focused
496
+ theme={DARK}
497
+ onActiveChange={noop}
498
+ onOpenFile={noop}
499
+ >
500
+ <ThreadView
501
+ session={value}
502
+ display={display}
503
+ marks={marksByDisplay(value.annotations, display)}
504
+ quickActions={[]}
505
+ observer={false}
506
+ focusedAnnotationId={focused}
507
+ onFocusAnnotation={setFocused}
508
+ onAnnotate={noop}
509
+ onReply={saveReply}
510
+ onUpdateAnnotation={noop}
511
+ onExit={noop}
512
+ />
513
+ </AgentThreadPane>
514
+ );
515
+ }
516
+ const setup = await testRender(<ChangesDiscussion />, {
517
+ width: 100,
518
+ height: 24,
519
+ kittyKeyboard: true,
520
+ });
521
+
522
+ try {
523
+ await waitForText(setup, root.body);
524
+ const mirror = locateText(setup, root.body);
525
+
526
+ await setup.mockMouse.click(mirror.column, mirror.row);
527
+ await typeText(setup, "Follow up on this change");
528
+ await pressKey(setup, "RETURN", { meta: true });
529
+ await waitForText(setup, "Follow up on this change");
530
+ expect(prompts).toHaveLength(0);
531
+ await pressKey(setup, "RETURN", { ctrl: true });
532
+ expect(prompts.map((prompt) => prompt.commentId)).toEqual(["followup"]);
533
+ } finally {
534
+ setup.renderer.destroy();
535
+ }
536
+ });
537
+
538
+ test("Ctrl+Enter submits only the comment just saved, leaving older notes editable", async () => {
539
+ const value: Thread = {
540
+ ...thread,
541
+ annotations: [
542
+ {
543
+ id: "older",
544
+ kind: "comment",
545
+ body: "Older unsent note",
546
+ createdAt: "2026-10-04",
547
+ anchor: { quote: "Original", prefix: "", suffix: " artifact" },
548
+ },
549
+ ],
550
+ };
551
+ const { client, prompts } = createTestAgentClient(empty);
552
+ const display = buildDisplay(value.artifact.content);
553
+ const setup = await testRender(
554
+ <AgentThreadPane
555
+ thread={value}
556
+ theme={DARK}
557
+ focused
558
+ onActiveChange={noop}
559
+ onOpenFile={noop}
560
+ client={client}
561
+ >
562
+ <ThreadView
563
+ session={value}
564
+ display={display}
565
+ marks={marksByDisplay(value.annotations, display)}
566
+ quickActions={[]}
567
+ observer={false}
568
+ onAnnotate={() => "selected"}
569
+ onReply={noop}
570
+ onUpdateAnnotation={noop}
571
+ onExit={noop}
572
+ />
573
+ </AgentThreadPane>,
574
+ { width: 100, height: 24, kittyKeyboard: true },
575
+ );
576
+
577
+ try {
578
+ await waitForText(setup, "Original artifact");
579
+ const artifact = locateText(setup, "Original artifact");
580
+
581
+ await setup.mockMouse.drag(
582
+ artifact.column + 9,
583
+ artifact.row,
584
+ artifact.column + 17,
585
+ artifact.row,
586
+ );
587
+ await typeText(setup, "Only this question");
588
+ await pressKey(setup, "RETURN", { ctrl: true });
589
+ await waitForState(setup, () => prompts.length === 1, "selected comment invocation");
590
+ expect(prompts[0]?.commentId).toBe("selected");
591
+ expect(prompts[0]?.id).toBe(value.id);
592
+ expect(value.annotations[0]?.body).toBe("Older unsent note");
593
+ } finally {
594
+ setup.renderer.destroy();
595
+ }
596
+ });
597
+
359
598
  test("Option+Enter saves a comment, Command+Enter leaves its draft, and Ctrl+Enter invokes", async () => {
360
599
  const saved: string[] = [];
361
600
  let invocations = 0;
@@ -367,7 +606,11 @@ test("Option+Enter saves a comment, Command+Enter leaves its draft, and Ctrl+Ent
367
606
  marks={marksByDisplay([], display)}
368
607
  quickActions={[]}
369
608
  observer={false}
370
- onAnnotate={(_span, body) => saved.push(body)}
609
+ onAnnotate={(_span, body) => {
610
+ saved.push(body);
611
+
612
+ return `saved-${saved.length}`;
613
+ }}
371
614
  onReply={noop}
372
615
  onUpdateAnnotation={noop}
373
616
  onExit={noop}
@@ -827,3 +1070,91 @@ test("a failed mutation flush restores the prompt alongside a newer visible draf
827
1070
  setup.renderer.destroy();
828
1071
  }
829
1072
  });
1073
+
1074
+ test("an offline shared agent accepts continuation below its status without model controls", async () => {
1075
+ const state: ThreadAgentState = {
1076
+ ...empty,
1077
+ messages: [
1078
+ { id: "online-answer", role: "agent", text: "An online answer", complete: true, revision: 1 },
1079
+ ],
1080
+ };
1081
+ const { client, prompts, publishTestAgentState } = createTestAgentClient(state);
1082
+ const setup = await testRender(
1083
+ <AgentThreadPane
1084
+ thread={thread}
1085
+ client={client}
1086
+ focused
1087
+ theme={DARK}
1088
+ onActiveChange={noop}
1089
+ onOpenFile={noop}
1090
+ >
1091
+ {artifactView(thread)}
1092
+ </AgentThreadPane>,
1093
+ { width: 80, height: 15, kittyKeyboard: true },
1094
+ );
1095
+
1096
+ try {
1097
+ await waitForText(setup, "An online answer");
1098
+ await waitForState(
1099
+ setup,
1100
+ () => setup.renderer.currentFocusedEditor !== null,
1101
+ "online continuation editor",
1102
+ );
1103
+ publishTestAgentState({ ...state, phase: { kind: "offline" } });
1104
+ await waitForText(setup, "Owner offline");
1105
+ expect(setup.captureCharFrame()).toContain("Original artifact");
1106
+ expect(setup.captureCharFrame()).not.toContain("Thinking");
1107
+ const status = locateText(setup, "Owner offline");
1108
+
1109
+ await setup.mockMouse.click(status.column + 5, status.row + 3);
1110
+ await waitForState(
1111
+ setup,
1112
+ () => setup.renderer.currentFocusedEditor !== null,
1113
+ "offline continuation editor",
1114
+ );
1115
+ await typeText(setup, "Queue until the owner reconnects");
1116
+ await waitForText(setup, "Queue until the owner reconnects");
1117
+ await pressKey(setup, "RETURN", { ctrl: true });
1118
+ expect(prompts[0]?.text).toBe("Queue until the owner reconnects");
1119
+ expect(prompts[0]?.inputOnly).toBe(true);
1120
+ } finally {
1121
+ setup.renderer.destroy();
1122
+ }
1123
+ });
1124
+
1125
+ test("shared viewers see pending activity without owner permission controls", async () => {
1126
+ const { client } = createTestAgentClient({
1127
+ ...empty,
1128
+ phase: {
1129
+ kind: "permission",
1130
+ permission: {
1131
+ id: "owner-permission",
1132
+ title: "Owner approval required",
1133
+ options: [{ optionId: "allow", name: "Allow once", kind: "allow_once" }],
1134
+ },
1135
+ },
1136
+ });
1137
+
1138
+ client.canControlAgent = false;
1139
+ const setup = await testRender(
1140
+ <AgentThreadPane
1141
+ thread={thread}
1142
+ client={client}
1143
+ focused
1144
+ theme={DARK}
1145
+ onActiveChange={noop}
1146
+ onOpenFile={noop}
1147
+ >
1148
+ {artifactView(thread)}
1149
+ </AgentThreadPane>,
1150
+ { width: 80, height: 15 },
1151
+ );
1152
+
1153
+ try {
1154
+ await waitForText(setup, "Thinking");
1155
+ expect(setup.captureCharFrame()).not.toContain("Allow once");
1156
+ expect(setup.captureCharFrame()).not.toContain("Owner approval required");
1157
+ } finally {
1158
+ setup.renderer.destroy();
1159
+ }
1160
+ });
@@ -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);
@@ -86,18 +88,20 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
86
88
  const notifyState = useEffectEvent(() => props.onStateChange?.(state));
87
89
  const notifyControls = useEffectEvent(() =>
88
90
  props.onControlsChange?.(
89
- <AgentConfigControls
90
- state={state}
91
- theme={theme}
92
- onConfigure={(configId, value) =>
93
- void agent.act((client) => {
94
- if (!client.agentConfigure)
95
- return Promise.reject(new Error("Agent configuration is unavailable"));
96
-
97
- return client.agentConfigure({ id: thread.id, configId, value });
98
- })
99
- }
100
- />,
91
+ props.client && !props.client.agentConfigure ? null : (
92
+ <AgentConfigControls
93
+ state={state}
94
+ theme={theme}
95
+ onConfigure={(configId, value) =>
96
+ void agent.act((client) => {
97
+ if (!client.agentConfigure)
98
+ return Promise.reject(new Error("Agent configuration is unavailable"));
99
+
100
+ return client.agentConfigure({ id: thread.id, configId, value });
101
+ })
102
+ }
103
+ />
104
+ ),
101
105
  ),
102
106
  );
103
107
 
@@ -113,9 +117,16 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
113
117
  notifyControls();
114
118
  }, [state, theme]);
115
119
 
116
- const invoke = async (): Promise<void> => {
120
+ const invoke = async (commentId?: string): Promise<void> => {
117
121
  if (thread.status !== "pending") return;
118
- invocations.current.push({ text: draft.current, writes: pendingWrites.current.splice(0) });
122
+ if (!commentId && !draft.current.trim()) return;
123
+ invocations.current.push({
124
+ text: draft.current,
125
+ commentId: commentId
126
+ ? (projection.mirrors.get(commentId)?.commentId ?? commentId)
127
+ : undefined,
128
+ writes: pendingWrites.current.splice(0),
129
+ });
119
130
  draft.current = "";
120
131
  if (invoking.current) return;
121
132
  invoking.current = true;
@@ -135,7 +146,13 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
135
146
  }
136
147
  await props.flushMutations?.();
137
148
  const accepted = await agent.act((client) =>
138
- client.agentPrompt({ id: thread.id, text: input.text }),
149
+ client.agentPrompt({
150
+ id: thread.id,
151
+ text: input.text,
152
+ commentId: input.commentId,
153
+ inputOnly: Boolean(input.text.trim()),
154
+ operationId: newAnnotationId(),
155
+ }),
139
156
  );
140
157
 
141
158
  if (!accepted) rejected.push(input.text);
@@ -151,32 +168,36 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
151
168
  if (text) setPromptRestoreRequest({ id: ++restoreSequence.current, text });
152
169
  }
153
170
  };
154
- const invokeRef = useRef<() => Promise<void>>(async () => {});
171
+ const invokeRef = useRef<(commentId?: string) => Promise<void>>(async () => {});
155
172
 
156
173
  useEffect(() => {
157
174
  invokeRef.current = invoke;
158
175
  });
159
- const notifyInvoke = useEffectEvent((invoke: (() => void) | undefined) =>
176
+ const notifyInvoke = useEffectEvent((invoke: ((commentId?: string) => void) | undefined) =>
160
177
  props.onInvokeChange?.(invoke),
161
178
  );
162
179
 
163
180
  useEffect(() => {
164
- notifyInvoke(() => void invokeRef.current());
181
+ notifyInvoke((commentId) => void invokeRef.current(commentId));
165
182
 
166
183
  return () => notifyInvoke(undefined);
167
184
  }, []);
168
- const reply = (id: string, body: string): void => {
185
+ const reply = (id: string, body: string): string | void => {
169
186
  const origin = projection.mirrors.get(id)?.commentId ?? id;
170
187
  const root = agentCommentRoot(state, origin);
188
+ const replyFocusId = (commentId: string | void): string | void =>
189
+ commentId && projection.mirrors.has(id) ? `${id}:${commentId}` : commentId;
190
+
191
+ if (root) {
192
+ const commentId = newAnnotationId();
171
193
 
172
- if (root)
173
194
  pendingWrites.current.push(
174
195
  agent.act((client) =>
175
196
  client.agentComment({
176
197
  id: thread.id,
177
198
  comment: {
178
199
  ...root,
179
- id: newAnnotationId(),
200
+ id: commentId,
180
201
  body,
181
202
  sent: false,
182
203
  author: undefined,
@@ -185,7 +206,11 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
185
206
  }),
186
207
  ),
187
208
  );
188
- else child?.onReply(origin, body);
209
+
210
+ return replyFocusId(commentId);
211
+ }
212
+
213
+ return replyFocusId(child?.onReply(origin, body));
189
214
  };
190
215
  const actionFor = (id: string) => {
191
216
  const mirror = projection.mirrors.get(id);
@@ -194,7 +219,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
194
219
  );
195
220
 
196
221
  if (!submission) return undefined;
197
- if (submission.status === "failed")
222
+ if (submission.status === "failed" && props.client?.canControlAgent !== false)
198
223
  return {
199
224
  label: "Retry",
200
225
  run: () =>
@@ -238,7 +263,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
238
263
  Boolean(state.comments.find((entry) => entry.id === id)?.sent)
239
264
  }
240
265
  annotationAction={actionFor}
241
- onInvoke={() => void invoke()}
266
+ onInvoke={(commentId) => void invoke(commentId)}
242
267
  isPromptBlock={(index) => index === projection.tailIndex}
243
268
  promptRestoreRequest={promptRestoreRequest}
244
269
  promptFocusRequest={
@@ -269,14 +294,14 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
269
294
  return;
270
295
  }
271
296
  if (source?.kind === "mirror") {
272
- if (source.commentId) reply(source.commentId, body);
297
+ const mirrorId = projection.marks.get(span.start.blockIndex)?.[0]?.annotationId;
298
+
299
+ if (mirrorId) return reply(mirrorId, body);
273
300
 
274
301
  return;
275
302
  }
276
303
  if (source?.kind === "artifact" && end?.kind === "artifact") {
277
- child?.onAnnotate(span, body);
278
-
279
- return;
304
+ return child?.onAnnotate(span, body);
280
305
  }
281
306
  if (
282
307
  source?.kind !== "message" ||
@@ -300,6 +325,8 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
300
325
  pendingWrites.current.push(
301
326
  agent.act((client) => client.agentComment({ id: thread.id, comment })),
302
327
  );
328
+
329
+ return comment.id;
303
330
  }}
304
331
  onReply={reply}
305
332
  onUpdateAnnotation={(id, body) => {
@@ -317,8 +344,11 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
317
344
  renderBlock={(index) =>
318
345
  index === projection.activityIndex ? (
319
346
  <box style={{ flexDirection: "column", paddingLeft: 2 }}>
347
+ {state.phase.kind === "offline" ? (
348
+ <text fg={theme.textMuted}>Owner offline</text>
349
+ ) : null}
320
350
  {busy ? <text fg={pulse ? theme.textDim : theme.textMuted}>Thinking…</text> : null}
321
- {state.phase.kind === "permission" ? (
351
+ {state.phase.kind === "permission" && props.client?.canControlAgent !== false ? (
322
352
  <>
323
353
  <text fg={theme.text}>{state.phase.permission.title}</text>
324
354
  <box style={{ flexDirection: "row", gap: 2 }}>
@@ -39,6 +39,7 @@ import {
39
39
  type Artifact,
40
40
  type DiffFileContents,
41
41
  type ShareLink,
42
+ type SharePolicy,
42
43
  type Thread,
43
44
  type SessionHistory,
44
45
  type MessageOutcome,
@@ -241,6 +242,7 @@ export interface NewShareLink {
241
242
  name?: string;
242
243
  requireAuth: boolean;
243
244
  allowlist: string[];
245
+ agentEnabled?: boolean;
244
246
  }
245
247
 
246
248
  export interface ReviewControllerOptions {
@@ -1697,15 +1699,16 @@ class Controller implements ReviewController {
1697
1699
  this.setStatus("sharing…");
1698
1700
  this.freezeForShare(session)
1699
1701
  .then((shared) => {
1700
- return this.shareTransport.publish(
1701
- shared,
1702
- {},
1703
- {
1704
- requireAuth: input.requireAuth,
1705
- allowlist: input.allowlist,
1706
- shareBranch,
1707
- },
1708
- );
1702
+ const policy: ShareLink = {
1703
+ id: session.id,
1704
+ requireAuth: input.requireAuth,
1705
+ allowlist: input.allowlist,
1706
+ shareBranch,
1707
+ };
1708
+
1709
+ if (input.agentEnabled !== undefined) policy.agentEnabled = input.agentEnabled;
1710
+
1711
+ return this.shareTransport.publish(shared, {}, policy);
1709
1712
  })
1710
1713
  .then(async ({ line, copied }) => {
1711
1714
  const id = this.shareTransport.parseShareId(line);
@@ -1719,6 +1722,7 @@ class Controller implements ReviewController {
1719
1722
  shareBranch,
1720
1723
  };
1721
1724
 
1725
+ if (input.agentEnabled !== undefined) link.agentEnabled = input.agentEnabled;
1722
1726
  await client.sessionSetShares(session.id, [...this.linksFor(this.snapshot.session), link]);
1723
1727
  this.setStatus("");
1724
1728
  this.showToast(line, copied ? "link copied" : "link created");
@@ -1738,20 +1742,24 @@ class Controller implements ReviewController {
1738
1742
  if (!links.some((link) => link.id === id)) return;
1739
1743
  const allowlist = input.requireAuth ? input.allowlist : [];
1740
1744
 
1745
+ const policy: SharePolicy = { requireAuth: input.requireAuth, allowlist };
1746
+
1747
+ if (input.agentEnabled !== undefined) policy.agentEnabled = input.agentEnabled;
1741
1748
  // Publish the same policy stored on the local link.
1742
- void this.shareTransport
1743
- .push(id, [], { requireAuth: input.requireAuth, allowlist })
1744
- .catch(() => {});
1745
- const next = links.map((link) =>
1746
- link.id === id
1747
- ? {
1748
- ...link,
1749
- name: input.name?.trim() || undefined,
1750
- requireAuth: input.requireAuth,
1751
- allowlist,
1752
- }
1753
- : link,
1754
- );
1749
+ void this.shareTransport.push(id, [], policy).catch(() => {});
1750
+ const next = links.map((link) => {
1751
+ if (link.id !== id) return link;
1752
+ const updated = {
1753
+ ...link,
1754
+ name: input.name?.trim() || undefined,
1755
+ requireAuth: input.requireAuth,
1756
+ allowlist,
1757
+ };
1758
+
1759
+ if (input.agentEnabled !== undefined) updated.agentEnabled = input.agentEnabled;
1760
+
1761
+ return updated;
1762
+ });
1755
1763
 
1756
1764
  this.applyOptimistic(
1757
1765
  { ...session, shares: next },