Skip to content

feat(desktop): add annotation-to-agent integration for in-app browser - #1524

Closed
AviPeltz wants to merge 1 commit into
mainfrom
annotation
Closed

AviPeltz wants to merge 1 commit into
mainfrom
annotation

Conversation

@AviPeltz

@AviPeltz AviPeltz commented Feb 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Adds an annotation system to the in-app browser that lets users visually annotate web page elements and send feedback to Claude Code or Codex
  • Injects the agentation overlay into browser webviews via a bundled IIFE (React + agentation component)
  • Uses a fetch interceptor + console.log bridge to reliably communicate from the webview to the Electron main process, bypassing CSP connect-src restrictions that block webview→localhost fetches
  • Adds a tRPC annotation router with subscriptions for real-time annotation events
  • Auto-send mode: Toggle a lightning bolt button on the toolbar — each annotation immediately spawns a background agent tab
  • Manual mode: Click "Send" to batch all annotations, pick Claude Code or Codex, add optional context, then launch an agent tab
  • Formats annotations into heredoc CLI commands containing element details, CSS selectors, React component paths, and user feedback

New files

  • annotation/injection-wrapper.ts — IIFE entry point injected into webviews
  • annotation/build-bundle.ts — esbuild script to bundle agentation + React into IIFE
  • routers/annotation/annotation.ts — tRPC router (inject, remove, subscriptions, prompt formatting)
  • routers/annotation/utils/formatAnnotationPrompt.ts — Formats annotations into claude/codex CLI commands
  • SendAnnotationsDialog.tsx — Dialog for batch send with agent selector and context textarea
  • useAnnotationMode.ts — Hook managing overlay lifecycle, subscriptions, and auto-send

Test plan

  • Open a browser pane, navigate to a web page
  • Click the pencil button in the browser toolbar to activate annotation mode
  • Click elements on the page and add notes — agentation overlay should appear
  • Click the lightning bolt to enable auto-send, add an annotation — a new background tab should spawn with Claude Code
  • Disable auto-send, add more annotations, click "Send" in the agentation toolbar
  • Verify the SendAnnotationsDialog opens with annotation count and page URL
  • Select Codex, add optional context, click Send — new tab with codex command should open
  • Click the pencil button again to deactivate — overlay should be removed

Summary by CodeRabbit

Release Notes

  • New Features
    • Added annotation mode toggle to browser pane—users can now capture page annotations with a pencil button
    • New Send Annotations dialog enabling users to submit captured annotations to Claude or Codex AI agents
    • Support for additional context when sending annotations
    • Auto-send toggle for streamlined annotation workflow

Add an annotation system to the in-app browser that lets users click
elements on a web page, add notes, then send annotations to Claude Code
or Codex running in a new terminal tab.

Key components:
- Inject agentation overlay into browser webview via IIFE bundle
- Webhook server + fetch interceptor for reliable webview→main IPC
  (routes through console.log to bypass CSP connect-src restrictions)
- tRPC annotation router with subscriptions for real-time events
- Auto-send toggle: each annotation immediately spawns an agent tab
- Manual Send button: batch annotations with additional context
- Claude Code / Codex agent selector in send dialog
- Format annotations into heredoc CLI commands with element details,
  CSS selectors, React component paths, and user feedback
@coderabbitai

coderabbitai Bot commented Feb 16, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive annotation feature for the desktop application. Users can toggle annotation mode to capture page elements, which are collected via an injected overlay bundle, streamed through a webhook server, and submitted to AI agents (Claude or Codex) with formatted prompts. Integration includes a new TRPC router, browser manager enhancements, and UI components for annotation handling.

Changes

