@cueloop/client 0.1.0-alpha.64 → 0.1.0-alpha.66

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 (109) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +8 -73
  4. package/src/App.export.test.tsx +2 -2
  5. package/src/App.plan-review.test.tsx +60 -412
  6. package/src/App.prototype.test.tsx +1 -8
  7. package/src/App.readonly.test.tsx +85 -62
  8. package/src/App.rendering.test.tsx +5 -6
  9. package/src/App.submit-confirm.test.tsx +32 -70
  10. package/src/App.test.tsx +136 -66
  11. package/src/App.toast.test.tsx +17 -8
  12. package/src/App.tsx +243 -284
  13. package/src/App.walk.test.tsx +1 -1
  14. package/src/app-screens.tsx +67 -52
  15. package/src/app-view-model.ts +6 -100
  16. package/src/components/AnnotationCard.stories.tsx +1 -1
  17. package/src/components/AnnotationCard.tsx +2 -2
  18. package/src/components/AppShell.stories.tsx +84 -0
  19. package/src/components/AppShell.tsx +127 -0
  20. package/src/components/Breadcrumb.stories.tsx +1 -1
  21. package/src/components/ChangesColumn.stories.tsx +39 -0
  22. package/src/components/ChangesColumn.tsx +147 -0
  23. package/src/components/CodeBlock.stories.tsx +1 -1
  24. package/src/components/CompletionOverlay.stories.tsx +1 -1
  25. package/src/components/ConfirmCard.stories.tsx +1 -1
  26. package/src/components/ConfirmCard.tsx +3 -4
  27. package/src/components/ConfirmDialog.stories.tsx +1 -1
  28. package/src/components/DiffSheet.stories.tsx +1 -1
  29. package/src/components/InboxList.stories.tsx +23 -8
  30. package/src/components/InboxList.tsx +45 -19
  31. package/src/components/KeybindsDialog.stories.tsx +1 -1
  32. package/src/components/KeybindsDialog.tsx +61 -17
  33. package/src/components/MarkerPopover.stories.tsx +1 -1
  34. package/src/components/MenuBar.stories.tsx +3 -12
  35. package/src/components/MenuBar.tsx +25 -56
  36. package/src/components/PromptDialog.stories.tsx +1 -1
  37. package/src/components/PrototypeSheet.stories.tsx +1 -1
  38. package/src/components/ReviewPanel.stories.tsx +15 -1
  39. package/src/components/ReviewRail.stories.tsx +15 -1
  40. package/src/components/ReviewRail.tsx +13 -6
  41. package/src/components/SettingsDialog.stories.tsx +7 -1
  42. package/src/components/SettingsDialog.tsx +112 -66
  43. package/src/components/SettingsRows.stories.tsx +1 -1
  44. package/src/components/ShellHeader.stories.tsx +50 -0
  45. package/src/components/ShellHeader.tsx +137 -0
  46. package/src/components/ThreadFooter.stories.tsx +30 -0
  47. package/src/components/ThreadFooter.tsx +49 -0
  48. package/src/components/ThreadView.selection.test.tsx +812 -0
  49. package/src/components/ThreadView.slash.test.tsx +113 -0
  50. package/src/components/ThreadView.stories.tsx +26 -0
  51. package/src/components/ThreadView.tsx +1708 -0
  52. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  53. package/src/components/ThreadsSidebar.tsx +57 -0
  54. package/src/components/Toast.stories.tsx +1 -1
  55. package/src/components/TreePane.stories.tsx +106 -0
  56. package/src/components/TreePane.tsx +156 -0
  57. package/src/components/WalkWizard.stories.tsx +1 -1
  58. package/src/components/__snapshots__/stories.test.tsx.snap +463 -305
  59. package/src/components/agent-launcher.stories.tsx +1 -1
  60. package/src/components/file-tree.test.ts +45 -0
  61. package/src/components/file-tree.ts +57 -0
  62. package/src/components/pages/Workspace.stories.tsx +181 -0
  63. package/src/components/primitives/Button.stories.tsx +1 -1
  64. package/src/components/primitives/Card.stories.tsx +1 -1
  65. package/src/components/primitives/Dialog.stories.tsx +1 -1
  66. package/src/components/primitives/IconButton.stories.tsx +36 -0
  67. package/src/components/primitives/IconButton.tsx +43 -0
  68. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  69. package/src/components/primitives/Tabs.stories.tsx +1 -1
  70. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  71. package/src/components/primitives/Tree.stories.tsx +124 -0
  72. package/src/components/primitives/Tree.tsx +87 -0
  73. package/src/components/primitives/icons.ts +38 -0
  74. package/src/components/primitives/tree-model.test.ts +99 -0
  75. package/src/components/primitives/tree-model.ts +137 -0
  76. package/src/components/quick-actions-editor.stories.tsx +1 -1
  77. package/src/components/session-tree.test.ts +114 -0
  78. package/src/components/session-tree.ts +94 -0
  79. package/src/components/stories-app.tsx +142 -82
  80. package/src/components/story-tree.test.ts +100 -0
  81. package/src/components/story.ts +55 -0
  82. package/src/components/theme-context.stories.tsx +1 -1
  83. package/src/diff-hunk-curate.ts +4 -104
  84. package/src/index.ts +1 -0
  85. package/src/intent-dispatch.test.ts +136 -3
  86. package/src/intent-dispatch.ts +92 -24
  87. package/src/key-bindings.test.ts +1 -1
  88. package/src/keymap.test.ts +4 -4
  89. package/src/keymap.ts +14 -7
  90. package/src/session-controller.curate.test.ts +89 -2
  91. package/src/session-controller.share.test.ts +195 -55
  92. package/src/session-controller.tree.test.ts +228 -0
  93. package/src/session-controller.ts +331 -72
  94. package/src/share.ts +114 -16
  95. package/src/test-support.test.ts +193 -1
  96. package/src/test-support.ts +64 -0
  97. package/src/theme.ts +4 -4
  98. package/src/thread-chords.test.ts +154 -0
  99. package/src/thread-chords.ts +144 -0
  100. package/src/thread-selection.test.ts +210 -0
  101. package/src/thread-selection.ts +243 -0
  102. package/src/tree-view.test.ts +128 -0
  103. package/src/tree-view.ts +148 -0
  104. package/src/use-settings-dialog.tsx +9 -0
  105. package/src/view-plan.ts +29 -12
  106. package/src/App.review-panel.test.tsx +0 -143
  107. package/src/components/PlanSheet.stories.tsx +0 -113
  108. package/src/components/PlanSheet.tsx +0 -419
  109. package/src/components/plan-sheet-run-style.ts +0 -88
