fix(web-probe): add observe new session command

This commit is contained in:
Codex
2026-06-20 16:14:42 +00:00
parent 72c2081710
commit cb761a1691
3 changed files with 50 additions and 3 deletions
+2 -1
View File
@@ -68,6 +68,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --url https://hwlab.pikapython.com --fresh-session --message 'ping'",
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 --script-file .state/probes/workbench.mjs",
"bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type newSession",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectProvider --provider codex-api",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'",
"bun scripts/cli.ts hwlab nodes web-probe observe status webobs-xxxx",
@@ -84,7 +85,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
"Issue-ready evidence is available under issueEvidence and summary.issueEvidence; full script report is persisted under probe.reportPath with a SHA-256 fingerprint.",
"observe sampling is passive by default: it records DOM summaries and natural page request/response/requestfailed events with observerInitiated=false; it does not actively fetch Workbench APIs, reload, switch sessions, route/intercept, or call repair helpers.",
"observe start registers a local UniDesk-side observer id under .state/web-observe/index.json; after start, prefer observe status|command|stop|collect|analyze <id> instead of repeating --node/--lane/--state-dir.",
"observe command actions are explicit user/control actions and are appended to control.jsonl; use --type selectProvider/sendPrompt/goto/screenshot/mark/stop and keep prompt text out of issue comments by citing textHash/textBytes.",
"observe command actions are explicit user/control actions and are appended to control.jsonl; use --type newSession/selectProvider/sendPrompt/goto/screenshot/mark/stop and keep prompt text out of issue comments by citing textHash/textBytes.",
"observe analyze is offline-only: it reads artifact JSONL and writes analysis/report.md plus analysis/report.json without accessing Workbench APIs or driving the browser.",
"Use recordStep(name, data) or fetchApiMatrix(paths) to keep structured partial evidence when a later step fails.",
"Use reloadStable(), gotoCurrentStable(), or safeReload() for bounded retries around page reload/current-URL navigation jitter such as ERR_NETWORK_CHANGED.",
+3 -2
View File
@@ -58,7 +58,7 @@ interface NodeWebProbeScriptOptions {
}
type NodeWebProbeObserveAction = "start" | "status" | "command" | "stop" | "collect" | "analyze";
type NodeWebProbeObserveCommandType = "login" | "preflight" | "goto" | "sendPrompt" | "selectProvider" | "clickSession" | "screenshot" | "mark" | "stop";
type NodeWebProbeObserveCommandType = "login" | "preflight" | "goto" | "newSession" | "sendPrompt" | "selectProvider" | "clickSession" | "screenshot" | "mark" | "stop";
interface NodeWebProbeObserveOptions {
action: "observe";
@@ -6152,6 +6152,7 @@ function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbeObserve
value === "login"
|| value === "preflight"
|| value === "goto"
|| value === "newSession"
|| value === "sendPrompt"
|| value === "selectProvider"
|| value === "clickSession"
@@ -6159,7 +6160,7 @@ function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbeObserve
|| value === "mark"
|| value === "stop"
) return value;
throw new Error(`web-probe observe command --type must be login, preflight, goto, sendPrompt, selectProvider, clickSession, screenshot, mark, or stop; got ${value}`);
throw new Error(`web-probe observe command --type must be login, preflight, goto, newSession, sendPrompt, selectProvider, clickSession, screenshot, mark, or stop; got ${value}`);
}
function nodeWebProbeAutoCommandTimeoutSeconds(input: {
@@ -224,6 +224,7 @@ async function processCommand(command) {
case "login": return authenticate(context);
case "preflight": return preflightSummary();
case "goto": return gotoTarget(command.path || command.url || targetPath);
case "newSession": return createSessionFromUi();
case "sendPrompt": return sendPrompt(String(command.text || ""));
case "selectProvider": return selectProvider(String(command.provider || command.value || command.text || ""));
case "clickSession": return clickSession(String(command.sessionId || command.value || ""));
@@ -286,6 +287,50 @@ async function gotoTarget(rawTarget) {
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus: response ? response.status() : null, pageId };
}
async function createSessionFromUi() {
const beforeUrl = currentPageUrl();
const before = await workbenchSessionSnapshot();
const create = page.locator("#session-create").first();
await create.waitFor({ state: "visible", timeout: 15000 });
await create.click();
await page.waitForFunction((initial) => {
const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
const sessionId = activeTab?.getAttribute("data-session-id") || "";
const warning = document.querySelector(".composer-warning")?.textContent?.trim() || "";
const input = document.querySelector("#command-input");
return Boolean(activeTab && sessionId && sessionId !== initial.sessionId && input && !input.disabled && !warning);
}, { sessionId: before?.activeSessionId || "" }, { timeout: 30000 }).catch(() => null);
const after = await workbenchSessionSnapshot();
return {
beforeUrl,
afterUrl: currentPageUrl(),
ok: Boolean(after?.activeSessionId && after.activeSessionId !== before?.activeSessionId),
before,
after,
sessionId: after?.activeSessionId || null,
pageId
};
}
async function workbenchSessionSnapshot() {
return page.evaluate(() => {
const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) || window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u);
const input = document.querySelector("#command-input");
const warning = document.querySelector(".composer-warning")?.textContent?.trim() || null;
return {
url: window.location.href,
routeSessionId: routeMatch ? decodeURIComponent(routeMatch[1] || "") : null,
activeSessionId: activeTab?.getAttribute("data-session-id") || null,
activeConversationId: activeTab?.getAttribute("data-conversation-id") || null,
activeStatus: activeTab?.getAttribute("data-status") || null,
tabCount: document.querySelectorAll(".session-tab").length,
composerReady: Boolean(activeTab && input && !input.disabled && !warning),
warning
};
}).catch(() => null);
}
async function sendPrompt(text) {
if (text.trim().length === 0) throw new Error("sendPrompt requires non-empty text");
const beforeUrl = currentPageUrl();