Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
78 changes: 74 additions & 4 deletions apps/web/src/features/browser/automation/inject/inspect-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ window.__deusInspectMode = true;
var dragStartY = 0;
var dragSelectionBox = null;
var elementIdCounter = 0;
var selectionIdCounter = 0;
// Last-clicked element — the host's prompt overlay is anchored to it, so
// we keep the blue border painted around it instead of snapping back to
// mouse hover. Cleared on a fresh click (updated to the new element),
// on host-initiated clearSelection, or on disableSelectionMode.
var pinnedElement = null;
var pinnedSelectionKey = null;

// ========================================================================
// Event Buffer — sole communication path to React
Expand Down Expand Up @@ -79,6 +86,11 @@ window.__deusInspectMode = true;
return id;
}

function createSelectionKey(ref) {
selectionIdCounter++;
return ref + "@" + selectionIdCounter;
}

// ========================================================================
// React Fiber Detection
// ========================================================================
Expand Down Expand Up @@ -563,6 +575,8 @@ window.__deusInspectMode = true;
function enableSelectionMode() {
if (selectionMode) return;
selectionMode = true;
pinnedElement = null;
pinnedSelectionKey = null;
document.body.style.cursor = "none";

if (!cursorStyleOverride) {
Expand Down Expand Up @@ -602,6 +616,8 @@ window.__deusInspectMode = true;
function disableSelectionMode() {
if (!selectionMode) return;
selectionMode = false;
pinnedElement = null;
pinnedSelectionKey = null;
document.body.style.cursor = "";

if (cursorStyleOverride) {
Expand Down Expand Up @@ -682,7 +698,7 @@ window.__deusInspectMode = true;
dragSelectionBox.style.top = top + "px";
dragSelectionBox.style.width = width + "px";
dragSelectionBox.style.height = height + "px";
} else if (!isDragging && overlay && overlayLabel) {
} else if (!isDragging && !pinnedElement && overlay && overlayLabel) {
// Use composedPath() for shadow DOM support; fall back to elementFromPoint
var composed = e.composedPath ? e.composedPath() : [];
var element = null;
Expand Down Expand Up @@ -763,10 +779,10 @@ window.__deusInspectMode = true;
function captureElement(clientX, clientY) {
// Use elementFromPoint — reliable since we have exact coordinates
var element = document.elementFromPoint(clientX, clientY);
if (!element || isInspectElement(element)) return;
if (!element || isInspectElement(element)) return null;
// Walk up past text nodes or non-element nodes
while (element && element.nodeType !== 1) element = element.parentElement;
if (!element || !element.tagName) return;
if (!element || !element.tagName) return null;

var rect = element.getBoundingClientRect();
var cs = window.getComputedStyle(element);
Expand Down Expand Up @@ -813,6 +829,7 @@ window.__deusInspectMode = true;

// Counter-based stable element ID (replaces random refs)
var ref = getOrAssignElementId(element);
var selectionKey = createSelectionKey(ref);

// Only keep non-Tailwind semantic classes (Tailwind utilities are noise for the AI)
var className = "";
Expand Down Expand Up @@ -931,6 +948,7 @@ window.__deusInspectMode = true;
sendToFrontend("element-event", {
type: "element-selected",
ref: ref,
selectionKey: selectionKey,
context: context,
element: {
tagName: element.tagName,
Expand Down Expand Up @@ -959,6 +977,7 @@ window.__deusInspectMode = true;
url: window.location.href,
timestamp: Date.now(),
});
return { element: element, selectionKey: selectionKey };
}

function handleMouseUp(e) {
Expand Down Expand Up @@ -991,7 +1010,25 @@ window.__deusInspectMode = true;
// Done here in mouseup instead of handleClick because WKWebView
// may not synthesize a click event when mousedown and mouseup
// targets differ (common with tiny trackpad movements).
captureElement(dragStartX, dragStartY);
var captured = captureElement(dragStartX, dragStartY);
if (captured) {
// Pin the blue border to the selected element so the user keeps
// a visible "this is what you're editing" anchor while the host
// prompt is open. Hover redraws are suppressed while pinned.
pinnedElement = captured.element;
pinnedSelectionKey = captured.selectionKey;
if (overlay) {
var pr = captured.element.getBoundingClientRect();
overlay.style.display = "";
overlay.style.left = pr.left + "px";
overlay.style.top = pr.top + "px";
overlay.style.width = pr.width + "px";
overlay.style.height = pr.height + "px";
}
// The host overlay shows the element identity — keep the guest
// dimension label hidden in pinned state.
if (overlayLabel) overlayLabel.style.display = "none";
}
}

if (dragSelectionBox) {
Expand Down Expand Up @@ -1037,6 +1074,39 @@ window.__deusInspectMode = true;
var events = eventBuffer.splice(0, eventBuffer.length);
return JSON.stringify(events);
},
/** Hide or show the hover overlay + label + custom cursor. The host
* toggles these OFF briefly before capturing a region screenshot so
* the inspector's paint doesn't appear on top of the element, then
* toggles them back ON. No-op if the elements don't exist (inspect
* mode isn't active). Respects pinned state: when restoring, the
* dimension label stays hidden so pinning reads clean. */
setOverlaysVisible: function(visible) {
if (!visible) {
if (overlay) overlay.style.display = "none";
if (overlayLabel) overlayLabel.style.display = "none";
if (selectionCursor) selectionCursor.style.display = "none";
return;
}
if (overlay) overlay.style.display = "";
if (selectionCursor) selectionCursor.style.display = "";
// Only un-hide the dimension label when there's no pinned selection;
// the host overlay already shows the element identity in pinned mode.
if (overlayLabel && !pinnedElement) overlayLabel.style.display = "";
},
/** Release the pinned selection so the blue border returns to
* hover-tracking. When `expectedSelectionKey` is provided, only clear
* if that same click selection is still pinned. This prevents an older
* async host cleanup from erasing a newer click on the same element. */
clearSelection: function(expectedSelectionKey) {
if (pinnedElement && expectedSelectionKey !== undefined) {
if (pinnedSelectionKey !== expectedSelectionKey) return false;
}
pinnedElement = null;
pinnedSelectionKey = null;
if (overlay) overlay.style.display = "none";
if (overlayLabel) overlayLabel.style.display = "none";
return true;
},
};

console.log("[deus-inspect] SETUP complete — window.__deusInspect installed");
Expand Down
30 changes: 30 additions & 0 deletions apps/web/src/features/browser/automation/inspect-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ export const INSPECT_MODE_VERIFY = `(function(){
});
})()`;

/** Hide the inspect-mode visuals (hover overlay + label + custom cursor).
* Use before `capturePage(rect)` so the screenshot shows only the element,
* not the inspector chrome. No-op if inspect mode isn't active. */
export const INSPECT_MODE_HIDE_OVERLAYS = `(function(){
if (window.__deusInspect && window.__deusInspect.setOverlaysVisible) {
window.__deusInspect.setOverlaysVisible(false);
}
})()`;

/** Restore the inspect-mode visuals hidden by INSPECT_MODE_HIDE_OVERLAYS. */
export const INSPECT_MODE_SHOW_OVERLAYS = `(function(){
if (window.__deusInspect && window.__deusInspect.setOverlaysVisible) {
window.__deusInspect.setOverlaysVisible(true);
}
})()`;

/** Build a script that clears the pinned selection border. When
* `expectedSelectionKey` is provided, the guest only clears if that same
* click selection is still pinned — this prevents an older async submit
* cleanup from wiping out a newer click on the same element. */
export function buildInspectModeClearSelection(expectedSelectionKey?: string): string {
const selectionKeyArg =
expectedSelectionKey === undefined ? "undefined" : JSON.stringify(expectedSelectionKey);
return `(function(){
if (window.__deusInspect && window.__deusInspect.clearSelection) {
window.__deusInspect.clearSelection(${selectionKeyArg});
}
})()`;
}

/**
* Drain buffered inspect events from the WKWebView.
*
Expand Down
40 changes: 40 additions & 0 deletions apps/web/src/features/browser/lib/attachScreenshotToComposer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* attachScreenshotToComposer — decode a PNG data URL captured from a
* `<webview>` and push it onto a session's composer as an image attachment.
*
* Two call sites:
* - BrowserPanel's camera button (full-page screenshot)
* - InspectPromptOverlay's submit (region screenshot of a selected element)
*
* Returns true iff the image was attached. Swallows errors with a console
* warning — screenshot failures are never fatal for the flow that called
* us (the user's text and element metadata should still land in the chat).
*/

import { sessionComposerActions } from "@/features/session/store/sessionComposerStore";
import { processImageFiles } from "@/features/session/lib/imageAttachments";

export async function attachScreenshotToComposer(
sessionId: string,
dataUrl: string | null,
filenameLabel = "screenshot"
): Promise<boolean> {
if (!dataUrl) return false;
try {
const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, "");
const binaryStr = atob(base64);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
const blob = new Blob([bytes], { type: "image/png" });
const file = new File([blob], `browser-${filenameLabel}-${Date.now()}.png`, {
type: "image/png",
});
const processed = await processImageFiles([file]);
if (!processed.length) return false;
sessionComposerActions.addImageAttachments(sessionId, processed);
return true;
} catch (err) {
console.warn(`[browser] attachScreenshotToComposer failed (${filenameLabel}):`, err);
return false;
}
}
26 changes: 24 additions & 2 deletions apps/web/src/features/browser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface BrowserTabState {
export interface ElementSelectedEvent {
type: "element-selected" | "area-selected";
ref?: string;
selectionKey?: string;
/** "local" = own dev server (localhost), "external" = any other website */
context?: "local" | "external";
element?: {
Expand Down Expand Up @@ -124,8 +125,29 @@ export interface BrowserTabHandle {
/** Inject inspect-mode + visual-effects scripts. Returns true on success. */
injectAutomation: () => Promise<boolean>;
toggleElementSelector: () => void;
/** Capture the current page as a PNG data URL (or null on failure). */
captureScreenshot?: () => Promise<string | null>;
/** Capture the current page — or a sub-rect of it — as a PNG data URL.
* `rect` is in webview-local pixels (same coord space Electron's
* `webContents.capturePage` uses). Returns null on failure. */
captureScreenshot?: (rect?: {
x: number;
y: number;
width: number;
height: number;
}) => Promise<string | null>;
/** Hide or restore the inspect-mode visuals (blue hover border, element
* label, custom cursor). Used to capture a clean screenshot of the
* selected element without the inspector painting on top. No-op when
* inspect mode isn't active or automation hasn't been injected. */
setInspectOverlaysVisible?: (visible: boolean) => Promise<void>;
/** Release the pinned selection border. When `expectedSelectionKey` is
* provided, the guest only clears if that same click selection is still
* pinned, preventing stale async cleanup from wiping out a newer one. */
clearInspectSelection?: (expectedSelectionKey?: string) => Promise<void>;
/** Current screen-space bounds of the webview's page area (accounts for
* mobile-view centering + DevTools docking). Returns null if the tab
* hasn't been measured yet. Read once at click time — the InspectPrompt
* overlay uses it to translate guest-viewport rects into host coords. */
getWebviewBounds?: () => { x: number; y: number; width: number; height: number } | null;
/** Open devtools in the tab-owning web contents. */
openDevtools?: () => Promise<void>;
/** Close devtools. */
Expand Down
Loading
Loading