@@ -1,4 +1,4 @@
1
- /** Observer mode (tier 2): the App with readOnly renders and navigates like the controller's, but every mutating verb answers "observer - read-only" and leaves daemon state untouched. */
1
+ /** Observer mode (tier 2): the App with readOnly renders and navigates like the controller's, but every mutating primitive answers "observer - read-only" and leaves daemon state untouched. */
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
4
  import { mkdtempSync, rmSync } from "node:fs";
@@ -9,8 +9,15 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { DARK } from "./theme";
13
- import { isolateUserConfig, press, waitForState, waitForText } from "./test-support";
12
+ import {
13
+ clickText,
14
+ dragText,
15
+ frameRow,
16
+ isolateUserConfig,
17
+ pressKey,
18
+ typeText,
19
+ waitForText,
20
+ } from "./test-support";
14
21
 
15
22
  const PLAN = `# Migration Plan
16
23
 
@@ -60,7 +67,9 @@ async function renderObserver() {
60
67
  return setup;
61
68
  }
62
69
 
63
- /** Session state that any mutating verb would change. */
70
+ type Setup = Awaited<ReturnType<typeof renderObserver>>;
71
+
72
+ /** Session state that any mutating primitive would change. */
64
73
  function snapshot() {
65
74
  const stored = server.core.sessionGet(session.id);
66
75
 
@@ -72,79 +81,110 @@ function snapshot() {
72
81
  }
73
82
 
74
83
  describe("observer rendering", () => {
75
- test("shows the observer badge and the bottom menu", async () => {
84
+ test("shows the observer badge in the header", async () => {
76
85
  // Arrange
77
86
  const setup = await renderObserver();
78
87
 
79
88
  // Assert
80
- const frame = setup.captureCharFrame();
81
-
82
- expect(frame).toContain("· observer");
83
- expect(frame).toContain("menu");
89
+ expect(setup.captureCharFrame()).toContain("· observer");
84
90
  });
85
91
  });
86
92
 
87
- describe("observer verbs are blocked", () => {
88
- for (const [key, verb] of [
89
- ["c", "comment"],
90
- ["x", "cut"],
91
- ["e", "edit"],
92
- ["enter", "submit"],
93
- ] as const) {
94
- test(`${verb} (${key}) answers observer - read-only and mutates nothing`, async () => {
93
+ describe("observer primitives are blocked", () => {
94
+ const attempts: Array<[string, (setup: Setup) => Promise<void>]> = [
95
+ ["comment (typing)", (setup) => typeText(setup, "c")],
96
+ ["cut (option+x)", (setup) => pressKey(setup, "x", { meta: true })],
97
+ ["edit (ctrl+e)", (setup) => pressKey(setup, "e", { ctrl: true })],
98
+ ["submit (cmd+enter)", (setup) => pressKey(setup, "RETURN", { meta: true })],
99
+ ];
100
+
101
+ for (const [primitive, attempt] of attempts) {
102
+ test(`${primitive} answers observer - read-only and mutates nothing`, async () => {
95
103
  const setup = await renderObserver();
96
104
  const before = snapshot();
97
105
 
98
- await press(setup, "j");
99
- await press(setup, "j");
100
- await press(setup, key);
106
+ await clickText(setup, "daemon");
107
+ await attempt(setup);
101
108
  const frame = await waitForText(setup, "observer - read-only");
102
109
 
103
- // no overlay opened: compose/submit bars never appear
104
- expect(frame).not.toContain('comment on "');
105
- expect(frame).not.toContain("verdict ←/→");
110
+ // no draft card and no submit card ever appear
111
+ expect(frame).not.toContain("● c");
112
+ expect(frame).not.toContain("[Approve]");
106
113
  expect(snapshot()).toEqual(before);
107
114
  });
108
115
  }
109
116
 
110
- test("span mode c is blocked too", async () => {
117
+ test("typing over a marked span is blocked too", async () => {
111
118
  // Arrange
112
119
  const setup = await renderObserver();
113
120
 
114
121
  // Act
115
- await press(setup, "j");
116
- await press(setup, "j");
117
- await press(setup, "v");
118
- await press(setup, "c");
119
- await setup.renderOnce();
122
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
123
+ await typeText(setup, "c");
120
124
 
121
125
  // Assert
122
- const frame = setup.captureCharFrame();
126
+ const frame = await waitForText(setup, "observer - read-only");
123
127
 
124
- expect(frame).toContain("observer - read-only");
125
- expect(frame).not.toContain('comment on "');
128
+ expect(frame).not.toContain("● c");
126
129
  expect(snapshot().annotations).toBe(0);
127
130
  });
128
131
  });
129
132
 
133
+ describe("a resolved review is read-only for its owner too", () => {
134
+ test("typing, deleting a card, and editing answer review submitted - read-only", async () => {
135
+ // Arrange: a comment exists and the verdict is in
136
+ server.core.sessionAnnotate(session.id, {
137
+ id: "a_done",
138
+ kind: "comment",
139
+ anchor: makeAnchor(parseBlocks(PLAN), 2, 0, 10),
140
+ body: "settled",
141
+ });
142
+ server.core.sessionResolve(session.id, "approve", "");
143
+ const setup = await testRender(<App home={home} sessionId={session.id} />, {
144
+ width: 120,
145
+ height: 32,
146
+ });
147
+
148
+ await waitForText(setup, "settled");
149
+ const before = snapshot();
150
+
151
+ // Act + Assert: a draft never opens
152
+ await clickText(setup, "daemon");
153
+ await typeText(setup, "c");
154
+ await waitForText(setup, "review submitted - read-only");
155
+ expect(setup.captureCharFrame()).not.toContain("● c");
156
+
157
+ // Act + Assert: the focused card cannot be deleted or edited
158
+ await pressKey(setup, "n", { meta: true });
159
+ await pressKey(setup, "BACKSPACE", { meta: true });
160
+ await pressKey(setup, "e", { meta: true });
161
+ await pressKey(setup, "x", { meta: true });
162
+ expect(snapshot()).toEqual(before);
163
+ expect(server.core.sessionGet(session.id).annotations).toHaveLength(1);
164
+ });
165
+ });
166
+
130
167
  describe("observer navigation still works", () => {
131
- test("j/k moves the cursor between blocks", async () => {
168
+ test("↓ moves the caret between blocks", async () => {
132
169
  // Arrange
133
170
  const setup = await renderObserver();
134
171
 
135
172
  // Act
136
- await press(setup, "j");
137
- await press(setup, "j");
173
+ await pressKey(setup, "ARROW_DOWN");
174
+ await pressKey(setup, "ARROW_DOWN");
138
175
  await setup.renderOnce();
139
176
 
140
- // Assert
141
- const lines = setup.captureCharFrame().split("\n");
142
- const cursorLine = lines.find((line) => line.includes("▎"))!;
177
+ // Assert: the caret cell is painted on the paragraph's row
178
+ const caretRow = setup
179
+ .captureSpans()
180
+ .lines.findIndex((line) =>
181
+ line.spans.some((span) => span.bg.toInts().slice(0, 3).join() === "86,91,104"),
182
+ );
143
183
 
144
- expect(cursorLine).toContain("persists sessions");
184
+ expect(caretRow).toBe(frameRow(setup, "persists sessions"));
145
185
  });
146
186
 
147
- test("n focuses annotations made by the controller", async () => {
187
+ test("option+n focuses annotations made by the controller", async () => {
148
188
  // Arrange
149
189
  server.core.sessionAnnotate(session.id, {
150
190
  id: "a_obs1",
@@ -154,29 +194,12 @@ describe("observer navigation still works", () => {
154
194
  });
155
195
  const setup = await renderObserver();
156
196
 
157
- // Act
158
- await press(setup, "n");
197
+ // Act - the observer navigates to the annotation without mutating anything
198
+ await pressKey(setup, "n", { meta: true });
159
199
  await setup.renderOnce();
160
200
 
161
- // Assert - focus shows as the card's blue border (transparent fill); a
162
- // comment card is the only blue-bordered box in the frame
163
- await waitForText(setup, "COMMENT · me");
164
- await waitForState(setup, () => hasBorderColor(setup, DARK.blue));
201
+ // Assert - the observer reads the controller's comment inline in the thread
202
+ await waitForText(setup, "From the controller.");
203
+ expect(snapshot().annotations).toBe(1);
165
204
  });
166
205
  });
167
-
168
- /** Whether any border character (box-drawing) is painted in the given foreground hex. */
169
- function hasBorderColor(setup: Awaited<ReturnType<typeof renderObserver>>, hex: string): boolean {
170
- for (const line of setup.captureSpans().lines) {
171
- for (const span of line.spans) {
172
- if (!/[╭╮╰╯│─┌┐└┘]/.test(span.text)) continue;
173
- const [red, green, blue] = span.fg.toInts();
174
- const rendered =
175
- "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
176
-
177
- if (rendered === hex) return true;
178
- }
179
- }
180
-
181
- return false;
182
- }
@@ -83,10 +83,9 @@ describe("code blocks", () => {
83
83
  });
84
84
 
85
85
  describe("block spacing", () => {
86
- // the review panel's full-height divider paints a `│` on every row, so read
87
- // the plan column (everything left of the divider) before asserting spacing
88
- // the plan body is framed: content sits between the left border and the next │
89
- const planColumn = (line: string): string => line.split("│")[1]?.trimEnd() ?? "";
86
+ // the review rail paints box edges on the right, so read the document
87
+ // column (everything left of the first rail glyph) before asserting spacing
88
+ const planColumn = (line: string): string => line.split(/[│╭╰]/)[0]!.trimEnd();
90
89
 
91
90
  test("a code block never glues to the list item above it", async () => {
92
91
  // Arrange
@@ -129,8 +128,8 @@ describe("block spacing", () => {
129
128
  .captureCharFrame()
130
129
  .split("\n")
131
130
  .map((line) => line.trimEnd());
132
- const one = lines.findIndex((line) => line.includes("- one"));
131
+ const one = lines.findIndex((line) => line.includes("· one"));
133
132
 
134
- expect(lines[one + 1]).toContain("- two");
133
+ expect(lines[one + 1]).toContain("· two");
135
134
  });
136
135
  });
@@ -1,4 +1,4 @@
1
- /** The rail submit confirm (tier 2): pressing submit expands the rail's Submit button into a bordered confirm card - counts, verdict selector, summary input, word-buttons. Char-frame assertions over the real App and an in-process daemon, like App.test.tsx. */
1
+ /** The send-message confirm: cmd+enter opens a centered overlay with a verdict selector, summary input, and word-buttons. Char-frame assertions over the real App and an in-process daemon, like App.test.tsx. */
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
4
  import { mkdtempSync, rmSync } from "node:fs";
@@ -9,7 +9,14 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { isolateUserConfig, press, settle, waitForText, waitForTextGone } from "./test-support";
12
+ import {
13
+ isolateUserConfig,
14
+ press,
15
+ pressKey,
16
+ settle,
17
+ waitForText,
18
+ waitForTextGone,
19
+ } from "./test-support";
13
20
 
14
21
  const PLAN = `# Migration Plan
