cc-haha/desktop/scripts/e2e-context-usage-live-agent-browser.sh
程序员阿江(Relakkes) a4aba9ec5a Show context compaction as transcript state
Desktop users need to see automatic context compaction as a transient transcript state instead of a stray stdout bubble or large card. The UI now collapses prior visible content into a compact timeline divider, shows the compacting phase, then updates the same divider with the completed summary affordance.

Constraint: CLI compaction emits both status and synthetic transcript artifacts that must be normalized before rendering

Rejected: Keep the existing small pill marker | it did not show the in-progress state and looked like chat content

Confidence: high

Scope-risk: moderate

Directive: Do not render local compact stdout as a user-visible chat message

Tested: cd desktop && bun run test -- src/components/chat/MessageList.test.tsx src/stores/chatStore.test.ts

Tested: bun test src/server/__tests__/ws-memory-events.test.ts

Tested: bun run check:desktop

Not-tested: full root bun run verify after the final UI iteration
2026-05-19 23:13:01 +08:00

258 lines
9.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DESKTOP_DIR="$ROOT_DIR/desktop"
if ! command -v agent-browser >/dev/null 2>&1; then
echo "agent-browser is required but was not found in PATH" >&2
exit 1
fi
PORTS="$(bun -e 'import { createServer } from "node:net"; const getPort = () => new Promise((resolve) => { const server = createServer(); server.listen(0, "127.0.0.1", () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; server.close(() => resolve(port)); }); }); const apiPort = await getPort(); const webPort = await getPort(); console.log(`${apiPort} ${webPort}`);')"
API_PORT="${PORTS%% *}"
WEB_PORT="${PORTS##* }"
BASE_URL="http://127.0.0.1:${API_PORT}"
WEB_URL="http://127.0.0.1:${WEB_PORT}/?serverUrl=${BASE_URL}"
RUN_ID="$(date +%s)-$RANDOM"
SESSION_NAME="cc-haha-context-live-${RUN_ID}"
ARTIFACT_DIR="${ARTIFACT_DIR:-$(mktemp -d "/tmp/cc-haha-context-live-${RUN_ID}-XXXX")}"
PROJECT_DIR="${ARTIFACT_DIR}/project"
SERVER_LOG="${ARTIFACT_DIR}/server.log"
WEB_LOG="${ARTIFACT_DIR}/web.log"
BROWSER_LOG="${ARTIFACT_DIR}/browser.log"
RESULT_FILE="${PROJECT_DIR}/context-live-result.txt"
cleanup() {
local exit_code=$?
if [[ -n "${PREVIOUS_PERMISSION_MODE:-}" ]]; then
curl -fsS -X PUT "${BASE_URL}/api/permissions/mode" \
-H 'Content-Type: application/json' \
-d "{\"mode\":\"${PREVIOUS_PERMISSION_MODE}\"}" >/dev/null 2>&1 || true
fi
if [[ -n "${SESSION_ID:-}" && "${KEEP_SESSION:-0}" != "1" ]]; then
curl -fsS -X DELETE "${BASE_URL}/api/sessions/${SESSION_ID}" >/dev/null 2>&1 || true
fi
agent-browser --session "${SESSION_NAME}" close >/dev/null 2>&1 || true
if [[ -n "${SERVER_PID:-}" ]]; then
kill "${SERVER_PID}" >/dev/null 2>&1 || true
wait "${SERVER_PID}" >/dev/null 2>&1 || true
fi
if [[ -n "${WEB_PID:-}" ]]; then
kill "${WEB_PID}" >/dev/null 2>&1 || true
wait "${WEB_PID}" >/dev/null 2>&1 || true
fi
if [[ $exit_code -ne 0 || "${KEEP_ARTIFACTS:-0}" == "1" ]]; then
echo "Artifacts kept at: ${ARTIFACT_DIR}" >&2
else
rm -rf "${ARTIFACT_DIR}"
fi
}
trap cleanup EXIT
mkdir -p "${PROJECT_DIR}"
cat > "${PROJECT_DIR}/README.md" <<'EOF'
# Context Usage Live E2E
This throwaway project is used by a real-model desktop WebUI test.
EOF
echo "Starting backend on ${BASE_URL}"
(
cd "${ROOT_DIR}"
SERVER_PORT="${API_PORT}" bun run src/server/index.ts --host 127.0.0.1 --port "${API_PORT}"
) >"${SERVER_LOG}" 2>&1 &
SERVER_PID=$!
echo "Starting web UI on http://127.0.0.1:${WEB_PORT}"
(
cd "${DESKTOP_DIR}"
bun run dev -- --host 127.0.0.1 --port "${WEB_PORT}" --strictPort
) >"${WEB_LOG}" 2>&1 &
WEB_PID=$!
wait_for_http() {
local url="$1"
for _ in $(seq 1 120); do
if curl --max-time 2 -fsS "$url" >/dev/null 2>&1; then
return 0
fi
sleep 0.5
done
echo "Timed out waiting for ${url}" >&2
return 1
}
fetch_json_field() {
local url="$1"
local expression="$2"
curl -fsS "$url" | bun -e "const input = await Bun.stdin.text(); const data = JSON.parse(input); ${expression}"
}
AB=(agent-browser --session "${SESSION_NAME}")
browser_text() {
"${AB[@]}" get text body 2>>"${BROWSER_LOG}"
}
wait_for_text() {
local needle="$1"
local attempts="${2:-120}"
for _ in $(seq 1 "${attempts}"); do
if browser_text | grep -Fq "$needle"; then
return 0
fi
sleep 1
done
echo "Timed out waiting for page text: ${needle}" >&2
return 1
}
session_contains() {
local needle="$1"
curl -fsS "${BASE_URL}/api/sessions/${SESSION_ID}/messages" | NEEDLE="${needle}" bun -e '
const input = await Bun.stdin.text()
const data = JSON.parse(input)
const haystack = JSON.stringify(data)
process.exit(haystack.includes(process.env.NEEDLE ?? "") ? 0 : 1)
'
}
wait_for_session_text() {
local needle="$1"
local attempts="${2:-180}"
for _ in $(seq 1 "${attempts}"); do
if session_contains "${needle}"; then
return 0
fi
sleep 1
done
echo "Timed out waiting for transcript text: ${needle}" >&2
return 1
}
context_label() {
"${AB[@]}" eval "document.querySelector('[aria-label^=\"Context usage\"], [aria-label^=\"上下文用量\"]')?.getAttribute('aria-label') || ''" 2>>"${BROWSER_LOG}" || true
}
wait_for_context_indicator() {
for _ in $(seq 1 120); do
if context_label | grep -Fq "Context usage"; then
return 0
fi
sleep 1
done
echo "Timed out waiting for context usage indicator" >&2
return 1
}
show_context_details() {
"${AB[@]}" eval "(() => { const el = document.querySelector('[aria-label^=\"Context usage\"], [aria-label^=\"上下文用量\"]'); if (el) { el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); el.focus(); } })()" >>"${BROWSER_LOG}" 2>&1 || true
}
wait_for_context_window_text() {
for _ in $(seq 1 80); do
show_context_details
if browser_text | grep -Eq "Window|Input context|Free space|上下文|空闲"; then
return 0
fi
sleep 0.5
done
echo "Context usage detail popover text was not detected; continuing with indicator and screenshot evidence" >&2
return 0
}
wait_for_result_file() {
for _ in $(seq 1 240); do
if [[ -f "${RESULT_FILE}" ]] && grep -Fq "context-live-ok" "${RESULT_FILE}"; then
return 0
fi
sleep 2
done
echo "Timed out waiting for real model tool execution" >&2
return 1
}
wait_for_compact_boundary() {
for _ in $(seq 1 240); do
if session_contains "compact_boundary" || session_contains "Context compacted" || session_contains "Compacted"; then
return 0
fi
sleep 2
done
echo "Timed out waiting for real /compact boundary" >&2
return 1
}
wait_for_http "${BASE_URL}/health"
wait_for_http "http://127.0.0.1:${WEB_PORT}"
PREVIOUS_PERMISSION_MODE="$(fetch_json_field "${BASE_URL}/api/permissions/mode" 'console.log(data.mode)')"
curl -fsS -X PUT "${BASE_URL}/api/permissions/mode" \
-H 'Content-Type: application/json' \
-d '{"mode":"bypassPermissions"}' >/dev/null
SESSION_ID="$(curl -fsS -X POST "${BASE_URL}/api/sessions" \
-H 'Content-Type: application/json' \
-d "{\"workDir\":\"${PROJECT_DIR//\"/\\\"}\"}" \
| bun -e 'const input = await Bun.stdin.text(); const data = JSON.parse(input); console.log(data.sessionId);')"
UNIQUE_TITLE="Context Live ${RUN_ID}"
curl -fsS -X PATCH "${BASE_URL}/api/sessions/${SESSION_ID}" \
-H 'Content-Type: application/json' \
-d "{\"title\":\"${UNIQUE_TITLE//\"/\\\"}\"}" >/dev/null
CURRENT_MODEL="$(fetch_json_field "${BASE_URL}/api/models/current" 'console.log(data.model?.name || data.model?.id || "current")')"
TAB_STATE="{\"openTabs\":[{\"sessionId\":\"${SESSION_ID}\",\"title\":\"${UNIQUE_TITLE}\",\"type\":\"session\"}],\"activeTabId\":\"${SESSION_ID}\"}"
"${AB[@]}" open "${WEB_URL}" >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" wait 1200 >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" eval "localStorage.setItem('cc-haha-locale', 'en'); localStorage.setItem('cc-haha-open-tabs', '${TAB_STATE}'); location.reload();" >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" wait 1800 >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" fill '#sidebar-search' "${UNIQUE_TITLE}" >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" eval "Array.from(document.querySelectorAll('button')).find((el) => el.textContent?.includes('Context Live'))?.click()" >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" wait 1200 >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" wait 'textarea' >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" screenshot "${ARTIFACT_DIR}/01-session-open.png" >/dev/null
wait_for_context_indicator
show_context_details
wait_for_context_window_text
"${AB[@]}" screenshot "${ARTIFACT_DIR}/02-initial-real-context.png" >/dev/null
PROMPT="Use Bash to run exactly: sleep 18 && printf context-live-ok > context-live-result.txt . After the command finishes, reply exactly CONTEXT_LIVE_DONE. Do not edit any other file."
"${AB[@]}" fill 'textarea' "${PROMPT}" >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" press 'Enter' >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" wait 3500 >>"${BROWSER_LOG}" 2>&1
wait_for_context_indicator
show_context_details
wait_for_context_window_text
"${AB[@]}" screenshot "${ARTIFACT_DIR}/03-running-real-model-context.png" >/dev/null
wait_for_result_file
wait_for_session_text "CONTEXT_LIVE_DONE" 180
"${AB[@]}" wait 1000 >>"${BROWSER_LOG}" 2>&1
show_context_details
"${AB[@]}" screenshot "${ARTIFACT_DIR}/04-after-real-model-turn-context.png" >/dev/null
COMPACT_PROMPT="/compact Keep a concise summary that mentions context-live-ok and the Bash command result."
"${AB[@]}" fill 'textarea' "${COMPACT_PROMPT}" >>"${BROWSER_LOG}" 2>&1
"${AB[@]}" press 'Enter' >>"${BROWSER_LOG}" 2>&1
wait_for_compact_boundary
show_context_details
wait_for_context_window_text
"${AB[@]}" screenshot "${ARTIFACT_DIR}/05-after-real-compact-context.png" >/dev/null
INSPECTION_SUMMARY="$(curl -fsS "${BASE_URL}/api/sessions/${SESSION_ID}/inspection?includeContext=1" | bun -e 'const input = await Bun.stdin.text(); const data = JSON.parse(input); const ctx = data.context || data.contextEstimate; console.log(JSON.stringify({ model: ctx?.model, totalTokens: ctx?.totalTokens, rawMaxTokens: ctx?.rawMaxTokens, percentage: ctx?.percentage, categories: ctx?.categories?.slice(0, 5)?.map((c) => ({ name: c.name, tokens: c.tokens })) }, null, 2));')"
echo "Context usage live WebUI E2E passed"
echo "Model: ${CURRENT_MODEL}"
echo "Session: ${SESSION_ID}"
echo "API port: ${API_PORT}"
echo "Web port: ${WEB_PORT}"
echo "Artifacts: ${ARTIFACT_DIR}"
echo "Inspection:"
echo "${INSPECTION_SUMMARY}"