@cueloop/client 0.1.0-alpha.5 → 0.1.0-alpha.51
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/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +36 -31
- package/src/App.autoclose.test.tsx +137 -0
- package/src/App.diff.test.tsx +158 -22
- package/src/App.export.test.tsx +44 -37
- package/src/App.plan-review.test.tsx +672 -0
- package/src/App.prototype.test.tsx +155 -0
- package/src/App.readonly.test.tsx +60 -36
- package/src/App.rendering.test.tsx +132 -0
- package/src/App.review-panel.test.tsx +136 -0
- package/src/App.submit-confirm.test.tsx +218 -0
- package/src/App.test.tsx +128 -69
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +289 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +38 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +30 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +241 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +44 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +83 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +59 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +127 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +355 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +62 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +58 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +108 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +456 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +78 -0
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +358 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +101 -0
- package/src/components/ReviewRail.stories.tsx +218 -0
- package/src/components/ReviewRail.tsx +317 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +141 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +108 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +49 -0
- package/src/components/agent-launcher.tsx +147 -0
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +51 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +59 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +64 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +22 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +98 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +136 -0
- package/src/components/stories-app.tsx +123 -0
- package/src/components/stories.test.tsx +62 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +69 -0
- package/src/components/syntax-highlight.ts +92 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +181 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +36 -0
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +430 -31
- package/src/config.ts +304 -26
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +143 -0
- package/src/editor.ts +123 -9
- package/src/ghostty-terminal.test.ts +82 -0
- package/src/ghostty-terminal.ts +168 -0
- package/src/herdr.test.ts +41 -0
- package/src/herdr.ts +27 -0
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +30 -0
- package/src/intent-dispatch.test.ts +493 -0
- package/src/intent-dispatch.ts +428 -0
- package/src/key-bindings.test.ts +152 -0
- package/src/key-bindings.ts +430 -0
- package/src/keymap.test.ts +622 -0
- package/src/keymap.ts +339 -0
- package/src/kitty-image.test.ts +20 -0
- package/src/kitty-image.ts +75 -0
- package/src/prototype-browser.test.ts +55 -0
- package/src/prototype-browser.ts +243 -0
- package/src/pty.test.ts +78 -0
- package/src/pty.ts +222 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +9 -5
- package/src/serve.test.ts +24 -9
- package/src/serve.ts +7 -7
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +309 -0
- package/src/session-controller.ts +914 -0
- package/src/share.ts +113 -0
- package/src/test-support.test.ts +55 -0
- package/src/test-support.ts +187 -0
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +38 -17
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +19 -8
- package/src/view-diff.ts +34 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +399 -0
- package/src/view-plan.ts +495 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +70 -0
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -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 =="
|
package/native/build.sh
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Rebuild libcueloopvt.dylib: libghostty-vt (Ghostty's VT core) statically linked
|
|
3
|
+
# behind the flat FFI shim. Pinned to a Ghostty commit + Zig 0.16.0.
|
|
4
|
+
# Output lands in native/<os>-<arch>/libcueloopvt.dylib. Requires curl and git.
|
|
5
|
+
set -eu
|
|
6
|
+
|
|
7
|
+
GHOSTTY_COMMIT="5834a0e3df621802e9578e4562d88b0c2ad4ada8"
|
|
8
|
+
ZIG_VERSION="0.16.0"
|
|
9
|
+
|
|
10
|
+
here="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
work="${TMPDIR:-/tmp}/cueloop-libghostty-build"
|
|
12
|
+
mkdir -p "$work"
|
|
13
|
+
export ZIG_GLOBAL_CACHE_DIR="$work/zig-gcache"
|
|
14
|
+
export ZIG_LOCAL_CACHE_DIR="$work/zig-lcache"
|
|
15
|
+
cd "$work"
|
|
16
|
+
|
|
17
|
+
# Host triple for the Bun prebuilt dir and Zig download. The two tools use
|
|
18
|
+
# different architecture names.
|
|
19
|
+
arch="$(uname -m)"; os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
20
|
+
case "$os" in darwin) zig_os="macos"; dylib="libcueloopvt.dylib";; linux) zig_os="linux"; dylib="libcueloopvt.so";; *) echo "unsupported os $os" >&2; exit 1;; esac
|
|
21
|
+
case "$arch-$os" in
|
|
22
|
+
arm64-darwin|aarch64-linux) zig_arch="aarch64"; bun_arch="arm64";;
|
|
23
|
+
x86_64-darwin|x86_64-linux) zig_arch="x86_64"; bun_arch="x64";;
|
|
24
|
+
*) echo "unsupported architecture $arch on $os" >&2; exit 1;;
|
|
25
|
+
esac
|
|
26
|
+
case "$zig_arch-$zig_os" in
|
|
27
|
+
aarch64-macos) zig_sha="b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489";;
|
|
28
|
+
x86_64-macos) zig_sha="0387557ed1877bc6a2e1802c8391953baddba76081876301c522f52977b52ba7";;
|
|
29
|
+
aarch64-linux) zig_sha="ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17";;
|
|
30
|
+
x86_64-linux) zig_sha="70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00";;
|
|
31
|
+
esac
|
|
32
|
+
outdir="$here/${os}-${bun_arch}"
|
|
33
|
+
zig_archive="zig-$zig_arch-$zig_os-$ZIG_VERSION.tar.xz"
|
|
34
|
+
|
|
35
|
+
if [ ! -f "$zig_archive" ]; then
|
|
36
|
+
echo "== fetching zig $ZIG_VERSION =="
|
|
37
|
+
curl -fsSL -o "$zig_archive" "https://ziglang.org/download/$ZIG_VERSION/$zig_archive"
|
|
38
|
+
fi
|
|
39
|
+
printf '%s %s\n' "$zig_sha" "$zig_archive" | shasum -a 256 -c -
|
|
40
|
+
|
|
41
|
+
if [ ! -d ghostty ]; then
|
|
42
|
+
echo "== cloning ghostty @ $GHOSTTY_COMMIT =="
|
|
43
|
+
mkdir ghostty && cd ghostty && git init -q
|
|
44
|
+
git remote add origin https://github.com/ghostty-org/ghostty.git
|
|
45
|
+
cd ..
|
|
46
|
+
fi
|
|
47
|
+
if ! git -C ghostty cat-file -e "$GHOSTTY_COMMIT^{commit}" 2>/dev/null; then
|
|
48
|
+
git -C ghostty fetch -q --depth 1 origin "$GHOSTTY_COMMIT"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Build only from fresh trees materialized from the verified archive and exact
|
|
52
|
+
# Git object. Cached working-tree edits and stale install outputs cannot enter.
|
|
53
|
+
build_root="$(mktemp -d "$work/native-build.XXXXXX")"
|
|
54
|
+
trap 'rm -rf "$build_root"' EXIT HUP INT TERM
|
|
55
|
+
tar -xf "$zig_archive" -C "$build_root"
|
|
56
|
+
zig="$build_root/zig-$zig_arch-$zig_os-$ZIG_VERSION/zig"
|
|
57
|
+
ghostty_source="$build_root/ghostty"
|
|
58
|
+
mkdir "$ghostty_source"
|
|
59
|
+
git -C ghostty archive "$GHOSTTY_COMMIT" | tar -x -C "$ghostty_source"
|
|
60
|
+
|
|
61
|
+
echo "== building libghostty-vt (ReleaseFast) =="
|
|
62
|
+
cd "$ghostty_source"
|
|
63
|
+
# The final xcframework packaging step can fail while the .a we need is produced,
|
|
64
|
+
# so tolerate a non-zero exit here but assert the static lib exists below.
|
|
65
|
+
vt_install="$build_root/vt-install"
|
|
66
|
+
"$zig" build -Demit-lib-vt -Doptimize=ReleaseFast --prefix "$vt_install" || true
|
|
67
|
+
cd ..
|
|
68
|
+
staticlib="$vt_install/lib/libghostty-vt.a"
|
|
69
|
+
[ -f "$staticlib" ] || { echo "build failed: $staticlib not produced" >&2; exit 1; }
|
|
70
|
+
|
|
71
|
+
echo "== linking shim -> $outdir/$dylib =="
|
|
72
|
+
mkdir -p "$outdir"
|
|
73
|
+
if [ "$os" = "darwin" ]; then
|
|
74
|
+
"$zig" build-lib -dynamic -OReleaseFast -lc -femit-bin="$outdir/$dylib" \
|
|
75
|
+
-install_name "@rpath/$dylib" \
|
|
76
|
+
-I"$vt_install/include" "$here/src/shim.zig" "$staticlib" \
|
|
77
|
+
-framework CoreFoundation -framework CoreText -framework CoreGraphics
|
|
78
|
+
else
|
|
79
|
+
"$zig" build-lib -dynamic -OReleaseFast -lc -femit-bin="$outdir/$dylib" \
|
|
80
|
+
-I"$vt_install/include" "$here/src/shim.zig" "$staticlib" -lm -lpthread
|
|
81
|
+
fi
|
|
82
|
+
echo "== done: $outdir/$dylib =="
|
|
Binary file
|
|
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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Flat FFI wrapper over libghostty-vt. It hides the allocator vtable, the
|
|
2
|
+
// by-value GhosttyPoint that bun:ffi cannot pass, and packed-cell decoding.
|
|
3
|
+
const std = @import("std");
|
|
4
|
+
const ghostty = @cImport({
|
|
5
|
+
@cInclude("ghostty/vt/types.h");
|
|
6
|
+
@cInclude("ghostty/vt/allocator.h");
|
|
7
|
+
@cInclude("ghostty/vt/terminal.h");
|
|
8
|
+
@cInclude("ghostty/vt/point.h");
|
|
9
|
+
@cInclude("ghostty/vt/grid_ref.h");
|
|
10
|
+
@cInclude("ghostty/vt/screen.h");
|
|
11
|
+
@cInclude("ghostty/vt/style.h");
|
|
12
|
+
@cInclude("ghostty/vt/color.h");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const CvtCell = extern struct {
|
|
16
|
+
codepoint: u32,
|
|
17
|
+
fg_r: u8,
|
|
18
|
+
fg_g: u8,
|
|
19
|
+
fg_b: u8,
|
|
20
|
+
fg_kind: u8,
|
|
21
|
+
bg_r: u8,
|
|
22
|
+
bg_g: u8,
|
|
23
|
+
bg_b: u8,
|
|
24
|
+
bg_kind: u8,
|
|
25
|
+
width: u8,
|
|
26
|
+
flags: u8,
|
|
27
|
+
_pad: u16,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
comptime {
|
|
31
|
+
std.debug.assert(@sizeOf(CvtCell) == 16);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export fn cvt_new(cols: u16, rows: u16) ?*anyopaque {
|
|
35
|
+
var terminal: ghostty.GhosttyTerminal = null;
|
|
36
|
+
if (ghostty.ghostty_terminal_new(null, &terminal, cols, rows) != ghostty.GHOSTTY_SUCCESS) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return terminal;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export fn cvt_free(terminal: ?*anyopaque) void {
|
|
43
|
+
if (terminal) |handle| ghostty.ghostty_terminal_free(@ptrCast(handle));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export fn cvt_write(terminal: *anyopaque, data: [*]const u8, len: usize) void {
|
|
47
|
+
ghostty.ghostty_terminal_vt_write(@ptrCast(terminal), data, len);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export fn cvt_resize(terminal: *anyopaque, cols: u16, rows: u16) void {
|
|
51
|
+
// Cell pixel size does not affect the character grid.
|
|
52
|
+
_ = ghostty.ghostty_terminal_resize(@ptrCast(terminal), cols, rows, 1, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Writes [col u16 LE, row u16 LE, visible u8] into the caller's eight bytes.
|
|
56
|
+
export fn cvt_cursor(terminal: *anyopaque, out: [*]u8) void {
|
|
57
|
+
var x: u16 = 0;
|
|
58
|
+
var y: u16 = 0;
|
|
59
|
+
var visible = false;
|
|
60
|
+
_ = ghostty.ghostty_terminal_get(@ptrCast(terminal), ghostty.GHOSTTY_TERMINAL_DATA_CURSOR_X, &x);
|
|
61
|
+
_ = ghostty.ghostty_terminal_get(@ptrCast(terminal), ghostty.GHOSTTY_TERMINAL_DATA_CURSOR_Y, &y);
|
|
62
|
+
_ = ghostty.ghostty_terminal_get(@ptrCast(terminal), ghostty.GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE, &visible);
|
|
63
|
+
out[0] = @truncate(x);
|
|
64
|
+
out[1] = @truncate(x >> 8);
|
|
65
|
+
out[2] = @truncate(y);
|
|
66
|
+
out[3] = @truncate(y >> 8);
|
|
67
|
+
out[4] = @intFromBool(visible);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn decodeColor(color: *const ghostty.GhosttyStyleColor, red: *u8, green: *u8, blue: *u8, kind: *u8) void {
|
|
71
|
+
if (color.tag == ghostty.GHOSTTY_STYLE_COLOR_RGB) {
|
|
72
|
+
kind.* = 2;
|
|
73
|
+
red.* = color.value.rgb.r;
|
|
74
|
+
green.* = color.value.rgb.g;
|
|
75
|
+
blue.* = color.value.rgb.b;
|
|
76
|
+
} else if (color.tag == ghostty.GHOSTTY_STYLE_COLOR_PALETTE) {
|
|
77
|
+
kind.* = 1;
|
|
78
|
+
red.* = @intCast(color.value.palette);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export fn cvt_cell(terminal: *anyopaque, x: u16, y: u32, out: *CvtCell) c_int {
|
|
83
|
+
out.* = std.mem.zeroes(CvtCell);
|
|
84
|
+
|
|
85
|
+
var point: ghostty.GhosttyPoint = undefined;
|
|
86
|
+
point.tag = ghostty.GHOSTTY_POINT_TAG_VIEWPORT;
|
|
87
|
+
point.value.coordinate.x = x;
|
|
88
|
+
point.value.coordinate.y = y;
|
|
89
|
+
|
|
90
|
+
var reference: ghostty.GhosttyGridRef = undefined;
|
|
91
|
+
if (ghostty.ghostty_terminal_grid_ref(@ptrCast(terminal), point, &reference) != ghostty.GHOSTTY_SUCCESS) return -1;
|
|
92
|
+
|
|
93
|
+
var cell: ghostty.GhosttyCell = undefined;
|
|
94
|
+
if (ghostty.ghostty_grid_ref_cell(&reference, &cell) != ghostty.GHOSTTY_SUCCESS) return -1;
|
|
95
|
+
|
|
96
|
+
_ = ghostty.ghostty_cell_get(cell, ghostty.GHOSTTY_CELL_DATA_CODEPOINT, &out.codepoint);
|
|
97
|
+
var width: u32 = 0;
|
|
98
|
+
_ = ghostty.ghostty_cell_get(cell, ghostty.GHOSTTY_CELL_DATA_WIDE, &width);
|
|
99
|
+
out.width = @intCast(width);
|
|
100
|
+
|
|
101
|
+
var style: ghostty.GhosttyStyle = undefined;
|
|
102
|
+
ghostty.ghostty_style_default(&style);
|
|
103
|
+
if (ghostty.ghostty_grid_ref_style(&reference, &style) == ghostty.GHOSTTY_SUCCESS) {
|
|
104
|
+
decodeColor(&style.fg_color, &out.fg_r, &out.fg_g, &out.fg_b, &out.fg_kind);
|
|
105
|
+
decodeColor(&style.bg_color, &out.bg_r, &out.bg_g, &out.bg_b, &out.bg_kind);
|
|
106
|
+
if (style.bold) out.flags |= 1;
|
|
107
|
+
if (style.italic) out.flags |= 2;
|
|
108
|
+
if (style.underline != 0) out.flags |= 4;
|
|
109
|
+
if (style.inverse) out.flags |= 8;
|
|
110
|
+
if (style.faint) out.flags |= 16;
|
|
111
|
+
if (style.strikethrough) out.flags |= 32;
|
|
112
|
+
}
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,45 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.51",
|
|
4
|
+
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
|
+
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/mmurakaru/cueloop/issues"
|
|
8
|
+
},
|
|
4
9
|
"license": "Apache-2.0",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/mmurakaru/cueloop.git",
|
|
13
|
+
"directory": "packages/client"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"native",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
5
20
|
"type": "module",
|
|
6
21
|
"exports": {
|
|
7
|
-
".": "./src/index.ts"
|
|
22
|
+
".": "./src/index.ts",
|
|
23
|
+
"./config": "./src/config.ts"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
8
27
|
},
|
|
9
28
|
"dependencies": {
|
|
10
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
11
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
12
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
13
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
14
|
-
"@opentui/core": "^0.
|
|
15
|
-
"@opentui/
|
|
29
|
+
"@cueloop/daemon": "0.1.0-alpha.51",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.51",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.51",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.51",
|
|
33
|
+
"@opentui/core": "^0.5.2",
|
|
34
|
+
"@opentui/keymap": "0.5.2",
|
|
35
|
+
"@opentui/react": "^0.5.2",
|
|
36
|
+
"@opentui/ssh": "0.5.2",
|
|
16
37
|
"@pierre/diffs": "^1.3.5",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
38
|
+
"diff": "^8",
|
|
39
|
+
"puppeteer-core": "^25.8.0",
|
|
40
|
+
"react": "^19.2.0"
|
|
19
41
|
},
|
|
20
42
|
"devDependencies": {
|
|
21
43
|
"@types/react": "^19.2.0",
|
|
22
|
-
"ssh2": "^1.
|
|
23
|
-
"
|
|
44
|
+
"@types/ssh2": "^1.15.5",
|
|
45
|
+
"ssh2": "^1.17.0"
|
|
24
46
|
},
|
|
25
|
-
"publishConfig": {
|
|
26
|
-
"access": "public"
|
|
27
|
-
},
|
|
28
|
-
"files": [
|
|
29
|
-
"src",
|
|
30
|
-
"README.md"
|
|
31
|
-
],
|
|
32
47
|
"engines": {
|
|
33
48
|
"bun": ">=1.3.0"
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/mmurakaru/cueloop.git",
|
|
38
|
-
"directory": "packages/client"
|
|
39
|
-
},
|
|
40
|
-
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
41
|
-
"bugs": {
|
|
42
|
-
"url": "https://github.com/mmurakaru/cueloop/issues"
|
|
43
|
-
},
|
|
44
|
-
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail"
|
|
45
|
-
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Post-submit completion overlay: prompt, opt-in persistence, countdown, dismissal. Countdown seconds tick on an injected ManualClock, so tests advance time instead of waiting it out. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
+
import { ManualClock } from "@opentui/core/testing";
|
|
10
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
11
|
+
import type { ReviewSession } from "@cueloop/schema";
|
|
12
|
+
import { App } from "./App";
|
|
13
|
+
import { isolateUserConfig, press, settle, waitForText } from "./test-support";
|
|
14
|
+
|
|
15
|
+
const PLAN = "# Plan\n\nShip the thing.\n";
|
|
16
|
+
|
|
17
|
+
let home: string;
|
|
18
|
+
let server: DaemonServer;
|
|
19
|
+
let session: ReviewSession;
|
|
20
|
+
let configPath: string;
|
|
21
|
+
let restoreUserConfig: () => void;
|
|
22
|
+
let clock: ManualClock;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-close-"));
|
|
26
|
+
configPath = join(home, "config.toml");
|
|
27
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
28
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
29
|
+
server.start();
|
|
30
|
+
session = server.core.sessionCreate({
|
|
31
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
32
|
+
artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
|
|
33
|
+
});
|
|
34
|
+
clock = new ManualClock();
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
restoreUserConfig();
|
|
38
|
+
server.stop();
|
|
39
|
+
rmSync(home, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function renderApp(onExit?: (code: number) => void) {
|
|
43
|
+
const setup = await testRender(
|
|
44
|
+
<App home={home} sessionId={session.id} onExit={onExit} clock={clock} />,
|
|
45
|
+
{
|
|
46
|
+
width: 120,
|
|
47
|
+
height: 30,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
await waitForText(setup, "cueloop");
|
|
51
|
+
return setup;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
55
|
+
|
|
56
|
+
async function submitApprove(setup: Setup): Promise<void> {
|
|
57
|
+
await press(setup, "enter"); // open submit (approve default: no pending items)
|
|
58
|
+
await press(setup, "enter"); // confirm
|
|
59
|
+
await waitForText(setup, "review approved");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("completion overlay", () => {
|
|
63
|
+
test("submit with auto-close off counts down from 5; ⏎ exits now", async () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
let exited = -1;
|
|
66
|
+
const setup = await renderApp((code) => (exited = code));
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
await submitApprove(setup);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
const frame = await waitForText(setup, "closing in 5s");
|
|
73
|
+
expect(frame).toContain("review approved");
|
|
74
|
+
expect(frame).toContain("close [return]");
|
|
75
|
+
expect(frame).toContain("return to plan [esc]");
|
|
76
|
+
|
|
77
|
+
// Act
|
|
78
|
+
await press(setup, "enter");
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
81
|
+
expect(exited).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a remembers the countdown as the persisted default", async () => {
|
|
85
|
+
// Arrange
|
|
86
|
+
const setup = await renderApp();
|
|
87
|
+
await submitApprove(setup);
|
|
88
|
+
await waitForText(setup, "closing in 5s");
|
|
89
|
+
|
|
90
|
+
// Act
|
|
91
|
+
await press(setup, "a");
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(existsSync(configPath)).toBe(true);
|
|
95
|
+
expect(readFileSync(configPath, "utf8")).toContain("auto_close = 5");
|
|
96
|
+
|
|
97
|
+
// Act
|
|
98
|
+
// the countdown keeps running; each second is one manual-clock tick
|
|
99
|
+
clock.advance(1000);
|
|
100
|
+
|
|
101
|
+
// Assert
|
|
102
|
+
await waitForText(setup, "closing in 4s");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("esc dismisses to the resolved read-only view", async () => {
|
|
106
|
+
// Arrange
|
|
107
|
+
const setup = await renderApp();
|
|
108
|
+
await submitApprove(setup);
|
|
109
|
+
await waitForText(setup, "closing in 5s");
|
|
110
|
+
|
|
111
|
+
// Act
|
|
112
|
+
// a bare ESC sits in the input parser's escape-sequence disambiguation
|
|
113
|
+
// window before it is delivered - the frame wait absorbs it
|
|
114
|
+
await press(setup, "escape");
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
const frame = await waitForText(setup, "resolved: approve");
|
|
118
|
+
expect(frame).not.toContain("closing in");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("configured auto_close counts down and exits without interaction", async () => {
|
|
122
|
+
// Arrange
|
|
123
|
+
writeFileSync(configPath, "[ui]\nauto_close = 1\n");
|
|
124
|
+
let exited = -1;
|
|
125
|
+
const setup = await renderApp((code) => (exited = code));
|
|
126
|
+
await submitApprove(setup);
|
|
127
|
+
await waitForText(setup, "closing in 1s");
|
|
128
|
+
expect(exited).toBe(-1);
|
|
129
|
+
|
|
130
|
+
// Act
|
|
131
|
+
clock.advance(1000);
|
|
132
|
+
await settle(setup);
|
|
133
|
+
|
|
134
|
+
// Assert
|
|
135
|
+
expect(exited).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
});
|