15
22
 
@@ -76,33 +83,31 @@ function seedAnnotations(count: number): void {
76
83
  }
77
84
  }
78
85
 
79
- describe("rail submit confirm", () => {
80
- test("submit expands the Submit button into the confirm card", async () => {
86
+ describe("send message confirm", () => {
87
+ test("cmd+enter opens the send-message confirm overlay", async () => {
81
88
  // Arrange
82
89
  const setup = await renderApp();
83
90
 
84
- // Assert
85
- expect(setup.captureCharFrame()).toContain("Submit review");
91
+ // Assert - the footer carries the send control before the overlay opens
92
+ expect(setup.captureCharFrame()).toContain("send message");
86
93
 
87
94
  // Act
88
- await press(setup, "enter");
95
+ await pressKey(setup, "RETURN", { meta: true });
89
96
 
90
- // Assert
91
- await waitForText(setup, "submit review");
97
+ // Assert - the overlay: verdict selector and word-buttons
98
+ await waitForText(setup, "[Approve]"); // nothing pending: approve default
92
99
  const frame = setup.captureCharFrame();
93
100
 
94
- // the card: title, verdict selector, word-buttons
95
- expect(frame).toContain("submit review");
96
- expect(frame).toContain("[Approve]"); // nothing pending: approve default
97
- expect(frame).toContain(" Submit ");
101
+ expect(frame).toContain("[Approve]");
102
+ expect(frame).toContain(" send message ");
98
103
  expect(frame).toContain(" Cancel ");
99
104
  });
100
105
 
101
- test("left/right cycles the verdict selector in the card", async () => {
106
+ test("left/right cycles the verdict selector in the overlay", async () => {
102
107
  // Arrange
103
108
  const setup = await renderApp();
104
109
 
105
- await press(setup, "enter");
110
+ await pressKey(setup, "RETURN", { meta: true });
106
111
 
107
112
  // Assert
108
113
  await waitForText(setup, "[Approve]");
@@ -126,11 +131,11 @@ describe("rail submit confirm", () => {
126
131
  await waitForText(setup, "[Changes]");
127
132
  });
128
133
 
129
- test("esc cancels the card and restores the plain Submit button", async () => {
134
+ test("esc closes the overlay", async () => {
130
135
  // Arrange
131
136
  const setup = await renderApp();
132
137
 
133
- await press(setup, "enter");
138
+ await pressKey(setup, "RETURN", { meta: true });
134
139
 
135
140
  // Assert
136
141
  await waitForText(setup, "[Approve]");
@@ -138,34 +143,28 @@ describe("rail submit confirm", () => {
138
143
  // Act
139
144
  await press(setup, "escape");
140
145
 
141
- // Assert
142
- // a bare ESC settles after the parser's escape-sequence window
146
+ // Assert - a bare ESC settles after the parser's escape-sequence window
143
147
  const frame = await waitForTextGone(setup, "[Approve]");
144
148
 
145
- expect(frame).not.toContain("submit review");
146
- expect(frame).toContain("Submit review");
149
+ expect(frame).not.toContain(" Cancel ");
147
150
  });
148
151
 
149
- test("enter in the card resolves the session through the controller", async () => {
152
+ test("enter in the overlay resolves the session through the controller", async () => {
150
153
  // Arrange
151
154
  seedAnnotations(1);
152
155
  const setup = await renderApp();
153
156
 
154
157
  // Act
155
- await press(setup, "enter");
158
+ await pressKey(setup, "RETURN", { meta: true });
156
159
 
157
160
  // Assert
158
161
  await waitForText(setup, "[Changes]"); // pending items: request changes default
159
- const frame = setup.captureCharFrame();
160
-
161
- expect(frame).toContain("submit review");
162
162
 
163
163
  // Act
164
164
  await setup.mockInput.typeText("Tighten the steps.");
165
165
  await press(setup, "enter");
166
166
 
167
- // Assert
168
- // the completion flow after submit is unchanged
167
+ // Assert - the completion flow after submit is unchanged
169
168
  await waitForText(setup, "feedback sent");
170
169
  const stored = server.core.sessionGet(session.id);
171
170
 
@@ -173,57 +172,20 @@ describe("rail submit confirm", () => {
173
172
  expect(stored.verdict!.kind).toBe("request_changes");
174
173
  });
175
174
 
176
- test("the annotation stack stays scrollable while the card is open", async () => {
177
- // Arrange
178
- seedAnnotations(12);
179
- const setup = await renderApp();
180
-
181
- // Act
182
- // walk annotation focus to the last card: the rail scrolls it into view
183
- for (let index = 0; index < 12; index++) await press(setup, "n");
184
-
185
- // Assert
186
- await waitForText(setup, "note 12");
187
- expect(setup.captureCharFrame()).not.toContain("note 01");
188
-
189
- // Act
190
- await press(setup, "enter");
191
-
192
- // Assert
193
- await waitForText(setup, "[Changes]");
194
- // the card is pinned outside the scrollbox; the stack stays scrolled off the top
195
- expect(setup.captureCharFrame()).not.toContain("note 01");
196
-
197
- // Act
198
- // the stack still scrolls with the card open: wheel over the rail
199
- // brings the last card back into view while the confirm card stays put
200
- for (let turn = 0; turn < 12; turn++) await setup.mockMouse.scroll(100, 10, "down");
201
- await settle(setup);
202
-
203
- // Assert
204
- await waitForText(setup, "note 12");
205
- const scrolledWithCard = setup.captureCharFrame();
206
-
207
- expect(scrolledWithCard).toContain("submit review");
208
- expect(scrolledWithCard).not.toContain("note 01");
209
- });
210
-
211
- test("read-only observers never see the confirm card", async () => {
175
+ test("read-only observers cannot open the confirm overlay", async () => {
212
176
  // Arrange
213
177
  seedAnnotations(1);
214
178
  const setup = await renderApp({ readOnly: true });
215
179
 
216
180
  // Act
217
- await press(setup, "enter");
181
+ await pressKey(setup, "RETURN", { meta: true });
182
+ await settle(setup);
218
183
 
219
- // Assert
220
- await waitForText(setup, "observer - read-only");
184
+ // Assert - no overlay, and the session stays pending
221
185
  const frame = setup.captureCharFrame();
222
186
 
223
- expect(frame).toContain("observer - read-only");
224
- expect(frame).not.toContain("1 annotations");
225
187
  expect(frame).not.toContain("[Changes]");
226
- expect(setup.captureCharFrame()).not.toContain("verdict ←/→");
188
+ expect(frame).not.toContain(" Cancel ");
227
189
  expect(server.core.sessionGet(session.id).status).toBe("pending");
228
190
  });
229
191
  });