mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(codex): improve web rendering for Codex events (#544)
* test(codex): add web event rendering harness * fix(codex): surface plan updates in web * fix(codex): render MCP tool calls in web * fix(codex): improve terminal and context display * fix(codex): format token usage events * fix(codex): show status context in web * fix(codex): preserve tool result errors
This commit is contained in:
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
import { chromium } from 'playwright'
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { expects: [], timeout: 15000, out: '', screenshot: '' }
|
||||
const positional = []
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i]
|
||||
if (arg === '--expect' || arg === '--wait-text') {
|
||||
args.expects.push(argv[++i])
|
||||
} else if (arg === '--timeout') {
|
||||
args.timeout = Number(argv[++i])
|
||||
} else if (arg === '--out') {
|
||||
args.out = argv[++i]
|
||||
} else if (arg === '--screenshot') {
|
||||
args.screenshot = argv[++i]
|
||||
} else {
|
||||
positional.push(arg)
|
||||
}
|
||||
}
|
||||
if (!positional[0]) {
|
||||
throw new Error('usage: read-hapi-web.mjs <url> [--expect TEXT] [--out FILE] [--screenshot FILE] [--timeout MS]')
|
||||
}
|
||||
return { ...args, url: positional[0] }
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } })
|
||||
const consoleMessages = []
|
||||
const failedRequests = []
|
||||
page.on('console', (msg) => consoleMessages.push(`${msg.type()}: ${msg.text()}`))
|
||||
page.on('requestfailed', (request) => failedRequests.push(`${request.method()} ${request.url()} ${request.failure()?.errorText ?? ''}`))
|
||||
|
||||
try {
|
||||
await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: args.timeout })
|
||||
for (const expected of args.expects) {
|
||||
await page.getByText(expected, { exact: false }).first().waitFor({ timeout: args.timeout })
|
||||
}
|
||||
const text = await page.locator('body').innerText({ timeout: args.timeout }).catch(async () => await page.textContent('body') ?? '')
|
||||
const html = await page.locator('body').evaluate((node) => node.innerHTML).catch(() => '')
|
||||
const result = {
|
||||
ok: args.expects.every((expected) => text.includes(expected)),
|
||||
url: page.url().replace(/([?&]token=)[^&]+/g, '$1<redacted>'),
|
||||
title: await page.title(),
|
||||
text,
|
||||
textLength: text.length,
|
||||
htmlLength: html.length,
|
||||
expects: args.expects.map((expected) => ({ text: expected, found: text.includes(expected) })),
|
||||
consoleMessages,
|
||||
failedRequests
|
||||
}
|
||||
if (args.out) {
|
||||
const out = resolve(args.out)
|
||||
mkdirSync(dirname(out), { recursive: true })
|
||||
writeFileSync(out, text)
|
||||
}
|
||||
if (args.screenshot) {
|
||||
const screenshot = resolve(args.screenshot)
|
||||
mkdirSync(dirname(screenshot), { recursive: true })
|
||||
await page.screenshot({ path: screenshot, fullPage: true })
|
||||
result.screenshot = screenshot
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
if (!result.ok) process.exitCode = 2
|
||||
} catch (error) {
|
||||
if (args.screenshot) {
|
||||
const screenshot = resolve(args.screenshot)
|
||||
mkdirSync(dirname(screenshot), { recursive: true })
|
||||
await page.screenshot({ path: screenshot, fullPage: true }).catch(() => {})
|
||||
}
|
||||
console.error(JSON.stringify({
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
url: page.url().replace(/([?&]token=)[^&]+/g, '$1<redacted>'),
|
||||
consoleMessages,
|
||||
failedRequests
|
||||
}, null, 2))
|
||||
process.exitCode = 1
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bun
|
||||
import { Database } from 'bun:sqlite'
|
||||
import { mkdirSync, rmSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { Store } from '../../hub/src/store'
|
||||
|
||||
function argValue(name: string, fallback?: string): string | undefined {
|
||||
const prefix = `${name}=`
|
||||
const directIndex = process.argv.indexOf(name)
|
||||
if (directIndex >= 0) return process.argv[directIndex + 1]
|
||||
const direct = process.argv.find((arg) => arg.startsWith(prefix))
|
||||
return direct ? direct.slice(prefix.length) : fallback
|
||||
}
|
||||
|
||||
const dbPath = resolve(argValue('--db', process.env.DB_PATH ?? '/tmp/hapi-dev-codex-web/hapi.db')!)
|
||||
const reset = process.argv.includes('--reset')
|
||||
const namespace = argValue('--namespace', 'default')!
|
||||
const tag = argValue('--tag', 'codex-web-fixture')!
|
||||
const now = Date.now()
|
||||
|
||||
mkdirSync(dirname(dbPath), { recursive: true })
|
||||
if (reset) {
|
||||
rmSync(dbPath, { force: true })
|
||||
rmSync(`${dbPath}-wal`, { force: true })
|
||||
rmSync(`${dbPath}-shm`, { force: true })
|
||||
}
|
||||
|
||||
const store = new Store(dbPath)
|
||||
const session = store.sessions.getOrCreateSession(tag, {
|
||||
path: '/tmp/hapi-fixture-workspace',
|
||||
host: 'hapi-dev-fixture',
|
||||
version: 'dev',
|
||||
flavor: 'codex',
|
||||
codexSessionId: 'codex-fixture-session',
|
||||
name: 'Codex Web Fixture'
|
||||
}, {
|
||||
controlledByUser: false,
|
||||
requests: {},
|
||||
completedRequests: {}
|
||||
}, namespace, 'gpt-5.4', undefined, 'xhigh')
|
||||
|
||||
const messages = [
|
||||
{
|
||||
localId: 'fixture-user-1',
|
||||
content: {
|
||||
role: 'user',
|
||||
content: { type: 'text', text: 'Fixture request: show Codex reasoning, MCP calls, and plan status.' },
|
||||
meta: { sentFrom: 'fixture' }
|
||||
}
|
||||
},
|
||||
{
|
||||
localId: 'fixture-agent-message',
|
||||
content: {
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'codex',
|
||||
data: {
|
||||
type: 'message',
|
||||
message: 'Codex fixture response visible in HAPI Web.',
|
||||
id: 'fixture-msg-1'
|
||||
}
|
||||
},
|
||||
meta: { sentFrom: 'fixture' }
|
||||
}
|
||||
},
|
||||
{
|
||||
localId: 'fixture-reasoning',
|
||||
content: {
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'codex',
|
||||
data: {
|
||||
type: 'reasoning',
|
||||
message: 'Fixture reasoning detail: inspect event normalization before rendering.',
|
||||
id: 'fixture-reasoning-1'
|
||||
}
|
||||
},
|
||||
meta: { sentFrom: 'fixture' }
|
||||
}
|
||||
},
|
||||
{
|
||||
localId: 'fixture-mcp-call',
|
||||
content: {
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'codex',
|
||||
data: {
|
||||
type: 'tool-call',
|
||||
name: 'mcp__fixture__lookup_context',
|
||||
callId: 'fixture-mcp-call-1',
|
||||
input: {
|
||||
server: 'fixture-mcp-server',
|
||||
tool: 'lookup_context',
|
||||
query: 'codex plan visibility'
|
||||
},
|
||||
id: 'fixture-tool-1'
|
||||
}
|
||||
},
|
||||
meta: { sentFrom: 'fixture' }
|
||||
}
|
||||
},
|
||||
{
|
||||
localId: 'fixture-mcp-result',
|
||||
content: {
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'codex',
|
||||
data: {
|
||||
type: 'tool-call-result',
|
||||
callId: 'fixture-mcp-call-1',
|
||||
output: {
|
||||
server: 'fixture-mcp-server',
|
||||
tool: 'lookup_context',
|
||||
result: 'Fixture MCP result visible in Web.'
|
||||
},
|
||||
id: 'fixture-tool-result-1'
|
||||
}
|
||||
},
|
||||
meta: { sentFrom: 'fixture' }
|
||||
}
|
||||
},
|
||||
{
|
||||
localId: 'fixture-plan-update',
|
||||
content: {
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'codex',
|
||||
data: {
|
||||
type: 'plan_update',
|
||||
plan: [
|
||||
{ step: 'Inspect event stream', status: 'completed' },
|
||||
{ step: 'Render plan card', status: 'in_progress' },
|
||||
{ step: 'Verify web DOM', status: 'pending' }
|
||||
],
|
||||
id: 'fixture-plan-update-1'
|
||||
}
|
||||
},
|
||||
meta: { sentFrom: 'fixture' }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
for (const message of messages) {
|
||||
store.messages.addMessage(session.id, message.content, message.localId)
|
||||
}
|
||||
|
||||
// Make the fixture visually prominent in session lists without needing a live CLI heartbeat.
|
||||
const db = new Database(dbPath)
|
||||
db.prepare('UPDATE sessions SET active = 1, active_at = ?, updated_at = ? WHERE id = ?').run(now, now, session.id)
|
||||
db.close()
|
||||
|
||||
console.log(JSON.stringify({
|
||||
dbPath,
|
||||
sessionId: session.id,
|
||||
namespace,
|
||||
tag,
|
||||
urlPath: `/sessions/${session.id}`,
|
||||
messageCount: messages.length
|
||||
}, null, 2))
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
export HAPI_DEV_HOME="${HAPI_DEV_HOME:-/tmp/hapi-dev-codex-web}"
|
||||
export HAPI_DEV_TOKEN="${HAPI_DEV_TOKEN:-hapi-dev-token}"
|
||||
export HAPI_DEV_HUB_PORT="${HAPI_DEV_HUB_PORT:-3106}"
|
||||
export HAPI_DEV_WEB_PORT="${HAPI_DEV_WEB_PORT:-5174}"
|
||||
ARTIFACT_DIR="${ARTIFACT_DIR:-/tmp/hapi-dev-codex-web-artifacts}"
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
|
||||
if ss -ltn | grep -q ":${HAPI_DEV_HUB_PORT} "; then
|
||||
echo "port ${HAPI_DEV_HUB_PORT} already in use" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ss -ltn | grep -q ":${HAPI_DEV_WEB_PORT} "; then
|
||||
echo "port ${HAPI_DEV_WEB_PORT} already in use" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
rm -rf "$HAPI_DEV_HOME"
|
||||
mkdir -p "$HAPI_DEV_HOME"
|
||||
SEED_JSON="$ARTIFACT_DIR/seed.json"
|
||||
WEB_JSON="$ARTIFACT_DIR/web-read.json"
|
||||
WEB_TEXT="$ARTIFACT_DIR/web-visible.txt"
|
||||
SCREENSHOT="$ARTIFACT_DIR/web.png"
|
||||
HUB_LOG="$ARTIFACT_DIR/hub.log"
|
||||
WEB_LOG="$ARTIFACT_DIR/web.log"
|
||||
|
||||
cd "$ROOT"
|
||||
bun scripts/dev/seed-codex-web-fixture.ts --db "$HAPI_DEV_HOME/hapi.db" --reset > "$SEED_JSON"
|
||||
SESSION_ID="$(node -e "console.log(JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8')).sessionId)" "$SEED_JSON")"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${HUB_PID:-}" ]]; then kill "$HUB_PID" 2>/dev/null || true; fi
|
||||
if [[ -n "${WEB_PID:-}" ]]; then kill "$WEB_PID" 2>/dev/null || true; fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
CLI_API_TOKEN="$HAPI_DEV_TOKEN" \
|
||||
HAPI_HOME="$HAPI_DEV_HOME" \
|
||||
DB_PATH="$HAPI_DEV_HOME/hapi.db" \
|
||||
HAPI_LISTEN_HOST=127.0.0.1 \
|
||||
HAPI_LISTEN_PORT="$HAPI_DEV_HUB_PORT" \
|
||||
bun run dev:hub > "$HUB_LOG" 2>&1 &
|
||||
HUB_PID=$!
|
||||
|
||||
for _ in {1..80}; do
|
||||
if curl -fsS "http://127.0.0.1:${HAPI_DEV_HUB_PORT}/api/auth" \
|
||||
-H 'content-type: application/json' \
|
||||
-d "{\"accessToken\":\"$HAPI_DEV_TOKEN\"}" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
VITE_HUB_PROXY="http://127.0.0.1:${HAPI_DEV_HUB_PORT}" \
|
||||
bun --cwd web vite --host 127.0.0.1 --port "$HAPI_DEV_WEB_PORT" --strictPort --force > "$WEB_LOG" 2>&1 &
|
||||
WEB_PID=$!
|
||||
|
||||
for _ in {1..80}; do
|
||||
if curl -fsS "http://127.0.0.1:${HAPI_DEV_WEB_PORT}/" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
node scripts/dev/read-hapi-web.mjs \
|
||||
"http://127.0.0.1:${HAPI_DEV_WEB_PORT}/sessions/${SESSION_ID}?token=${HAPI_DEV_TOKEN}" \
|
||||
--expect "Codex fixture response visible" \
|
||||
--expect "Fixture reasoning detail" \
|
||||
--expect "MCP: Fixture Lookup Context" \
|
||||
--expect "Inspect event stream" \
|
||||
--expect "Render plan card" \
|
||||
--expect "Verify web DOM" \
|
||||
--out "$WEB_TEXT" \
|
||||
--screenshot "$SCREENSHOT" \
|
||||
--timeout 20000 > "$WEB_JSON"
|
||||
|
||||
node -e "const j=JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8')); console.log(JSON.stringify({ok:j.ok, textLength:j.textLength, failedRequests:j.failedRequests.length, url:j.url}, null, 2))" "$WEB_JSON"
|
||||
echo "artifacts: $ARTIFACT_DIR"
|
||||
Reference in New Issue
Block a user