Skip to content

Commit 272d127

Browse files
committed
improved HUD in debug
1 parent 829b4fe commit 272d127

4 files changed

Lines changed: 271 additions & 82 deletions

File tree

CONTEXT.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,19 @@ Harness mechanism that limits agent effort when `agent-browser` bash commands fa
5050

5151
## Action overlay
5252

53-
In-page visual overlay shown during local Chrome headed runs to preview the agent's next browser action (cursor, highlight, intent banner). Distinct from verbose terminal logging. Enabled via `--action-overlay` or `extensions.actionOverlay.enabled`; on by default for `pqa debug`.
53+
In-page visual overlay shown during local Chrome headed runs to preview the agent's next browser action (ephemeral cursor and highlight) and a persistent stacked HUD panel. Distinct from verbose terminal logging. Enabled via `--action-overlay` or `extensions.actionOverlay.enabled`; on by default for `pqa debug`.
54+
55+
## HUD entry
56+
57+
One slot in the action overlay stack, tied to a single tool call preview: assistant text on top, parsed command label below in smaller type. Consecutive entries are separated by a subtle divider. Up to three entries are kept; the oldest is dropped when a fourth arrives. Entries persist until pushed out — no time-based fade or expiry. Stack order: oldest at top, newest at bottom.
58+
59+
## Action overlay panel
60+
61+
Fixed-size draggable rectangle (320 × 280 px) hosting the HUD entry stack. Drag via a top handle bar; panel body scrolls internally when content overflows; auto-scrolls to the newest entry on each append. Background opacity 0.7 by default, fully opaque on hover. Default position: top-right (12 px margin); user-dragged position is remembered for the rest of the browser session. Remains visible after the scenario ends until the browser window closes.
5462

5563
## Agent intent
5664

57-
Short assistant narration (`step.text`) emitted by the LLM in the same turn as a browser action — for example « Click the Sign in button — @e6 ». Distinct from extended thinking (`reasoningText`) and from the parsed command label shown in the action overlay HUD.
65+
Assistant text (`step.text`) captured from the LLM response in the same agent step as a browser action. Shown in full (never truncated) above the command label in each HUD entry. Distinct from extended thinking (`reasoningText`) and from the parsed command label itself.
5866

5967
## Nudge
6068

src/action-overlay/page-script.ts

Lines changed: 236 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,20 @@ export function buildActionOverlayScript(): string {
99
if (window.__pqaOverlay) return;
1010
1111
const ROOT_ID = "pqa-action-overlay-root";
12+
const PANEL_ID = "pqa-overlay-panel";
13+
const PANEL_BODY_ID = "pqa-overlay-panel-body";
1214
const HIGHLIGHT_CLASS = "pqa-overlay-highlight";
15+
const CURSOR_CLASS = "pqa-overlay-cursor";
16+
const MAX_ENTRIES = 3;
17+
const PANEL_WIDTH = 320;
18+
const PANEL_HEIGHT = 280;
19+
const PANEL_MARGIN = 12;
20+
21+
let panelLeft = null;
22+
let panelTop = null;
23+
let dragActive = false;
24+
let dragOffsetX = 0;
25+
let dragOffsetY = 0;
1326
1427
function ensureRoot() {
1528
let root = document.getElementById(ROOT_ID);
@@ -33,27 +46,46 @@ export function buildActionOverlayScript(): string {
3346
const style = document.createElement("style");
3447
style.id = "pqa-overlay-styles";
3548
style.textContent = [
36-
"#pqa-action-overlay-root .pqa-overlay-hud {",
37-
" position: fixed; top: 12px; left: 50%; transform: translateX(-50%);",
38-
" max-width: min(90vw, 640px); padding: 10px 16px;",
39-
" background: rgba(15, 23, 42, 0.92); color: #f8fafc;",
40-
" font: 600 14px/1.35 system-ui, -apple-system, sans-serif;",
49+
"#pqa-action-overlay-root .pqa-overlay-panel {",
50+
" position: fixed; width: 320px; height: 280px;",
51+
" display: flex; flex-direction: column;",
4152
" border-radius: 8px; box-shadow: 0 4px 24px rgba(0,0,0,0.35);",
42-
" text-align: center;",
53+
" overflow: hidden; pointer-events: auto;",
54+
" background: rgba(15, 23, 42, 0.7); color: #f8fafc;",
55+
" font: 500 13px/1.4 system-ui, -apple-system, sans-serif;",
56+
" transition: background 0.15s ease;",
57+
"}",
58+
"#pqa-action-overlay-root .pqa-overlay-panel:hover {",
59+
" background: rgba(15, 23, 42, 1);",
60+
"}",
61+
"#pqa-action-overlay-root .pqa-overlay-panel-handle {",
62+
" height: 24px; flex-shrink: 0; cursor: grab;",
63+
" background: rgba(255,255,255,0.06);",
64+
" border-bottom: 1px solid rgba(148, 163, 184, 0.25);",
65+
"}",
66+
"#pqa-action-overlay-root .pqa-overlay-panel-handle:active { cursor: grabbing; }",
67+
"#pqa-action-overlay-root .pqa-overlay-panel-body {",
68+
" flex: 1; overflow-y: auto; padding: 8px 12px; text-align: left;",
4369
"}",
44-
"#pqa-action-overlay-root .pqa-overlay-hud-title {",
45-
" display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",
70+
"#pqa-action-overlay-root .pqa-overlay-entry {",
71+
" padding: 8px 0;",
72+
" border-bottom: 1px solid rgba(148, 163, 184, 0.2);",
4673
"}",
47-
"#pqa-action-overlay-root .pqa-overlay-hud-detail {",
48-
" display: block; margin-top: 2px;",
49-
" font-size: 12px; font-weight: 500; color: #94a3b8;",
50-
" white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",
74+
"#pqa-action-overlay-root .pqa-overlay-entry:last-child { border-bottom: none; }",
75+
"#pqa-action-overlay-root .pqa-overlay-entry-intent {",
76+
" display: block; font-size: 14px; font-weight: 600; line-height: 1.35;",
77+
" word-break: break-word; white-space: pre-wrap;",
78+
"}",
79+
"#pqa-action-overlay-root .pqa-overlay-entry-command {",
80+
" display: block; margin-top: 4px; font-size: 12px; font-weight: 500;",
81+
" color: #94a3b8; line-height: 1.45; word-break: break-word;",
5182
"}",
5283
"#pqa-action-overlay-root .pqa-overlay-highlight {",
5384
" position: fixed; border: 3px solid #f59e0b;",
5485
" background: rgba(245, 158, 11, 0.15); border-radius: 4px;",
5586
" box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.25);",
5687
" transition: top 0.75s ease, left 0.75s ease, width 0.75s ease, height 0.75s ease;",
88+
" pointer-events: none;",
5789
"}",
5890
"#pqa-action-overlay-root .pqa-overlay-cursor {",
5991
" position: fixed; width: 32px; height: 32px;",
@@ -66,94 +98,225 @@ export function buildActionOverlayScript(): string {
6698
document.documentElement.appendChild(style);
6799
}
68100
101+
function defaultPanelPosition() {
102+
return {
103+
left: Math.max(PANEL_MARGIN, window.innerWidth - PANEL_WIDTH - PANEL_MARGIN),
104+
top: PANEL_MARGIN,
105+
};
106+
}
107+
108+
function applyPanelPosition(panel) {
109+
const left = panelLeft ?? defaultPanelPosition().left;
110+
const top = panelTop ?? defaultPanelPosition().top;
111+
Object.assign(panel.style, {
112+
left: left + "px",
113+
top: top + "px",
114+
right: "auto",
115+
transform: "none",
116+
});
117+
}
118+
119+
function onDragMove(event) {
120+
if (!dragActive) return;
121+
panelLeft = event.clientX - dragOffsetX;
122+
panelTop = event.clientY - dragOffsetY;
123+
const panel = document.getElementById(PANEL_ID);
124+
if (panel) applyPanelPosition(panel);
125+
}
126+
127+
function onDragEnd() {
128+
dragActive = false;
129+
}
130+
131+
function ensurePanel(root) {
132+
let panel = document.getElementById(PANEL_ID);
133+
if (panel) return panel;
134+
135+
panel = document.createElement("div");
136+
panel.id = PANEL_ID;
137+
panel.className = "pqa-overlay-panel";
138+
139+
const handle = document.createElement("div");
140+
handle.className = "pqa-overlay-panel-handle";
141+
handle.setAttribute("aria-hidden", "true");
142+
handle.addEventListener("mousedown", (event) => {
143+
if (event.button !== 0) return;
144+
dragActive = true;
145+
const rect = panel.getBoundingClientRect();
146+
dragOffsetX = event.clientX - rect.left;
147+
dragOffsetY = event.clientY - rect.top;
148+
panelLeft = rect.left;
149+
panelTop = rect.top;
150+
event.preventDefault();
151+
});
152+
153+
const body = document.createElement("div");
154+
body.id = PANEL_BODY_ID;
155+
body.className = "pqa-overlay-panel-body";
156+
157+
panel.appendChild(handle);
158+
panel.appendChild(body);
159+
root.appendChild(panel);
160+
applyPanelPosition(panel);
161+
162+
window.addEventListener("mousemove", onDragMove);
163+
window.addEventListener("mouseup", onDragEnd);
164+
165+
return panel;
166+
}
167+
168+
function clearAnimation(root) {
169+
root.querySelectorAll("." + HIGHLIGHT_CLASS + ", ." + CURSOR_CLASS).forEach((el) => el.remove());
170+
}
171+
69172
function clearOverlay() {
70173
const root = document.getElementById(ROOT_ID);
71174
if (root) root.replaceChildren();
72175
}
73176
74-
function normalizeHudPayload(input) {
177+
function normalizeEntryPayload(input) {
75178
if (typeof input === "string") {
76-
return { intent: "", detail: input };
179+
return { command: input, intent: "" };
77180
}
78181
if (input && typeof input === "object") {
182+
const command =
183+
input.command != null && input.command !== ""
184+
? String(input.command)
185+
: input.detail != null && input.detail !== ""
186+
? String(input.detail)
187+
: input.label != null && input.label !== ""
188+
? String(input.label)
189+
: "";
79190
return {
191+
command,
80192
intent: input.intent ? String(input.intent) : "",
81-
detail: input.detail ? String(input.detail) : input.label ? String(input.label) : "",
82193
};
83194
}
84-
return { intent: "", detail: "" };
195+
return { command: "", intent: "" };
196+
}
197+
198+
function trimEntryStack(body) {
199+
while (body.children.length >= MAX_ENTRIES) {
200+
body.firstElementChild?.remove();
201+
}
202+
}
203+
204+
function scrollPanelToBottom(body) {
205+
body.scrollTop = body.scrollHeight;
85206
}
86207
87-
function appendHud(root, payload) {
88-
const { intent, detail } = normalizeHudPayload(payload);
89-
const hud = document.createElement("div");
90-
hud.className = "pqa-overlay-hud";
208+
function appendEntry(root, payload) {
209+
const { command, intent } = normalizeEntryPayload(payload);
210+
if (!command && !intent) return;
211+
212+
const panel = ensurePanel(root);
213+
const body = panel.querySelector("#" + PANEL_BODY_ID);
214+
if (!body) return;
215+
216+
trimEntryStack(body);
217+
218+
const entry = document.createElement("div");
219+
entry.className = "pqa-overlay-entry";
220+
91221
if (intent) {
92-
const title = document.createElement("span");
93-
title.className = "pqa-overlay-hud-title";
94-
title.textContent = intent;
95-
hud.appendChild(title);
222+
const text = document.createElement("span");
223+
text.className = "pqa-overlay-entry-intent";
224+
text.textContent = intent;
225+
entry.appendChild(text);
96226
}
97-
if (detail) {
98-
const sub = document.createElement("span");
99-
sub.className = intent ? "pqa-overlay-hud-detail" : "pqa-overlay-hud-title";
100-
sub.textContent = detail;
101-
hud.appendChild(sub);
227+
if (command) {
228+
const cmd = document.createElement("span");
229+
cmd.className = "pqa-overlay-entry-command";
230+
cmd.textContent = command;
231+
entry.appendChild(cmd);
102232
}
103-
root.appendChild(hud);
233+
234+
body.appendChild(entry);
235+
scrollPanelToBottom(body);
104236
}
105237
106238
function showHud(payload) {
107239
ensureStyles();
108240
const root = ensureRoot();
109-
clearOverlay();
110-
appendHud(root, payload);
241+
clearAnimation(root);
242+
appendEntry(root, payload);
111243
}
112244
113-
function showMutation(payload) {
114-
ensureStyles();
115-
const root = ensureRoot();
116-
clearOverlay();
117-
118-
appendHud(root, payload);
119-
const box = payload && payload.box ? payload.box : null;
120-
121-
if (box && typeof box.x === "number" && typeof box.y === "number") {
122-
const w = typeof box.width === "number" ? box.width : 0;
123-
const h = typeof box.height === "number" ? box.height : 0;
124-
const cx = box.x + w / 2;
125-
const cy = box.y + h / 2;
126-
127-
const highlight = document.createElement("div");
128-
highlight.className = HIGHLIGHT_CLASS;
129-
Object.assign(highlight.style, {
130-
top: box.y + "px",
131-
left: box.x + "px",
132-
width: Math.max(w, 4) + "px",
133-
height: Math.max(h, 4) + "px",
134-
});
135-
root.appendChild(highlight);
136-
137-
const cursor = document.createElement("div");
138-
cursor.className = "pqa-overlay-cursor";
139-
cursor.innerHTML =
140-
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32">' +
141-
'<path fill="#f59e0b" stroke="#0f172a" stroke-width="1.2" d="M4 2l14 14h-6l-4 8z"/></svg>';
142-
const startX = Math.max(0, cx - 100);
143-
const startY = Math.max(0, cy - 80);
144-
Object.assign(cursor.style, {
145-
top: startY + "px",
146-
left: startX + "px",
147-
});
148-
root.appendChild(cursor);
245+
function scrollBoxIntoView(box) {
246+
const w = typeof box.width === "number" ? box.width : 0;
247+
const h = typeof box.height === "number" ? box.height : 0;
248+
let x = box.x;
249+
let y = box.y;
250+
const margin = 72;
251+
const vw = window.innerWidth;
252+
const vh = window.innerHeight;
253+
const cx = x + w / 2;
254+
const cy = y + h / 2;
255+
256+
let scrollX = 0;
257+
let scrollY = 0;
258+
259+
if (cy < margin) scrollY = cy - margin - (vh - margin) / 2;
260+
else if (cy > vh - margin) scrollY = cy - (vh + margin) / 2;
261+
262+
if (cx < margin) scrollX = cx - margin - (vw - margin) / 2;
263+
else if (cx > vw - margin) scrollX = cx - (vw + margin) / 2;
264+
265+
if (scrollX !== 0 || scrollY !== 0) {
266+
window.scrollBy({ top: scrollY, left: scrollX, behavior: "instant" });
267+
x -= scrollX;
268+
y -= scrollY;
269+
}
149270
271+
return { x, y, width: w, height: h };
272+
}
273+
274+
function showMutationAnimation(root, rawBox) {
275+
if (!rawBox || typeof rawBox.x !== "number" || typeof rawBox.y !== "number") return;
276+
277+
const box = scrollBoxIntoView(rawBox);
278+
const w = box.width;
279+
const h = box.height;
280+
const cx = box.x + w / 2;
281+
const cy = box.y + h / 2;
282+
283+
const highlight = document.createElement("div");
284+
highlight.className = HIGHLIGHT_CLASS;
285+
Object.assign(highlight.style, {
286+
top: box.y + "px",
287+
left: box.x + "px",
288+
width: Math.max(w, 4) + "px",
289+
height: Math.max(h, 4) + "px",
290+
});
291+
root.appendChild(highlight);
292+
293+
const cursor = document.createElement("div");
294+
cursor.className = CURSOR_CLASS;
295+
cursor.innerHTML =
296+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32">' +
297+
'<path fill="#f59e0b" stroke="#0f172a" stroke-width="1.2" d="M4 2l14 14h-6l-4 8z"/></svg>';
298+
const startX = Math.max(0, cx - 100);
299+
const startY = Math.max(0, cy - 80);
300+
Object.assign(cursor.style, {
301+
top: startY + "px",
302+
left: startX + "px",
303+
});
304+
root.appendChild(cursor);
305+
306+
requestAnimationFrame(() => {
150307
requestAnimationFrame(() => {
151-
requestAnimationFrame(() => {
152-
cursor.style.top = cy + "px";
153-
cursor.style.left = cx + "px";
154-
});
308+
cursor.style.top = cy + "px";
309+
cursor.style.left = cx + "px";
155310
});
156-
}
311+
});
312+
}
313+
314+
function showMutation(payload) {
315+
ensureStyles();
316+
const root = ensureRoot();
317+
clearAnimation(root);
318+
appendEntry(root, payload);
319+
showMutationAnimation(root, payload && payload.box ? payload.box : null);
157320
}
158321
159322
window.__pqaOverlay = {

0 commit comments

Comments
 (0)