@cueloop/client 0.1.0-alpha.42 → 0.1.0-alpha.44

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.
@@ -0,0 +1,51 @@
1
+ #!/bin/sh
2
+ # Build libcuelooppty: the flat forkpty(3) FFI shim in src/pty.zig. Pure libc, so
3
+ # unlike build.sh this needs no Ghostty clone - just the pinned Zig toolchain.
4
+ # Output lands in native/<os>-<arch>/libcuelooppty.{dylib,so}. Requires curl.
5
+ set -eu
6
+
7
+ ZIG_VERSION="0.16.0"
8
+
9
+ here="$(cd "$(dirname "$0")" && pwd)"
10
+ work="${TMPDIR:-/tmp}/cueloop-libghostty-build"
11
+ mkdir -p "$work"
12
+ export ZIG_GLOBAL_CACHE_DIR="$work/zig-gcache"
13
+ export ZIG_LOCAL_CACHE_DIR="$work/zig-lcache"
14
+ cd "$work"
15
+
16
+ arch="$(uname -m)"; os="$(uname -s | tr '[:upper:]' '[:lower:]')"
17
+ case "$os" in darwin) zig_os="macos"; dylib="libcuelooppty.dylib";; linux) zig_os="linux"; dylib="libcuelooppty.so";; *) echo "unsupported os $os" >&2; exit 1;; esac
18
+ case "$arch-$os" in
19
+ arm64-darwin|aarch64-linux) zig_arch="aarch64"; bun_arch="arm64";;
20
+ x86_64-darwin|x86_64-linux) zig_arch="x86_64"; bun_arch="x64";;
21
+ *) echo "unsupported architecture $arch on $os" >&2; exit 1;;
22
+ esac
23
+ case "$zig_arch-$zig_os" in
24
+ aarch64-macos) zig_sha="b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489";;
25
+ x86_64-macos) zig_sha="0387557ed1877bc6a2e1802c8391953baddba76081876301c522f52977b52ba7";;
26
+ aarch64-linux) zig_sha="ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17";;
27
+ x86_64-linux) zig_sha="70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00";;
28
+ esac
29
+ outdir="$here/${os}-${bun_arch}"
30
+ zig_archive="zig-$zig_arch-$zig_os-$ZIG_VERSION.tar.xz"
31
+
32
+ if [ ! -f "$zig_archive" ]; then
33
+ echo "== fetching zig $ZIG_VERSION =="
34
+ curl -fsSL -o "$zig_archive" "https://ziglang.org/download/$ZIG_VERSION/$zig_archive"
35
+ fi
36
+ printf '%s %s\n' "$zig_sha" "$zig_archive" | shasum -a 256 -c -
37
+
38
+ build_root="$(mktemp -d "$work/native-pty-build.XXXXXX")"
39
+ trap 'rm -rf "$build_root"' EXIT HUP INT TERM
40
+ tar -xf "$zig_archive" -C "$build_root"
41
+ zig="$build_root/zig-$zig_arch-$zig_os-$ZIG_VERSION/zig"
42
+
43
+ echo "== building pty shim -> $outdir/$dylib =="
44
+ mkdir -p "$outdir"
45
+ if [ "$os" = "darwin" ]; then
46
+ "$zig" build-lib -dynamic -OReleaseFast -lc -femit-bin="$outdir/$dylib" \
47
+ -install_name "@rpath/$dylib" "$here/src/pty.zig"
48
+ else
49
+ "$zig" build-lib -dynamic -OReleaseFast -lc -femit-bin="$outdir/$dylib" "$here/src/pty.zig"
50
+ fi
51
+ echo "== done: $outdir/$dylib =="
@@ -0,0 +1,193 @@
1
+ // Flat, FFI-trivial pseudo-terminal over libc forkpty(3). Exposes exactly what
2
+ // the embedded Agent-tab terminal needs: spawn a child on a PTY, non-blocking
3
+ // read of its output, write, resize, and reap. Handles are small ints into a
4
+ // fixed table so bun:ffi only ever passes ints and byte pointers, never structs.
5
+ //
6
+ // Packed-argument contract (avoids shell parsing and per-token FFI marshalling):
7
+ // argv_packed: "arg0\0arg1\0...\0" - NUL-separated, terminated by an empty
8
+ // token (a trailing extra NUL).
9
+ // env_packed: "K=V\0K=V\0...\0" - same shape; empty means inherit parent.
10
+ // cwd: a plain C string, or empty to keep the parent's directory.
11
+
12
+ const std = @import("std");
13
+ const builtin = @import("builtin");
14
+
15
+ const c = @cImport({
16
+ @cInclude("unistd.h");
17
+ @cInclude("fcntl.h");
18
+ @cInclude("errno.h");
19
+ @cInclude("signal.h");
20
+ @cInclude("stdlib.h");
21
+ @cInclude("string.h");
22
+ @cInclude("termios.h");
23
+ @cInclude("sys/ioctl.h");
24
+ @cInclude("sys/wait.h");
25
+ if (builtin.os.tag == .macos) @cInclude("util.h") else @cInclude("pty.h");
26
+ });
27
+
28
+ extern var environ: [*c][*c]u8;
29
+
30
+ const MAX_SESSIONS = 64;
31
+ const MAX_TOKENS = 1024;
32
+
33
+ const PtySession = struct {
34
+ in_use: bool = false,
35
+ master_fd: c_int = -1,
36
+ pid: c.pid_t = 0,
37
+ reaped: bool = false,
38
+ exit_code: c_int = 0,
39
+ };
40
+
41
+ var sessions = [_]PtySession{.{}} ** MAX_SESSIONS;
42
+
43
+ // Split a double-NUL-terminated packed buffer into a NULL-terminated array of
44
+ // pointers into that same buffer. Returns the token count, or -1 on overflow.
45
+ fn splitPacked(packed_buffer: [*c]u8, out: [][*c]u8) c_int {
46
+ var count: usize = 0;
47
+ var cursor: [*c]u8 = packed_buffer;
48
+ while (cursor[0] != 0) {
49
+ if (count >= out.len - 1) return -1;
50
+ out[count] = cursor;
51
+ count += 1;
52
+ cursor += c.strlen(cursor) + 1;
53
+ }
54
+ out[count] = null;
55
+ return @intCast(count);
56
+ }
57
+
58
+ // Reap the child once, recording its exit status. WNOHANG unless `block`.
59
+ fn reapSession(session: *PtySession, block: bool) void {
60
+ if (session.reaped) return;
61
+ var status: c_int = 0;
62
+ const result = c.waitpid(session.pid, &status, if (block) 0 else c.WNOHANG);
63
+ if (result != session.pid) return;
64
+ session.reaped = true;
65
+ if (c.WIFEXITED(status)) {
66
+ session.exit_code = c.WEXITSTATUS(status);
67
+ } else if (c.WIFSIGNALED(status)) {
68
+ session.exit_code = 128 + c.WTERMSIG(status);
69
+ } else {
70
+ session.exit_code = -1;
71
+ }
72
+ }
73
+
74
+ fn sessionOf(handle: c_int) ?*PtySession {
75
+ if (handle < 0 or handle >= MAX_SESSIONS) return null;
76
+ const session = &sessions[@intCast(handle)];
77
+ if (!session.in_use) return null;
78
+ return session;
79
+ }
80
+
81
+ // Spawn `argv_packed` on a fresh PTY sized cols x rows, in cwd, with env_packed
82
+ // (or the parent's env when empty). Returns a session handle, or -1 on failure.
83
+ export fn cueloop_pty_spawn(argv_packed: [*c]u8, cwd: [*c]const u8, env_packed: [*c]u8, cols: c_int, rows: c_int) c_int {
84
+ var handle: c_int = -1;
85
+ for (0..MAX_SESSIONS) |index| {
86
+ if (!sessions[index].in_use) {
87
+ handle = @intCast(index);
88
+ break;
89
+ }
90
+ }
91
+ if (handle < 0) return -1;
92
+
93
+ var argv: [MAX_TOKENS][*c]u8 = undefined;
94
+ if (splitPacked(argv_packed, &argv) < 1) return -1;
95
+
96
+ var envp: [MAX_TOKENS][*c]u8 = undefined;
97
+ const have_env = env_packed != null and env_packed[0] != 0;
98
+ if (have_env and splitPacked(env_packed, &envp) < 0) return -1;
99
+
100
+ var winsize = std.mem.zeroes(c.struct_winsize);
101
+ winsize.ws_col = @intCast(cols);
102
+ winsize.ws_row = @intCast(rows);
103
+
104
+ var master_fd: c_int = -1;
105
+ const pid = c.forkpty(&master_fd, null, null, &winsize);
106
+ if (pid < 0) return -1;
107
+
108
+ if (pid == 0) {
109
+ // Child: adopt cwd/env then exec; _exit on any failure so a broken child never runs JS.
110
+ if (cwd != null and cwd[0] != 0) {
111
+ if (c.chdir(cwd) != 0) c._exit(127);
112
+ }
113
+ if (have_env) environ = &envp;
114
+ _ = c.execvp(argv[0], &argv);
115
+ c._exit(127);
116
+ }
117
+
118
+ // Parent: non-blocking master so reads can poll without stalling the loop.
119
+ const flags = c.fcntl(master_fd, c.F_GETFL, @as(c_int, 0));
120
+ _ = c.fcntl(master_fd, c.F_SETFL, flags | c.O_NONBLOCK);
121
+
122
+ sessions[@intCast(handle)] = .{
123
+ .in_use = true,
124
+ .master_fd = master_fd,
125
+ .pid = pid,
126
+ .reaped = false,
127
+ .exit_code = 0,
128
+ };
129
+ return handle;
130
+ }
131
+
132
+ export fn cueloop_pty_write(handle: c_int, data: [*c]const u8, len: c_int) c_int {
133
+ const session = sessionOf(handle) orelse return -1;
134
+ const written = c.write(session.master_fd, data, @intCast(len));
135
+ return @intCast(written);
136
+ }
137
+
138
+ // Non-blocking read of child output. Returns bytes read (>0), 0 when nothing is
139
+ // available yet, -2 once the child has exited and its output is drained, or -1.
140
+ export fn cueloop_pty_read(handle: c_int, buf: [*c]u8, len: c_int) c_int {
141
+ const session = sessionOf(handle) orelse return -1;
142
+ const bytes_read = c.read(session.master_fd, buf, @intCast(len));
143
+ if (bytes_read > 0) return @intCast(bytes_read);
144
+ if (bytes_read == 0) {
145
+ reapSession(session, true);
146
+ return -2; // EOF: slave closed
147
+ }
148
+ const err = std.c._errno().*;
149
+ if (err == c.EAGAIN or err == c.EWOULDBLOCK) {
150
+ reapSession(session, false);
151
+ return if (session.reaped) -2 else 0; // exited with no more output vs still running
152
+ }
153
+ if (err == c.EIO) {
154
+ reapSession(session, true);
155
+ return -2; // Linux: slave gone
156
+ }
157
+ return -1;
158
+ }
159
+
160
+ export fn cueloop_pty_resize(handle: c_int, cols: c_int, rows: c_int) c_int {
161
+ const session = sessionOf(handle) orelse return -1;
162
+ var winsize = std.mem.zeroes(c.struct_winsize);
163
+ winsize.ws_col = @intCast(cols);
164
+ winsize.ws_row = @intCast(rows);
165
+ return c.ioctl(session.master_fd, c.TIOCSWINSZ, &winsize);
166
+ }
167
+
168
+ export fn cueloop_pty_kill(handle: c_int) c_int {
169
+ const session = sessionOf(handle) orelse return -1;
170
+ return c.kill(session.pid, c.SIGTERM);
171
+ }
172
+
173
+ export fn cueloop_pty_get_pid(handle: c_int) c_int {
174
+ const session = sessionOf(handle) orelse return -1;
175
+ return @intCast(session.pid);
176
+ }
177
+
178
+ export fn cueloop_pty_get_exit_code(handle: c_int) c_int {
179
+ const session = sessionOf(handle) orelse return -1;
180
+ reapSession(session, true);
181
+ return session.exit_code;
182
+ }
183
+
184
+ export fn cueloop_pty_close(handle: c_int) void {
185
+ const session = sessionOf(handle) orelse return;
186
+ // Close the master first (hangs up the child), then block to reap it: after the
187
+ // SIGTERM from kill() plus this SIGHUP a well-behaved child exits at once, so the
188
+ // wait returns promptly and never leaves a zombie.
189
+ if (session.master_fd >= 0) _ = c.close(session.master_fd);
190
+ reapSession(session, true);
191
+ session.in_use = false;
192
+ session.master_fd = -1;
193
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.42",
3
+ "version": "0.1.0-alpha.44",
4
4
  "description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -26,16 +26,15 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@cueloop/daemon": "0.1.0-alpha.42",
30
- "@cueloop/extension-api": "0.1.0-alpha.42",
31
- "@cueloop/integration-obsidian": "0.1.0-alpha.42",
32
- "@cueloop/schema": "0.1.0-alpha.42",
29
+ "@cueloop/daemon": "0.1.0-alpha.44",
30
+ "@cueloop/extension-api": "0.1.0-alpha.44",
31
+ "@cueloop/integration-obsidian": "0.1.0-alpha.44",
32
+ "@cueloop/schema": "0.1.0-alpha.44",
33
33
  "@opentui/core": "^0.5.2",
34
34
  "@opentui/keymap": "0.5.2",
35
35
  "@opentui/react": "^0.5.2",
36
36
  "@opentui/ssh": "0.5.2",
37
37
  "@pierre/diffs": "^1.3.5",
38
- "bun-pty": "^0.4.10",
39
38
  "diff": "^8",
40
39
  "react": "^19.2.0"
41
40
  },
