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

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.
Files changed (126) hide show
  1. package/package.json +5 -5
  2. package/src/annotations/annotation-palette.ts +1 -0
  3. package/src/annotations/components/AnnotationCards.tsx +4 -0
  4. package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
  5. package/src/annotations/discussions.ts +1 -0
  6. package/src/annotations/thread-selection.ts +6 -0
  7. package/src/annotations/use-annotation-surface.tsx +97 -0
  8. package/src/app/App.deleted-scroll.test.tsx +3 -0
  9. package/src/app/App.harness-settings.test.tsx +77 -0
  10. package/src/app/App.plan-render.test.tsx +2 -0
  11. package/src/app/App.plan-review.test.tsx +1 -0
  12. package/src/app/App.readonly.test.tsx +63 -0
  13. package/src/app/App.test.tsx +1 -0
  14. package/src/app/App.tsx +94 -12
  15. package/src/app/App.visual.test.tsx +1 -0
  16. package/src/app/App.walk.test.tsx +1 -0
  17. package/src/app/App.workbench.test.tsx +1 -0
  18. package/src/app/app-screens.tsx +3 -2
  19. package/src/app/app-view-model.ts +11 -1
  20. package/src/app/components/WorkspacePanels.tsx +1 -0
  21. package/src/app/intent-dispatch.test.ts +1 -0
  22. package/src/app/intent-dispatch.ts +26 -0
  23. package/src/app/ready-signal.ts +2 -0
  24. package/src/app/run.ts +2 -0
  25. package/src/app/serve.test.ts +4 -0
  26. package/src/diff/components/AnnotatableFileView.tsx +1 -0
  27. package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
  28. package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
  29. package/src/diff/components/DiffContentView.tsx +25 -0
  30. package/src/diff/components/diff-content-view-layout.ts +3 -0
  31. package/src/diff/diff-fold.ts +4 -0
  32. package/src/diff/diff-hunk-curate.ts +3 -0
  33. package/src/diff/diff-intraline.ts +6 -0
  34. package/src/diff/diff-syntax.ts +8 -0
  35. package/src/diff/split-diff.ts +6 -0
  36. package/src/diff/view-diff.ts +18 -0
  37. package/src/diff/walk.ts +3 -0
  38. package/src/index.ts +1 -0
  39. package/src/integrations/attribution.ts +2 -0
  40. package/src/integrations/client-extension-registry.ts +4 -0
  41. package/src/integrations/clipboard.ts +1 -0
  42. package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
  43. package/src/integrations/components/ShareDialog.tsx +28 -0
  44. package/src/integrations/editor.ts +3 -0
  45. package/src/integrations/github-identity.test.ts +1 -0
  46. package/src/integrations/github-identity.ts +1 -0
  47. package/src/integrations/herdr.ts +2 -0
  48. package/src/integrations/pasted-image.ts +2 -0
  49. package/src/integrations/share.ts +46 -1
  50. package/src/keyboard/components/HandleChipsInput.tsx +4 -0
  51. package/src/keyboard/components/SlashComposer.tsx +3 -0
  52. package/src/keyboard/key-bindings.ts +3 -0
  53. package/src/keyboard/keymap.ts +51 -0
  54. package/src/keyboard/skills.ts +2 -0
  55. package/src/keyboard/slash-palette.ts +8 -0
  56. package/src/keyboard/thread-chords.ts +5 -0
  57. package/src/keyboard/use-shared-keyboard.ts +1 -0
  58. package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
  59. package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
  60. package/src/markdown/components/ThreadView.tsx +13 -0
  61. package/src/markdown/mark-runs.ts +4 -0
  62. package/src/markdown/markdown-editor-syntax-style.ts +1 -0
  63. package/src/markdown/markdown-grid.ts +6 -0
  64. package/src/markdown/markdown-highlight.ts +1 -0
  65. package/src/markdown/thread-code-syntax.ts +3 -0
  66. package/src/markdown/view-plan.ts +52 -0
  67. package/src/performance/perf-report.ts +1 -0
  68. package/src/performance/perf-timings.ts +1 -0
  69. package/src/prototype/components/PrototypeContentView.tsx +17 -0
  70. package/src/prototype/prototype-browser.ts +11 -0
  71. package/src/settings/components/SettingsDialog.tsx +3 -0
  72. package/src/settings/config.test.ts +79 -1
  73. package/src/settings/config.ts +91 -1
  74. package/src/settings/use-settings-dialog.test.tsx +172 -0
  75. package/src/settings/use-settings-dialog.tsx +115 -1
  76. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  77. package/src/settings/use-thread-harness-setting.ts +86 -0
  78. package/src/stories/stories.test.tsx +4 -0
  79. package/src/stories/story.ts +6 -0
  80. package/src/terminal/ansi-screen.ts +5 -0
  81. package/src/terminal/components/terminal-pane.ts +12 -0
  82. package/src/terminal/ghostty-terminal.ts +10 -0
  83. package/src/terminal/kitty-image.ts +3 -0
  84. package/src/terminal/pty.ts +9 -0
  85. package/src/testing/test-support.test.ts +2 -0
  86. package/src/testing/test-support.ts +16 -0
  87. package/src/thread/agent-continuation.test.ts +2 -0
  88. package/src/thread/agent-transcript.ts +7 -0
  89. package/src/thread/components/AgentConfigControls.test.tsx +41 -0
  90. package/src/thread/components/AgentConfigControls.tsx +8 -1
  91. package/src/thread/components/AgentThreadPane.test.tsx +91 -3
  92. package/src/thread/components/AgentThreadPane.tsx +61 -0
  93. package/src/thread/components/ThreadTree.tsx +1 -0
  94. package/src/thread/components/thread-tree.test.ts +1 -0
  95. package/src/thread/thread-controller.curate.test.ts +2 -0
  96. package/src/thread/thread-controller.share.test.ts +47 -2
  97. package/src/thread/thread-controller.ts +129 -4
  98. package/src/thread/tree-view.ts +4 -0
  99. package/src/thread/use-thread-agent.test.tsx +1 -0
  100. package/src/thread/use-thread-agent.ts +5 -0
  101. package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
  102. package/src/ui/components/OverlayScrollbar.tsx +5 -0
  103. package/src/ui/components/PromptDialog.tsx +2 -0
  104. package/src/ui/components/RootOverlay.tsx +1 -0
  105. package/src/ui/components/ScrollArea.tsx +2 -0
  106. package/src/ui/components/primitives/Button.tsx +1 -0
  107. package/src/ui/components/primitives/Tabs.tsx +1 -0
  108. package/src/ui/components/primitives/Tree.tsx +4 -0
  109. package/src/ui/components/primitives/tree-model.ts +7 -0
  110. package/src/ui/components/syntax-highlight.ts +5 -0
  111. package/src/ui/components/truncate-title.ts +1 -0
  112. package/src/ui/use-frame-measure.ts +3 -0
  113. package/src/ui/use-terminal-virtualizer.ts +6 -0
  114. package/src/workbench/components/ChangesColumn.tsx +7 -0
  115. package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
  116. package/src/workbench/components/EditorGrid.tsx +8 -0
  117. package/src/workbench/components/FileContentsView.tsx +2 -0
  118. package/src/workbench/components/GridTabContent.tsx +2 -0
  119. package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
  120. package/src/workbench/components/ProjectTreeView.tsx +8 -0
  121. package/src/workbench/components/TreePane.tsx +2 -0
  122. package/src/workbench/components/editor-grid.ts +3 -0
  123. package/src/workbench/components/file-tree.ts +2 -0
  124. package/src/workbench/use-changes-workbench.ts +5 -0
  125. package/src/workbench/use-remember-layout.ts +2 -0
  126. package/src/workbench/use-repo-changes.ts +2 -0
@@ -110,3 +110,44 @@ test("model and reasoning choices share the existing overlay and close after sel
110
110
  setup.renderer.destroy();
111
111
  }
112
112
  });
113
+
114
+ for (const harness of ["pi", "fx"]) {
115
+ test(`${harness} footer identifies its harness, subscription, model and effort`, async () => {
116
+ const setup = await testRender(
117
+ <AgentConfigControls
118
+ theme={DARK}
119
+ onConfigure={() => {}}
120
+ state={{
121
+ ...initial,
122
+ harness: { id: harness, label: harness },
123
+ configOptions: [
124
+ {
125
+ id: "provider",
126
+ name: "Provider",
127
+ currentValue: harness,
128
+ options: [
129
+ {
130
+ value: harness,
131
+ name: harness === "pi" ? "Codex subscription" : "Grok subscription",
132
+ },
133
+ ],
134
+ },
135
+ ...initial.configOptions!,
136
+ ],
137
+ }}
138
+ />,
139
+ { width: 100, height: 3 },
140
+ );
141
+
142
+ try {
143
+ await waitForText(setup, "First model");
144
+ const frame = setup.captureCharFrame();
145
+
146
+ expect(frame).toContain(harness);
147
+ expect(frame).toContain(harness === "pi" ? "Codex subscription" : "Grok subscription");
148
+ expect(frame).toContain("Medium");
149
+ } finally {
150
+ setup.renderer.destroy();
151
+ }
152
+ });
153
+ }
@@ -18,10 +18,12 @@ export function AgentConfigControls({
18
18
  }): React.ReactNode {
19
19
  return (
20
20
  <box style={{ flexDirection: "row", gap: 2, marginRight: 2 }}>
21
+ {state.harness ? <text fg={theme.textMuted}>{state.harness.label}</text> : null}
21
22
  {state.configOptions
22
23
  ?.filter(
23
24
  (option) =>
24
25
  option.category === "thought_level" ||
26
+ option.id === "effort" ||
25
27
  option.id === "model" ||
26
28
  option.id === "provider",
27
29
  )
@@ -62,6 +64,7 @@ function AgentConfigMenu({
62
64
 
63
65
  useEffect(() => {
64
66
  if (!open) return;
67
+
65
68
  overlay.setOverlay(
66
69
  "agent-config",
67
70
  <box
@@ -103,7 +106,11 @@ function AgentConfigMenu({
103
106
  return (
104
107
  <box
105
108
  ref={ref}
106
- onMouseUp={disabled ? undefined : () => menu.toggleMenu(`agent-config:${option.id}`)}
109
+ onMouseUp={
110
+ disabled || option.options.length < 2
111
+ ? undefined
112
+ : () => menu.toggleMenu(`agent-config:${option.id}`)
113
+ }
107
114
  >
108
115
  <text fg={disabled ? theme.textDim : theme.textMuted}>
109
116
  {option.options.find((choice) => choice.value === option.currentValue)?.name ??
@@ -9,11 +9,12 @@ import {
9
9
  } from "@cueloop/schema";
10
10
  import { AgentThreadPane, AgentThreadPrototype } from "./AgentThreadPane";
11
11
  import { ThreadFooter } from "./ThreadFooter";
12
- import { ThreadView } from "../../markdown/components/ThreadView";
12
+ import { ThreadView, type ThreadViewProps } from "../../markdown/components/ThreadView";
13
13
  import { buildDisplay, marksByDisplay } from "../../markdown/view-plan";
14
14
  import { DARK } from "../../appearance/theme";
15
15
  import {
16
16
  locateText,
17
+ dragText,
17
18
  waitForText,
18
19
  pressKey,
19
20
  typeText,
@@ -43,6 +44,33 @@ const empty: ThreadAgentState = {
43
44
  comments: [],
44
45
  };
45
46
 
47
+ test("a failed harness turn displays its error below the conversation", async () => {
48
+ const failure = "OAuth auth derivation failed: missing provider module";
49
+ const { client } = createTestAgentClient({ ...empty, phase: { kind: "failed", error: failure } });
50
+ const setup = await testRender(
51
+ <AgentThreadPane
52
+ thread={thread}
53
+ client={client}
54
+ focused
55
+ theme={DARK}
56
+ onActiveChange={noop}
57
+ onOpenFile={noop}
58
+ >
59
+ {artifactView(thread)}
60
+ </AgentThreadPane>,
61
+ { width: 100, height: 24 },
62
+ );
63
+
64
+ try {
65
+ await waitForText(setup, "Original artifact");
66
+ await settle(setup);
67
+ expect(setup.captureCharFrame()).toContain(failure);
68
+ expect(setup.captureCharFrame()).not.toContain("Thinking");
69
+ } finally {
70
+ setup.renderer.destroy();
71
+ }
72
+ });
73
+
46
74
  function createTestAgentClient(initial: ThreadAgentState) {
47
75
  let state = initial;
48
76
  const prompts: AgentPromptRequest[] = [];
@@ -56,6 +84,7 @@ function createTestAgentClient(initial: ThreadAgentState) {
56
84
  agentGet: async () => state,
57
85
  agentPrompt: async (params) => {
58
86
  prompts.push(params);
87
+
59
88
  if (params.text)
60
89
  state = {
61
90
  ...state,
@@ -90,6 +119,8 @@ function artifactView(
90
119
  value: Thread,
91
120
  onReply: (id: string, body: string) => void = noop,
92
121
  onUpdateAnnotation: (id: string, body: string) => void = noop,
122
+ onAnnotate: ThreadViewProps["onAnnotate"] = noop,
123
+ observer = false,
93
124
  ) {
94
125
  const display = buildDisplay(value.artifact.content);
95
126
 
@@ -99,8 +130,8 @@ function artifactView(
99
130
  display={display}
100
131
  marks={marksByDisplay(value.annotations, display)}
101
132
  quickActions={[]}
102
- observer={false}
103
- onAnnotate={noop}
133
+ observer={observer}
134
+ onAnnotate={onAnnotate}
104
135
  onReply={onReply}
105
136
  onUpdateAnnotation={onUpdateAnnotation}
106
137
  onExit={noop}
@@ -287,6 +318,7 @@ test("three bottom prompts survive delayed acceptance and remain separate submis
287
318
 
288
319
  client.agentPrompt = async (params) => {
289
320
  prompts.push(params.text);
321
+
290
322
  if (prompts.length === 1)
291
323
  return new Promise((resolve) => {
292
324
  release = resolve;
@@ -868,6 +900,7 @@ test("an arriving reply preserves a bottom draft and submits it as the next prom
868
900
 
869
901
  client.agentPrompt = async (params) => {
870
902
  prompts.push(params);
903
+
871
904
  if (prompts.length === 1)
872
905
  return new Promise<ThreadAgentState>((resolve) => {
873
906
  finish = resolve;
@@ -1038,6 +1071,7 @@ test("a failed mutation flush restores the prompt alongside a newer visible draf
1038
1071
  onOpenFile={noop}
1039
1072
  flushMutations={() => {
1040
1073
  if (flushStarted) return Promise.resolve();
1074
+
1041
1075
  flushStarted = true;
1042
1076
 
1043
1077
  return failedFlush;
@@ -1158,3 +1192,57 @@ test("shared viewers see pending activity without owner permission controls", as
1158
1192
  setup.renderer.destroy();
1159
1193
  }
1160
1194
  });
1195
+
1196
+ for (const observer of [false, true]) {
1197
+ test(`shared snapshot preserves artifact annotation permissions (observer=${observer})`, async () => {
1198
+ const value = {
1199
+ ...thread,
1200
+ agentSnapshot: {
1201
+ ...empty,
1202
+ messages: [
1203
+ {
1204
+ id: "answer",
1205
+ role: "agent" as const,
1206
+ text: "Frozen conversation",
1207
+ complete: true,
1208
+ revision: 1,
1209
+ },
1210
+ ],
1211
+ },
1212
+ };
1213
+ const annotations: string[] = [];
1214
+ const setup = await testRender(
1215
+ <AgentThreadPrototype
1216
+ enabled={false}
1217
+ observer={observer}
1218
+ pixelPrototype={false}
1219
+ thread={value}
1220
+ focused
1221
+ theme={DARK}
1222
+ onActiveChange={noop}
1223
+ onOpenFile={noop}
1224
+ >
1225
+ {artifactView(
1226
+ value,
1227
+ noop,
1228
+ noop,
1229
+ (_span, body) => {
1230
+ annotations.push(body);
1231
+ },
1232
+ observer,
1233
+ )}
1234
+ </AgentThreadPrototype>,
1235
+ { width: 100, height: 24, kittyKeyboard: true },
1236
+ );
1237
+
1238
+ try {
1239
+ await waitForText(setup, "Frozen conversation");
1240
+ await dragText(setup, "Original artifact", "Original artifact", "Original artifact".length);
1241
+ await typeText(setup, "Artifact feedback");
1242
+ await pressKey(setup, "RETURN", { meta: true });
1243
+ expect(annotations).toEqual(observer ? [] : ["Artifact feedback"]);
1244
+ } finally {
1245
+ setup.renderer.destroy();
1246
+ }
1247
+ });
1248
+ }
@@ -80,6 +80,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
80
80
 
81
81
  useEffect(() => {
82
82
  if (!busy) return;
83
+
83
84
  const timer = setInterval(() => setPulse((value) => !value), 600);
84
85
 
85
86
  return () => clearInterval(timer);
@@ -119,7 +120,9 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
119
120
 
120
121
  const invoke = async (commentId?: string): Promise<void> => {
121
122
  if (thread.status !== "pending") return;
123
+
122
124
  if (!commentId && !draft.current.trim()) return;
125
+
123
126
  invocations.current.push({
124
127
  text: draft.current,
125
128
  commentId: commentId
@@ -128,7 +131,9 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
128
131
  writes: pendingWrites.current.splice(0),
129
132
  });
130
133
  draft.current = "";
134
+
131
135
  if (invoking.current) return;
136
+
132
137
  invoking.current = true;
133
138
 
134
139
  const rejected: string[] = [];
@@ -144,6 +149,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
144
149
  rejected.push(input.text);
145
150
  continue;
146
151
  }
152
+
147
153
  await props.flushMutations?.();
148
154
  const accepted = await agent.act((client) =>
149
155
  client.agentPrompt({
@@ -219,6 +225,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
219
225
  );
220
226
 
221
227
  if (!submission) return undefined;
228
+
222
229
  if (submission.status === "failed" && props.client?.canControlAgent !== false)
223
230
  return {
224
231
  label: "Retry",
@@ -293,6 +300,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
293
300
 
294
301
  return;
295
302
  }
303
+
296
304
  if (source?.kind === "mirror") {
297
305
  const mirrorId = projection.marks.get(span.start.blockIndex)?.[0]?.annotationId;
298
306
 
@@ -300,15 +308,18 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
300
308
 
301
309
  return;
302
310
  }
311
+
303
312
  if (source?.kind === "artifact" && end?.kind === "artifact") {
304
313
  return child?.onAnnotate(span, body);
305
314
  }
315
+
306
316
  if (
307
317
  source?.kind !== "message" ||
308
318
  end?.kind !== "message" ||
309
319
  source.messageId !== end.messageId
310
320
  )
311
321
  return;
322
+
312
323
  const message = state.messages.find((entry) => entry.id === source.messageId)!;
313
324
  const localState = { ...state, messages: [message] };
314
325
  const comment = commentOnAgentSpan(
@@ -347,6 +358,7 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
347
358
  {state.phase.kind === "offline" ? (
348
359
  <text fg={theme.textMuted}>Owner offline</text>
349
360
  ) : null}
361
+ {state.phase.kind === "failed" ? <text fg={theme.red}>{state.phase.error}</text> : null}
350
362
  {busy ? <text fg={pulse ? theme.textDim : theme.textMuted}>Thinking…</text> : null}
351
363
  {state.phase.kind === "permission" && props.client?.canControlAgent !== false ? (
352
364
  <>
@@ -386,6 +398,55 @@ export function AgentThreadPane(props: AgentThreadPaneProps): React.ReactNode {
386
398
  export function AgentThreadPrototype(
387
399
  props: AgentThreadPaneProps & { enabled: boolean; observer: boolean; pixelPrototype: boolean },
388
400
  ): React.ReactNode {
401
+ if (
402
+ (!props.enabled || props.observer) &&
403
+ props.thread.agentSnapshot &&
404
+ React.isValidElement<ThreadViewProps>(props.children) &&
405
+ props.children.type === ThreadView
406
+ ) {
407
+ const child = props.children.props;
408
+ const display =
409
+ child.display ??
410
+ buildDisplay(
411
+ props.thread.artifact.type === "diff"
412
+ ? (props.thread.artifact.meta.prBrief ?? "")
413
+ : (props.thread.workingCopy ?? props.thread.artifact.content),
414
+ );
415
+ const projection = projectThreadConversation(
416
+ props.thread,
417
+ props.thread.agentSnapshot,
418
+ display,
419
+ child.marks ?? marksByDisplay(props.thread.annotations, display),
420
+ false,
421
+ );
422
+
423
+ return (
424
+ <ThreadView
425
+ {...child}
426
+ session={projection.session}
427
+ display={projection.display}
428
+ marks={projection.marks}
429
+ canAnnotateBlock={(index) =>
430
+ projection.sources[index]?.kind === "artifact" &&
431
+ (child.canAnnotateBlock?.(index) ?? true)
432
+ }
433
+ isAnnotationReadOnly={(id) =>
434
+ !props.thread.annotations.some((entry) => entry.id === id) ||
435
+ Boolean(child.isAnnotationReadOnly?.(id))
436
+ }
437
+ onAnnotate={(span, body) => {
438
+ if (
439
+ projection.sources[span.start.blockIndex]?.kind !== "artifact" ||
440
+ projection.sources[span.end.blockIndex]?.kind !== "artifact"
441
+ )
442
+ return;
443
+
444
+ return child.onAnnotate(span, body);
445
+ }}
446
+ />
447
+ );
448
+ }
449
+
389
450
  if (!props.enabled || props.observer || props.pixelPrototype) return props.children;
390
451
 
391
452
  return <AgentThreadPane {...props} />;
@@ -116,6 +116,7 @@ function ThreadRow(props: ThreadRowProps): React.ReactNode {
116
116
 
117
117
  useEffect(() => {
118
118
  if (!menuOpen) return;
119
+
119
120
  // the full-screen box closes the menu on an outside click; the menu drops from under the kebab
120
121
  setOverlay(
121
122
  "thread-menu",
@@ -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
 
@@ -8,9 +8,16 @@ import {
8
8
  SHARE_RECONNECT_MIN_MS,
9
9
  type ShareTransport,
10
10
  } from "./thread-controller";
11
- import { mergeFromShare, type ShareWatchHandlers } from "../integrations/share";
11
+ import {
12
+ snapshotThreadConversation,
13
+ mergeFromShare,
14
+ type ShareWatchHandlers,
15
+ } from "../integrations/share";
12
16
 
13
- const publishShare = mock(async () => ({ line: "ssh p_abc123xy@cueloop.dev", copied: true }));
17
+ const publishShare = mock(async (_session: Thread) => ({
18
+ line: "ssh p_abc123xy@cueloop.dev",
19
+ copied: true,
20
+ }));
14
21
  let remote: Thread;
15
22
  const pullShare = mock(async () => remote);
16
23
  const pushShare = mock(
@@ -571,3 +578,41 @@ describe("revoke", () => {
571
578
  expect(client.sessionDelete).toHaveBeenCalledWith(session.id);
572
579
  });
573
580
  });
581
+
582
+ test("share publication captures an empty artifact's separate conversation without execution records", async () => {
583
+ const thread = sessionFixture({ artifact: { type: "plan", content: "", meta: {} } });
584
+ const { controller, client } = await connectedController(thread);
585
+ const state = {
586
+ threadId: thread.id,
587
+ phase: { kind: "idle" as const },
588
+ harness: { id: "pi", label: "pi", sessionId: "private-session" },
589
+ messages: [
590
+ {
591
+ id: "answer",
592
+ role: "agent" as const,
593
+ text: "Visible conversation",
594
+ complete: true,
595
+ revision: 1,
596
+ },
597
+ ],
598
+ comments: [],
599
+ tools: [],
600
+ promptOperations: [
601
+ { operationId: "private-operation", fingerprint: "private-hash", result: [] },
602
+ ],
603
+ };
604
+
605
+ client.agentGet = async () => state;
606
+ publishShare.mockClear();
607
+ controller.share();
608
+ await tick();
609
+ await tick();
610
+ const shared = publishShare.mock.calls[0]![0];
611
+
612
+ expect(shared.agentSnapshot?.messages).toEqual(state.messages);
613
+ expect(shared.agentSnapshot?.harness).toEqual({ id: "pi", label: "pi" });
614
+ expect(shared.agentSnapshot?.promptOperations).toBeUndefined();
615
+ expect(thread.agentSnapshot).toBeUndefined();
616
+ expect(await snapshotThreadConversation(thread, {})).toBe(thread);
617
+ controller.close();
618
+ });