diff --git a/scripts/src/hwlab-node-help.ts b/scripts/src/hwlab-node-help.ts index 6438ae41..80ad1433 100644 --- a/scripts/src/hwlab-node-help.ts +++ b/scripts/src/hwlab-node-help.ts @@ -68,6 +68,7 @@ export function hwlabNodeWebProbeHelp(): Record { "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 { "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 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.", diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 9bf6c0e3..d42dac16 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -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: { diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 281ba012..6fbae4ab 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -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();