@cueloop/client 0.1.0-alpha.42 → 0.1.0-alpha.43
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.
- package/native/build-pty.sh +51 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/package.json +5 -6
- package/src/App.plan-review.test.tsx +56 -16
- package/src/components/ReviewRail.tsx +1 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +146 -146
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/agent-launcher.test.tsx +32 -4
- package/src/components/agent-launcher.tsx +20 -53
- package/src/components/primitives/Button.tsx +4 -1
- package/src/components/terminal-pane.ts +15 -8
- package/src/key-bindings.ts +6 -0
- package/src/pty.test.ts +78 -0
- package/src/pty.ts +222 -0
|
@@ -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 =="
|
|
Binary file
|
|
@@ -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.
|
|
3
|
+
"version": "0.1.0-alpha.43",
|
|
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.
|
|
30
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
29
|
+
"@cueloop/daemon": "0.1.0-alpha.43",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.43",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.43",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.43",
|
|
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
|
},
|
|
@@ -107,6 +107,14 @@ async function toContextParagraph(setup: Setup): Promise<void> {
|
|
|
107
107
|
await press(setup, "j");
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** Click the first on-screen occurrence of a string, by its char-frame position. */
|
|
111
|
+
async function clickText(setup: Setup, needle: string): Promise<void> {
|
|
112
|
+
const lines = setup.captureCharFrame().split("\n");
|
|
113
|
+
const row = lines.findIndex((line) => line.includes(needle));
|
|
114
|
+
await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
|
|
115
|
+
await setup.renderOnce();
|
|
116
|
+
}
|
|
117
|
+
|
|
110
118
|
describe("share button", () => {
|
|
111
119
|
test("the owner's plan header shows the Share button next to Edit", async () => {
|
|
112
120
|
// Arrange / Act
|
|
@@ -296,6 +304,45 @@ describe("inline compose keeps the anchor painted", () => {
|
|
|
296
304
|
// runner, and the whole-suite publish lane has timed even 15s out; give the
|
|
297
305
|
// heaviest frame-wait chain generous headroom so runner load cannot flake it.
|
|
298
306
|
}, 60_000);
|
|
307
|
+
|
|
308
|
+
test("clicking the Cancel word-button closes the compose box", async () => {
|
|
309
|
+
// Arrange
|
|
310
|
+
const setup = await renderApp();
|
|
311
|
+
await toContextParagraph(setup);
|
|
312
|
+
await press(setup, "v");
|
|
313
|
+
await press(setup, "l");
|
|
314
|
+
await press(setup, "c");
|
|
315
|
+
await waitForText(setup, "Cancel");
|
|
316
|
+
|
|
317
|
+
// Act
|
|
318
|
+
await clickText(setup, "Cancel");
|
|
319
|
+
|
|
320
|
+
// Assert
|
|
321
|
+
await waitForTextGone(setup, 'comment on "');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("clicking Cancel in a saved card's edit composer closes it", async () => {
|
|
325
|
+
// Arrange
|
|
326
|
+
const setup = await renderApp();
|
|
327
|
+
await toContextParagraph(setup);
|
|
328
|
+
await press(setup, "v");
|
|
329
|
+
await press(setup, "l");
|
|
330
|
+
await press(setup, "c");
|
|
331
|
+
await waitForText(setup, "Cancel");
|
|
332
|
+
await type(setup, "note body");
|
|
333
|
+
await press(setup, "enter");
|
|
334
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
335
|
+
// select the saved card, then activate it to open the in-place editor
|
|
336
|
+
await clickText(setup, "note body");
|
|
337
|
+
await clickText(setup, "note body");
|
|
338
|
+
await waitForText(setup, "Cancel");
|
|
339
|
+
|
|
340
|
+
// Act
|
|
341
|
+
await clickText(setup, "Cancel");
|
|
342
|
+
|
|
343
|
+
// Assert - the editor closes back to the plain saved card
|
|
344
|
+
await waitForTextGone(setup, "Cancel");
|
|
345
|
+
});
|
|
299
346
|
});
|
|
300
347
|
|
|
301
348
|
describe("compose newline convention", () => {
|
|
@@ -525,7 +572,7 @@ describe("sheet header", () => {
|
|
|
525
572
|
expect(frame).toContain("Share");
|
|
526
573
|
});
|
|
527
574
|
|
|
528
|
-
test("the Agent tab shows the harness launcher
|
|
575
|
+
test("the Agent tab shows the text-only harness launcher buttons", async () => {
|
|
529
576
|
// Arrange
|
|
530
577
|
const setup = await renderApp();
|
|
531
578
|
const lines = setup.captureCharFrame().split("\n");
|
|
@@ -536,12 +583,11 @@ describe("sheet header", () => {
|
|
|
536
583
|
await setup.mockMouse.click(tabColumn + 1, tabRow);
|
|
537
584
|
await setup.renderOnce();
|
|
538
585
|
|
|
539
|
-
// Assert -
|
|
540
|
-
await waitForText(setup, "
|
|
586
|
+
// Assert - the three launcher buttons, in order
|
|
587
|
+
await waitForText(setup, "Claude Code");
|
|
541
588
|
const frame = setup.captureCharFrame();
|
|
542
|
-
expect(frame).toContain("
|
|
543
|
-
expect(frame).toContain("
|
|
544
|
-
expect(frame).toContain("agent/worker-3");
|
|
589
|
+
expect(frame).toContain("Pi");
|
|
590
|
+
expect(frame).toContain("OpenAI Codex");
|
|
545
591
|
});
|
|
546
592
|
});
|
|
547
593
|
|
|
@@ -549,18 +595,12 @@ describe("quick-actions settings editor", () => {
|
|
|
549
595
|
test("expanding an action edits its system prompt inline and persists it", async () => {
|
|
550
596
|
// Arrange
|
|
551
597
|
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
598
|
|
|
559
599
|
// 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");
|
|
600
|
+
await clickText(setup, "menu");
|
|
601
|
+
await clickText(setup, "Settings");
|
|
602
|
+
await clickText(setup, "Actions");
|
|
603
|
+
await clickText(setup, "Zoom out, research in depth");
|
|
564
604
|
await type(setup, "CUSTOM");
|
|
565
605
|
await setup.renderOnce();
|
|
566
606
|
|
|
@@ -187,7 +187,6 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
187
187
|
width,
|
|
188
188
|
backgroundColor: tokens.panel,
|
|
189
189
|
flexDirection: "column",
|
|
190
|
-
paddingLeft: 1,
|
|
191
190
|
paddingBottom: 1,
|
|
192
191
|
}}
|
|
193
192
|
>
|
|
@@ -202,8 +201,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
202
201
|
</TabList>
|
|
203
202
|
</Tabs>
|
|
204
203
|
{railTab === "agent" ? (
|
|
205
|
-
<box style={{ flexGrow: 1, flexDirection: "column"
|
|
206
|
-
<text> </text>
|
|
204
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
207
205
|
<AgentLauncher
|
|
208
206
|
session={session}
|
|
209
207
|
onLaunchHarness={onLaunchHarness}
|