@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
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * The Submit button expanded into a bordered confirm card at the rail bottom:
3
- * the verdict selector (arrow keys or click), the optional summary, and plain
3
+ * the message selector (arrow keys or click), the optional summary, and plain
4
4
  * word-buttons - key hints live in the status line only. The bordered height
5
5
  * derives from the content rows through Card, so layout and render never drift.
6
6
  */
7
7
 
8
8
  import React, { useContext } from "react";
9
9
  import { useTerminalDimensions } from "@opentui/react";
10
- import type { VerdictKind } from "@cueloop/schema";
10
+ import type { MessageOutcome } from "@cueloop/schema";
11
11
  import type { Theme } from "../theme";
12
12
  import type { QuickAction } from "../config";
13
13
  import { SlashSkillsContext } from "../skills";
@@ -21,17 +21,16 @@ import { DialogActions } from "./primitives/DialogActions";
21
21
  const SUBMIT_CARD_MAX_WIDTH = 48;
22
22
  const PALETTE_WINDOW = 5;
23
23
 
24
- export const VERDICTS: VerdictKind[] = ["comment", "approve", "request_changes"];
24
+ export const MESSAGE_OUTCOMES: MessageOutcome[] = ["approved", "changes_requested"];
25
25
 
26
- /** Selector words in the confirm card - one word per verdict. */
27
- export const VERDICT_LABEL: Record<VerdictKind, string> = {
28
- comment: "Comment",
29
- approve: "Approve",
30
- request_changes: "Changes",
26
+ /** Selector words in the confirm card - one word per message. */
27
+ export const MESSAGE_OUTCOME_LABEL: Record<MessageOutcome, string> = {
28
+ approved: "Approve",
29
+ changes_requested: "Changes",
31
30
  };
32
31
 
33
32
  export interface ConfirmCardProps {
34
- verdict: VerdictKind;
33
+ message: MessageOutcome;
35
34
  summary: string;
36
35
  /**
37
36
  * The guided walk's honest coverage line for diff sessions, e.g.
@@ -39,53 +38,53 @@ export interface ConfirmCardProps {
39
38
  */
40
39
  viewedSummary?: string;
41
40
  onInput: (summary: string) => void;
42
- onSelectVerdict: (verdict: VerdictKind) => void;
41
+ onSelectMessage: (message: MessageOutcome) => void;
43
42
  onSubmit: () => void;
44
43
  onCancel: () => void;
45
44
  quickActions: QuickAction[];
46
45
  theme?: Theme;
47
46
  }
48
47
 
49
- /** 1-row verdict selector, spacer, summary input, spacer, buttons. */
48
+ /** 1-row message selector, spacer, summary input, spacer, buttons. */
50
49
  const CONFIRM_CONTENT_ROWS = 5;
51
50
 
52
- export function verdictColor(verdict: VerdictKind, tokens: Theme): string {
53
- return verdict === "approve"
51
+ export function outcomeColor(message: MessageOutcome, tokens: Theme): string {
52
+ return message === "approved"
54
53
  ? tokens.green
55
- : verdict === "request_changes"
54
+ : message === "changes_requested"
56
55
  ? tokens.red
57
56
  : tokens.blue;
58
57
  }
59
58
 
60
59
  /**
61
- * The verdict selector: one row of pressable words, matching the reading
62
- * direction of a choice between three peers. Selection stays controlled by
63
- * the grammar (←/→ cycle the verdict); a click selects directly. The
64
- * selected verdict wears brackets and its color.
60
+ * The message selector: one row of pressable words, matching the reading
61
+ * direction of a choice between two outcomes. Selection stays controlled by
62
+ * the grammar (←/→ cycle the message); a click selects directly. The
63
+ * selected message wears brackets and its color.
65
64
  */
66
- function VerdictSelector({
67
- verdict,
68
- onSelectVerdict,
65
+ function MessageSelector({
66
+ message,
67
+ onSelectMessage,
69
68
  theme,
70
69
  }: {
71
- verdict: VerdictKind;
72
- onSelectVerdict: (verdict: VerdictKind) => void;
70
+ message: MessageOutcome;
71
+ onSelectMessage: (message: MessageOutcome) => void;
73
72
  theme?: Theme;
74
73
  }): React.ReactNode {
75
74
  const tokens = useComponentTheme(theme);
76
75
 
77
76
  return (
78
77
  <box style={{ flexDirection: "row", height: 1, width: "100%", justifyContent: "center" }}>
79
- {VERDICTS.map((candidate) => (
78
+ {MESSAGE_OUTCOMES.map((candidate) => (
80
79
  <box
81
80
  key={candidate}
82
81
  style={{ paddingRight: 1 }}
83
- onMouseUp={() => onSelectVerdict(candidate)}
82
+ onMouseUp={() => onSelectMessage(candidate)}
84
83
  >
85
- <text fg={candidate === verdict ? verdictColor(candidate, tokens) : tokens.textDim}>
86
- {candidate === verdict
87
- ? `[${VERDICT_LABEL[candidate]}]`
88
- : ` ${VERDICT_LABEL[candidate]} `}
84
+ <text fg={candidate === message ? outcomeColor(candidate, tokens) : tokens.textDim}>
85
+ {candidate === message
86
+ ? `[${MESSAGE_OUTCOME_LABEL[candidate]}]`
87
+ : ` ${MESSAGE_OUTCOME_LABEL[candidate]} `}
89
88
  </text>
90
89
  </box>
91
90
  ))}
@@ -94,11 +93,11 @@ function VerdictSelector({
94
93
  }
95
94
 
96
95
  export function ConfirmCard({
97
- verdict,
96
+ message,
98
97
  summary,
99
98
  viewedSummary,
100
99
  onInput,
101
- onSelectVerdict,
100
+ onSelectMessage,
102
101
  onSubmit,
103
102
  onCancel,
104
103
  quickActions,
@@ -133,7 +132,7 @@ export function ConfirmCard({
133
132
  >
134
133
  {viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
135
134
  {viewedSummary !== undefined ? <box style={{ height: 1 }} /> : null}
136
- <VerdictSelector verdict={verdict} onSelectVerdict={onSelectVerdict} theme={theme} />
135
+ <MessageSelector message={message} onSelectMessage={onSelectMessage} theme={theme} />
137
136
  <box style={{ height: 1 }} />
138
137
  <SlashComposer
139
138
  seed={summary}
@@ -79,7 +79,7 @@ export interface DiffContentViewProps {
79
79
  observer: boolean;
80
80
  /** Whether comments can be drafted here; false for a non-diff thread's view-only live diff. */
81
81
  commentsEnabled?: boolean;
82
- /** A verdict is in: no draft may open; the app answers with its read-only status. */
82
+ /** A message is in: no draft may open; the app answers with its read-only status. */
83
83
  resolved?: boolean;
84
84
  /** True while a menu, dialog, or overlay owns the keyboard. */
85
85
  suspended?: boolean;
@@ -18,7 +18,7 @@ const SECTIONS = [
18
18
  {
19
19
  title: "Submit",
20
20
  entries: [
21
- { keys: "← / →", label: "verdict" },
21
+ { keys: "← / →", label: "message" },
22
22
  { keys: "esc", label: "cancel" },
23
23
  ],
24
24
  },
@@ -27,10 +27,21 @@ test("a long branch truncates on one line and keeps the send control in place",
27
27
  const frame = setup.captureCharFrame();
28
28
  const footerRow = frame.split("\n").find((line) => line.includes("cueloop"))!;
29
29
  // the repo/branch context and the send control share the single footer row
30
- expect(footerRow).toContain("send message");
30
+ expect(footerRow).toContain("Send message (0)");
31
31
  // the branch is clipped to an ellipsis, not wrapped onto a second row
32
32
  expect(footerRow).toContain("…");
33
33
  expect(frame).not.toContain("long-name");
34
34
 
35
35
  setup.renderer.destroy();
36
36
  });
37
+
38
+ test("the footer shows the open annotation count", async () => {
39
+ const setup = await testRender(
40
+ <ThreadFooter repo="cueloop" branch="main" pendingAnnotations={3} theme={DARK} />,
41
+ { width: 60, height: 3 },
42
+ );
43
+
44
+ await settle(setup);
45
+ expect(setup.captureCharFrame()).toContain("Send message (3)");
46
+ setup.renderer.destroy();
47
+ });
@@ -1,6 +1,6 @@
1
1
  // The thread's bottom band: a header-like bar under the thread column, or the
2
2
  // Changes column when a zoom drops the thread column. It carries the repo/branch
3
- // context on the left and a written, clickable "Send message" control on the right.
3
+ // context on the left and a written, clickable "Send message (n)" control on the right.
4
4
 
5
5
  import React, { useRef } from "react";
6
6
  import type { BoxRenderable } from "@opentui/core";
@@ -15,6 +15,8 @@ export interface ThreadFooterProps {
15
15
  onSubmit?: () => void;
16
16
  /** When false the send control dims and does not fire (e.g. an observer). */
17
17
  canSubmit?: boolean;
18
+ /** Open reviewer annotations included in the next Message. */
19
+ pendingAnnotations?: number;
18
20
  theme?: Theme;
19
21
  }
20
22
 
@@ -29,6 +31,7 @@ export function ThreadFooter({
29
31
  branch,
30
32
  onSubmit,
31
33
  canSubmit = true,
34
+ pendingAnnotations = 0,
32
35
  theme,
33
36
  }: ThreadFooterProps): React.ReactNode {
34
37
  const tokens = theme ?? DARK;
@@ -64,7 +67,9 @@ export function ThreadFooter({
64
67
  </box>
65
68
  <box style={{ flexGrow: 1 }} />
66
69
  <box onMouseUp={canSubmit ? onSubmit : undefined} style={{ flexShrink: 0 }}>
67
- <text fg={canSubmit ? tokens.accent : tokens.textDim}>send message</text>
70
+ <text fg={canSubmit ? tokens.accent : tokens.textDim}>
71
+ {`Send message (${pendingAnnotations})`}
72
+ </text>
68
73
  </box>
69
74
  </box>
70
75
  );
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { ThreadTree } from "./ThreadTree";
5
- import { groupInbox } from "./session-tree";
5
+ import { groupInbox } from "./thread-tree";
6
6
  import { fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
7
7
 
8
8
  export const meta: StoryMeta = { title: "Surfaces/ThreadTree" };
@@ -17,7 +17,7 @@ import { useMenuControl } from "./menu-control";
17
17
  import { IconButton } from "./primitives/IconButton";
18
18
  import { NERD } from "./primitives/icons";
19
19
  import { truncateTitle } from "./truncate-title";
20
- import { threadTitle, type InboxRow } from "./session-tree";
20
+ import { threadTitle, type InboxRow } from "./thread-tree";
21
21
 
22
22
  export interface ThreadTreeProps {
23
23
  rows: InboxRow[];
@@ -143,7 +143,7 @@ export interface ThreadViewProps {
143
143
  editOrphanCount?: number;
144
144
  /** Reports whether a composer is open, so session chords can yield to typing. */
145
145
  onComposingChange?: (composing: boolean) => void;
146
- /** A verdict is in: no draft may open; the app answers with its read-only status. */
146
+ /** A message is in: no draft may open; the app answers with its read-only status. */
147
147
  resolved?: boolean;
148
148
  /** An observer or a resolved review refused a draft; the app shows why. */
149
149
  onObserverBlocked?: (reason: "observer" | "resolved") => void;
@@ -64,7 +64,7 @@ function playgroundThread(annotations: Annotation[]): Thread {
64
64
  artifact: { type: "plan", content: "", meta: {} },
65
65
  revisions: [],
66
66
  annotations,
67
- verdict: null,
67
+ message: null,
68
68
  status: "pending",
69
69
  createdAt: "1970-01-01T00:00:00.000Z",
70
70
  };
@@ -151,7 +151,7 @@ exports[`stories catalog Surfaces/BareWorkbenchFileView/Loaded renders, snapshot
151
151
  `;
152
152
 
153
153
  exports[`stories catalog Chrome/Breadcrumb/SessionHeader renders, snapshots, and carries its colors: Chrome/Breadcrumb/SessionHeader 1`] = `
154
- " cueloop · Migration Plan · rev 2 · resolved: approve
154
+ " cueloop · Migration Plan · rev 2 · resolved: approved
155
155
 
156
156
 
157
157
 
@@ -185,7 +185,7 @@ exports[`stories catalog Surfaces/ChangesColumn/Changed renders, snapshots, and
185
185
  │  components
186
186
  │  ChangesColumn.tsx A
187
187
  │  daemon/src
188
- │  review.ts D
188
+ │  thread-review.ts D
189
189
  │  README.md M
190
190
  │
191
191
  │
@@ -364,7 +364,7 @@ exports[`stories catalog Overlays/CompletionOverlay/FeedbackCounting renders, sn
364
364
 
365
365
  exports[`stories catalog Cards/ConfirmCard/ApproveDefault renders, snapshots, and carries its colors: Cards/ConfirmCard/ApproveDefault 1`] = `
366
366
  "┌────────────────────────────────┐
367
- │ Comment [Approve] Changes │
367
+ │ [Approve] Changes │
368
368
  │ │
369
369
  │ summary for the agent ( │
370
370
  │ │
@@ -382,7 +382,7 @@ exports[`stories catalog Cards/ConfirmCard/ApproveDefault renders, snapshots, an
382
382
 
383
383
  exports[`stories catalog Cards/ConfirmCard/RequestChanges renders, snapshots, and carries its colors: Cards/ConfirmCard/RequestChanges 1`] = `
384
384
  "┌────────────────────────────────┐
385
- │ Comment Approve [Changes] │
385
+ │ Approve [Changes] │
386
386
  │ │
387
387
  │ Tighten the steps. │
388
388
  │ │
@@ -402,7 +402,7 @@ exports[`stories catalog Cards/ConfirmCard/WithViewedSummary renders, snapshots,
402
402
  "┌────────────────────────────────┐
403
403
  │ 2/3 files viewed │
404
404
  │ │
405
- │ Comment [Approve] Changes │
405
+ │ [Approve] Changes │
406
406
  │ │
407
407
  │ summary for the agent ( │
408
408
  │ │
@@ -689,7 +689,7 @@ exports[`stories catalog Overlays/KeybindsDialog/Cheatsheet renders, snapshots,
689
689
  │ enter submit │
690
690
  │ │
691
691
  │ Submit │
692
- │ ← / → verdict │
692
+ │ ← / → message │
693
693
  │ esc cancel │
694
694
  │ │
695
695
  │ [esc] │
@@ -1270,13 +1270,13 @@ exports[`stories catalog Chrome/SlashComposer/WithDraft renders, snapshots, and
1270
1270
 
1271
1271
  exports[`stories catalog Chrome/ThreadFooter/Default renders, snapshots, and carries its colors: Chrome/ThreadFooter/Default 1`] = `
1272
1272
  "────────────────────────────────────────────────────────────────────────────────
1273
-  cueloop / main send message
1273
+  cueloop / main Send message (0)
1274
1274
  "
1275
1275
  `;
1276
1276
 
1277
1277
  exports[`stories catalog Chrome/ThreadFooter/ReadOnly renders, snapshots, and carries its colors: Chrome/ThreadFooter/ReadOnly 1`] = `
1278
1278
  "────────────────────────────────────────────────────────────────────────────────
1279
-  cueloop / design/tui send message
1279
+  cueloop / design/tui Send message (0)
1280
1280
  "
1281
1281
  `;
1282
1282
 
@@ -63,7 +63,7 @@ export function fixturePlanSession(overrides: Partial<Thread> = {}): Thread {
63
63
  },
64
64
  revisions: [{ revision: 1, content: FIXTURE_PLAN, submittedAt: "2026-01-01T00:00:00Z" }],
65
65
  annotations: FIXTURE_ANNOTATIONS,
66
- verdict: null,
66
+ message: null,
67
67
  status: "pending",
68
68
  createdAt: "2026-01-01T00:00:00Z",
69
69
  ...overrides,
@@ -82,7 +82,7 @@ export function fixtureDiffSession(overrides: Partial<Thread> = {}): Thread {
82
82
  },
83
83
  revisions: [{ revision: 1, content: FIXTURE_PATCH, submittedAt: "2026-01-01T00:00:00Z" }],
84
84
  annotations: [],
85
- verdict: null,
85
+ message: null,
86
86
  status: "pending",
87
87
  createdAt: "2026-01-01T00:00:00Z",
88
88
  ...overrides,
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { Thread, WorkspaceKey } from "@cueloop/schema";
3
- import { groupInbox, projectName } from "./session-tree";
3
+ import { groupInbox, projectName } from "./thread-tree";
4
4
 
5
5
  function session(id: string, title: string, rootCommit?: string, remote?: string): Thread {
6
6
  const workspace: WorkspaceKey = { repoRoot: `/home/dev/${id}-checkout`, branch: "main" };
@@ -14,7 +14,7 @@ function session(id: string, title: string, rootCommit?: string, remote?: string
14
14
  artifact: { type: "plan", content: `# ${title}\n`, meta: { title } },
15
15
  revisions: [],
16
16
  annotations: [],
17
- verdict: null,
17
+ message: null,
18
18
  status: "pending",
19
19
  createdAt: "2026-01-01T00:00:00.000Z",
20
20
  };
@@ -143,19 +143,19 @@ describe("loadConfig", () => {
143
143
  }
144
144
  });
145
145
 
146
- test("[ui] default_verdict defaults to approve and parses request_changes", () => {
146
+ test("[ui] default_message defaults to approve and parses changes_requested", () => {
147
147
  // Arrange
148
- const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-verdict-"));
148
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-message-"));
149
149
  const path = join(dir, "config.toml");
150
150
 
151
- writeFileSync(path, `[ui]\ndefault_verdict = "request_changes"\n`);
151
+ writeFileSync(path, `[ui]\ndefault_message = "changes_requested"\n`);
152
152
 
153
153
  try {
154
154
  // Assert
155
- expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.defaultVerdict).toBe(
156
- "approve",
155
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.defaultMessage).toBe(
156
+ "approved",
157
157
  );
158
- expect(loadConfig({ userConfigPath: path }).ui.defaultVerdict).toBe("request_changes");
158
+ expect(loadConfig({ userConfigPath: path }).ui.defaultMessage).toBe("changes_requested");
159
159
  } finally {
160
160
  rmSync(dir, { recursive: true, force: true });
161
161
  }
@@ -393,7 +393,7 @@ describe("integrations.obsidian config", () => {
393
393
 
394
394
  writeFileSync(
395
395
  path,
396
- `[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approve"\nseparator = "comma"\n`,
396
+ `[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approved"\nseparator = "comma"\n`,
397
397
  );
398
398
 
399
399
  try {
@@ -403,7 +403,7 @@ describe("integrations.obsidian config", () => {
403
403
  // Assert
404
404
  expect(config.integrations.obsidian.vault).toBe("/notes/vault");
405
405
  expect(config.integrations.obsidian.folder).toBe("plans");
406
- expect(config.integrations.obsidian.exportOn).toBe("approve");
406
+ expect(config.integrations.obsidian.exportOn).toBe("approved");
407
407
  expect(config.integrations.obsidian.separator).toBe("space"); // invalid value falls back
408
408
  expect(config.keys["comment"]).toEqual(["c"]); // other sections untouched
409
409
  } finally {
@@ -417,7 +417,7 @@ describe("integrations.obsidian config", () => {
417
417
  const user = join(dir, "user.toml");
418
418
  const repoRoot = join(dir, "repo");
419
419
 
420
- writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "resolve"\n`);
420
+ writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "message"\n`);
421
421
  Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
422
422
  writeFileSync(
423
423
  join(repoRoot, ".cueloop", "config.toml"),
@@ -430,7 +430,7 @@ describe("integrations.obsidian config", () => {
430
430
 
431
431
  // Assert
432
432
  expect(config.integrations.obsidian.vault).toBe("/user/vault"); // user layer survives
433
- expect(config.integrations.obsidian.exportOn).toBe("resolve");
433
+ expect(config.integrations.obsidian.exportOn).toBe("message");
434
434
  expect(config.integrations.obsidian.folder).toBe("repo-plans"); // repo layer wins
435
435
  } finally {
436
436
  rmSync(dir, { recursive: true, force: true });
package/src/config.ts CHANGED
@@ -9,7 +9,7 @@ import { existsSync, readFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { OBSIDIAN_DEFAULTS, type ObsidianConfig } from "@cueloop/integration-obsidian";
12
- import type { VerdictKind } from "@cueloop/schema";
12
+ import type { MessageOutcome } from "@cueloop/schema";
13
13
  import { writeFileSync, mkdirSync } from "node:fs";
14
14
  import { dirname } from "node:path";
15
15
  import * as v from "valibot";
@@ -108,7 +108,7 @@ export interface CueloopConfig {
108
108
  theme: ThemeName;
109
109
  /** How the Changes diff renders when wide: old|new side by side or one stacked column. */
110
110
  diffView: DiffViewMode;
111
- defaultVerdict: VerdictKind;
111
+ defaultMessage: MessageOutcome;
112
112
  /** Session ids the user has pinned to the top of the sidebar; client-local view state. */
113
113
  pins: string[];
114
114
  /** The last pane layout a bare launch restores; unset until the user changes one. */
@@ -213,10 +213,7 @@ const UiSchema = v.object({
213
213
  editor: v.fallback(v.optional(v.string()), undefined),
214
214
  theme: v.fallback(v.optional(v.string()), undefined),
215
215
  diff_view: v.fallback(v.optional(v.picklist(["split", "stacked", "unified"])), undefined),
216
- default_verdict: v.fallback(
217
- v.optional(v.picklist(["comment", "approve", "request_changes"])),
218
- undefined,
219
- ),
216
+ default_message: v.fallback(v.optional(v.picklist(["approved", "changes_requested"])), undefined),
220
217
  pins: v.fallback(v.optional(v.array(v.string())), undefined),
221
218
  layout: v.fallback(
222
219
  v.optional(
@@ -234,7 +231,7 @@ const ObsidianSchema = v.object({
234
231
  folder: v.fallback(v.optional(v.string()), undefined),
235
232
  filenameFormat: v.fallback(v.optional(v.string()), undefined),
236
233
  separator: v.fallback(v.optional(v.picklist(["space", "dash", "underscore"])), undefined),
237
- exportOn: v.fallback(v.optional(v.picklist(["approve", "resolve", "manual"])), undefined),
234
+ exportOn: v.fallback(v.optional(v.picklist(["approved", "message", "manual"])), undefined),
238
235
  });
239
236
  const IntegrationsSchema = v.object({ obsidian: v.optional(ObsidianSchema) });
240
237
  const ExperimentalSchema = v.object({
@@ -284,7 +281,7 @@ function applyUi(ui: CueloopConfig["ui"], parsed: v.InferOutput<typeof UiSchema>
284
281
  // "unified" is the pre-rename spelling of "stacked"; keep loading it so an upgrade never flips the layout
285
282
  if (parsed.diff_view !== undefined)
286
283
  ui.diffView = parsed.diff_view === "unified" ? "stacked" : parsed.diff_view;
287
- if (parsed.default_verdict !== undefined) ui.defaultVerdict = parsed.default_verdict;
284
+ if (parsed.default_message !== undefined) ui.defaultMessage = parsed.default_message;
288
285
  if (parsed.pins !== undefined) ui.pins = parsed.pins;
289
286
  if (parsed.layout !== undefined) {
290
287
  ui.layout = {
@@ -364,7 +361,7 @@ export function loadConfig(
364
361
  autoClose: "off",
365
362
  theme: DEFAULT_THEME_NAME,
366
363
  diffView: "split",
367
- defaultVerdict: "approve",
364
+ defaultMessage: "approved",
368
365
  pins: [],
369
366
  },
370
367
  authors: {},
@@ -18,24 +18,24 @@ describe("loadBundledExporters", () => {
18
18
 
19
19
  test("runsOn mirrors the configured export policy", async () => {
20
20
  // Arrange
21
- const onApprove = (await loadBundledExporters(integrations({ exportOn: "approve" })))[0]!;
21
+ const onApprove = (await loadBundledExporters(integrations({ exportOn: "approved" })))[0]!;
22
22
 
23
23
  // Assert
24
- expect(onApprove.runsOn("approve")).toBeTrue();
25
- expect(onApprove.runsOn("request_changes")).toBeFalse();
24
+ expect(onApprove.runsOn("approved")).toBeTrue();
25
+ expect(onApprove.runsOn("changes_requested")).toBeFalse();
26
26
 
27
27
  // Arrange
28
- const onResolve = (await loadBundledExporters(integrations({ exportOn: "resolve" })))[0]!;
28
+ const onMessage = (await loadBundledExporters(integrations({ exportOn: "message" })))[0]!;
29
29
 
30
30
  // Assert
31
- expect(onResolve.runsOn("approve")).toBeTrue();
32
- expect(onResolve.runsOn("request_changes")).toBeTrue();
31
+ expect(onMessage.runsOn("approved")).toBeTrue();
32
+ expect(onMessage.runsOn("changes_requested")).toBeTrue();
33
33
 
34
34
  // Arrange
35
35
  const manual = (await loadBundledExporters(integrations({ exportOn: "manual" })))[0]!;
36
36
 
37
37
  // Assert
38
- expect(manual.runsOn("approve")).toBeFalse();
39
- expect(manual.runsOn("request_changes")).toBeFalse();
38
+ expect(manual.runsOn("approved")).toBeFalse();
39
+ expect(manual.runsOn("changes_requested")).toBeFalse();
40
40
  });
41
41
  });
@@ -7,14 +7,14 @@
7
7
 
8
8
  import { Registry, type Exporter } from "@cueloop/extension-api";
9
9
  import { createObsidianExtension, shouldExport } from "@cueloop/integration-obsidian";
10
- import type { VerdictKind } from "@cueloop/schema";
10
+ import type { MessageOutcome } from "@cueloop/schema";
11
11
  import type { IntegrationsConfig } from "./config";
12
12
 
13
- /** An exporter plus the policy deciding which verdicts trigger it. */
13
+ /** An exporter plus the policy deciding which messages trigger it. */
14
14
  export interface BundledExporter {
15
15
  name: string;
16
16
  run: Exporter;
17
- runsOn(verdict: VerdictKind): boolean;
17
+ runsOn(message: MessageOutcome): boolean;
18
18
  }
19
19
 
20
20
  /** Load the bundled integrations from config as generic exporters. */
@@ -28,5 +28,5 @@ export async function loadBundledExporters(
28
28
 
29
29
  if (!run) return [];
30
30
 
31
- return [{ name: "obsidian", run, runsOn: (verdict) => shouldExport(obsidian.exportOn, verdict) }];
31
+ return [{ name: "obsidian", run, runsOn: (message) => shouldExport(obsidian.exportOn, message) }];
32
32
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The intent reducer in isolation: with the App component's dependencies
3
3
  * mocked, each Intent maps to exactly the controller call or state update it
4
- * should - the navigation clamps, the annotation wrap-around, the verdict
4
+ * should - the navigation clamps, the annotation wrap-around, the message
5
5
  * cycle, and the submit-reveal fix, none of which needed a rendered TUI.
6
6
  */
7
7
 
@@ -36,7 +36,7 @@ function sessionWith(annotations: Annotation[], overrides: Partial<Thread> = {})
36
36
  artifact: { type: "plan", content: "# Plan\n", meta: {} },
37
37
  revisions: [],
38
38
  annotations,
39
- verdict: null,
39
+ message: null,
40
40
  status: "pending",
41
41
  createdAt: "2026-01-01T00:00:00.000Z",
42
42
  ...overrides,
@@ -143,7 +143,7 @@ function makeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDe
143
143
  inboxCursor: 0,
144
144
  mode: { type: "normal" },
145
145
  session: null,
146
- defaultVerdict: "approve",
146
+ defaultMessage: "approved",
147
147
  focusedAnnotationId: undefined,
148
148
  selectedCurationId: undefined,
149
149
  railTab: "review",
@@ -255,19 +255,19 @@ describe("annotation navigation", () => {
255
255
  });
256
256
  });
257
257
 
258
- describe("cycleVerdict", () => {
259
- test("advances through the verdict list in the submit overlay", () => {
258
+ describe("cycleMessage", () => {
259
+ test("advances through the message list in the submit overlay", () => {
260
260
  // Arrange
261
- const deps = makeDeps({ mode: { type: "submit", verdict: "approve", summary: "" } });
261
+ const deps = makeDeps({ mode: { type: "submit", message: "approved", summary: "" } });
262
262
  const dispatch = createIntentDispatch(deps);
263
263
 
264
264
  // Act
265
- dispatch({ type: "cycleVerdict", direction: 1 });
265
+ dispatch({ type: "cycleMessage", direction: 1 });
266
266
 
267
267
  // Assert
268
268
  expect(deps.setMode).toHaveBeenCalledWith({
269
269
  type: "submit",
270
- verdict: "request_changes",
270
+ message: "changes_requested",
271
271
  summary: "",
272
272
  });
273
273
  });
@@ -368,7 +368,7 @@ describe("marker-actions popover", () => {
368
368
  });
369
369
 
370
370
  describe("openSubmit", () => {
371
- test("opens the submit confirm with the default verdict", () => {
371
+ test("opens the submit confirm with the default message", () => {
372
372
  // Arrange
373
373
  const deps = makeDeps({ session: sessionWith([]) });
374
374
  const dispatch = createIntentDispatch(deps);
@@ -377,7 +377,7 @@ describe("openSubmit", () => {
377
377
  dispatch({ type: "openSubmit" });
378
378
 
379
379
  // Assert
380
- expect(deps.setMode).toHaveBeenCalledWith({ type: "submit", verdict: "approve", summary: "" });
380
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "submit", message: "approved", summary: "" });
381
381
  });
382
382
 
383
383
  test("does nothing without a session", () => {