Cohort / File(s) Summary
Dependencies & Configuration
apps/desktop/package.json, apps/desktop/tsconfig.json
Added runtime dependency agentation (^2.2.1) and dev dependency esbuild (^0.24.0). Excluded annotation bundle output from TypeScript compilation.
TRPC Annotation Router
apps/desktop/src/lib/trpc/routers/annotation/annotation.ts, apps/desktop/src/lib/trpc/routers/index.ts
New annotation router exposing mutations (inject, remove, formatPrompt, formatSinglePrompt) and subscriptions (onAnnotationsSubmitted, onAnnotationAdded) to manage overlay lifecycle and format prompts for AI agents.
Annotation Prompt Utilities
apps/desktop/src/lib/trpc/routers/annotation/utils/formatAnnotationPrompt.ts
Utility functions to format annotation data into CLI commands for Claude and Codex agents, handling prompt assembly with context and metadata.
Annotation Injection & Bundling
apps/desktop/src/main/lib/browser/annotation/build-bundle.ts, apps/desktop/src/main/lib/browser/annotation/injection-wrapper.ts
Build script to bundle annotation overlay IIFE using esbuild; injection wrapper mounts Agentation component, intercepts fetch requests, manages annotations state, and exposes global control API.
Browser Manager Enhancements
apps/desktop/src/main/lib/browser/browser-manager.ts
Extended with webhook server for annotation event handling, overlay lifecycle APIs (injectAnnotationOverlay, removeAnnotationOverlay, getAnnotations), annotation state tracking, and console log filtering for webhook payloads.
Renderer Components & Hooks
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/BrowserPane.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SendAnnotationsDialog/*, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/useAnnotationMode/*
New useAnnotationMode hook managing annotation state (overlay toggle, dialog, agent selection); SendAnnotationsDialog component for submitting annotations; BrowserPane integration with toolbar button and workspace context; index barrel exports.
Store & Type Updates
apps/desktop/src/renderer/stores/tabs/store.ts, apps/desktop/src/renderer/stores/tabs/types.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx, apps/desktop/src/shared/tabs-types.ts
Extended TabsStore with toggleAnnotation and setAnnotationActive actions; added annotation state property to BrowserPaneState; passed workspaceId prop through component hierarchy.

Sequence Diagram

sequenceDiagram
    participant User
    participant BrowserPane
    participant useAnnotationMode Hook
    participant TRPC Router
    participant BrowserManager
    participant Webhook Server
    participant Annotation Overlay
    participant AI Agent

    User->>BrowserPane: Click annotation toggle
    BrowserPane->>useAnnotationMode Hook: toggle()
    useAnnotationMode Hook->>TRPC Router: injectOverlay mutation
    TRPC Router->>BrowserManager: injectAnnotationOverlay(paneId)
    BrowserManager->>BrowserManager: startWebhookServer()
    BrowserManager->>Annotation Overlay: Inject bundle with webhook URL
    Annotation Overlay->>Annotation Overlay: Mount Agentation component
    User->>Annotation Overlay: Select elements & annotate
    Annotation Overlay->>Webhook Server: POST annotation event
    Webhook Server->>BrowserManager: Parse & emit event
    BrowserManager->>TRPC Router: Stream via subscription
    TRPC Router->>useAnnotationMode Hook: onAnnotationAdded event
    useAnnotationMode Hook->>useAnnotationMode Hook: formatSinglePrompt mutation
    useAnnotationMode Hook->>AI Agent: Create tab with formatted command
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Implement in-app browser #1519: Modifies apps/desktop/src/main/lib/browser/browser-manager.ts with core browser management functionality that this PR extends for annotation webhook handling.

Poem

🐰 Hoppy annotations, bundled with care,
Webhooks and overlays floating through air,
Claude and Codex shall hear every word,
Of page elements annotated and stirred! 🎯✨

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (13 files):

⚔️ apps/desktop/package.json (content)
⚔️ apps/desktop/src/lib/trpc/routers/index.ts (content)
⚔️ apps/desktop/src/main/lib/browser/browser-manager.ts (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/BrowserPane.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (content)
⚔️ apps/desktop/src/renderer/stores/tabs/store.ts (content)
⚔️ apps/desktop/src/renderer/stores/tabs/types.ts (content)
⚔️ apps/desktop/src/shared/tabs-types.ts (content)
⚔️ apps/desktop/tsconfig.json (content)
⚔️ bun.lock (content)
⚔️ packages/db/drizzle/meta/_journal.json (content)
⚔️ packages/db/src/schema/schema.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature: adding annotation-to-agent integration for the in-app browser.
Description check ✅ Passed The description is comprehensive and well-structured, covering summary, new files, and a detailed test plan that exceeds the template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch annotation
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch annotation
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) ✅ View Branch
Fly.io Electric (Fly.io) ❌ Failed to deploy
Fly.io Streams (Fly.io) ✅ View App
Vercel API (Vercel) ✅ Open Preview
Vercel Web (Vercel) ✅ Open Preview
Vercel Marketing (Vercel) ✅ Open Preview
Vercel Admin (Vercel) ✅ Open Preview
Vercel Docs (Vercel) ✅ Open Preview

Preview updates automatically with new commits

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/lib/browser/annotation/build-bundle.ts`:
- Around line 47-52: The generated bundle files (written by build-bundle.ts via
writeFileSync using variables outfile and tsModulePath and exporting
ANNOTATION_BUNDLE/moduleContent) are being checked into git; update the
repository gitignore (root .gitignore or apps/desktop/.gitignore) to ignore
these generated artifacts by adding entries for
apps/desktop/src/main/lib/browser/annotation/annotation-bundle.js and
apps/desktop/src/main/lib/browser/annotation/annotation-bundle-string.ts (or a
single pattern like
apps/desktop/src/main/lib/browser/annotation/annotation-bundle*) so future runs
of the writeFileSync calls won’t produce tracked files.

In `@apps/desktop/src/main/lib/browser/annotation/injection-wrapper.ts`:
- Around line 287-298: The destroy() function currently omits restoring the
original window.fetch and disconnecting the MutationObserver created in
hideWebhookSettings; add module-scope variables (e.g., originalFetch: typeof
window.fetch | null and settingsObserver: MutationObserver | null), assign
originalFetch in installFetchIntercept and settingsObserver in
hideWebhookSettings, and then update destroy() to restore window.fetch from
originalFetch, disconnect and null out settingsObserver, reset originalFetch to
null, and set fetchInterceptInstalled = false so the fetch override and observer
are fully cleaned up.
- Around line 178-196: The MutationObserver created in hideWebhookSettings runs
indefinitely and should be disconnected once it has successfully hidden the
"Manage MCP & Webhooks" UI to avoid continuous callbacks; modify
hideWebhookSettings so that after finding the toolbar and hiding the matching
section (inside the for loop where btn.textContent?.includes("Manage MCP") is
checked) you call observer.disconnect() (or scope/replace the observer to the
toolbar element if you need continued observation), similar to how
injectToolbarButton disconnects its observer, and ensure you still handle the
case where the toolbar isn't yet present by leaving the observer active until
the hide action completes.
- Around line 65-69: The bug is that the ternary for body (init?.body ?? (input
instanceof Request ? null : null)) always returns null, dropping Request bodies;
fix by deriving body from either init.body or, when input is a Request, by
reading its body text (use await input.clone().text() to avoid consuming the
original stream), e.g. set body = init?.body ?? (input instanceof Request ?
await input.clone().text() : null), then keep the existing string check and
console.log(WEBHOOK_MARKER + body) logic; update the surrounding function to be
async if needed so you can await reading the Request body.

In `@apps/desktop/src/main/lib/browser/browser-manager.ts`:
- Around line 230-244: The annotationActive set is stale across navigations
causing injectAnnotationOverlay(paneId) to early-return after a page navigation;
update the logic so annotation state is cleared or the overlay re-injected on
navigation: add a did-navigate (or equivalent webview) listener for the pane
that either removes paneId from annotationActive or calls
injectAnnotationOverlay again (ensuring startWebhookServer(), evaluateJS(paneId,
...) and getAnnotationBundle() are invoked after navigation), and ensure any
cleanup/unsubscribe when panes are destroyed so annotationActive reflects the
actual page state and isAnnotationActive() returns correct results.
- Around line 178-216: The request-body is accumulated into the local variable
`body` in the `req.on("data", ...)` handler with no size cap; modify the HTTP
request handler in browser-manager (the `req.on("data")` / `req.on("end")`
block) to enforce a maximum body size (e.g. a few hundred KB), incrementally
track bytes received and if the limit is exceeded stop reading, respond with a
413 Payload Too Large (or destroy the socket) and return early; only attempt
JSON.parse and the existing logic that extracts `paneId` and emits
`annotations:submitted:${paneId}` or `annotation:added:${paneId}` after
confirming the body size is within limits. Ensure the size check references the
same `body` accumulation path so malformed or huge requests are rejected before
parsing or emitting.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SendAnnotationsDialog/SendAnnotationsDialog.tsx`:
- Around line 45-66: The handleSend flow currently treats formatPrompt
rejections silently because errors fall through to finally; update handleSend to
catch errors from formatPrompt and any subsequent operations (inside the try)
and surface them to the user (e.g., set an inline error state or call the app's
toast notifier). Specifically, wrap the await formatPrompt and following
addTab/setActiveTab/onOpenChange calls in a try/catch in the handleSend
function, set a local error state (or call the global toast API) with the caught
error message, and ensure setIsPending is still cleared in finally; reference
formatPrompt, handleSend, addTab, setActiveTab, onOpenChange when making
changes.
- Around line 105-107: The render uses new URL(annotationData.pageUrl).pathname
which will throw for malformed or relative values and crash the component;
update SendAnnotationsDialog to safely derive the pathname by wrapping the URL
parsing in a try/catch (or add a helper like safePathname(url: string): string
that returns new URL(url).pathname on success and falls back to the original url
or a default like "page" on failure) and replace the direct new URL(...) call
with a call to that safe function or inline try/catch so the render never throws
for bad annotationData.pageUrl.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/useAnnotationMode/useAnnotationMode.ts`:
- Around line 66-73: The promise returned by formatSinglePrompt in the
onAnnotationAdded flow is not handled for rejections; update the
onAnnotationAdded handler (the call to formatSinglePrompt(...) that currently
.then(({ command }) => addTab(workspaceId, { initialCommands: [command] }))) to
handle errors—either convert to async/await with try/catch or append a .catch()
to the promise chain, and in the catch log the error and avoid throwing (or
surface a user-facing error) so that failures in formatSinglePrompt don't become
unhandled promise rejections; keep references to formatSinglePrompt and addTab
when implementing the fix.
- Around line 78-85: The toggle callback currently calls
toggleAnnotation(paneId) regardless of whether injectOverlay or removeOverlay
throws, causing state desync and unhandled rejections; update the useCallback
toggle implementation to await the overlay mutation inside a try/catch, only
call toggleAnnotation(paneId) when the overlay operation succeeds, rethrow or
return the error so the caller can handle the rejection (preserve the async
return), and ensure dependencies remain [isActive, paneId, injectOverlay,
removeOverlay, toggleAnnotation]; reference functions: toggle (the callback),
injectOverlay, removeOverlay, toggleAnnotation, and the isActive/paneId
parameters.
🧹 Nitpick comments (8)
apps/desktop/package.json (1)

179-179: Update esbuild to the latest version for bug fixes and performance improvements.

^0.24.0 is now significantly outdated (released late 2024). The latest esbuild is 0.27.3. Consider bumping when convenient to pick up improvements from the intervening releases.

apps/desktop/src/lib/trpc/routers/annotation/annotation.ts (1)

72-106: formatPrompt and formatSinglePrompt are pure transformations — consider using .query() instead of .mutation().

These procedures have no side effects (no state changes, no I/O). Using .query() would be more semantically accurate and would allow tRPC client-side caching if needed. Not blocking, but worth considering.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/useAnnotationMode/useAnnotationMode.ts (1)

48-50: Unnecessary type assertion — subscription data should already be typed.

The tRPC subscription onAnnotationsSubmitted is typed as observable<AnnotationSubmission>, so data in the onData callback should already carry the correct type. The as AnnotationData cast bypasses type checking. If the types actually diverge, define a proper type mapping instead of casting.

apps/desktop/src/lib/trpc/routers/annotation/utils/formatAnnotationPrompt.ts (1)

61-125: Consider extracting the shared heredoc command wrapping into a helper.

Both formatAnnotationPrompt and formatSingleAnnotationPrompt duplicate the delimiter generation and agent-specific command wrapping (lines 39-55 vs 108-124). A small helper like wrapInHeredocCommand(prompt, agent) would reduce duplication.

apps/desktop/src/main/lib/browser/annotation/injection-wrapper.ts (1)

87-171: Brittle DOM traversal for toolbar injection.

The button injection relies on positional child indexing (container.children[1]), divider detection by element width (offsetWidth < 5), and sibling class copying. Any change to the agentation library's internal DOM structure will silently break this. This is inherently fragile for an external dependency's internals.

Consider adding a defensive early-return with a console warning if the expected structure isn't found, and document the agentation version this targets.

apps/desktop/src/main/lib/browser/browser-manager.ts (3)

155-228: Duplicated webhook event parsing and emission logic.

The event parsing and this.emit(...) calls for annotations:submitted and annotation:added are duplicated almost identically between startWebhookServer (lines 183–209) and the console-message handler (lines 308–329). This is a DRY violation that will lead to divergence if the event schema changes.

Consider extracting a shared private handleAnnotationPayload(paneId: string, data: {...}) method that both paths call.

Also applies to: 296-334


190-198: No validation that paneId corresponds to a registered pane.

The paneId is extracted directly from the URL path with no check against paneToWebContentsId. Any HTTP client on localhost can emit events for arbitrary pane IDs. While the blast radius is limited (localhost-only), this could cause unexpected behavior in tRPC subscription listeners.

A quick if (!this.paneToWebContentsId.has(paneId)) guard before emitting would tighten this up.


31-33: Webhook server is never shut down.

webhookServer is started lazily but never closed — not on unregister, not when all annotation panes are gone. For a long-running Electron app this is a minor leak (port stays open). Consider shutting the server down when annotationActive becomes empty, or exposing a teardown method.

Comment on lines +47 to +52
const moduleContent = `// Auto-generated by build-bundle.ts — do not edit manually\nexport const ANNOTATION_BUNDLE = ${JSON.stringify(code)};\n`;
writeFileSync(outfile, code);

// Also write as a TS module for direct import
const tsModulePath = resolve(__dirname, "annotation-bundle-string.ts");
writeFileSync(tsModulePath, moduleContent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check gitignore status for the generated files
# First, try the provided approach
cd apps/desktop/src/main/lib/browser/annotation/ 2>/dev/null && git check-ignore annotation-bundle.js annotation-bundle-string.ts 2>&1 || echo "---"

# Check all .gitignore files in the repository for relevant patterns
echo "=== Searching for gitignore patterns related to annotation-bundle ==="
find . -name ".gitignore" -type f | xargs grep -l "annotation-bundle" 2>/dev/null || echo "No direct matches found"

# Check for broader patterns that might catch these files
echo "=== Checking for .gitignore files at various levels ==="
git ls-files -o --exclude-standard | grep -E "annotation-bundle\.(js|ts)$" || echo "Files are not in untracked list"

# Check the actual .gitignore files
echo "=== All .gitignore files in the repo ==="
find . -name ".gitignore" -type f | head -20

Repository: superset-sh/superset

Length of output: 290


🏁 Script executed:

#!/bin/bash
# Use fd to find .gitignore files more reliably
echo "=== Finding all .gitignore files ==="
fd -g ".gitignore" -type f

echo "=== Content of root .gitignore ==="
if [ -f ".gitignore" ]; then cat .gitignore; fi

echo "=== Content of apps/desktop .gitignore if exists ==="
if [ -f "apps/desktop/.gitignore" ]; then cat apps/desktop/.gitignore; fi

echo "=== Checking git status of generated files ==="
git status apps/desktop/src/main/lib/browser/annotation/annotation-bundle.js 2>&1 || true
git status apps/desktop/src/main/lib/browser/annotation/annotation-bundle-string.ts 2>&1 || true

echo "=== Checking if files are tracked ==="
git ls-files | grep -E "annotation-bundle\.(js|ts)$" || echo "NOT in tracked files"

echo "=== Checking untracked and ignored ==="
git status --porcelain apps/desktop/src/main/lib/browser/annotation/ 2>&1 || true

Repository: superset-sh/superset

Length of output: 2153


Add annotation-bundle.js and annotation-bundle-string.ts to .gitignore.

These generated build artifacts are currently tracked in git. Add patterns to the root .gitignore or apps/desktop/.gitignore to exclude them:

apps/desktop/src/main/lib/browser/annotation/annotation-bundle.js
apps/desktop/src/main/lib/browser/annotation/annotation-bundle-string.ts

Alternatively, add a pattern to cover generated files in this directory:

apps/desktop/src/main/lib/browser/annotation/annotation-bundle*
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/browser/annotation/build-bundle.ts` around lines 47
- 52, The generated bundle files (written by build-bundle.ts via writeFileSync
using variables outfile and tsModulePath and exporting
ANNOTATION_BUNDLE/moduleContent) are being checked into git; update the
repository gitignore (root .gitignore or apps/desktop/.gitignore) to ignore
these generated artifacts by adding entries for
apps/desktop/src/main/lib/browser/annotation/annotation-bundle.js and
apps/desktop/src/main/lib/browser/annotation/annotation-bundle-string.ts (or a
single pattern like
apps/desktop/src/main/lib/browser/annotation/annotation-bundle*) so future runs
of the writeFileSync calls won’t produce tracked files.

Comment on lines +65 to +69
if (targetUrl === webhookUrl) {
const body = init?.body ?? (input instanceof Request ? null : null);
if (body && typeof body === "string") {
console.log(WEBHOOK_MARKER + body);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Request body is never extracted — ternary always evaluates to null.

Line 66: init?.body ?? (input instanceof Request ? null : null) — both branches of the ternary return null, so if agentation passes a Request object to fetch (rather than a URL string + init), the body is silently dropped and the webhook message is never forwarded through console.log.

Proposed fix
-		const body = init?.body ?? (input instanceof Request ? null : null);
-		if (body && typeof body === "string") {
-			console.log(WEBHOOK_MARKER + body);
-		}
+		const body = init?.body ?? (input instanceof Request ? (input as Request).body : null);
+		if (body && typeof body === "string") {
+			console.log(WEBHOOK_MARKER + body);
+		} else if (input instanceof Request) {
+			// Request.body is a ReadableStream; read it as text
+			input.text().then((text) => {
+				if (text) console.log(WEBHOOK_MARKER + text);
+			}).catch(() => {});
+		}

Note: If agentation always calls fetch(url, { body: "..." }) (i.e. never constructs a Request), this is a latent bug. But it's worth fixing defensively.

🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/browser/annotation/injection-wrapper.ts` around
lines 65 - 69, The bug is that the ternary for body (init?.body ?? (input
instanceof Request ? null : null)) always returns null, dropping Request bodies;
fix by deriving body from either init.body or, when input is a Request, by
reading its body text (use await input.clone().text() to avoid consuming the
original stream), e.g. set body = init?.body ?? (input instanceof Request ?
await input.clone().text() : null), then keep the existing string check and
console.log(WEBHOOK_MARKER + body) logic; update the surrounding function to be
async if needed so you can await reading the Request body.

Comment on lines +178 to +196
function hideWebhookSettings() {
const observer = new MutationObserver(() => {
const toolbar = document.querySelector("[data-feedback-toolbar]");
if (!toolbar) return;

// Find and hide the "Manage MCP & Webhooks" nav link
const buttons = toolbar.querySelectorAll("button");
for (const btn of buttons) {
if (btn.textContent?.includes("Manage MCP")) {
const section = btn.closest("div");
if (section?.parentElement) {
(section as HTMLElement).style.display = "none";
}
}
}
});

observer.observe(document.body, { childList: true, subtree: true });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

MutationObserver in hideWebhookSettings is never disconnected.

Unlike injectToolbarButton which calls observer.disconnect() after success (line 167), the observer here runs indefinitely on document.body with subtree: true. This fires the callback on every DOM mutation for the lifetime of the page, which can degrade performance — especially on complex pages with frequent DOM updates.

Proposed fix — disconnect after settings are hidden
 function hideWebhookSettings() {
 	const observer = new MutationObserver(() => {
 		const toolbar = document.querySelector("[data-feedback-toolbar]");
 		if (!toolbar) return;

 		const buttons = toolbar.querySelectorAll("button");
+		let hidden = false;
 		for (const btn of buttons) {
 			if (btn.textContent?.includes("Manage MCP")) {
 				const section = btn.closest("div");
 				if (section?.parentElement) {
 					(section as HTMLElement).style.display = "none";
+					hidden = true;
 				}
 			}
 		}
+		if (hidden) observer.disconnect();
 	});

 	observer.observe(document.body, { childList: true, subtree: true });
 }

If the settings can reappear (e.g., after a panel re-render), you'd need to keep observing — but in that case, consider throttling or scoping the observer to the toolbar element once found.

🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/browser/annotation/injection-wrapper.ts` around
lines 178 - 196, The MutationObserver created in hideWebhookSettings runs
indefinitely and should be disconnected once it has successfully hidden the
"Manage MCP & Webhooks" UI to avoid continuous callbacks; modify
hideWebhookSettings so that after finding the toolbar and hiding the matching
section (inside the for loop where btn.textContent?.includes("Manage MCP") is
checked) you call observer.disconnect() (or scope/replace the observer to the
toolbar element if you need continued observation), similar to how
injectToolbarButton disconnects its observer, and ensure you still handle the
case where the toolbar isn't yet present by leaving the observer active until
the hide action completes.

Comment on lines +287 to +298
function destroy() {
if (reactRoot) {
reactRoot.unmount();
reactRoot = null;
}
const container = document.getElementById(CONTAINER_ID);
if (container) {
container.remove();
}
currentAnnotations = [];
autoSendEnabled = false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

destroy() does not clean up the fetch override or the hideWebhookSettings observer.

After destroy(), the patched window.fetch remains in place and the hideWebhookSettings MutationObserver continues running. If the overlay is destroyed and the page continues to be used, the fetch intercept silently inspects all fetches matching the webhook URL, and the observer fires on every DOM mutation.

Consider storing references to the original fetch and active observers so destroy() can restore them:

// In module scope:
let originalFetch: typeof window.fetch | null = null;
let settingsObserver: MutationObserver | null = null;

// In installFetchIntercept:
originalFetch = window.fetch;

// In hideWebhookSettings:
settingsObserver = observer;

// In destroy:
if (originalFetch) { window.fetch = originalFetch; originalFetch = null; }
if (settingsObserver) { settingsObserver.disconnect(); settingsObserver = null; }
fetchInterceptInstalled = false;
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/browser/annotation/injection-wrapper.ts` around
lines 287 - 298, The destroy() function currently omits restoring the original
window.fetch and disconnecting the MutationObserver created in
hideWebhookSettings; add module-scope variables (e.g., originalFetch: typeof
window.fetch | null and settingsObserver: MutationObserver | null), assign
originalFetch in installFetchIntercept and settingsObserver in
hideWebhookSettings, and then update destroy() to restore window.fetch from
originalFetch, disconnect and null out settingsObserver, reset originalFetch to
null, and set fetchInterceptInstalled = false so the fetch override and observer
are fully cleaned up.

Comment on lines +178 to +216
let body = "";
req.on("data", (chunk: Buffer) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const data = JSON.parse(body) as {
event: string;
annotation?: Record<string, unknown>;
annotations?: unknown[];
output?: string;
url?: string;
};
// Extract paneId from URL path: /<paneId>
const paneId = req.url?.replace(/^\//, "");
if (paneId && data.event === "submit" && data.output) {
this.emit(`annotations:submitted:${paneId}`, {
annotations: data.annotations ?? [],
output: data.output,
pageUrl: data.url ?? "",
} satisfies AnnotationSubmission);
}
if (
paneId &&
data.event === "annotation.add" &&
data.annotation
) {
this.emit(`annotation:added:${paneId}`, {
annotation: data.annotation,
pageUrl: data.url ?? "",
});
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
} catch {
res.writeHead(400);
res.end();
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unbounded request body accumulation — add a size limit.

There's no cap on how much data body can accumulate. A malicious or buggy client on localhost could send an arbitrarily large POST, consuming unbounded memory. Even though the server is bound to 127.0.0.1, the webview content is untrusted (arbitrary web pages).

🛡️ Proposed fix
+				const MAX_BODY = 1024 * 256; // 256 KB
 				let body = "";
 				req.on("data", (chunk: Buffer) => {
+					if (body.length + chunk.length > MAX_BODY) {
+						res.writeHead(413);
+						res.end();
+						req.destroy();
+						return;
+					}
 					body += chunk.toString();
 				});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let body = "";
req.on("data", (chunk: Buffer) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const data = JSON.parse(body) as {
event: string;
annotation?: Record<string, unknown>;
annotations?: unknown[];
output?: string;
url?: string;
};
// Extract paneId from URL path: /<paneId>
const paneId = req.url?.replace(/^\//, "");
if (paneId && data.event === "submit" && data.output) {
this.emit(`annotations:submitted:${paneId}`, {
annotations: data.annotations ?? [],
output: data.output,
pageUrl: data.url ?? "",
} satisfies AnnotationSubmission);
}
if (
paneId &&
data.event === "annotation.add" &&
data.annotation
) {
this.emit(`annotation:added:${paneId}`, {
annotation: data.annotation,
pageUrl: data.url ?? "",
});
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
} catch {
res.writeHead(400);
res.end();
}
});
const MAX_BODY = 1024 * 256; // 256 KB
let body = "";
req.on("data", (chunk: Buffer) => {
if (body.length + chunk.length > MAX_BODY) {
res.writeHead(413);
res.end();
req.destroy();
return;
}
body += chunk.toString();
});
req.on("end", () => {
try {
const data = JSON.parse(body) as {
event: string;
annotation?: Record<string, unknown>;
annotations?: unknown[];
output?: string;
url?: string;
};
// Extract paneId from URL path: /<paneId>
const paneId = req.url?.replace(/^\//, "");
if (paneId && data.event === "submit" && data.output) {
this.emit(`annotations:submitted:${paneId}`, {
annotations: data.annotations ?? [],
output: data.output,
pageUrl: data.url ?? "",
} satisfies AnnotationSubmission);
}
if (
paneId &&
data.event === "annotation.add" &&
data.annotation
) {
this.emit(`annotation:added:${paneId}`, {
annotation: data.annotation,
pageUrl: data.url ?? "",
});
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
} catch {
res.writeHead(400);
res.end();
}
});
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/browser/browser-manager.ts` around lines 178 - 216,
The request-body is accumulated into the local variable `body` in the
`req.on("data", ...)` handler with no size cap; modify the HTTP request handler
in browser-manager (the `req.on("data")` / `req.on("end")` block) to enforce a
maximum body size (e.g. a few hundred KB), incrementally track bytes received
and if the limit is exceeded stop reading, respond with a 413 Payload Too Large
(or destroy the socket) and return early; only attempt JSON.parse and the
existing logic that extracts `paneId` and emits
`annotations:submitted:${paneId}` or `annotation:added:${paneId}` after
confirming the body size is within limits. Ensure the size check references the
same `body` accumulation path so malformed or huge requests are rejected before
parsing or emitting.

Comment on lines +230 to +244
async injectAnnotationOverlay(paneId: string): Promise<void> {
if (this.annotationActive.has(paneId)) return;

// Start the webhook server and set the per-pane webhook URL on the window
// so the injection bundle can read it.
const port = await this.startWebhookServer();
const webhookUrl = `http://127.0.0.1:${port}/${paneId}`;
await this.evaluateJS(
paneId,
`window.__supersetWebhookUrl = ${JSON.stringify(webhookUrl)}`,
);

const bundle = this.getAnnotationBundle();
await this.evaluateJS(paneId, bundle);
this.annotationActive.add(paneId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Annotation state becomes stale after page navigation.

injectAnnotationOverlay adds the paneId to annotationActive and injects JS into the current page. If the user navigates to a different URL, the injected overlay and window.__supersetWebhookUrl are lost, but annotationActive still contains the paneId. Subsequent calls to injectAnnotationOverlay will early-return (line 231), and isAnnotationActive will return a false positive.

Consider listening for the webview's did-navigate event to clear annotation state, or re-inject the overlay on navigation.

🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/browser/browser-manager.ts` around lines 230 - 244,
The annotationActive set is stale across navigations causing
injectAnnotationOverlay(paneId) to early-return after a page navigation; update
the logic so annotation state is cleared or the overlay re-injected on
navigation: add a did-navigate (or equivalent webview) listener for the pane
that either removes paneId from annotationActive or calls
injectAnnotationOverlay again (ensuring startWebhookServer(), evaluateJS(paneId,
...) and getAnnotationBundle() are invoked after navigation), and ensure any
cleanup/unsubscribe when panes are destroyed so annotationActive reflects the
actual page state and isAnnotationActive() returns correct results.

Comment on lines +45 to +66
const handleSend = useCallback(async () => {
if (!annotationData) return;

setIsPending(true);
try {
const { command } = await formatPrompt({
output: annotationData.output,
pageUrl: annotationData.pageUrl,
additionalContext: additionalContext.trim() || undefined,
agent,
});

const { tabId: newTabId } = addTab(workspaceId, {
initialCommands: [command],
});
setActiveTab(workspaceId, newTabId);

onOpenChange(false);
setAdditionalContext("");
} finally {
setIsPending(false);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent failure — no user feedback when formatPrompt rejects.

If the mutation throws, execution skips to finally and the dialog remains open with no error message. The user has no indication of what went wrong.

Consider catching the error and surfacing it (e.g., via a toast or inline error state).

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SendAnnotationsDialog/SendAnnotationsDialog.tsx`
around lines 45 - 66, The handleSend flow currently treats formatPrompt
rejections silently because errors fall through to finally; update handleSend to
catch errors from formatPrompt and any subsequent operations (inside the try)
and surface them to the user (e.g., set an inline error state or call the app's
toast notifier). Specifically, wrap the await formatPrompt and following
addTab/setActiveTab/onOpenChange calls in a try/catch in the handleSend
function, set a local error state (or call the global toast API) with the caught
error message, and ensure setIsPending is still cleared in finally; reference
formatPrompt, handleSend, addTab, setActiveTab, onOpenChange when making
changes.

Comment on lines +105 to +107
{annotationData?.pageUrl
? new URL(annotationData.pageUrl).pathname
: "page"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

new URL() in render path will crash on malformed pageUrl.

If annotationData.pageUrl is not a valid URL (e.g., about:blank, a relative path, or an empty string), new URL(...) throws a TypeError which will unmount this component tree.

Proposed fix
-						{annotationData?.pageUrl
-							? new URL(annotationData.pageUrl).pathname
-							: "page"}
+						{annotationData?.pageUrl
+							? (() => {
+									try {
+										return new URL(annotationData.pageUrl).pathname;
+									} catch {
+										return annotationData.pageUrl;
+									}
+								})()
+							: "page"}

Or extract a helper:

function safePathname(url: string): string {
  try { return new URL(url).pathname; } catch { return url; }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{annotationData?.pageUrl
? new URL(annotationData.pageUrl).pathname
: "page"}
{annotationData?.pageUrl
? (() => {
try {
return new URL(annotationData.pageUrl).pathname;
} catch {
return annotationData.pageUrl;
}
})()
: "page"}
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SendAnnotationsDialog/SendAnnotationsDialog.tsx`
around lines 105 - 107, The render uses new URL(annotationData.pageUrl).pathname
which will throw for malformed or relative values and crash the component;
update SendAnnotationsDialog to safely derive the pathname by wrapping the URL
parsing in a try/catch (or add a helper like safePathname(url: string): string
that returns new URL(url).pathname on success and falls back to the original url
or a default like "page" on failure) and replace the direct new URL(...) call
with a call to that safe function or inline try/catch so the render never throws
for bad annotationData.pageUrl.

Comment on lines +66 to +73
formatSinglePrompt({
annotation,
pageUrl,
agent: agentRef.current,
}).then(({ command }) => {
// Create a tab in the background — don't switch to it
addTab(workspaceId, { initialCommands: [command] });
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unhandled promise rejection in onAnnotationAdded handler.

formatSinglePrompt(...).then(...) has no .catch(). If the mutation fails, the rejection propagates as an unhandled promise rejection, which can crash in strict environments or silently swallow errors.

Proposed fix
 formatSinglePrompt({
 	annotation,
 	pageUrl,
 	agent: agentRef.current,
 }).then(({ command }) => {
 	addTab(workspaceId, { initialCommands: [command] });
-});
+}).catch((err) => {
+	console.error("Failed to format single annotation prompt:", err);
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
formatSinglePrompt({
annotation,
pageUrl,
agent: agentRef.current,
}).then(({ command }) => {
// Create a tab in the background — don't switch to it
addTab(workspaceId, { initialCommands: [command] });
});
formatSinglePrompt({
annotation,
pageUrl,
agent: agentRef.current,
}).then(({ command }) => {
// Create a tab in the background — don't switch to it
addTab(workspaceId, { initialCommands: [command] });
}).catch((err) => {
console.error("Failed to format single annotation prompt:", err);
});
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/useAnnotationMode/useAnnotationMode.ts`
around lines 66 - 73, The promise returned by formatSinglePrompt in the
onAnnotationAdded flow is not handled for rejections; update the
onAnnotationAdded handler (the call to formatSinglePrompt(...) that currently
.then(({ command }) => addTab(workspaceId, { initialCommands: [command] }))) to
handle errors—either convert to async/await with try/catch or append a .catch()
to the promise chain, and in the catch log the error and avoid throwing (or
surface a user-facing error) so that failures in formatSinglePrompt don't become
unhandled promise rejections; keep references to formatSinglePrompt and addTab
when implementing the fix.

Comment on lines +78 to +85
const toggle = useCallback(async () => {
if (isActive) {
await removeOverlay({ paneId });
} else {
await injectOverlay({ paneId });
}
toggleAnnotation(paneId);
}, [isActive, paneId, injectOverlay, removeOverlay, toggleAnnotation]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Store state desyncs from overlay if the mutation rejects.

If injectOverlay or removeOverlay throws (e.g., webview is gone, IPC failure), toggleAnnotation(paneId) on line 84 still executes, leaving isActive out of sync with the actual overlay state. The caller in BrowserPane also ignores the returned promise, so the rejection is unhandled.

Proposed fix — catch and guard
 const toggle = useCallback(async () => {
-	if (isActive) {
-		await removeOverlay({ paneId });
-	} else {
-		await injectOverlay({ paneId });
+	try {
+		if (isActive) {
+			await removeOverlay({ paneId });
+		} else {
+			await injectOverlay({ paneId });
+		}
+		toggleAnnotation(paneId);
+	} catch (err) {
+		console.error("Failed to toggle annotation overlay:", err);
 	}
-	toggleAnnotation(paneId);
 }, [isActive, paneId, injectOverlay, removeOverlay, toggleAnnotation]);
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/useAnnotationMode/useAnnotationMode.ts`
around lines 78 - 85, The toggle callback currently calls
toggleAnnotation(paneId) regardless of whether injectOverlay or removeOverlay
throws, causing state desync and unhandled rejections; update the useCallback
toggle implementation to await the overlay mutation inside a try/catch, only
call toggleAnnotation(paneId) when the overlay operation succeeds, rethrow or
return the error so the caller can handle the rejection (preserve the async
return), and ensure dependencies remain [isActive, paneId, injectOverlay,
removeOverlay, toggleAnnotation]; reference functions: toggle (the callback),
injectOverlay, removeOverlay, toggleAnnotation, and the isActive/paneId
parameters.

@Kitenite

Copy link
Copy Markdown
Member

Closing: stale experimental browser annotation PR from Feb 16 with no recent activity.

@Kitenite Kitenite closed this Mar 13, 2026
@Kitenite

Copy link
Copy Markdown
Member

Hey — just a heads up, this was closed as part of an automated stale PR cleanup. If you think this was done in error, feel free to reopen it!

@Kitenite

Copy link
Copy Markdown
Member

Hey — this was closed by an automated cleanup of PRs with major merge conflicts that are 3+ weeks old. If you think this was done incorrectly, please feel free to reopen it. Sorry for any inconvenience!

@Kitenite
Kitenite deleted the annotation branch March 15, 2026 16:07

This branch was previously deployed

1 inactive deployment
preview — 8a47aa46 Deployed Feb 16, 2026 by AviPeltz via Deploy API #2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants