@cueloop/client 0.1.0-alpha.79 → 0.1.0-alpha.80

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 (56) hide show
  1. package/package.json +5 -5
  2. package/src/App.diff.test.tsx +2 -2
  3. package/src/App.export.test.tsx +3 -3
  4. package/src/App.plan-review.test.tsx +1 -1
  5. package/src/App.readonly.test.tsx +2 -2
  6. package/src/App.submit-confirm.test.tsx +7 -7
  7. package/src/App.test.tsx +11 -11
  8. package/src/App.tsx +16 -11
  9. package/src/App.walk.test.tsx +5 -5
  10. package/src/app-key-state.ts +1 -1
  11. package/src/app-screens.tsx +5 -5
  12. package/src/app-view-model.ts +5 -5
  13. package/src/components/AnnotatableFileView.stories.tsx +1 -1
  14. package/src/components/AnnotatableFileView.test.tsx +1 -1
  15. package/src/components/BareWorkbenchFileView.tsx +1 -1
  16. package/src/components/Breadcrumb.stories.tsx +1 -1
  17. package/src/components/ChangesColumn.stories.tsx +6 -1
  18. package/src/components/CompletionOverlay.stories.tsx +2 -2
  19. package/src/components/CompletionOverlay.tsx +4 -4
  20. package/src/components/ConfirmCard.stories.tsx +4 -4
  21. package/src/components/ConfirmCard.tsx +31 -32
  22. package/src/components/DiffContentView.tsx +1 -1
  23. package/src/components/KeybindsDialog.stories.tsx +1 -1
  24. package/src/components/ThreadFooter.test.tsx +12 -1
  25. package/src/components/ThreadFooter.tsx +7 -2
  26. package/src/components/ThreadTree.stories.tsx +1 -1
  27. package/src/components/ThreadTree.tsx +1 -1
  28. package/src/components/ThreadView.tsx +1 -1
  29. package/src/components/WelcomePlayground.tsx +1 -1
  30. package/src/components/__snapshots__/stories.test.tsx.snap +8 -8
  31. package/src/components/story-fixtures.ts +2 -2
  32. package/src/components/{session-tree.test.ts → thread-tree.test.ts} +2 -2
  33. package/src/config.test.ts +10 -10
  34. package/src/config.ts +6 -9
  35. package/src/integrations.test.ts +8 -8
  36. package/src/integrations.ts +4 -4
  37. package/src/intent-dispatch.test.ts +10 -10
  38. package/src/intent-dispatch.ts +15 -14
  39. package/src/key-bindings.test.ts +4 -4
  40. package/src/key-bindings.ts +10 -10
  41. package/src/keymap.test.ts +5 -5
  42. package/src/keymap.ts +7 -7
  43. package/src/thread-controller.curate.test.ts +15 -5
  44. package/src/thread-controller.derivation.test.ts +1 -1
  45. package/src/thread-controller.live-diff.test.ts +10 -10
  46. package/src/thread-controller.optimistic.test.ts +1 -1
  47. package/src/thread-controller.share.test.ts +8 -8
  48. package/src/thread-controller.switch-cache.test.ts +4 -4
  49. package/src/thread-controller.tree.test.ts +4 -4
  50. package/src/thread-controller.ts +12 -12
  51. package/src/tree-view.test.ts +1 -1
  52. package/src/tree-view.ts +3 -3
  53. package/src/use-annotation-surface.tsx +1 -1
  54. package/src/view-diff.test.ts +1 -1
  55. package/src/workbench-snapshot.test.ts +1 -1
  56. /package/src/components/{session-tree.ts → thread-tree.ts} +0 -0
@@ -8,14 +8,14 @@
8
8
  */
9
9
 
10
10
  import type { Dispatch, MutableRefObject, SetStateAction } from "react";
11
- import { isAddressed, type Thread, type VerdictKind } from "@cueloop/schema";
11
+ import { isAddressed, type Thread, type MessageOutcome } from "@cueloop/schema";
12
12
  import { displayText, spanKey, startSpan, type DisplayBlock, type SpanState } from "./view-plan";
13
13
  import type { DiffRow } from "./view-diff";
14
14
  import type { ReviewController } from "./thread-controller";
15
15
  import type { Intent } from "./keymap";
16
16
  import type { TreeRow } from "./tree-view";
17
17
  import { quickActionBody, type QuickAction } from "./config";
18
- import { VERDICTS } from "./components/ConfirmCard";
18
+ import { MESSAGE_OUTCOMES } from "./components/ConfirmCard";
19
19
 
20
20
  /** Which pane of the session tree / review the keyboard grammar is aimed at. */
21
21
  export type RailTab = "review" | "tree";
@@ -34,7 +34,7 @@ export type Mode =
34
34
  text: string;
35
35
  }
36
36
  | { type: "railEdit"; id: string; text: string }
37
- | { type: "submit"; verdict: VerdictKind; summary: string }
37
+ | { type: "submit"; message: MessageOutcome; summary: string }
38
38
  | { type: "confirmDelete"; sessionId: string; title: string }
39
39
  | { type: "rename"; authorId: string; text: string }
40
40
  | { type: "renameThread"; sessionId: string; text: string }
@@ -68,7 +68,7 @@ export interface IntentDispatchDeps {
68
68
  inboxCursor: number;
69
69
  mode: Mode;
70
70
  session: Thread | null;
71
- defaultVerdict: VerdictKind;
71
+ defaultMessage: MessageOutcome;
72
72
  focusedAnnotationId: string | undefined;
73
73
  /** The curation item selected for undo, if any. */
74
74
  selectedCurationId: string | undefined;
@@ -368,7 +368,7 @@ function handleOpenSubmit(_intent: IntentOfType<"openSubmit">, deps: IntentDispa
368
368
 
369
369
  if (!session) return;
370
370
  deps.liveInput.current = "";
371
- deps.setMode({ type: "submit", verdict: deps.defaultVerdict, summary: "" });
371
+ deps.setMode({ type: "submit", message: deps.defaultMessage, summary: "" });
372
372
  }
373
373
 
374
374
  // share opens the share dialog; it owns its own keys, links list, and publish wizard
@@ -524,24 +524,25 @@ function handleSaveCompose(_intent: IntentOfType<"saveCompose">, deps: IntentDis
524
524
  deps.setMode({ type: "normal" });
525
525
  }
526
526
 
527
- function handleSubmitVerdict(
528
- _intent: IntentOfType<"submitVerdict">,
527
+ function handleSubmitMessage(
528
+ _intent: IntentOfType<"submitMessage">,
529
529
  deps: IntentDispatchDeps,
530
530
  ): void {
531
531
  const { mode } = deps;
532
532
 
533
- if (mode.type === "submit") deps.controller.submit(mode.verdict, deps.liveInput.current);
533
+ if (mode.type === "submit") deps.controller.submit(mode.message, deps.liveInput.current);
534
534
  deps.setMode({ type: "normal" });
535
535
  }
536
536
 
537
- function handleCycleVerdict(intent: IntentOfType<"cycleVerdict">, deps: IntentDispatchDeps): void {
537
+ function handleCycleMessage(intent: IntentOfType<"cycleMessage">, deps: IntentDispatchDeps): void {
538
538
  const { mode } = deps;
539
539
 
540
540
  if (mode.type !== "submit") return;
541
- const verdictIndex =
542
- (VERDICTS.indexOf(mode.verdict) + intent.direction + VERDICTS.length) % VERDICTS.length;
541
+ const messageIndex =
542
+ (MESSAGE_OUTCOMES.indexOf(mode.message) + intent.direction + MESSAGE_OUTCOMES.length) %
543
+ MESSAGE_OUTCOMES.length;
543
544
 
544
- deps.setMode({ ...mode, verdict: VERDICTS[verdictIndex]! });
545
+ deps.setMode({ ...mode, message: MESSAGE_OUTCOMES[messageIndex]! });
545
546
  }
546
547
 
547
548
  function handleFinishReview(_intent: IntentOfType<"finishReview">, deps: IntentDispatchDeps): void {
@@ -664,8 +665,8 @@ const intentHandlers: IntentHandlers = {
664
665
  deselect: handleDeselect,
665
666
  closeOverlay: handleCloseOverlay,
666
667
  saveCompose: handleSaveCompose,
667
- submitVerdict: handleSubmitVerdict,
668
- cycleVerdict: handleCycleVerdict,
668
+ submitMessage: handleSubmitMessage,
669
+ cycleMessage: handleCycleMessage,
669
670
  finishReview: handleFinishReview,
670
671
  optInAutoClose: handleOptInAutoClose,
671
672
  dismissCompletion: handleDismissCompletion,
@@ -63,14 +63,14 @@ describe("mode layers own their keys", () => {
63
63
  expect(resolver.resolveAction({ name: "j", shift: false })).toBeUndefined();
64
64
  });
65
65
 
66
- test("the submit overlay adds the verdict arrows", () => {
66
+ test("the submit overlay adds the message arrows", () => {
67
67
  // Arrange
68
68
  const resolver = bindings({ overlay: "submit" });
69
69
 
70
70
  // Assert
71
- expect(resolver.resolveAction({ name: "left", shift: false })).toBe("cycle_verdict_left");
72
- expect(resolver.resolveAction({ name: "right", shift: false })).toBe("cycle_verdict_right");
73
- expect(resolver.resolveAction({ name: "return", shift: false })).toBe("submit_verdict");
71
+ expect(resolver.resolveAction({ name: "left", shift: false })).toBe("cycle_outcome_left");
72
+ expect(resolver.resolveAction({ name: "right", shift: false })).toBe("cycle_outcome_right");
73
+ expect(resolver.resolveAction({ name: "return", shift: false })).toBe("send_message");
74
74
  });
75
75
 
76
76
  test("the walk overlay owns the bracket stepping and leaves the grammar keys dead", () => {
@@ -58,7 +58,7 @@ const HINT_TEMPLATES: Record<HintMode, HintEntry[]> = {
58
58
  { commands: ["submit"], label: "submit" },
59
59
  { commands: ["quit"], label: "quit" },
60
60
  ],
61
- // A share viewer: annotate and navigate, no plan edit / verdict submit.
61
+ // A share viewer: annotate and navigate, no plan edit / message submit.
62
62
  collaborator: [
63
63
  { commands: ["down", "up"], label: "move" },
64
64
  { commands: ["span"], label: "span" },
@@ -90,11 +90,11 @@ const HINT_TEMPLATES: Record<HintMode, HintEntry[]> = {
90
90
  ],
91
91
  submit: [
92
92
  {
93
- commands: ["cycle_verdict_left", "cycle_verdict_right"],
93
+ commands: ["cycle_outcome_left", "cycle_outcome_right"],
94
94
  label: "decision",
95
95
  labelFirst: true,
96
96
  },
97
- { commands: ["submit_verdict"], label: "submit" },
97
+ { commands: ["send_message"], label: "submit" },
98
98
  { commands: ["cancel_submit"], label: "cancel" },
99
99
  ],
100
100
  walk: [
@@ -329,16 +329,16 @@ export class KeyBindings {
329
329
  priority: 20,
330
330
  when: () => this.context.overlay === "submit",
331
331
  bindings: [
332
- { key: "return", cmd: "submit_verdict" },
333
- { key: "enter", cmd: "submit_verdict" },
334
- { key: "left", cmd: "cycle_verdict_left" },
335
- { key: "right", cmd: "cycle_verdict_right" },
332
+ { key: "return", cmd: "send_message" },
333
+ { key: "enter", cmd: "send_message" },
334
+ { key: "left", cmd: "cycle_outcome_left" },
335
+ { key: "right", cmd: "cycle_outcome_right" },
336
336
  { key: "escape", cmd: "cancel_submit" },
337
337
  ],
338
338
  commands: [
339
- record("submit_verdict"),
340
- record("cycle_verdict_left"),
341
- record("cycle_verdict_right"),
339
+ record("send_message"),
340
+ record("cycle_outcome_left"),
341
+ record("cycle_outcome_right"),
342
342
  record("cancel_submit"),
343
343
  ],
344
344
  });
@@ -61,8 +61,8 @@ describe("submit overlay", () => {
61
61
  ["escape", [{ type: "closeOverlay" }]],
62
62
  ["return", []],
63
63
  ["enter", []],
64
- ["left", [{ type: "cycleVerdict", direction: -1 }]],
65
- ["right", [{ type: "cycleVerdict", direction: 1 }]],
64
+ ["left", [{ type: "cycleMessage", direction: -1 }]],
65
+ ["right", [{ type: "cycleMessage", direction: 1 }]],
66
66
  ["j", []],
67
67
  ["q", []],
68
68
  ];
@@ -134,7 +134,7 @@ describe("inbox mode", () => {
134
134
  describe("collaborator (share) capabilities", () => {
135
135
  test("can annotate: comment opens the composer", () => {
136
136
  // Arrange
137
- const collab = state({ canEditPlan: false, canSubmitVerdict: false });
137
+ const collab = state({ canEditPlan: false, canSubmitMessage: false });
138
138
 
139
139
  // Act / Assert
140
140
  expect(reduceKey(collab, key("c"), "comment")).toEqual([
@@ -159,9 +159,9 @@ describe("collaborator (share) capabilities", () => {
159
159
  expect(reduceKey(collab, key("c"), "cut")).toEqual([]);
160
160
  });
161
161
 
162
- test("cannot submit a verdict: there is no agent on a share", () => {
162
+ test("cannot submit a message: there is no agent on a share", () => {
163
163
  // Arrange
164
- const collab = state({ canSubmitVerdict: false });
164
+ const collab = state({ canSubmitMessage: false });
165
165
 
166
166
  // Act / Assert
167
167
  expect(reduceKey(collab, key("x"), "submit")).toEqual([
package/src/keymap.ts CHANGED
@@ -46,8 +46,8 @@ export type Intent =
46
46
  | { type: "deselect" }
47
47
  | { type: "closeOverlay" }
48
48
  | { type: "saveCompose" }
49
- | { type: "submitVerdict" }
50
- | { type: "cycleVerdict"; direction: -1 | 1 }
49
+ | { type: "submitMessage" }
50
+ | { type: "cycleMessage"; direction: -1 | 1 }
51
51
  | { type: "finishReview" }
52
52
  | { type: "optInAutoClose" }
53
53
  | { type: "dismissCompletion" }
@@ -73,10 +73,10 @@ export interface KeyState {
73
73
  /**
74
74
  * Owner-only primitives a share collaborator lacks (undefined = owner, allowed).
75
75
  * A collaborator annotates but cannot edit the plan (cut / $EDITOR runs on
76
- * the gateway) or submit an agent verdict (there is no agent on a share).
76
+ * the gateway) or submit an agent message (there is no agent on a share).
77
77
  */
78
78
  canEditPlan?: boolean;
79
- canSubmitVerdict?: boolean;
79
+ canSubmitMessage?: boolean;
80
80
  /** Owner-only: publish the plan as a share. A collaborator never re-shares. */
81
81
  canShare?: boolean;
82
82
  /** Layer that owns keys before the grammar runs. */
@@ -193,7 +193,7 @@ function submitOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
193
193
 
194
194
  if (name === "escape") return [{ type: "closeOverlay" }];
195
195
  if (name === "left" || name === "right") {
196
- return [{ type: "cycleVerdict", direction: name === "left" ? -1 : 1 }];
196
+ return [{ type: "cycleMessage", direction: name === "left" ? -1 : 1 }];
197
197
  }
198
198
 
199
199
  return [];
@@ -398,8 +398,8 @@ function annotationCluster(state: KeyState, action: string | undefined): Intent[
398
398
  return [{ type: "openRename" }];
399
399
  }
400
400
  if (action === "submit") {
401
- // a collaborator's notes union back as they go; there is no verdict to submit
402
- if (state.canSubmitVerdict === false)
401
+ // a collaborator's notes union back as they go; there is no message to submit
402
+ if (state.canSubmitMessage === false)
403
403
  return status("shared view - your notes save as you go; q to leave");
404
404
 
405
405
  return state.resolved ? [] : [{ type: "openSubmit" }];
@@ -9,7 +9,7 @@ import {
9
9
  type Thread,
10
10
  } from "@cueloop/schema";
11
11
  import { curateDiff } from "@cueloop/daemon/curate";
12
- import type { SessionClient } from "@cueloop/daemon/client";
12
+ import type { ThreadClient } from "@cueloop/daemon/client";
13
13
  import { createReviewController } from "./thread-controller";
14
14
 
15
15
  const PATCH = `diff --git a/src/store.ts b/src/store.ts
@@ -40,7 +40,7 @@ function diffSession(files?: DiffFileContents[]): Thread {
40
40
  artifact: { type: "diff", content: PATCH, meta: {}, files },
41
41
  revisions: [{ revision: 1, content: PATCH, submittedAt: "2026-01-01T00:00:00.000Z" }],
42
42
  annotations: [],
43
- verdict: null,
43
+ message: null,
44
44
  status: "pending",
45
45
  createdAt: "2026-01-01T00:00:00.000Z",
46
46
  };
@@ -54,7 +54,7 @@ const unimplemented = (member: string) => () =>
54
54
  Promise.reject(new Error(`fakeClient does not implement ${member}`));
55
55
 
56
56
  /** A fake client that records the working copy the controller writes. */
57
- function fakeClient(initial: Thread, sink: WorkingCopySink): SessionClient {
57
+ function fakeClient(initial: Thread, sink: WorkingCopySink): ThreadClient {
58
58
  let session = initial;
59
59
 
60
60
  return {
@@ -122,7 +122,7 @@ function fakeClient(initial: Thread, sink: WorkingCopySink): SessionClient {
122
122
  sessionMergeShared: unimplemented("sessionMergeShared"),
123
123
  sessionDelete: unimplemented("sessionDelete"),
124
124
  sessionSetSelfName: unimplemented("sessionSetSelfName"),
125
- sessionResolve: unimplemented("sessionResolve"),
125
+ sessionSendMessage: unimplemented("sessionSendMessage"),
126
126
  close: () => {},
127
127
  };
128
128
  }
@@ -299,6 +299,16 @@ describe("diff hunk curation", () => {
299
299
  expect(controller.getSnapshot().status).toContain("hunk curation needs full file contents");
300
300
  expect(sink.workingCopy).toBeUndefined();
301
301
  });
302
+
303
+ test("curation is disabled when a revision cleared its file snapshots", async () => {
304
+ const { controller, client } = await connected(diffSession([]));
305
+
306
+ controller.toggleRejectChange(4);
307
+ await tick();
308
+
309
+ expect(controller.getSnapshot().status).toContain("hunk curation needs full file contents");
310
+ expect(client.sessionCurate).not.toHaveBeenCalled();
311
+ });
302
312
  });
303
313
 
304
314
  const PLAN_CONTENT = "# Title\n\nFirst paragraph.\n\nSecond paragraph.\n";
@@ -313,7 +323,7 @@ function planSession(workingCopy?: string): Thread {
313
323
  artifact: { type: "plan", content: PLAN_CONTENT, meta: {} },
314
324
  revisions: [{ revision: 1, content: PLAN_CONTENT, submittedAt: "2026-01-01T00:00:00.000Z" }],
315
325
  annotations: [],
316
- verdict: null,
326
+ message: null,
317
327
  status: "pending",
318
328
  createdAt: "2026-01-01T00:00:00.000Z",
319
329
  workingCopy,
@@ -12,7 +12,7 @@ function threadFixture(overrides: Partial<Thread> = {}): Thread {
12
12
  { revision: 1, content: "# Plan\n\nShip it.\n", submittedAt: "2026-01-01T00:00:00.000Z" },
13
13
  ],
14
14
  annotations: [],
15
- verdict: null,
15
+ message: null,
16
16
  status: "pending",
17
17
  createdAt: "2026-01-01T00:00:00.000Z",
18
18
  ...overrides,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { describe, expect, mock, test } from "bun:test";
9
9
  import { SCHEMA_VERSION, type Annotation, type Thread } from "@cueloop/schema";
10
- import type { SessionClient } from "@cueloop/daemon/client";
10
+ import type { ThreadClient } from "@cueloop/daemon/client";
11
11
  import { createReviewController, type ShareTransport } from "./thread-controller";
12
12
  import { mergeFromShare } from "./share";
13
13
 
@@ -53,7 +53,7 @@ function planSession(id = "ses_plan", repoRoot = "/repo"): Thread {
53
53
  artifact: { type: "plan", content: "# Plan\n", meta: {} },
54
54
  revisions: [{ revision: 1, content: "# Plan\n", submittedAt: AT }],
55
55
  annotations: [],
56
- verdict: null,
56
+ message: null,
57
57
  status: "pending",
58
58
  createdAt: AT,
59
59
  };
@@ -62,7 +62,7 @@ function planSession(id = "ses_plan", repoRoot = "/repo"): Thread {
62
62
  const unimplemented = (member: string) => () =>
63
63
  Promise.reject(new Error(`fakeClient does not implement ${member}`));
64
64
 
65
- function fakeClient(session: Thread): SessionClient {
65
+ function fakeClient(session: Thread): ThreadClient {
66
66
  return {
67
67
  onEvent: () => () => {},
68
68
  subscribe: async () => {},
@@ -88,10 +88,10 @@ function fakeClient(session: Thread): SessionClient {
88
88
  sessionMergeShared: unimplemented("sessionMergeShared"),
89
89
  sessionDelete: unimplemented("sessionDelete"),
90
90
  sessionSetSelfName: unimplemented("sessionSetSelfName"),
91
- sessionResolve: unimplemented("sessionResolve"),
91
+ sessionSendMessage: unimplemented("sessionSendMessage"),
92
92
  repoDiff: async () => ({ patch: PATCH, files: FILES }),
93
93
  close: () => {},
94
- } satisfies SessionClient;
94
+ } satisfies ThreadClient;
95
95
  }
96
96
 
97
97
  const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
@@ -133,7 +133,7 @@ describe("live working-tree diff for a non-diff thread", () => {
133
133
 
134
134
  return { ...session, annotations: [{ ...annotation, createdAt: AT }] };
135
135
  },
136
- } satisfies SessionClient;
136
+ } satisfies ThreadClient;
137
137
  const controller = createReviewController({
138
138
  sessionId: session.id,
139
139
  openClient: async () => client,
@@ -176,7 +176,7 @@ describe("live working-tree diff for a non-diff thread", () => {
176
176
  ...fakeClient(sessionA),
177
177
  sessionList: async () => [sessionA, sessionB],
178
178
  repoDiff: async (cwd: string) => (cwd === "/repoA" ? pendingA : diffB),
179
- } satisfies SessionClient;
179
+ } satisfies ThreadClient;
180
180
 
181
181
  const controller = createReviewController({
182
182
  sessionId: sessionA.id,
@@ -228,7 +228,7 @@ function diffSession(meta: Thread["artifact"]["meta"], id = "ses_diff"): Thread
228
228
  artifact: { type: "diff", content: STALE_PATCH, files: STALE_FILES, meta },
229
229
  revisions: [{ revision: 1, content: STALE_PATCH, submittedAt: AT }],
230
230
  annotations: [],
231
- verdict: null,
231
+ message: null,
232
232
  status: "pending",
233
233
  createdAt: AT,
234
234
  };
@@ -258,7 +258,7 @@ describe("frozen vs live diff by thread kind", () => {
258
258
  test("a plain diff review pins its captured snapshot and never queries the live tree", async () => {
259
259
  const session = diffSession({});
260
260
  const repoDiff = mock(async () => ({ patch: PATCH, files: FILES }));
261
- const client = { ...fakeClient(session), repoDiff } satisfies SessionClient;
261
+ const client = { ...fakeClient(session), repoDiff } satisfies ThreadClient;
262
262
  const controller = createReviewController({
263
263
  sessionId: session.id,
264
264
  openClient: async () => client,
@@ -289,7 +289,7 @@ describe("serve mode pins the served thread to a frozen snapshot", () => {
289
289
  test("the served artifact wins over the thread's own capture and the live tree", async () => {
290
290
  const session = diffSession({ workbench: true });
291
291
  const repoDiff = mock(async () => ({ patch: PATCH, files: FILES }));
292
- const client = { ...fakeClient(session), repoDiff } satisfies SessionClient;
292
+ const client = { ...fakeClient(session), repoDiff } satisfies ThreadClient;
293
293
  const controller = createReviewController({
294
294
  sessionId: session.id,
295
295
  openClient: async () => client,
@@ -12,7 +12,7 @@ function threadFixture(annotations: Annotation[] = []): Thread {
12
12
  { revision: 1, content: "# Plan\n\nShip it.\n", submittedAt: "2026-01-01T00:00:00.000Z" },
13
13
  ],
14
14
  annotations,
15
- verdict: null,
15
+ message: null,
16
16
  status: "pending",
17
17
  createdAt: "2026-01-01T00:00:00.000Z",
18
18
  };
@@ -1,7 +1,7 @@
1
1
  import { beforeEach, describe, expect, mock, test, type Mock } from "bun:test";
2
2
  import { ManualClock } from "@opentui/core/testing";
3
3
  import { SCHEMA_VERSION, type Annotation, type ShareAccess, type Thread } from "@cueloop/schema";
4
- import type { SessionClient } from "@cueloop/daemon/client";
4
+ import type { ThreadClient } from "@cueloop/daemon/client";
5
5
  import {
6
6
  createReviewController,
7
7
  SHARE_RECONNECT_MAX_MS,
@@ -43,7 +43,7 @@ function sessionFixture(overrides: Partial<Thread> = {}): Thread {
43
43
  artifact: { type: "plan", content: "# Plan\n", meta: {} },
44
44
  revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00.000Z" }],
45
45
  annotations: [],
46
- verdict: null,
46
+ message: null,
47
47
  status: "pending",
48
48
  createdAt: "2026-01-01T00:00:00.000Z",
49
49
  ...overrides,
@@ -61,20 +61,20 @@ function annotation(id: string, author: string): Annotation {
61
61
  };
62
62
  }
63
63
 
64
- interface FakeSessionClient extends SessionClient {
65
- sessionComment: Mock<SessionClient["sessionComment"]>;
64
+ interface FakeThreadClient extends ThreadClient {
65
+ sessionComment: Mock<ThreadClient["sessionComment"]>;
66
66
  }
67
67
 
68
68
  const unimplemented = (member: string) => () =>
69
69
  Promise.reject(new Error(`fakeClient does not implement ${member}`));
70
70
 
71
- function fakeClient(session: Thread): FakeSessionClient {
71
+ function fakeClient(session: Thread): FakeThreadClient {
72
72
  return {
73
73
  onEvent: () => () => {},
74
74
  subscribe: async () => {},
75
75
  sessionGet: async () => session,
76
76
  sessionList: async () => [session],
77
- sessionComment: mock<SessionClient["sessionComment"]>(async () => session),
77
+ sessionComment: mock<ThreadClient["sessionComment"]>(async () => session),
78
78
  sessionAnnotate: unimplemented("sessionAnnotate"),
79
79
  sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
80
80
  sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
@@ -108,7 +108,7 @@ function fakeClient(session: Thread): FakeSessionClient {
108
108
  }),
109
109
  sessionDelete: unimplemented("sessionDelete"),
110
110
  sessionSetSelfName: unimplemented("sessionSetSelfName"),
111
- sessionResolve: unimplemented("sessionResolve"),
111
+ sessionSendMessage: unimplemented("sessionSendMessage"),
112
112
  close: () => {},
113
113
  };
114
114
  }
@@ -119,7 +119,7 @@ async function connectedController(
119
119
  session: Thread,
120
120
  clock?: ManualClock,
121
121
  transport: ShareTransport = shareTransport,
122
- ): Promise<{ controller: ReturnType<typeof createReviewController>; client: FakeSessionClient }> {
122
+ ): Promise<{ controller: ReturnType<typeof createReviewController>; client: FakeThreadClient }> {
123
123
  const client = fakeClient(session);
124
124
  const controller = createReviewController({
125
125
  sessionId: session.id,
@@ -6,7 +6,7 @@ import {
6
6
  type Thread,
7
7
  } from "@cueloop/schema";
8
8
  import { curateDiff } from "@cueloop/daemon/curate";
9
- import type { SessionClient } from "@cueloop/daemon/client";
9
+ import type { ThreadClient } from "@cueloop/daemon/client";
10
10
  import { createReviewController } from "./thread-controller";
11
11
 
12
12
  const PATCH_A = `diff --git a/src/a.ts b/src/a.ts
@@ -39,7 +39,7 @@ function diffSession(id: string, patch: string, files: DiffFileContents[]): Thre
39
39
  artifact: { type: "diff", content: patch, meta: { title: id }, files },
40
40
  revisions: [{ revision: 1, content: patch, submittedAt: "2026-01-01T00:00:00.000Z" }],
41
41
  annotations: [],
42
- verdict: null,
42
+ message: null,
43
43
  status: "pending",
44
44
  createdAt: "2026-01-01T00:00:00.000Z",
45
45
  };
@@ -66,7 +66,7 @@ const unimplemented = (member: string) => () =>
66
66
  Promise.reject(new Error(`fakeClient does not implement ${member}`));
67
67
 
68
68
  /** A fake client over a fixed set of sessions, so open(id) can switch between them from the inbox. */
69
- function fakeClient(sessions: Thread[]): SessionClient {
69
+ function fakeClient(sessions: Thread[]): ThreadClient {
70
70
  const byId = new Map(sessions.map((session) => [session.id, session]));
71
71
 
72
72
  return {
@@ -108,7 +108,7 @@ function fakeClient(sessions: Thread[]): SessionClient {
108
108
  sessionMergeShared: unimplemented("sessionMergeShared"),
109
109
  sessionDelete: unimplemented("sessionDelete"),
110
110
  sessionSetSelfName: unimplemented("sessionSetSelfName"),
111
- sessionResolve: unimplemented("sessionResolve"),
111
+ sessionSendMessage: unimplemented("sessionSendMessage"),
112
112
  close: () => {},
113
113
  };
114
114
  }
@@ -14,7 +14,7 @@ import {
14
14
  type Annotation,
15
15
  type Thread,
16
16
  } from "@cueloop/schema";
17
- import type { SessionClient } from "@cueloop/daemon/client";
17
+ import type { ThreadClient } from "@cueloop/daemon/client";
18
18
  import { createReviewController, type ShareTransport } from "./thread-controller";
19
19
  import { mergeFromShare } from "./share";
20
20
 
@@ -39,7 +39,7 @@ function sessionFixture(): Thread {
39
39
  artifact: { type: "plan", content: "# Plan\n", meta: {} },
40
40
  revisions: [{ revision: 1, content: "# Plan\n", submittedAt: AT }],
41
41
  annotations: [annotation("a1", "first")],
42
- verdict: null,
42
+ message: null,
43
43
  status: "pending",
44
44
  createdAt: AT,
45
45
  };
@@ -101,9 +101,9 @@ function fakeClient(session: Thread) {
101
101
  sessionMergeShared: unimplemented("sessionMergeShared"),
102
102
  sessionDelete: unimplemented("sessionDelete"),
103
103
  sessionSetSelfName: unimplemented("sessionSetSelfName"),
104
- sessionResolve: unimplemented("sessionResolve"),
104
+ sessionSendMessage: unimplemented("sessionSendMessage"),
105
105
  close: () => {},
106
- } satisfies SessionClient;
106
+ } satisfies ThreadClient;
107
107
  }
108
108
 
109
109
  const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { SystemClock, type Clock, type TimerHandle } from "@opentui/core";
11
11
  import { createStore, type StoreApi } from "zustand/vanilla";
12
- import { DaemonClient, type SessionClient } from "@cueloop/daemon/client";
12
+ import { DaemonClient, type ThreadClient } from "@cueloop/daemon/client";
13
13
  import {
14
14
  applyPathView,
15
15
  createBranch,
@@ -36,7 +36,7 @@ import {
36
36
  type ShareLink,
37
37
  type Thread,
38
38
  type SessionHistory,
39
- type VerdictKind,
39
+ type MessageOutcome,
40
40
  } from "@cueloop/schema";
41
41
  import { loadBundledExporters, type BundledExporter } from "./integrations";
42
42
  import {
@@ -140,7 +140,7 @@ function planCutId(base: { lineStart: number; lineEnd: number }): string {
140
140
  /**
141
141
  * Whether two records of the same thread yield the same document projection (display, rows, files,
142
142
  * models). Those derive only from the artifact content and the working copy, so an update that
143
- * touched only annotations, status, or the verdict leaves them identical - the projection can be
143
+ * touched only annotations, status, or the message leaves them identical - the projection can be
144
144
  * reused rather than re-parsed. Content and working copy are strings, compared here by value.
145
145
  */
146
146
  export function sameDerivationInputs(previous: Thread, next: Thread): boolean {
@@ -238,7 +238,7 @@ export interface ReviewControllerOptions {
238
238
  * daemon; the sharing gateway injects a blob-backed client so the same <App>
239
239
  * renders a decrypted share instead.
240
240
  */
241
- openClient?: () => Promise<SessionClient>;
241
+ openClient?: () => Promise<ThreadClient>;
242
242
  shareTransport?: ShareTransport;
243
243
  /** Serve mode: pin this frozen diff onto the served thread so an observer sees a stable snapshot. */
244
244
  servedArtifact?: Artifact;
@@ -359,7 +359,7 @@ export interface ReviewController {
359
359
  /** Leave the walk; the viewed set stays on the session record. */
360
360
  walkLeave(): void;
361
361
  /** Resolve the review, run the export, start the completion hand-back. */
362
- submit(verdict: VerdictKind, summary: string): void;
362
+ submit(message: MessageOutcome, summary: string): void;
363
363
  /** Publish the current session as a public share link; the ssh line lands on the clipboard. */
364
364
  share(): void;
365
365
  /** Stop sharing the current session, revoking every link at the gateway. Owner only. */
@@ -434,7 +434,7 @@ const DERIVED_CACHE_LIMIT = 8;
434
434
 
435
435
  class Controller implements ReviewController {
436
436
  readonly readOnly: boolean;
437
- private client: SessionClient | null = null;
437
+ private client: ThreadClient | null = null;
438
438
  private closed = false;
439
439
  /** The controller's state lives in a zustand store; internal reads go through the snapshot getter. */
440
440
  private readonly store: StoreApi<ControllerSnapshot> = createStore<ControllerSnapshot>(() => ({
@@ -582,7 +582,7 @@ class Controller implements ReviewController {
582
582
  /**
583
583
  * The projection reuses when the same thread's derivation inputs are unchanged. display, rows,
584
584
  * files, and models derive only from the artifact and working copy - never from annotations,
585
- * status, or the verdict - so an update that touched only those produces an all-new session
585
+ * status, or the message - so an update that touched only those produces an all-new session
586
586
  * record but an identical projection.
587
587
  */
588
588
  private reusesDerived(session: Thread, liveDiff: LiveWorkingDiff | null): boolean {
@@ -998,7 +998,7 @@ class Controller implements ReviewController {
998
998
  const session = this.snapshot.session;
999
999
 
1000
1000
  if (!session || session.status === "resolved") return null;
1001
- if (!session.artifact.files) {
1001
+ if (!session.artifact.files?.length) {
1002
1002
  this.setStatus("hunk curation needs full file contents (PR diffs cannot be curated)");
1003
1003
 
1004
1004
  return null;
@@ -1503,7 +1503,7 @@ class Controller implements ReviewController {
1503
1503
  if (this.snapshot.walk) this.update({ walk: null });
1504
1504
  }
1505
1505
 
1506
- submit(verdict: VerdictKind, summary: string): void {
1506
+ submit(message: MessageOutcome, summary: string): void {
1507
1507
  const session = this.snapshot.session;
1508
1508
 
1509
1509
  if (!session) return;
@@ -1511,14 +1511,14 @@ class Controller implements ReviewController {
1511
1511
  const actionBodies = Object.fromEntries(
1512
1512
  slashItemsFrom(this.quickActions).map((item) => [item.name, item.body]),
1513
1513
  );
1514
- this.client!.sessionResolve(session.id, verdict, summary, actionBodies)
1514
+ this.client!.sessionSendMessage(session.id, message, summary, actionBodies)
1515
1515
  .then((resolved) => {
1516
- // The completion overlay heading already states the verdict, so the
1516
+ // The completion overlay heading already states the message, so the
1517
1517
  // status line stays empty here - only export/error messages fill it.
1518
1518
  this.update({ session: resolved, status: "" });
1519
1519
  // notes-vault export: guarded by each exporter's policy (default manual = no-op)
1520
1520
  for (const exporter of this.exporters) {
1521
- if (!exporter.runsOn(verdict)) continue;
1521
+ if (!exporter.runsOn(message)) continue;
1522
1522
  void exporter.run(resolved).then((exportResult) => {
1523
1523
  this.setStatus(
1524
1524
  exportResult.success && exportResult.path
@@ -20,7 +20,7 @@ function forked(): SessionHistory {
20
20
  id: "ses_1",
21
21
  revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
22
22
  annotations: [],
23
- verdict: null,
23
+ message: null,
24
24
  createdAt: AT,
25
25
  });
26
26
 
package/src/tree-view.ts CHANGED
@@ -55,10 +55,10 @@ export function describeEntry(entry: SessionEntry, round?: number): EntryDescrip
55
55
  return { glyph: "·", text: "comment" };
56
56
  case "comment-removed":
57
57
  return { glyph: "·", text: "comment removed" };
58
- case "verdict":
58
+ case "message":
59
59
  return {
60
- glyph: entry.verdict.kind === "approve" ? "✓" : "✗",
61
- text: entry.verdict.kind.replace("_", " "),
60
+ glyph: entry.message.outcome === "approved" ? "✓" : "✗",
61
+ text: entry.message.outcome.replace("_", " "),
62
62
  };
63
63
  case "branch-summary":
64
64
  return { glyph: "↩", text: entry.text ? `"${entry.text}"` : "returned" };
@@ -87,7 +87,7 @@ export interface AnnotationSurfaceOptions {
87
87
  * thread anchors comments in plan coordinates, so a draft here would misanchor.
88
88
  */
89
89
  commentsEnabled?: boolean;
90
- /** A verdict is in: no draft may open; the app answers with its read-only status. */
90
+ /** A message is in: no draft may open; the app answers with its read-only status. */
91
91
  resolved: boolean;
92
92
  /** True while a menu, dialog, or overlay owns the keyboard. */
93
93
  suspended: boolean;