Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/acceptance/browser-screenshot-viewer-20261022/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Agent Browser screenshot viewer

Clicking an Agent Browser screenshot now opens the existing original-image viewer: fit or 100% size, zoom, copy PNG, download the original, and Escape to return focus. An updated screenshot, browser session, or Run closes the previous preview. Verification or image-decoding failures do not enable a preview.

## Real UI comparison

Captured in Microsoft Edge on Windows. Both versions use the same production Go API (`cmd/workbenchjourney`) and the same screenshot produced by the native Chrome browser tool against its owned local test page. The offline provider supplies deterministic tool calls. The baseline is `fc0935d6fc03ca7abccbd148ce52d26c2bc154f8`; the updated view is the production Vite build from this change.

The API keeps its startup bundle in memory, so the updated production bundle was served with Vite preview proxying the same API. The card comparisons are direct Playwright element screenshots at a 1280 × 900 viewport. The viewer screenshots use 1280 × 900 and 480 × 600 viewports.

| Before: thumbnail only | After: open original |
| --- | --- |
| ![Before](before-card.png) | ![After](after-card.png) |

![Original viewer](after-viewer.png)

![Original viewer in the minimum desktop window](after-viewer-narrow.png)

## Verification

- Agent Browser component: 21 tests passed; existing image-viewer suite: 8 tests passed. Covers verified bytes, original dimensions, copy/download, focus restoration, screenshot/session/Run changes, rejected/undecodable images, and recovery after a failed screenshot download.
- TypeScript and production build passed.
- Actual browser clicks opened the screenshot, switched to 100%, downloaded the original, copied PNG to the clipboard, fitted the view, and closed with Escape. Clipboard image dimensions remained 1258 × 802; focus returned to the screenshot button. The 480 × 600 dialog stayed inside the viewport.
- Downloaded file SHA-256 matched the server screenshot: `1caabbcbf77b317544d9040675eb72da092e7163b769ec64f6e218622f2562b8` (36,058 bytes).

The approach keeps UC's existing verified-screenshot model and reuses its image viewer. Browser inspection is an established workflow in [Codex](https://learn.chatgpt.com/docs/browser?surface=app) and [Claude Code Desktop](https://code.claude.com/docs/en/desktop); this change improves inspection of UC's captured pages.

## Recovering a failed screenshot request

A follow-up review reproduced a missing recovery action: if the first screenshot download failed, the card asked the user to refresh the status but provided no reload button. An unchanged screenshot identity also prevented subsequent status reads from retrying the image. The card now offers **Reload screenshot** after a failed read; it repeats the verified image GET for the same captured page.

These additional screenshots use the production Go API, an actual native browser capture, and Edge at 1280 × 900. In each frontend, Playwright interrupted only the first screenshot request with `connectionreset`; no response bodies or DOM content were replaced. Before is commit `2358e46d4f80535858f10f0dba7a0976421992ad`; after is the production build containing the retry action. Both use the same browser session and image.

| Before: no recovery action | After: explicit retry |
| --- | --- |
| ![Failed screenshot without retry](retry-before.png) | ![Failed screenshot with retry](retry-after.png) |

![Same screenshot recovered after retry](retry-recovered.png)

The retry returned a verified 1258 × 802 image, cleared the error and allowed the original viewer to open. No API mutation was issued during retry or preview. The browser was then stopped through the existing control. The new regression test failed on the previous implementation and passed with the fix.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/acceptance/plan-confirmed-choice-20261022/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Confirmed plan selection — 2026-10-22

Real Chrome screenshots of the production frontend against an isolated Go API and deterministic local provider. The browser proposed two directions, selected direction two, confirmed it, refreshed, reconnected and reopened **View plan**.

The before build is commit `fc0935d6fc03ca7abccbd148ce52d26c2bc154f8`. The after build contains the implementation in `f26802e9c88492a23676359e6eb9feb18e932ab1`. Both read the same persisted server selection: proposal `plan-proposal-20261021181611-640a34e64617`, direction `2`. The API remained running while the frontend changed.

| Viewport | Before | After |
| --- | --- | --- |
| 1280 × 900 | [Neither direction selected](before-desktop.png) | [Direction two selected and marked 已确认](after-desktop.png) |
| 390 × 844 | [Neither direction selected](before-mobile.png) | [Read-only confirmation visible](after-mobile.png) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
103 changes: 52 additions & 51 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@tiptap/starter-kit": "3.32.0",
"@xterm/addon-fit": "0.11.0",
"@xterm/xterm": "6.0.0",
"dompurify": "3.4.17",
"highlight.js": "11.11.1",
"lucide-react": "1.31.0",
"monaco-editor": "0.56.0",
Expand All @@ -49,16 +50,16 @@
"openapi-typescript": "7.13.0",
"typescript": "5.9.3",
"vite": "8.3.4",
"vitest": "4.1.11"
"vitest": "4.1.12"
},
"overrides": {
"@redocly/openapi-core": {
"js-yaml": "4.3.2"
},
"brace-expansion": "5.0.12",
"dompurify": "3.4.16",
"dompurify": "3.4.17",
"nanoid": "3.3.18",
"postcss": "8.5.25",
"undici": "7.29.1"
"undici": "7.30.1"
}
}
21 changes: 21 additions & 0 deletions web/src/lib/monaco-sanitizer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import DOMPurify from "dompurify";
// @ts-expect-error Monaco ships this runtime sanitizer without declarations.
import { safeSetInnerHtml, sanitizeHtml } from "monaco-editor/base/browser/domSanitize.js";

describe("Monaco sanitizer", () => {
it.each(["trusted HTML", "DOM fragment"])("uses the installed DOMPurify for %s", (output) => {
const sanitize = vi.spyOn(DOMPurify, "sanitize");
const dirty = '<p>Kept <strong>formatting</strong></p><script>alert(1)</script>' +
'<img src="data:," onerror="alert(2)"><a href="javascript:alert(3)">unsafe</a>' +
'<a href="https://example.com">safe</a>';
const target = document.createElement("div");

if (output === "trusted HTML") target.innerHTML = String(sanitizeHtml(dirty));
else safeSetInnerHtml(target, dirty);

expect(sanitize).toHaveBeenCalled();
expect(target.querySelector("strong")).toHaveTextContent("formatting");
expect(target.querySelector('script, [onerror], a[href^="javascript:"]')).toBeNull();
expect(target.querySelector('a[href="https://example.com"]')).toHaveTextContent("safe");
});
});
3 changes: 3 additions & 0 deletions web/src/v2/components/agent-browser.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
.v2-agent-browser-page a { display:block; max-width:100%; color:var(--v2-accent); text-decoration:underline; text-underline-offset:3px; }
.v2-agent-browser-page a svg { display:inline; margin-left:5px; vertical-align:middle; }
.v2-agent-browser img { display:block; width:100%; height:auto; max-height:320px; object-fit:contain; border:1px solid var(--v2-border); border-radius:8px; background:var(--prayu-surface-solid); }
.v2-agent-browser .v2-agent-browser-screenshot { position:relative; display:block; width:100%; min-width:0; padding:0; border:0; cursor:zoom-in; }
.v2-agent-browser-screenshot span { position:absolute; right:8px; top:8px; display:inline-flex; align-items:center; gap:5px; padding:4px 8px; border:1px solid var(--v2-border); border-radius:6px; background:var(--prayu-surface-solid); }
.v2-agent-browser .v2-agent-browser-screenshot:disabled { cursor:default; }
.v2-agent-browser details { min-width:0; }
.v2-agent-browser summary { cursor:pointer; color:var(--v2-muted); }
.v2-agent-browser details p { margin-top:7px; }
Loading
Loading