@@ -87,26 +87,20 @@ function backgroundsOf(setup: Setup, needle: string): string[] {
87
87
  return backgrounds;
88
88
  }
89
89
 
90
- /** Whether any border character (box-drawing) is painted in the given foreground hex. */
91
- function hasBorderColor(setup: Setup, hex: string): boolean {
92
- for (const line of setup.captureSpans().lines) {
93
- for (const span of line.spans) {
94
- if (!/[╭╮╰╯│─┌┐└┘]/.test(span.text)) continue;
95
- const [red, green, blue] = span.fg.toInts();
96
- const rendered =
97
- "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
98
- if (rendered === hex) return true;
99
- }
100
- }
101
- return false;
102
- }
103
-
104
90
  /** Move the cursor to "The daemon persists sessions to disk atomically." */
105
91
  async function toContextParagraph(setup: Setup): Promise<void> {
106
92
  await press(setup, "j");
107
93
  await press(setup, "j");
108
94
  }
109
95
 
96
+ /** Click the first on-screen occurrence of a string, by its char-frame position. */
97
+ async function clickText(setup: Setup, needle: string): Promise<void> {
98
+ const lines = setup.captureCharFrame().split("\n");
99
+ const row = lines.findIndex((line) => line.includes(needle));
100
+ await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
101
+ await setup.renderOnce();
102
+ }
103
+
110
104
  describe("share button", () => {
111
105
  test("the owner's plan header shows the Share button next to Edit", async () => {
112
106
  // Arrange / Act
@@ -296,6 +290,45 @@ describe("inline compose keeps the anchor painted", () => {
296
290
  // runner, and the whole-suite publish lane has timed even 15s out; give the
297
291
  // heaviest frame-wait chain generous headroom so runner load cannot flake it.
298
292
  }, 60_000);
293
+
294
+ test("clicking the Cancel word-button closes the compose box", async () => {
295
+ // Arrange
296
+ const setup = await renderApp();
297
+ await toContextParagraph(setup);
298
+ await press(setup, "v");
299
+ await press(setup, "l");
300
+ await press(setup, "c");
301
+ await waitForText(setup, "Cancel");
302
+
303
+ // Act
304
+ await clickText(setup, "Cancel");
305
+
306
+ // Assert
307
+ await waitForTextGone(setup, 'comment on "');
308
+ });
309
+
310
+ test("clicking Cancel in a saved card's edit composer closes it", async () => {
311
+ // Arrange
312
+ const setup = await renderApp();
313
+ await toContextParagraph(setup);
314
+ await press(setup, "v");
315
+ await press(setup, "l");
316
+ await press(setup, "c");
317
+ await waitForText(setup, "Cancel");
318
+ await type(setup, "note body");
319
+ await press(setup, "enter");
320
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
321
+ // select the saved card, then activate it to open the in-place editor
322
+ await clickText(setup, "note body");
323
+ await clickText(setup, "note body");
324
+ await waitForText(setup, "Cancel");
325
+
326
+ // Act
327
+ await clickText(setup, "Cancel");
328
+
329
+ // Assert - the editor closes back to the plain saved card
330
+ await waitForTextGone(setup, "Cancel");
331
+ });
299
332
  });
300
333
 
301
334
  describe("compose newline convention", () => {
@@ -525,7 +558,7 @@ describe("sheet header", () => {
525
558
  expect(frame).toContain("Share");
526
559
  });
527
560
 
528
- test("the Agent tab shows the harness launcher and a compact status footer", async () => {
561
+ test("the Agent tab shows the text-only harness launcher buttons", async () => {
529
562
  // Arrange
530
563
  const setup = await renderApp();
531
564
  const lines = setup.captureCharFrame().split("\n");
@@ -536,12 +569,11 @@ describe("sheet header", () => {
536
569
  await setup.mockMouse.click(tabColumn + 1, tabRow);
537
570
  await setup.renderOnce();
538
571
 
539
- // Assert - branded launcher cards, and the submitter kept in the footer
540
- await waitForText(setup, "claude code");
572
+ // Assert - the three launcher buttons, in order
573
+ await waitForText(setup, "Claude Code");
541
574
  const frame = setup.captureCharFrame();
542
- expect(frame).toContain("Ask an agent about this plan");
543
- expect(frame).toContain("codex");
544
- expect(frame).toContain("agent/worker-3");
575
+ expect(frame).toContain("Pi");
576
+ expect(frame).toContain("OpenAI Codex");
545
577
  });
546
578
  });
547
579
 
@@ -549,18 +581,12 @@ describe("quick-actions settings editor", () => {
549
581
  test("expanding an action edits its system prompt inline and persists it", async () => {
550
582
  // Arrange
551
583
  const setup = await renderApp();
552
- const clickText = async (needle: string): Promise<void> => {
553
- const lines = setup.captureCharFrame().split("\n");
554
- const row = lines.findIndex((line) => line.includes(needle));
555
- await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
556
- await setup.renderOnce();
557
- };
558
584
 
559
585
  // Act - open Settings, enter Actions, expand the first action, type into its input
560
- await clickText("menu");
561
- await clickText("Settings");
562
- await clickText("Actions");
563
- await clickText("Zoom out, research in depth");
586
+ await clickText(setup, "menu");
587
+ await clickText(setup, "Settings");
588
+ await clickText(setup, "Actions");
589
+ await clickText(setup, "Zoom out, research in depth");
564
590
  await type(setup, "CUSTOM");
565
591
  await setup.renderOnce();
566
592
 
@@ -589,9 +615,9 @@ describe("edit-exit reconciliation", () => {
589
615
 
590
616
  // Act
591
617
  // deselect the card so e reaches the editor hand-off, then edit; deselection
592
- // shows as the card's accent border fading to a dim border (transparent fill)
618
+ // shows as the card's selected fill (elevated) clearing to a transparent card
593
619
  await press(setup, "escape");
594
- await waitForState(setup, () => !hasBorderColor(setup, DARK.accent));
620
+ await waitForState(setup, () => !backgroundsOf(setup, "Anchor me").includes(DARK.elevated));
595
621
  await press(setup, "e");
596
622
 
597
623
  // Assert
@@ -29,6 +29,7 @@ export const SavedSelected: Story = {
29
29
  isOrphan: false,
30
30
  isBlocking: false,
31
31
  author: "me",
32
+ tone: DARK.accent,
32
33
  editing: null,
33
34
  onPress: () => {},
34
35
  }}
@@ -48,12 +49,13 @@ export const SavedCollaborator: Story = {
48
49
  isOrphan: false,
49
50
  isBlocking: false,
50
51
  author: "Priya",
52
+ tone: DARK.blue,
51
53
  editing: null,
52
54
  onPress: () => {},
53
55
  }}
54
56
  />
55
57
  ),
56
- expectedColors: [DARK.border],
58
+ expectedColors: [DARK.blue],
57
59
  };
58
60
 
59
61
  export const SavedOrphanBlocking: Story = {
@@ -67,12 +69,13 @@ export const SavedOrphanBlocking: Story = {
67
69
  isOrphan: true,
68
70
  isBlocking: true,
69
71
  author: "me",
72
+ tone: DARK.accent,
70
73
  editing: null,
71
74
  onPress: () => {},
72
75
  }}
73
76
  />
74
77
  ),
75
- expectedColors: [DARK.border],
78
+ expectedColors: [DARK.accent],
76
79
  };
77
80
 
78
81
  export const SavedEditing: Story = {
@@ -86,6 +89,7 @@ export const SavedEditing: Story = {
86
89
  isOrphan: false,
87
90
  isBlocking: false,
88
91
  author: "me",
92
+ tone: DARK.accent,
89
93
  editing: { text: "new body", ...draftCallbacks },
90
94
  onPress: () => {},
91
95
  }}
@@ -34,6 +34,8 @@ export interface AnnotationSaved {
34
34
  isBlocking: boolean;
35
35
  /** Border-title author: a collaborator's display name, or "me" for the reviewer's own note. */
36
36
  author: string;
37
+ /** Border + title color for this card (own = salmon, collaborator = blue). */
38
+ tone: string;
37
39
  /** Non-null while the card body is being rewritten in place. */
38
40
  editing: AnnotationDraft | null;
39
41
  onPress: () => void;
@@ -202,9 +204,11 @@ export function AnnotationCard({
202
204
  height: cardHeight(contentRows),
203
205
  border: true,
204
206
  borderStyle: FRAME_BORDER_STYLE,
205
- // cards stay transparent in every theme; selection is the bright
206
- // kind-colored border, while unselected cards recede to a dim border
207
- borderColor: card.isSelected || card.editing ? kindColor : tokens.border,
207
+ // the border (and its title) always wears the card's tone - salmon for
208
+ // own notes, blue for a collaborator's; selection reads from a filled
209
+ // background so the color stays put whether or not the card is active
210
+ borderColor: card.tone,
211
+ backgroundColor: card.isSelected || card.editing ? tokens.elevated : "transparent",
208
212
  paddingLeft: 1,
209
213
  paddingRight: 1,
210
214
  }}
@@ -103,7 +103,7 @@ export function ConfirmCard({
103
103
  <Card
104
104
  title=" submit review "
105
105
  contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 1 : 0)}
106
- borderColor={tokens.accent}
106
+ borderColor={tokens.text}
107
107
  backgroundColor="transparent"
108
108
  marginRight={1}
109
109
  theme={theme}
@@ -140,6 +140,8 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
140
140
  : isAgentNote(annotation)
141
141
  ? "agent"
142
142
  : "me",
143
+ // own + agent notes read as salmon; a share collaborator's note as blue
144
+ tone: annotation.author && !isAgentNote(annotation) ? tokens.blue : tokens.accent,
143
145
  editing:
144
146
  cardEdit && cardEdit.id === annotation.id
145
147
  ? {
@@ -187,7 +189,6 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
187
189
  width,
188
190
  backgroundColor: tokens.panel,
189
191
  flexDirection: "column",
190
- paddingLeft: 1,
191
192
  paddingBottom: 1,
192
193
  }}
193
194
  >
@@ -202,8 +203,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
202
203
  </TabList>
203
204
  </Tabs>
204
205
  {railTab === "agent" ? (
205
- <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 2 }}>
206
- <text> </text>
206
+ <box style={{ flexGrow: 1, flexDirection: "column" }}>
207
207
  <AgentLauncher
208
208
  session={session}
209
209
  onLaunchHarness={onLaunchHarness}
@@ -295,8 +295,8 @@ function RemovalCard({
295
295
  <Card
296
296
  title={title}
297
297
  contentRows={contentRows}
298
- borderColor={isSelected ? tokens.red : tokens.border}
299
- backgroundColor="transparent"
298
+ borderColor={tokens.red}
299
+ backgroundColor={isSelected ? tokens.elevated : "transparent"}
300
300
  theme={theme}
301
301
  >
302
302
  {previewLines.length === 0 ? (