fix: restore browser inspect annotation submission (#2374) (#2381)

This commit is contained in:
Carlos Florêncio
2026-08-07 18:11:37 +01:00
committed by GitHub
parent 05e72c10e3
commit c97e85448b
8 changed files with 444 additions and 0 deletions
@@ -52,6 +52,40 @@ func TestInspectorScript_UsesPreviewRouteForAnnotationPagePath(t *testing.T) {
}
}
func TestInspectorScript_PreservesPendingAnnotationAcrossPopupCleanup(t *testing.T) {
openStart := strings.Index(inspectorScript, "function openCommentPopup(")
if openStart < 0 {
t.Fatal("inspector should define openCommentPopup")
}
openEnd := strings.Index(inspectorScript[openStart:], "function commitAnnotation(")
if openEnd < 0 {
t.Fatal("inspector should define commitAnnotation after openCommentPopup")
}
openBody := inspectorScript[openStart : openStart+openEnd]
keep := strings.Index(openBody, "var keep = pendingAnnotation;")
cleanup := strings.Index(openBody, "closePopup();")
restore := strings.Index(openBody, "pendingAnnotation = keep;")
if keep < 0 || cleanup < 0 || restore < 0 {
t.Fatal("openCommentPopup should preserve pendingAnnotation across preventive cleanup")
}
if keep >= cleanup || cleanup >= restore {
t.Fatal("openCommentPopup should preserve pendingAnnotation across preventive cleanup")
}
closeStart := strings.Index(inspectorScript, "function closePopup()")
if closeStart < 0 {
t.Fatal("inspector should define closePopup")
}
closeEnd := strings.Index(inspectorScript[closeStart:], "function currentPagePath()")
if closeEnd < 0 {
t.Fatal("inspector should define currentPagePath after closePopup")
}
if !strings.Contains(inspectorScript[closeStart:closeStart+closeEnd], "pendingAnnotation = null;") {
t.Fatal("direct popup cleanup should still clear pendingAnnotation")
}
}
func TestStripIframeSecurityHeaders_RemovesBlockingHeaders(t *testing.T) {
h := http.Header{}
h.Set("Content-Security-Policy", "default-src 'none'")
@@ -137,7 +137,9 @@
}
function openCommentPopup(anchorX, anchorY, onSubmit) {
var keep = pendingAnnotation;
closePopup();
pendingAnnotation = keep;
popup = document.createElement('div');
popup.style.cssText = 'position:fixed;left:' + anchorX + 'px;top:' + anchorY + 'px;z-index:' + (Z + 1) + ';'
+ 'background:#fff;border:1px solid #e5e7eb;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.15);'
@@ -0,0 +1,124 @@
import http from "node:http";
import type { AddressInfo } from "node:net";
import { expect } from "@playwright/test";
import { test } from "../../fixtures/test-base";
import { openTaskSession, createStandardProfile } from "../../helpers/git-helper";
const MOCK_HTML = `<!DOCTYPE html>
<html>
<head><title>Mock Dev Server</title></head>
<body>
<button id="submit-btn" role="button" aria-label="Submit form">Submit</button>
</body>
</html>`;
async function startMockDevServer(): Promise<{ port: number; close: () => Promise<void> }> {
const server = http.createServer((_req, res) => {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(MOCK_HTML);
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address() as AddressInfo;
return {
port,
close: () =>
new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve())),
),
};
}
async function openPreview(
testPage: import("@playwright/test").Page,
apiClient: import("../../helpers/api-client").ApiClient,
seedData: { workspaceId: string; workflowId: string; startStepId: string; repositoryId: string },
title: string,
port: number,
) {
await apiClient.updateRepository(seedData.repositoryId, { dev_script: "echo preview" });
const profile = await createStandardProfile(apiClient, `inspector-${title.toLowerCase()}`);
await apiClient.createTaskWithAgent(seedData.workspaceId, title, profile.id, {
description: "preview inspector submission",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const session = await openTaskSession(testPage, title);
await session.waitForChatIdle({ timeout: 30_000 });
const openBrowserPreview = testPage.getByRole("button", { name: /open browser preview/i });
await expect(openBrowserPreview).toBeVisible();
await openBrowserPreview.click();
const urlInput = testPage.locator('[placeholder*="localhost"], [placeholder*="3000"]').first();
await expect(urlInput).toBeVisible();
await urlInput.fill(`http://localhost:${port}`);
await urlInput.press("Enter");
const iframe = testPage.locator('iframe[title*="preview" i]');
await expect(iframe).toBeVisible({ timeout: 15_000 });
const preview = testPage.frameLocator('iframe[title*="preview" i]');
await expect(preview.locator("#submit-btn")).toBeVisible();
await testPage.getByTestId("preview-inspect-button").click();
await expect(preview.locator("#submit-btn")).toBeVisible();
return preview;
}
test.describe("Browser inspect annotation submission", () => {
test("Save submits a pin annotation", async ({ testPage, apiClient, seedData }) => {
const devServer = await startMockDevServer();
try {
const preview = await openPreview(
testPage,
apiClient,
seedData,
"Inspector Save",
devServer.port,
);
await preview.locator("#submit-btn").click();
const comment = preview.locator("textarea");
await expect(comment).toBeVisible();
await comment.fill("make this primary color");
await preview.getByRole("button", { name: "Save" }).click();
await expect(preview.locator("textarea")).toHaveCount(0);
await expect(preview.locator('[data-kandev-marker="1"]')).toBeVisible();
await expect(testPage.getByTestId("preview-annotation-item")).toContainText(
"make this primary color",
);
} finally {
await apiClient.updateRepository(seedData.repositoryId, { dev_script: "" });
await devServer.close();
}
});
test("Enter submits a pin annotation", async ({ testPage, apiClient, seedData }) => {
const devServer = await startMockDevServer();
try {
const preview = await openPreview(
testPage,
apiClient,
seedData,
"Inspector Enter",
devServer.port,
);
await preview.locator("#submit-btn").click();
const comment = preview.locator("textarea");
await expect(comment).toBeVisible();
await comment.fill("increase font size");
await comment.press("Enter");
await expect(preview.locator("textarea")).toHaveCount(0);
await expect(preview.locator('[data-kandev-marker="1"]')).toBeVisible();
await expect(testPage.getByTestId("preview-annotation-item")).toContainText(
"increase font size",
);
} finally {
await apiClient.updateRepository(seedData.repositoryId, { dev_script: "" });
await devServer.close();
}
});
});
@@ -0,0 +1,101 @@
---
spec: docs/specs/browser-inspect-annotations-save/spec.md
created: 2026-08-07
status: complete
---
# Implementation Plan: Browser inspect annotation submission
## Overview
Repair the injected inspector's pending-annotation lifecycle with the smallest
possible change: preserve the selection while `openCommentPopup` performs its
defensive popup cleanup. Add a script-level regression test and focused browser
coverage for both Save and plain-Enter submission, while keeping Cancel/Escape
cleanup unchanged.
The issue's root cause is confirmed in
`apps/backend/internal/agentctl/server/api/scripts/inspector.js`: `startPending`
sets `pendingAnnotation`, then `openCommentPopup` calls `closePopup`, which
unconditionally clears it before either submit handler runs.
## Backend
### Injected inspector state
- `apps/backend/internal/agentctl/server/api/scripts/inspector.js`
- Save the current `pendingAnnotation` before the preventive
`closePopup()` call in `openCommentPopup`.
- Restore it immediately after cleanup and before wiring the popup handlers.
- Leave direct Cancel/Escape calls to `closePopup()` unchanged so dismissal
still discards the pending selection.
## Frontend
### Preview submission coverage
- `apps/web/e2e/tests/preview/inspector-submission.spec.ts`
- Use the existing E2E fixtures and a local mock HTML server.
- Configure the seeded repository's dev script so the Browser panel action is
available, open the Browser panel, switch to Inspect mode, and interact
with the real injected iframe popup.
- Cover clicking Save and pressing plain Enter, asserting the popup closes,
the iframe marker exists, and the parent annotations panel contains the
submitted comment.
- Restore any seeded repository settings in teardown.
This is a state-only repair in the existing preview surface. It does not alter
responsive composition, touch targets, scrolling, navigation, or viewport
geometry, so a separate mobile Playwright test is not required; the shared
injected state transition is the same on mobile-sized previews.
## Tests
- **What:** `openCommentPopup` does not erase the selection created by
`startPending`, while the existing dismissal path still clears it.
**File:** `apps/backend/internal/agentctl/server/api/html_injector_test.go`.
**How:** assert the embedded inspector script preserves the pending value
across the cleanup call and retains the direct Cancel/Escape cleanup paths.
- **What:** the proxy-served inspector script remains syntactically valid and
the existing HTML injection behavior remains intact.
**File:** existing API package tests.
**How:** run the targeted Go package test and `node --check` on the injected
script.
## E2E Tests
- **Scenario:** **GIVEN** a pin selection with the comment popup open, **WHEN**
the user types a comment and clicks **Save**, **THEN** the popup closes, a
marker is present in the iframe, and the Browser annotations panel shows the
comment.
**File:** `apps/web/e2e/tests/preview/inspector-submission.spec.ts`.
- **Scenario:** **GIVEN** a pin selection with the comment popup open,
**WHEN** the user types a comment and presses plain **Enter**, **THEN** the
same marker and annotations-panel outcomes occur.
**File:** `apps/web/e2e/tests/preview/inspector-submission.spec.ts`.
## Verification Results
Implemented and verified:
- `cd apps/backend && go test ./internal/agentctl/server/api` — passed.
- `cd apps/backend && node --check internal/agentctl/server/api/scripts/inspector.js` — passed.
- `cd apps/backend && go test -run TestInspectorScript_PreservesPendingAnnotationAcrossPopupCleanup ./internal/agentctl/server/api` — passed after the expected red failure.
- `cd apps/web && pnpm e2e:run --project chromium tests/preview/inspector-submission.spec.ts` — 2 passed (Save and plain Enter).
- `git diff --check` — passed.
## Implementation Waves And Parallel Candidates
Wave 1 (sequential):
- [x] [task-01-inspector-state](task-01-inspector-state.md) — done
Wave 2 (sequential, depends on Wave 1):
- [x] [task-02-preview-submit-e2e](task-02-preview-submit-e2e.md) — done
## Open Questions
- The existing `preview-annotations.spec.ts` suite remains skipped because its
preview fixture lacks a configured `dev_script`; the focused regression test
sets that prerequisite locally rather than changing unrelated scenarios.
@@ -0,0 +1,64 @@
---
id: "01-inspector-state"
title: "Preserve inspector pending state"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/browser-inspect-annotations-save/spec.md"
---
# Task 01: Preserve inspector pending state
## Acceptance
- A pin or area selection remains available to `commitAnnotation` after the
popup is opened, so Save/Enter can emit `annotation-added` and close the
popup.
- Cancel and Escape still clear the pending selection without emitting an
annotation.
- The existing API package tests and JavaScript syntax check pass.
## Verification
- Add and run the focused regression test:
`cd apps/backend && go test -run TestInspectorScript_PreservesPendingAnnotationAcrossPopupCleanup ./internal/agentctl/server/api`
- Run the full affected package:
`cd apps/backend && go test ./internal/agentctl/server/api`
- Validate the embedded script parses:
`cd apps/backend && node --check internal/agentctl/server/api/scripts/inspector.js`
- Run `git diff --check`.
## Files likely touched
- `apps/backend/internal/agentctl/server/api/scripts/inspector.js`
- `apps/backend/internal/agentctl/server/api/html_injector_test.go`
## Dependencies
None.
## Parallelism
Sequential. The test and the embedded script share the same behavior contract.
## Inputs
- `docs/specs/browser-inspect-annotations-save/spec.md`, especially the Save,
Enter, and dismissal scenarios.
- `docs/plans/browser-inspect-annotations-save/plan.md`.
- Existing `closePopup`, `openCommentPopup`, `startPending`, and
`commitAnnotation` implementations in `inspector.js`.
## Output contract
Report the changed files, the red/green test results, exact commands, any
remaining E2E dependency, and synchronized task/plan statuses.
## Results
- RED: `cd apps/backend && go test -run TestInspectorScript_PreservesPendingAnnotationAcrossPopupCleanup ./internal/agentctl/server/api` failed at the new assertion before the production fix.
- GREEN: the same focused test passed after preserving `pendingAnnotation` across `closePopup()`.
- `cd apps/backend && go test ./internal/agentctl/server/api` — passed.
- `cd apps/backend && node --check internal/agentctl/server/api/scripts/inspector.js` — passed.
- `git diff --check` — passed.
@@ -0,0 +1,62 @@
---
id: "02-preview-submit-e2e"
title: "Cover preview annotation submission"
status: done
wave: 2
depends_on: ["01-inspector-state"]
plan: "plan.md"
spec: "../../specs/browser-inspect-annotations-save/spec.md"
---
# Task 02: Cover preview annotation submission
## Acceptance
- A real preview iframe test proves clicking Save creates the marker and
Browser annotations-panel entry.
- A real preview iframe test proves plain Enter follows the same successful
path for a pin selection.
- The test restores seeded repository settings and runs in the desktop
Chromium project without enabling the currently unrelated skipped suite.
## Verification
- Bootstrap a fresh worktree if needed:
`cd apps && pnpm install --frozen-lockfile`.
- Run the focused desktop E2E test with a production build:
`cd apps/web && pnpm e2e:run --project chromium tests/preview/inspector-submission.spec.ts`.
- Run `git diff --check`.
## Files likely touched
- `apps/web/e2e/tests/preview/inspector-submission.spec.ts`
## Dependencies
Task 01 must pass first.
## Parallelism
Sequential. This test depends on the repaired injected script and the same
preview fixture state.
## Inputs
- `docs/specs/browser-inspect-annotations-save/spec.md` scenarios.
- `docs/plans/browser-inspect-annotations-save/plan.md` E2E section.
- `apps/web/e2e/README.md` preview-fixture guidance.
- `apps/web/components/task/dockview-header-actions.tsx` Browser panel action.
- `apps/web/e2e/tests/preview/preview-annotations.spec.ts` mock server and
preview setup patterns.
## Output contract
Report the exact E2E command, test count and result, any browser artifacts,
cleanup performed, and synchronized task/plan statuses.
## Results
- `cd apps && pnpm install --frozen-lockfile` — dependencies already up to date.
- `cd apps/web && pnpm e2e:run --project chromium tests/preview/inspector-submission.spec.ts` — 2 passed in Chromium (Save and plain Enter).
- The test uses a local mock HTTP server, restores the seeded repository's
`dev_script`, and closes the server in teardown.
+1
View File
@@ -230,6 +230,7 @@ System pages (Radarr/Sonarr-style) for status, disk usage, database maintenance,
| [mobile-quick-chat-topbar](mobile-quick-chat-topbar/spec.md) | building |
| [native-code-review](native-code-review/spec.md) | building |
| [missing-task-route-recovery](missing-task-route-recovery/spec.md) | draft |
| [browser-inspect-annotations-save](browser-inspect-annotations-save/spec.md) | shipped |
---
@@ -0,0 +1,56 @@
---
status: shipped
created: 2026-08-07
owner: carlosflorencio
---
# Browser inspect annotation submission
## Why
Users can select a pin or area in the Browser panel's Inspect mode, but the
comment popup currently loses the selection before the user can submit it.
This makes the primary annotation workflow appear unresponsive and prevents
users from giving the agent visual change requests.
## What
- Selecting a pin or area opens the existing comment popup with that selection
pending until the user submits or cancels it.
- Clicking **Save** submits the pending annotation, closes the popup, places its
numbered marker in the preview, and adds it to the Browser panel's
annotations list.
- Pressing **Enter** without Shift has the same submission behavior as
clicking **Save**. Shift+Enter continues to insert a newline in the comment.
- The submitted annotation uses the existing `annotation-added` iframe message
contract; the parent continues to assign the display number.
- Clicking **Cancel** or pressing **Escape** closes the popup and discards the
pending selection without adding a marker or annotation.
- Empty comments remain valid submissions, matching the existing popup
behavior.
## Scenarios
- **GIVEN** Inspect mode is active and the user clicks an element, **WHEN** the
user enters a comment and clicks **Save**, **THEN** the popup closes, a
numbered marker appears at the clicked point, and the parent receives one
`annotation-added` message containing the pin and comment.
- **GIVEN** Inspect mode is active and the user clicks an element, **WHEN** the
user enters a comment and presses plain **Enter**, **THEN** the popup closes,
a numbered marker appears at the clicked point, and the parent receives one
`annotation-added` message containing the pin and comment.
- **GIVEN** a pin or area comment popup is open, **WHEN** the user presses
**Shift+Enter**, **THEN** no annotation is submitted and the popup remains
open with the comment newline inserted.
- **GIVEN** a pin or area comment popup is open, **WHEN** the user clicks
**Cancel** or presses **Escape**, **THEN** the popup closes and no marker or
`annotation-added` message is created.
## Out of scope
- Changing the parent React hook, annotation formatter, annotation panel, or
iframe message shape.
- Persisting annotations across iframe reloads, page reloads, or Kandev
restarts.
- Changing the Inspect button, preview layout, touch composition, or proxy
injection behavior.