refactor: archive GUI clients; keep minimal TUI with hooks/API
Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the Qt Quick Android APK of app/ (the engine is linked in-process, so
|
||||
# ARCHIVED: Qt APK front end under archive/qt-app; not part of default Hermit builds.
|
||||
# Build the Qt Quick Android APK of archive/qt-app/ (the engine is linked in-process, so
|
||||
# the APK opens no port). This is the front end counterpart of
|
||||
# scripts/build_android.sh, which cross-builds the engine alone.
|
||||
#
|
||||
@@ -150,7 +151,7 @@ cmake --build "$BUILD" --target apk -j"$JOBS"
|
||||
|
||||
# AGP names the file after the variant and signing state
|
||||
# (android-build-release.apk vs ...-unsigned.apk), so take the newest one.
|
||||
APK="$(find "$BUILD/app/android-build/build/outputs/apk" "$BUILD" -name '*.apk' \
|
||||
APK="$(find "$BUILD/archive/qt-app/android-build/build/outputs/apk" "$BUILD" -name '*.apk' \
|
||||
-printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)"
|
||||
[[ -n "$APK" && -f "$APK" ]] || { echo "no APK produced" >&2; exit 1; }
|
||||
echo
|
||||
|
||||
+25
-9
@@ -11,8 +11,13 @@ set -eu
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
REPLAY="${1:-$ROOT/tests/fixtures/sse/deepseek-native-eos}"
|
||||
PROTO="${2:-native-eos}"
|
||||
BIN="${CST_AGENT:-$ROOT/cst-agent}"
|
||||
[ -x "$BIN" ] || { echo "build cst-agent first ($BIN)"; exit 1; }
|
||||
BIN="${CST_AGENT:-}"
|
||||
if [ -z "$BIN" ]; then
|
||||
if [ -x "$ROOT/hermit" ]; then BIN="$ROOT/hermit"
|
||||
elif [ -x "$ROOT/cst-agent" ]; then BIN="$ROOT/cst-agent"
|
||||
fi
|
||||
fi
|
||||
[ -x "$BIN" ] || { echo "build hermit first ($ROOT/hermit)"; exit 1; }
|
||||
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/cst-webui-XXXXXX")" 2>/dev/null || {
|
||||
WORK="$ROOT/.build-tmp/e2e-webui"; rm -rf "$WORK"; mkdir -p "$WORK"; }
|
||||
@@ -30,6 +35,7 @@ echo "== start cst-agent --web (replay $REPLAY)"
|
||||
CST_REPLAY_SSE="$REPLAY" \
|
||||
CST_REPLAY_CHUNK=512 \
|
||||
"$BIN" --web --web-port 0 --workspace "$WORK" --new --api-key sk-e2e \
|
||||
--provider deepseek --model deepseek-chat \
|
||||
--protocol "$PROTO" --tools default --yolo > "$LOG" 2>&1 &
|
||||
SRV_PID=$!
|
||||
|
||||
@@ -61,10 +67,11 @@ stop_server() {
|
||||
[ "$st" -lt 128 ] || fail "$2 died with status $st on SIGTERM"
|
||||
}
|
||||
|
||||
# 1. the shell page and the state endpoint
|
||||
curl -fsS --max-time 5 "$BASE/" | grep -q "<title>CST-Agent" || fail "GET / is not the shell page"
|
||||
# 1. the health page and the state endpoint
|
||||
curl -fsS --max-time 5 "$BASE/" | grep -q "<title>Hermit API" || fail "GET / is not the health page"
|
||||
curl -fsS --max-time 5 "$BASE/api/state" > "$WORK/state.json" || fail "GET /api/state"
|
||||
grep -q '"protocol":"cst-webui/1"' "$WORK/state.json" || fail "state has no protocol tag"
|
||||
grep -q '"capabilities"' "$WORK/state.json" || fail "state has no capabilities"
|
||||
grep -q '"session":' "$WORK/state.json" || fail "state has no session id"
|
||||
|
||||
# 2. an unknown route is a 404, an oversized body a 413
|
||||
@@ -92,6 +99,7 @@ while [ $i -lt 450 ]; do
|
||||
done
|
||||
kill "$CURL_PID" 2>/dev/null || true
|
||||
grep -q "event: turn_end" "$EVENTS" || fail "the turn never finished"
|
||||
grep -q "event: turn_begin" "$EVENTS" || fail "no turn_begin event"
|
||||
grep -q "event: assistant_html" "$EVENTS" || fail "no assistant markdown was streamed"
|
||||
grep -q "event: tool" "$EVENTS" || fail "no tool event was streamed"
|
||||
grep -q '"rc":0' "$EVENTS" || fail "the turn did not end with rc=0"
|
||||
@@ -180,6 +188,7 @@ P_LOG="$WORK/permission-server.log" # its own log: the port parse must not
|
||||
# pick up the first server's URL
|
||||
CST_REPLAY_SSE="$REPLAY" CST_REPLAY_CHUNK=512 \
|
||||
"$BIN" --web --web-port 0 --workspace "$WORK/pw" --new --api-key sk-e2e \
|
||||
--provider deepseek --model deepseek-chat \
|
||||
--protocol "$PROTO" --tools default --approval ask --confirm all \
|
||||
> "$P_LOG" 2>&1 &
|
||||
P_PID=$!
|
||||
@@ -229,6 +238,7 @@ echo "== token auth and --web-dev"
|
||||
T_LOG="$WORK/token-server.log"
|
||||
CST_REPLAY_SSE="$REPLAY" \
|
||||
"$BIN" --web --web-port 0 --workspace "$WORK/pw" --new --api-key sk-e2e \
|
||||
--provider deepseek --model deepseek-chat \
|
||||
--protocol "$PROTO" --tools default --yolo --web-dev "$ROOT/web" \
|
||||
--web-token tok-e2e > "$T_LOG" 2>&1 &
|
||||
T_PID=$!
|
||||
@@ -257,11 +267,17 @@ code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
|
||||
-H 'Authorization: Bearer tok-e2' "$T_BASE/api/state")"
|
||||
[ "$code" = "401" ] || fail "a wrong token was accepted ($code)"
|
||||
|
||||
# --web-dev serves straight from the directory, so the bytes must match it
|
||||
curl -fsS --max-time 5 -H 'Authorization: Bearer tok-e2e' "$T_BASE/app.js" \
|
||||
> "$WORK/app.js" || fail "GET /app.js from --web-dev"
|
||||
cmp -s "$WORK/app.js" "$ROOT/web/app.js" ||
|
||||
fail "--web-dev did not serve web/app.js from disk"
|
||||
# --web-dev serves the health page from disk
|
||||
curl -fsS --max-time 5 -H 'Authorization: Bearer tok-e2e' "$T_BASE/" \
|
||||
> "$WORK/index.html" || fail "GET / from --web-dev"
|
||||
cmp -s "$WORK/index.html" "$ROOT/web/index.html" ||
|
||||
fail "--web-dev did not serve web/index.html from disk"
|
||||
curl -fsS --max-time 5 -H 'Authorization: Bearer tok-e2e' "$T_BASE/api/state" \
|
||||
> "$WORK/state.json" || fail "GET /api/state with token"
|
||||
grep -q '"capabilities"' "$WORK/state.json" ||
|
||||
fail "/api/state missing capabilities"
|
||||
grep -q 'turn_begin' "$WORK/state.json" ||
|
||||
fail "/api/state capabilities missing turn_begin"
|
||||
|
||||
# POST /api/quit must return and then take the server down on its own
|
||||
curl -fsS --max-time 5 -X POST -H 'Authorization: Bearer tok-e2e' \
|
||||
|
||||
@@ -113,11 +113,14 @@ def port():
|
||||
|
||||
|
||||
def settings():
|
||||
try:
|
||||
with open(home + "/.config/cst-agent/settings.json", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except OSError:
|
||||
return {}
|
||||
for name in ("hermit", "cst-agent"):
|
||||
path = home + "/.config/%s/settings.json" % name
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except OSError:
|
||||
continue
|
||||
return {}
|
||||
|
||||
|
||||
def busy(base):
|
||||
|
||||
@@ -141,8 +141,10 @@ try:
|
||||
check(status(r)[:1] == [200], "a client without Origin (curl) may POST")
|
||||
r = raw(PORT, req("GET", "/api/attach/img-0000.png", host="evil.example"))
|
||||
check(status(r)[:1] == [401], "session images need the Host check too (%s)" % status(r))
|
||||
r = raw(PORT, req("GET", "/app.js"))
|
||||
check(status(r)[:1] == [200], "assets are served")
|
||||
r = raw(PORT, req("GET", "/"))
|
||||
check(status(r)[:1] == [200], "health page is served")
|
||||
r = raw(PORT, req("GET", "/index.html"))
|
||||
check(status(r)[:1] == [200], "embedded index.html is served")
|
||||
# 2. request parsing
|
||||
r = raw(PORT, req("GET", "/api/state%00x"))
|
||||
check(status(r)[:1] == [400], "an encoded NUL in the path is a 400 (%s)" % status(r))
|
||||
@@ -206,8 +208,10 @@ try:
|
||||
ws2 = os.path.join(root, "b")
|
||||
os.makedirs(ws2)
|
||||
p, PORT, log = start(ws2, "--web-token", "tok-x")
|
||||
r = raw(PORT, req("GET", "/app.js"))
|
||||
check(status(r)[:1] == [200], "token mode: app.js loads without the token (%s)" % status(r))
|
||||
r = raw(PORT, req("GET", "/"))
|
||||
check(status(r)[:1] == [200], "token mode: health page loads without the token (%s)" % status(r))
|
||||
r = raw(PORT, req("GET", "/index.html"))
|
||||
check(status(r)[:1] == [200], "token mode: index.html loads without the token (%s)" % status(r))
|
||||
r = raw(PORT, req("GET", "/api/state"))
|
||||
check(status(r)[:1] == [401], "token mode: the API needs it")
|
||||
r = raw(PORT, req("GET", "/api/attach/img-0000.png"))
|
||||
|
||||
@@ -166,9 +166,23 @@ def messages(base):
|
||||
return call(base, "/api/session")[1].get("messages", [])
|
||||
|
||||
|
||||
def _is_prompt(m):
|
||||
"""Real user prompts only — skip notes, tool results, and engine meta rows."""
|
||||
if m.get("role") != "user" or m.get("note") or m.get("tool_result"):
|
||||
return False
|
||||
c = m.get("content")
|
||||
if not isinstance(c, str) or not c:
|
||||
return False
|
||||
kind = (m.get("meta") or {}).get("kind") if isinstance(m.get("meta"), dict) else None
|
||||
if kind:
|
||||
return False
|
||||
if c.startswith("[cst"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def prompts(base):
|
||||
return [m["content"] for m in messages(base)
|
||||
if m["role"] == "user" and not m.get("note") and not m.get("tool_result")]
|
||||
return [m["content"] for m in messages(base) if _is_prompt(m)]
|
||||
|
||||
|
||||
def answers(base):
|
||||
|
||||
@@ -151,8 +151,8 @@ def serve(handler):
|
||||
api_a, api_b = serve(A), serve(B)
|
||||
ws = tempfile.mkdtemp(prefix="cst-set-")
|
||||
home = tempfile.mkdtemp(prefix="cst-home-")
|
||||
cfgdir = home + "/.config/cst-agent"
|
||||
os.makedirs(cfgdir)
|
||||
cfgdir = home + "/.config/hermit"
|
||||
os.makedirs(cfgdir, exist_ok=True)
|
||||
with open(cfgdir + "/models.json", "w") as f:
|
||||
json.dump({"providers": {"fakeb": {"api_base": "http://127.0.0.1:%d/v1" % api_b.server_address[1],
|
||||
"key_env": "FAKEB_API_KEY", "default_model": ""}},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build and pack the Windows GUI (app/cst-agent-app.exe) from Linux.
|
||||
# ARCHIVED: Qt GUI lives under archive/qt-app; not part of default Hermit builds.
|
||||
# Build and pack the Windows GUI (archive/qt-app/cst-agent-app.exe) from Linux.
|
||||
#
|
||||
# scripts/package_windows_app.sh (run it from the repo root)
|
||||
# nix-shell shell-win.nix --run scripts/package_windows_app.sh
|
||||
@@ -161,7 +162,7 @@ cmake -S "$ROOT" -B "$BUILD" -G Ninja \
|
||||
# POSIX (tests/test_tools_ext.c calls mkdir with two arguments), exactly as in
|
||||
# the TUI cross build.
|
||||
cmake --build "$BUILD" -j"$(nproc)" --target cst-agent-app
|
||||
EXE="$BUILD/app/cst-agent-app.exe"
|
||||
EXE="$BUILD/archive/qt-app/cst-agent-app.exe"
|
||||
[[ -f "$EXE" ]] || { echo "no cst-agent-app.exe" >&2; exit 1; }
|
||||
|
||||
# --- the package -----------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user