" }),
+ ).resolves.toBeNull();
+ });
+
+ expect(mocks.publishHtmlPreview).not.toHaveBeenCalled();
+ expect(result.current.error).toBe("session-unavailable");
+ });
+
+ it("discards a stale publish that completes after reset", async () => {
+ let resolveFirst!: (value: { port: number; path: string; version: number }) => void;
+ mocks.publishHtmlPreview.mockReturnValueOnce(
+ new Promise((resolve) => {
+ resolveFirst = resolve;
+ }),
+ );
+
+ const { result } = renderHook(() => useHtmlPreviewPublisher("session-1"));
+ const firstPublish = result.current.publish({
+ path: "index.html",
+ content: "
v1
",
+ });
+
+ act(() => result.current.reset());
+ resolveFirst({ port: 43127, path: "/index.html", version: 1 });
+
+ await act(async () => {
+ await expect(firstPublish).resolves.toBeNull();
+ });
+
+ expect(result.current.status).toBe("idle");
+ expect(result.current.url).toBeNull();
+ expect(result.current.error).toBeNull();
+ });
+});
+
+describe("HTML preview publish error mapping", () => {
+ it("maps API limits and availability failures to localized keys", () => {
+ expect(getHtmlPreviewPublishErrorCode(new ApiError("too large", 413, null))).toBe("too-large");
+ expect(getHtmlPreviewPublishErrorCode(new ApiError("gone", 503, null))).toBe(
+ "session-unavailable",
+ );
+ expect(getHtmlPreviewPublishErrorKey("publish-failed")).toBe("task:htmlPreviewPublishFailed");
+ });
+});
diff --git a/apps/web/hooks/use-html-preview-publisher.ts b/apps/web/hooks/use-html-preview-publisher.ts
new file mode 100644
index 0000000000..955547262b
--- /dev/null
+++ b/apps/web/hooks/use-html-preview-publisher.ts
@@ -0,0 +1,97 @@
+"use client";
+
+import { useCallback, useEffect, useRef, useState } from "react";
+import { ApiError } from "@/lib/api/client";
+import {
+ buildHtmlPreviewProxyUrl,
+ publishHtmlPreview,
+ type HtmlPreviewPublishRequest,
+} from "@/lib/api/domains/process-api";
+
+export type HtmlPreviewPublishErrorCode = "session-unavailable" | "too-large" | "publish-failed";
+
+export type HtmlPreviewPublishState = {
+ status: "idle" | "publishing" | "ready" | "error";
+ url: string | null;
+ error: HtmlPreviewPublishErrorCode | null;
+};
+
+const IDLE_STATE: HtmlPreviewPublishState = {
+ status: "idle",
+ url: null,
+ error: null,
+};
+
+export function getHtmlPreviewPublishErrorCode(error: unknown): HtmlPreviewPublishErrorCode {
+ if (error instanceof ApiError && error.status === 413) return "too-large";
+ if (error instanceof ApiError && [404, 409, 503].includes(error.status)) {
+ return "session-unavailable";
+ }
+ return "publish-failed";
+}
+
+export function getHtmlPreviewPublishErrorKey(
+ error: HtmlPreviewPublishErrorCode,
+): "task:htmlPreviewTooLarge" | "task:htmlPreviewUnavailable" | "task:htmlPreviewPublishFailed" {
+ if (error === "too-large") return "task:htmlPreviewTooLarge";
+ if (error === "session-unavailable") return "task:htmlPreviewUnavailable";
+ return "task:htmlPreviewPublishFailed";
+}
+
+export async function publishHtmlPreviewUrl(
+ sessionId: string,
+ payload: HtmlPreviewPublishRequest,
+): Promise {
+ const response = await publishHtmlPreview(sessionId, payload);
+ return buildHtmlPreviewProxyUrl(sessionId, response);
+}
+
+export function useHtmlPreviewPublisher(sessionId: string | null) {
+ const [state, setState] = useState(IDLE_STATE);
+ const requestIdRef = useRef(0);
+
+ useEffect(() => {
+ requestIdRef.current += 1;
+ setState(IDLE_STATE);
+ }, [sessionId]);
+
+ const reset = useCallback(() => {
+ requestIdRef.current += 1;
+ setState(IDLE_STATE);
+ }, []);
+
+ const publish = useCallback(
+ async (payload: HtmlPreviewPublishRequest): Promise => {
+ const requestId = requestIdRef.current + 1;
+ requestIdRef.current = requestId;
+ if (!sessionId) {
+ setState({ status: "error", url: null, error: "session-unavailable" });
+ return null;
+ }
+
+ setState({ status: "publishing", url: null, error: null });
+ try {
+ const url = await publishHtmlPreviewUrl(sessionId, payload);
+ if (requestIdRef.current !== requestId) return null;
+ setState({ status: "ready", url, error: null });
+ return url;
+ } catch (error) {
+ if (requestIdRef.current !== requestId) return null;
+ setState({
+ status: "error",
+ url: null,
+ error: getHtmlPreviewPublishErrorCode(error),
+ });
+ return null;
+ }
+ },
+ [sessionId],
+ );
+
+ return {
+ ...state,
+ isPublishing: state.status === "publishing",
+ publish,
+ reset,
+ };
+}
diff --git a/apps/web/hooks/use-task-center-file-open.ts b/apps/web/hooks/use-task-center-file-open.ts
new file mode 100644
index 0000000000..78202936c5
--- /dev/null
+++ b/apps/web/hooks/use-task-center-file-open.ts
@@ -0,0 +1,54 @@
+"use client";
+
+import { useCallback } from "react";
+import type { OpenFileTab, FileContentResponse } from "@/lib/types/backend";
+import { getWebSocketClient } from "@/lib/ws/connection";
+import { requestFileContent } from "@/lib/ws/workspace-files";
+import { calculateHash } from "@/lib/utils/file-diff";
+import { t } from "@/lib/i18n";
+
+type UseTaskCenterFileOpenOptions = {
+ activeSessionId: string | null;
+ addFileTab: (fileTab: OpenFileTab) => void;
+ toast: (options: { title: string; description: string; variant: "error" }) => void;
+};
+
+export function useTaskCenterFileOpen({
+ activeSessionId,
+ addFileTab,
+ toast,
+}: UseTaskCenterFileOpenOptions) {
+ return useCallback(
+ async (filePath: string, repo?: string) => {
+ const client = getWebSocketClient();
+ if (!client || !activeSessionId) return;
+ try {
+ const response: FileContentResponse = await requestFileContent(
+ client,
+ activeSessionId,
+ filePath,
+ repo,
+ );
+ const fileName = filePath.split("/").pop() || filePath;
+ const hash = await calculateHash(response.content);
+ addFileTab({
+ path: filePath,
+ repo,
+ name: fileName,
+ content: response.content,
+ originalContent: response.content,
+ originalHash: hash,
+ isDirty: false,
+ isBinary: response.is_binary,
+ });
+ } catch (error) {
+ toast({
+ title: t("task:failedToOpenFile"),
+ description: error instanceof Error ? error.message : t("task:unknownError"),
+ variant: "error",
+ });
+ }
+ },
+ [activeSessionId, addFileTab, toast],
+ );
+}
diff --git a/apps/web/lib/api/domains/process-api.test.ts b/apps/web/lib/api/domains/process-api.test.ts
new file mode 100644
index 0000000000..d27a8d520a
--- /dev/null
+++ b/apps/web/lib/api/domains/process-api.test.ts
@@ -0,0 +1,72 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const fetchJsonMock = vi.hoisted(() => vi.fn());
+
+vi.mock("../client", () => ({
+ fetchJson: fetchJsonMock,
+}));
+
+vi.mock("@/lib/config", () => ({
+ getBackendConfig: () => ({ apiBaseUrl: "http://api.test" }),
+}));
+
+import {
+ buildHtmlPreviewProxyUrl,
+ publishHtmlPreview,
+ type HtmlPreviewPublishResponse,
+} from "./process-api";
+
+describe("HTML preview process API", () => {
+ beforeEach(() => fetchJsonMock.mockReset());
+
+ it("publishes the current buffer through the session endpoint", async () => {
+ const response: HtmlPreviewPublishResponse = {
+ port: 43127,
+ path: "/site/index.html",
+ version: 4,
+ };
+ fetchJsonMock.mockResolvedValueOnce(response);
+
+ await expect(
+ publishHtmlPreview("session-1", {
+ repo: "frontend",
+ path: "site/index.html",
+ content: "unsaved",
+ }),
+ ).resolves.toEqual(response);
+
+ expect(fetchJsonMock).toHaveBeenCalledWith(
+ "/api/v1/task-sessions/session-1/html-previews",
+ expect.objectContaining({
+ init: {
+ method: "POST",
+ body: JSON.stringify({
+ repo: "frontend",
+ path: "site/index.html",
+ content: "unsaved",
+ }),
+ },
+ }),
+ );
+ });
+
+ it("builds a versioned session port-proxy URL", () => {
+ expect(
+ buildHtmlPreviewProxyUrl("session/1", {
+ port: 43127,
+ path: "/site/index.html",
+ version: 4,
+ }),
+ ).toBe("http://api.test/port-proxy/session%2F1/43127/site/index.html?v=4");
+ });
+
+ it("encodes reserved characters in each preview path segment", () => {
+ expect(
+ buildHtmlPreviewProxyUrl("session-1", {
+ port: 43127,
+ path: "/site/report#1.html",
+ version: 5,
+ }),
+ ).toBe("http://api.test/port-proxy/session-1/43127/site/report%231.html?v=5");
+ });
+});
diff --git a/apps/web/lib/api/domains/process-api.ts b/apps/web/lib/api/domains/process-api.ts
index d00b380bc4..2d38a410d9 100644
--- a/apps/web/lib/api/domains/process-api.ts
+++ b/apps/web/lib/api/domains/process-api.ts
@@ -2,6 +2,50 @@ import { fetchJson, type ApiRequestOptions } from "../client";
import { getBackendConfig } from "@/lib/config";
import type { ProcessInfo } from "@/lib/types/http";
+export type HtmlPreviewPublishResponse = {
+ port: number;
+ path: string;
+ version: number;
+};
+
+export type HtmlPreviewPublishRequest = {
+ repo?: string;
+ path: string;
+ content: string;
+};
+
+export async function publishHtmlPreview(
+ sessionId: string,
+ payload: HtmlPreviewPublishRequest,
+ options?: ApiRequestOptions,
+) {
+ return fetchJson(
+ `/api/v1/task-sessions/${encodeURIComponent(sessionId)}/html-previews`,
+ {
+ ...options,
+ init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
+ },
+ );
+}
+
+export function buildHtmlPreviewProxyUrl(
+ sessionId: string,
+ response: HtmlPreviewPublishResponse,
+ options?: ApiRequestOptions,
+): string {
+ const baseUrl = (options?.baseUrl ?? getBackendConfig().apiBaseUrl).replace(/\/+$/, "");
+ const rawPath = response.path.startsWith("/") ? response.path : `/${response.path}`;
+ const path = rawPath
+ .split("/")
+ .map((segment) => (segment ? encodeURIComponent(segment) : segment))
+ .join("/");
+ const url = new URL(
+ `${baseUrl}/port-proxy/${encodeURIComponent(sessionId)}/${response.port}${path}`,
+ );
+ url.searchParams.set("v", String(response.version));
+ return url.toString();
+}
+
// Process operations
export async function startProcess(
sessionId: string,
diff --git a/apps/web/lib/e2e/backend-process.test.ts b/apps/web/lib/e2e/backend-process.test.ts
index 25b5b635a9..20e0fad868 100644
--- a/apps/web/lib/e2e/backend-process.test.ts
+++ b/apps/web/lib/e2e/backend-process.test.ts
@@ -1,6 +1,6 @@
import type { ChildProcess } from "node:child_process";
import { describe, expect, test, vi } from "vitest";
-import { killProcessGroup } from "../../e2e/fixtures/backend";
+import { killProcessGroup } from "../../e2e/fixtures/process-group";
function childProcessWithPid(pid: number): ChildProcess {
return { pid } as ChildProcess;
diff --git a/apps/web/lib/local-storage-file-tabs.ts b/apps/web/lib/local-storage-file-tabs.ts
new file mode 100644
index 0000000000..d755ed68de
--- /dev/null
+++ b/apps/web/lib/local-storage-file-tabs.ts
@@ -0,0 +1,8 @@
+import type { StoredFileTab } from "./local-storage";
+import { isMarkdownFile } from "./utils/file-types";
+
+export function normalizeStoredFileTab(tab: StoredFileTab): StoredFileTab {
+ const { markdownPreview, renderedPreview, ...current } = tab;
+ const preview = isMarkdownFile(tab.path) ? (renderedPreview ?? markdownPreview) : undefined;
+ return Object.assign(current, preview === undefined ? {} : { renderedPreview: preview });
+}
diff --git a/apps/web/lib/local-storage.test.ts b/apps/web/lib/local-storage.test.ts
index fe6145ea8d..ef366812ad 100644
--- a/apps/web/lib/local-storage.test.ts
+++ b/apps/web/lib/local-storage.test.ts
@@ -257,36 +257,82 @@ describe("task-scoped artifact notification storage", () => {
});
describe("open file tabs storage", () => {
+ const sourcePath = "README.md";
+
beforeEach(() => {
window.sessionStorage.clear();
});
- it("round-trips the multi-repo repo subpath so a restored tab refetches under the right repo", () => {
+ it("round-trips generic rendered preview state with the multi-repo repo subpath", () => {
setOpenFileTabs("sess-1", [
{
- path: "src/foo.ts",
- name: "foo.ts",
+ path: sourcePath,
+ name: "README.md",
repo: "enrichment-commons",
- markdownPreview: true,
+ renderedPreview: true,
pinned: true,
},
]);
+ expect(JSON.parse(window.sessionStorage.getItem("kandev.openFiles.sess-1") ?? "null")).toEqual([
+ {
+ path: sourcePath,
+ name: "README.md",
+ repo: "enrichment-commons",
+ renderedPreview: true,
+ pinned: true,
+ },
+ ]);
const tabs = getOpenFileTabs("sess-1");
expect(tabs).toHaveLength(1);
expect(tabs[0]).toEqual({
- path: "src/foo.ts",
- name: "foo.ts",
+ path: sourcePath,
+ name: "README.md",
repo: "enrichment-commons",
- markdownPreview: true,
+ renderedPreview: true,
pinned: true,
});
});
+ it("normalizes legacy Markdown preview state when reading session storage", () => {
+ window.sessionStorage.setItem(
+ "kandev.openFiles.sess-1",
+ JSON.stringify([
+ {
+ path: "README.md",
+ name: "README.md",
+ markdownPreview: true,
+ pinned: true,
+ },
+ ]),
+ );
+
+ expect(getOpenFileTabs("sess-1")).toEqual([
+ {
+ path: "README.md",
+ name: "README.md",
+ renderedPreview: true,
+ pinned: true,
+ },
+ ]);
+ });
+
it("leaves repo undefined for single-repo tabs", () => {
- setOpenFileTabs("sess-1", [{ path: "src/foo.ts", name: "foo.ts", pinned: true }]);
+ setOpenFileTabs("sess-1", [{ path: sourcePath, name: "foo.ts", pinned: true }]);
expect(getOpenFileTabs("sess-1")[0].repo).toBeUndefined();
});
+
+ it("drops obsolete in-place preview state for HTML tabs", () => {
+ setOpenFileTabs("sess-1", [
+ { path: "reports/index.html", name: "index.html", renderedPreview: true, pinned: true },
+ ]);
+
+ expect(getOpenFileTabs("sess-1")[0]).toEqual({
+ path: "reports/index.html",
+ name: "index.html",
+ pinned: true,
+ });
+ });
});
describe("chat draft attachment storage", () => {
diff --git a/apps/web/lib/local-storage.ts b/apps/web/lib/local-storage.ts
index e1b71c7bde..ce4b9ec9b5 100644
--- a/apps/web/lib/local-storage.ts
+++ b/apps/web/lib/local-storage.ts
@@ -1,6 +1,7 @@
import { setWalkthroughLastSeen } from "@/lib/walkthrough-notification-storage";
import { attachmentContentUrl } from "@/lib/api/domains/attachment-api";
import type { LayoutProfileIdentity } from "@/lib/layout/layout-profiles";
+import { normalizeStoredFileTab } from "./local-storage-file-tabs";
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
@@ -529,6 +530,8 @@ export interface StoredFileTab {
/** Multi-repo subpath (repository_name) so a restored tab re-fetches its
* content under the right repository after a refresh. */
repo?: string;
+ renderedPreview?: boolean;
+ /** Legacy Markdown-only preview field accepted during one-way migration. */
markdownPreview?: boolean;
pinned?: boolean;
}
@@ -554,14 +557,15 @@ export function getOpenFileTabs(sessionId: string): StoredFileTab[] {
let previewSeen = false;
const normalized: StoredFileTab[] = [];
for (let i = parsed.length - 1; i >= 0; i--) {
- const t = parsed[i];
- if (!t) continue;
- const isPinned = t.pinned === true || t.pinned === undefined;
+ const tab = parsed[i];
+ if (!tab) continue;
+ const normalizedTab = normalizeStoredFileTab(tab);
+ const isPinned = normalizedTab.pinned === true || normalizedTab.pinned === undefined;
if (isPinned) {
- normalized.unshift({ ...t, pinned: true });
+ normalized.unshift({ ...normalizedTab, pinned: true });
} else if (!previewSeen) {
previewSeen = true;
- normalized.unshift({ ...t, pinned: false });
+ normalized.unshift({ ...normalizedTab, pinned: false });
}
}
return normalized;
@@ -575,7 +579,7 @@ export function setOpenFileTabs(sessionId: string, tabs: StoredFileTab[]): void
if (typeof window === "undefined") return;
try {
const key = `${OPEN_FILES_KEY}.${sessionId}`;
- window.sessionStorage.setItem(key, JSON.stringify(tabs));
+ window.sessionStorage.setItem(key, JSON.stringify(tabs.map(normalizeStoredFileTab)));
} catch {
// Ignore write failures
}
diff --git a/apps/web/lib/state/dockview-store.ts b/apps/web/lib/state/dockview-store.ts
index e8f717225d..0cc7bd321a 100644
--- a/apps/web/lib/state/dockview-store.ts
+++ b/apps/web/lib/state/dockview-store.ts
@@ -152,7 +152,7 @@ export type FileEditorState = {
hasRemoteUpdate?: boolean;
remoteContent?: string;
remoteOriginalHash?: string;
- markdownPreview?: boolean;
+ renderedPreview?: boolean;
};
/** Direction relative to a reference panel or group. */
diff --git a/apps/web/lib/test-support/happy-dom-network.test.ts b/apps/web/lib/test-support/happy-dom-network.test.ts
index 735e841491..e27093714e 100644
--- a/apps/web/lib/test-support/happy-dom-network.test.ts
+++ b/apps/web/lib/test-support/happy-dom-network.test.ts
@@ -7,3 +7,9 @@ it("disables external stylesheet loads without dispatching an error", () => {
expect(settings.disableCSSFileLoading).toBe(true);
expect(settings.handleDisabledFileLoadingAsSuccess).toBe(true);
});
+
+it("does not navigate main-frame test fixtures over the network", () => {
+ const settings = (window as unknown as HappyDOMWindow).happyDOM.settings;
+
+ expect(settings.navigation.disableMainFrameNavigation).toBe(true);
+});
diff --git a/apps/web/lib/types/workspace-files.ts b/apps/web/lib/types/workspace-files.ts
index e83913828f..c3edfd9bd1 100644
--- a/apps/web/lib/types/workspace-files.ts
+++ b/apps/web/lib/types/workspace-files.ts
@@ -82,7 +82,7 @@ export type OpenFileTab = {
originalHash: string;
isDirty: boolean;
isBinary?: boolean;
- markdownPreview?: boolean;
+ renderedPreview?: boolean;
};
export const FILE_EXTENSION_COLORS: Record = {
diff --git a/apps/web/lib/utils/file-types.test.ts b/apps/web/lib/utils/file-types.test.ts
index 2b1d72a6db..476267f851 100644
--- a/apps/web/lib/utils/file-types.test.ts
+++ b/apps/web/lib/utils/file-types.test.ts
@@ -1,5 +1,19 @@
import { describe, expect, it } from "vitest";
-import { isMarkdownFile } from "./file-types";
+import { getFilePreviewKind, isMarkdownFile } from "./file-types";
+
+describe("getFilePreviewKind", () => {
+ it("recognizes Markdown and HTML preview extensions case-insensitively", () => {
+ expect(getFilePreviewKind("docs/README.md")).toBe("markdown");
+ expect(getFilePreviewKind("docs/README.MDX")).toBe("markdown");
+ expect(getFilePreviewKind("examples/index.html")).toBe("html");
+ expect(getFilePreviewKind("examples/index.HTM")).toBe("html");
+ });
+
+ it("rejects unsupported and binary files", () => {
+ expect(getFilePreviewKind("src/index.ts")).toBe("none");
+ expect(getFilePreviewKind("examples/index.html", true)).toBe("none");
+ });
+});
describe("isMarkdownFile", () => {
it("recognizes Markdown extensions case-insensitively", () => {
diff --git a/apps/web/lib/utils/file-types.ts b/apps/web/lib/utils/file-types.ts
index 5190ecd1ab..b1c07ac34a 100644
--- a/apps/web/lib/utils/file-types.ts
+++ b/apps/web/lib/utils/file-types.ts
@@ -1,4 +1,5 @@
export type FileCategory = "text" | "image" | "binary";
+export type FilePreviewKind = "markdown" | "html" | "none";
const IMAGE_EXTENSIONS = new Set([
"png",
@@ -97,8 +98,15 @@ export function getFileCategory(path: string): FileCategory {
}
export function isMarkdownFile(path: string): boolean {
+ return getFilePreviewKind(path) === "markdown";
+}
+
+export function getFilePreviewKind(path: string, isBinary = false): FilePreviewKind {
+ if (isBinary) return "none";
const ext = getExtension(path);
- return ext === "md" || ext === "mdx";
+ if (ext === "md" || ext === "mdx") return "markdown";
+ if (ext === "html" || ext === "htm") return "html";
+ return "none";
}
/** Return the MIME type for a known image extension, or a fallback. */
diff --git a/apps/web/src/locales/en/editors.json b/apps/web/src/locales/en/editors.json
index 67a760e1f9..ee659b3019 100644
--- a/apps/web/src/locales/en/editors.json
+++ b/apps/web/src/locales/en/editors.json
@@ -47,6 +47,7 @@
"pasteLink": "Paste link...",
"planFormattingToolbar": "Plan formatting toolbar",
"pleaseTryAgain": "Please try again.",
+ "previewHtml": "Preview HTML",
"previewMarkdown": "Preview markdown",
"queuedForTheAgent": "Queued for the agent.",
"reload": "Reload",
diff --git a/apps/web/src/locales/en/task.json b/apps/web/src/locales/en/task.json
index 05a324b62b..7f0867cb06 100644
--- a/apps/web/src/locales/en/task.json
+++ b/apps/web/src/locales/en/task.json
@@ -555,6 +555,14 @@
"failedToLoadDependencies": "Failed to load dependencies.",
"failedToLoadDependencyCandidates": "Failed to load dependency candidates.",
"failedToLoadPreview": "Failed to load preview.",
+ "htmlPreview": "HTML preview",
+ "htmlPreviewError": "Unable to render HTML preview.",
+ "htmlPreviewLoading": "Loading HTML preview...",
+ "htmlPreviewPublishFailed": "HTML preview could not be opened. Try again.",
+ "htmlPreviewTooLarge": "This HTML buffer is too large to preview. Show code and try again with a smaller file.",
+ "htmlPreviewTrustedCode": "Previewing runs workspace code with Browser-panel capabilities. Only preview files you trust.",
+ "htmlPreviewUnavailable": "The HTML preview session is not available. Check the task session and try again.",
+ "refreshHtmlPreview": "Refresh HTML preview",
"failedToLoadRevision": "Failed to load revision",
"failedToLoadRevisionContent": "Failed to load revision content",
"failedToOpenFile": "Failed to open file",
@@ -1029,6 +1037,7 @@
"openInEditorNamed": "Open in {{name}}",
"openInNewTab": "Open in new tab",
"openInOfficeView": "Open in office view",
+ "openHtmlPreview": "Open HTML preview",
"openInOtherEditor": "Open in other editor",
"openMarkdownPreview": "Open markdown preview",
"openPlanDetails": "Open plan details",
@@ -1445,6 +1454,7 @@
"shortcutTogglesChatInput": "Shortcut: <1>{{shortcut}}1> toggles this chat input.",
"showBranchAndGitCredentialDetails": "Show branch and Git credential details",
"showCode": "Show code",
+ "retryHtmlPreview": "Retry HTML preview",
"showCommandOutput": "Show command output",
"showDebugInfo": "Show Debug Info",
"showDetails": "Show details",
diff --git a/apps/web/src/locales/pseudo/editors.json b/apps/web/src/locales/pseudo/editors.json
index eff48a56c5..4dfefecf95 100644
--- a/apps/web/src/locales/pseudo/editors.json
+++ b/apps/web/src/locales/pseudo/editors.json
@@ -47,6 +47,7 @@
"pasteLink": "Ƥàśţē ĺĩńķ...",
"planFormattingToolbar": "Ƥĺàń ƒōŕḿàţţĩńĝ ţōōĺƀàŕ",
"pleaseTryAgain": "Ƥĺēàśē ţŕŷ àĝàĩń.",
+ "previewHtml": "Ƥŕēvĩēŵ ĤŢḾĹ",
"previewMarkdown": "Ƥŕēvĩēŵ ḿàŕķďōŵń",
"queuedForTheAgent": "Qũēũēď ƒōŕ ţĥē àĝēńţ.",
"reload": "Ŕēĺōàď",
diff --git a/apps/web/src/locales/pseudo/task.json b/apps/web/src/locales/pseudo/task.json
index 898f35874e..fab258d25f 100644
--- a/apps/web/src/locales/pseudo/task.json
+++ b/apps/web/src/locales/pseudo/task.json
@@ -555,6 +555,14 @@
"failedToLoadDependencies": "Ƒàĩĺēď ţō ĺōàď ďēƥēńďēńćĩēś.",
"failedToLoadDependencyCandidates": "Ƒàĩĺēď ţō ĺōàď ďēƥēńďēńćŷ ćàńďĩďàţēś.",
"failedToLoadPreview": "Ƒàĩĺēď ţō ĺōàď ƥŕēvĩēŵ.",
+ "htmlPreview": "ĤŢḾĹ ƥŕēvĩēŵ",
+ "htmlPreviewError": "Ũńàƀĺē ţō ŕēńďēŕ ĤŢḾĹ ƥŕēvĩēŵ.",
+ "htmlPreviewLoading": "Ĺōàďĩńĝ ĤŢḾĹ ƥŕēvĩēŵ...",
+ "htmlPreviewPublishFailed": "ĤŢḾĹ ƥŕēvĩēŵ ćōũĺď ńōţ ƀē ōƥēńēď. Ţŕŷ àĝàĩń.",
+ "htmlPreviewTooLarge": "Ţĥĩś ĤŢḾĹ ƀũƒƒēŕ ĩś ţōō ĺàŕĝē ţō ƥŕēvĩēŵ. Śĥōŵ ćōďē àńď ţŕŷ àĝàĩń ŵĩţĥ à śḿàĺĺēŕ ƒĩĺē.",
+ "htmlPreviewTrustedCode": "Ƥŕēvĩēŵĩńĝ ŕũńś ŵōŕķśƥàćē ćōďē ŵĩţĥ ƀŕōŵśēŕ-ƥàńēĺ ćàƥàƀĩĺĩţĩēś. Ōńĺŷ ƥŕēvĩēŵ ƒĩĺēś ŷōũ ţŕũśţ.",
+ "htmlPreviewUnavailable": "Ţĥē ĤŢḾĹ ƥŕēvĩēŵ śēśśĩōń ĩś ńōţ àvàĩĺàƀĺē. Ćĥēćķ ţĥē ţàśķ śēśśĩōń àńď ţŕŷ àĝàĩń.",
+ "refreshHtmlPreview": "Ŕēƒŕēśĥ ĤŢḾĹ ƥŕēvĩēŵ",
"failedToLoadRevision": "Ƒàĩĺēď ţō ĺōàď ŕēvĩśĩōń",
"failedToLoadRevisionContent": "Ƒàĩĺēď ţō ĺōàď ŕēvĩśĩōń ćōńţēńţ",
"failedToOpenFile": "Ƒàĩĺēď ţō ōƥēń ƒĩĺē",
@@ -1029,6 +1037,7 @@
"openInEditorNamed": "Ōƥēń ĩń {{name}}",
"openInNewTab": "Ōƥēń ĩń ńēŵ ţàƀ",
"openInOfficeView": "Ōƥēń ĩń ōƒƒĩćē vĩēŵ",
+ "openHtmlPreview": "Ōƥēń ĤŢḾĹ ƥŕēvĩēŵ",
"openInOtherEditor": "Ōƥēń ĩń ōţĥēŕ ēďĩţōŕ",
"openMarkdownPreview": "Ōƥēń ḿàŕķďōŵń ƥŕēvĩēŵ",
"openPlanDetails": "Ōƥēń ƥĺàń ďēţàĩĺś",
@@ -1445,6 +1454,7 @@
"shortcutTogglesChatInput": "Śĥōŕţćũţ: <1>{{shortcut}}1> ţōĝĝĺēś ţĥĩś ćĥàţ ĩńƥũţ.",
"showBranchAndGitCredentialDetails": "Śĥōŵ ƀŕàńćĥ àńď Ĝĩţ ćŕēďēńţĩàĺ ďēţàĩĺś",
"showCode": "Śĥōŵ ćōďē",
+ "retryHtmlPreview": "Řēţŕŷ ĤŢḾĹ ƥŕēvĩēŵ",
"showCommandOutput": "Śĥōŵ ćōḿḿàńď ōũţƥũţ",
"showDebugInfo": "Śĥōŵ Ďēƀũĝ Ĩńƒō",
"showDetails": "Śĥōŵ ďēţàĩĺś",
diff --git a/apps/web/src/locales/pt-pt/editors.json b/apps/web/src/locales/pt-pt/editors.json
index fd8e6504b7..168e65cb1e 100644
--- a/apps/web/src/locales/pt-pt/editors.json
+++ b/apps/web/src/locales/pt-pt/editors.json
@@ -47,6 +47,7 @@
"pasteLink": "Colar a ligação...",
"planFormattingToolbar": "Barra de formatação do plano",
"pleaseTryAgain": "Tente novamente.",
+ "previewHtml": "Pré-visualizar HTML",
"previewMarkdown": "Pré-visualizar o markdown",
"queuedForTheAgent": "Em fila para o agente.",
"reload": "Recarregar",
diff --git a/apps/web/src/locales/pt-pt/task.json b/apps/web/src/locales/pt-pt/task.json
index 116417299f..8ca3b3fe1b 100644
--- a/apps/web/src/locales/pt-pt/task.json
+++ b/apps/web/src/locales/pt-pt/task.json
@@ -536,6 +536,14 @@
"failedToLoadDependencies": "Não foi possível carregar as dependências.",
"failedToLoadDependencyCandidates": "Não foi possível carregar as tarefas disponíveis como dependências.",
"failedToLoadPreview": "Não foi possível carregar a pré-visualização.",
+ "htmlPreview": "Pré-visualização HTML",
+ "htmlPreviewError": "Não foi possível apresentar a pré-visualização HTML.",
+ "htmlPreviewLoading": "A carregar a pré-visualização HTML...",
+ "htmlPreviewPublishFailed": "Não foi possível abrir a pré-visualização HTML. Tente novamente.",
+ "htmlPreviewTooLarge": "Este buffer HTML é demasiado grande para pré-visualizar. Mostre o código e tente novamente com um ficheiro mais pequeno.",
+ "htmlPreviewTrustedCode": "A pré-visualização executa código da área de trabalho com as capacidades do painel do navegador. Pré-visualize apenas ficheiros em que confia.",
+ "htmlPreviewUnavailable": "A sessão de pré-visualização HTML não está disponível. Verifique a sessão da tarefa e tente novamente.",
+ "refreshHtmlPreview": "Atualizar pré-visualização HTML",
"failedToLoadRevision": "Não foi possível carregar a revisão",
"failedToLoadRevisionContent": "Não foi possível carregar o conteúdo da revisão",
"failedToOpenFile": "Não foi possível abrir o ficheiro",
@@ -982,6 +990,7 @@
"openInEditorNamed": "Abrir no {{name}}",
"openInNewTab": "Abrir num novo separador",
"openInOfficeView": "Abrir na vista de escritório",
+ "openHtmlPreview": "Abrir a pré-visualização HTML",
"openInOtherEditor": "Abrir noutro editor",
"openMarkdownPreview": "Abrir a pré-visualização de markdown",
"openPlanDetails": "Abrir os detalhes do plano",
@@ -1389,6 +1398,7 @@
"shortcutTogglesChatInput": "Atalho: <1>{{shortcut}}1> alterna esta caixa de chat.",
"showBranchAndGitCredentialDetails": "Mostrar os detalhes do branch e das credenciais Git",
"showCode": "Mostrar o código",
+ "retryHtmlPreview": "Tentar novamente a pré-visualização HTML",
"showCommandOutput": "Mostrar a saída do comando",
"showDebugInfo": "Mostrar a informação de depuração",
"showDetails": "Mostrar detalhes",
diff --git a/apps/web/src/locales/zh-cn/editors.json b/apps/web/src/locales/zh-cn/editors.json
index bedae4686c..cb346fb76b 100644
--- a/apps/web/src/locales/zh-cn/editors.json
+++ b/apps/web/src/locales/zh-cn/editors.json
@@ -47,6 +47,7 @@
"pasteLink": "粘贴链接…",
"planFormattingToolbar": "计划格式工具栏",
"pleaseTryAgain": "请重试。",
+ "previewHtml": "预览 HTML",
"previewMarkdown": "预览 Markdown",
"queuedForTheAgent": "已排队等待智能体。",
"reload": "重新加载",
diff --git a/apps/web/src/locales/zh-cn/task.json b/apps/web/src/locales/zh-cn/task.json
index 4993aad5a2..470de8f186 100644
--- a/apps/web/src/locales/zh-cn/task.json
+++ b/apps/web/src/locales/zh-cn/task.json
@@ -536,6 +536,14 @@
"failedToLoadDependencies": "加载依赖关系失败。",
"failedToLoadDependencyCandidates": "加载依赖任务失败。",
"failedToLoadPreview": "加载预览失败。",
+ "htmlPreview": "HTML 预览",
+ "htmlPreviewError": "无法呈现 HTML 预览。",
+ "htmlPreviewLoading": "正在加载 HTML 预览...",
+ "htmlPreviewPublishFailed": "无法打开 HTML 预览。请重试。",
+ "htmlPreviewTooLarge": "此 HTML 缓冲区太大,无法预览。请显示代码并使用较小的文件重试。",
+ "htmlPreviewTrustedCode": "预览会使用浏览器面板的功能执行工作区代码。请只预览你信任的文件。",
+ "htmlPreviewUnavailable": "HTML 预览会话不可用。请检查任务会话并重试。",
+ "refreshHtmlPreview": "刷新 HTML 预览",
"failedToLoadRevision": "加载修订失败",
"failedToLoadRevisionContent": "加载修订内容失败",
"failedToOpenFile": "打开文件失败",
@@ -981,6 +989,7 @@
"openInEditorNamed": "在 {{name}} 中打开",
"openInNewTab": "在新标签页中打开",
"openInOfficeView": "在办公室视图中打开",
+ "openHtmlPreview": "打开 HTML 预览",
"openInOtherEditor": "在其他编辑器中打开",
"openMarkdownPreview": "打开 Markdown 预览",
"openPlanDetails": "打开计划详情",
@@ -1388,6 +1397,7 @@
"shortcutTogglesChatInput": "快捷键:<1>{{shortcut}}1> 切换此聊天输入。",
"showBranchAndGitCredentialDetails": "显示分支与 Git 凭据详情",
"showCode": "显示代码",
+ "retryHtmlPreview": "重试 HTML 预览",
"showCommandOutput": "显示命令输出",
"showDebugInfo": "显示调试信息",
"showDetails": "显示详情",
diff --git a/apps/web/src/locales/zh-hk/editors.json b/apps/web/src/locales/zh-hk/editors.json
index 902375d085..410d07ea80 100644
--- a/apps/web/src/locales/zh-hk/editors.json
+++ b/apps/web/src/locales/zh-hk/editors.json
@@ -47,6 +47,7 @@
"pasteLink": "貼上連結…",
"planFormattingToolbar": "計劃格式工具欄",
"pleaseTryAgain": "請重試。",
+ "previewHtml": "預覽 HTML",
"previewMarkdown": "預覽 Markdown",
"queuedForTheAgent": "已排隊等待代理程式。",
"reload": "重新載入",
diff --git a/apps/web/src/locales/zh-hk/task.json b/apps/web/src/locales/zh-hk/task.json
index 9743b07bb9..e930a2d85f 100644
--- a/apps/web/src/locales/zh-hk/task.json
+++ b/apps/web/src/locales/zh-hk/task.json
@@ -536,6 +536,14 @@
"failedToLoadDependencies": "載入依賴關係失敗。",
"failedToLoadDependencyCandidates": "載入依賴任務失敗。",
"failedToLoadPreview": "載入預覽失敗。",
+ "htmlPreview": "HTML 預覽",
+ "htmlPreviewError": "無法呈現 HTML 預覽。",
+ "htmlPreviewLoading": "正在載入 HTML 預覽...",
+ "htmlPreviewPublishFailed": "無法開啟 HTML 預覽。請重試。",
+ "htmlPreviewTooLarge": "此 HTML 緩衝區太大,無法預覽。請顯示程式碼,並使用較小的檔案重試。",
+ "htmlPreviewTrustedCode": "預覽會使用瀏覽器面板的功能執行工作區程式碼。請只預覽你信任的檔案。",
+ "htmlPreviewUnavailable": "HTML 預覽工作階段不可用。請檢查工作項工作階段並重試。",
+ "refreshHtmlPreview": "刷新 HTML 預覽",
"failedToLoadRevision": "載入修訂失敗",
"failedToLoadRevisionContent": "載入修訂內容失敗",
"failedToOpenFile": "開啟檔案失敗",
@@ -981,6 +989,7 @@
"openInEditorNamed": "在 {{name}} 中開啟",
"openInNewTab": "在新標籤頁中開啟",
"openInOfficeView": "在辦公室視圖中開啟",
+ "openHtmlPreview": "開啟 HTML 預覽",
"openInOtherEditor": "在其他編輯器中開啟",
"openMarkdownPreview": "開啟 Markdown 預覽",
"openPlanDetails": "開啟計劃詳情",
@@ -1388,6 +1397,7 @@
"shortcutTogglesChatInput": "快捷鍵:<1>{{shortcut}}1> 切換此聊天輸入。",
"showBranchAndGitCredentialDetails": "顯示分支與 Git 認證資料詳情",
"showCode": "顯示程式碼",
+ "retryHtmlPreview": "重試 HTML 預覽",
"showCommandOutput": "顯示命令輸出",
"showDebugInfo": "顯示調試資訊",
"showDetails": "顯示詳情",
diff --git a/apps/web/src/locales/zh-tw/editors.json b/apps/web/src/locales/zh-tw/editors.json
index d1444959de..2154a51675 100644
--- a/apps/web/src/locales/zh-tw/editors.json
+++ b/apps/web/src/locales/zh-tw/editors.json
@@ -47,6 +47,7 @@
"pasteLink": "貼上連結…",
"planFormattingToolbar": "計劃格式工具欄",
"pleaseTryAgain": "請重試。",
+ "previewHtml": "預覽 HTML",
"previewMarkdown": "預覽 Markdown",
"queuedForTheAgent": "已排隊等待代理程式。",
"reload": "重新載入",
diff --git a/apps/web/src/locales/zh-tw/task.json b/apps/web/src/locales/zh-tw/task.json
index 6815ee81cd..b470d4b130 100644
--- a/apps/web/src/locales/zh-tw/task.json
+++ b/apps/web/src/locales/zh-tw/task.json
@@ -536,6 +536,14 @@
"failedToLoadDependencies": "載入依賴關係失敗。",
"failedToLoadDependencyCandidates": "載入依賴任務失敗。",
"failedToLoadPreview": "載入預覽失敗。",
+ "htmlPreview": "HTML 預覽",
+ "htmlPreviewError": "無法呈現 HTML 預覽。",
+ "htmlPreviewLoading": "正在載入 HTML 預覽...",
+ "htmlPreviewPublishFailed": "無法開啟 HTML 預覽。請重試。",
+ "htmlPreviewTooLarge": "此 HTML 緩衝區太大,無法預覽。請顯示程式碼,並使用較小的檔案重試。",
+ "htmlPreviewTrustedCode": "預覽會使用瀏覽器面板的功能執行工作區程式碼。請只預覽你信任的檔案。",
+ "htmlPreviewUnavailable": "HTML 預覽工作階段無法使用。請檢查工作項工作階段並重試。",
+ "refreshHtmlPreview": "重新整理 HTML 預覽",
"failedToLoadRevision": "載入修訂失敗",
"failedToLoadRevisionContent": "載入修訂內容失敗",
"failedToOpenFile": "開啟檔案失敗",
@@ -981,6 +989,7 @@
"openInEditorNamed": "在 {{name}} 中開啟",
"openInNewTab": "在新標籤頁中開啟",
"openInOfficeView": "在辦公室檢視中開啟",
+ "openHtmlPreview": "開啟 HTML 預覽",
"openInOtherEditor": "在其他編輯器中開啟",
"openMarkdownPreview": "開啟 Markdown 預覽",
"openPlanDetails": "開啟計劃詳情",
@@ -1388,6 +1397,7 @@
"shortcutTogglesChatInput": "快捷鍵:<1>{{shortcut}}1> 切換此聊天輸入。",
"showBranchAndGitCredentialDetails": "顯示分支與 Git 認證資訊詳情",
"showCode": "顯示程式碼",
+ "retryHtmlPreview": "重試 HTML 預覽",
"showCommandOutput": "顯示命令輸出",
"showDebugInfo": "顯示除錯資訊",
"showDetails": "顯示詳情",
diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts
index 9e39ce6806..d24be2080e 100644
--- a/apps/web/vitest.config.ts
+++ b/apps/web/vitest.config.ts
@@ -34,6 +34,7 @@ export default mergeConfig(
happyDOM: {
settings: {
navigation: {
+ disableMainFrameNavigation: true,
disableChildFrameNavigation: true,
},
},
diff --git a/docs/decisions/2026-09-05-trusted-browser-html-preview.md b/docs/decisions/2026-09-05-trusted-browser-html-preview.md
new file mode 100644
index 0000000000..4a49b74900
--- /dev/null
+++ b/docs/decisions/2026-09-05-trusted-browser-html-preview.md
@@ -0,0 +1,102 @@
+# ADR-2026-09-05-trusted-browser-html-preview: Treat HTML Preview as Trusted Workspace Code
+
+**Status:** accepted
+**Date:** 2026-09-05
+**Area:** frontend, backend, agentctl, security
+**Amended:** 2026-09-06 to make in-editor preview the primary desktop surface
+
+## Context
+
+Kandev needs a one-click preview for `.html` and `.htm` files. The desired
+product behavior is normal browser fidelity: relative assets, native DOM and
+browser APIs, links, forms, media, modules, and network behavior. The preview
+must also use the current unsaved editor buffer.
+
+The first design treated every workspace document as hostile. It used QuickJS
+in a Web Worker, a virtual DOM, a controlled renderer, resource filtering, and
+execution budgets. That design reduced ambient authority but did not provide a
+normal browser. It also duplicated a large and permanently incomplete browser
+compatibility surface.
+
+Kandev already routes executor-local HTTP ports through a session-authorized
+proxy and renders trusted workspace pages in iframes that permit scripts and
+same-origin behavior. Reusing this path gives static HTML the same fidelity and
+executor coverage as an explicitly started development server.
+
+[ADR-2026-07-24-operator-owned-agent-launcher-settings](2026-07-24-operator-owned-agent-launcher-settings.md)
+requires workspace-controlled web content to use a separate origin when it is
+treated as untrusted. The product owner explicitly selected the existing
+trusted native-browser model for HTML preview instead of a dedicated preview
+origin.
+
+## Decision
+
+Treat native HTML preview as execution of trusted workspace code. Use the
+existing Browser-panel iframe sandbox, origin behavior, and session port proxy.
+Do not claim that preview isolates users from hostile HTML or JavaScript.
+
+Agentctl will lazily start an ephemeral loopback static server for each selected
+workspace or repository root. The current editor buffer is stored as a bounded
+in-memory overlay for its workspace path. The server reads other relative assets
+from that root. The backend authorizes the task session and forwards publish
+requests to agentctl. Desktop and mobile replace the active file viewer body
+with the returned session proxy URL. Desktop can open the same URL in the
+Browser panel only through an explicit secondary action.
+
+This decision narrows the untrusted-content rule in the 2026-07-24 ADR: native
+HTML preview is an explicit trusted-content exception. That ADR continues to
+govern operator-owned launcher settings and any future feature that promises to
+render workspace-controlled content as untrusted. If Kandev later offers an
+untrusted HTML preview mode, that mode must use a dedicated origin or a stronger
+independently reviewed boundary.
+
+The static server must contain filesystem reads to the selected root and listen
+only on loopback. It must use the existing authenticated proxy and stop with
+agentctl. Logs must not contain source content. These requirements control the
+host and routing. They are not a browser-script sandbox.
+
+## Consequences
+
+- HTML, CSS, JavaScript, relative assets, and browser APIs behave like they do
+ in the existing Browser-panel iframe.
+- Unsaved HTML can be previewed without writing it to the workspace.
+- Local, Docker, SSH, and Kubernetes task sessions reuse one routing mechanism.
+- Users must trust previewed documents. The UI and public documentation must
+ state that previewing executes workspace code with Browser-panel authority.
+- Same-origin deployments can expose Kandev/browser authority to previewed code.
+ This risk is accepted for this feature and must not be described as isolated.
+- Kandev owns a small static-server and in-memory-overlay lifecycle instead of
+ an ECMAScript engine, virtual DOM, browser emulation layer, and safe renderer.
+- Applications that need builds, backend routes, HMR, or framework-specific
+ behavior continue to use their configured development server.
+
+## Alternatives considered
+
+### QuickJS with a virtual DOM and controlled renderer
+
+Rejected because it is a browser reimplementation with limited compatibility.
+Its complexity is not justified when browser fidelity is the product goal.
+
+### Dedicated preview origin
+
+Rejected for the current feature because it adds deployment, DNS, cookie,
+desktop, and remote-executor routing work. It remains the preferred direction
+if Kandev needs to claim hostile-content isolation later.
+
+### Scriptless `srcDoc` iframe
+
+Rejected because it does not support the required scripts, relative assets, or
+normal browser APIs.
+
+### Require users to configure a development-server command
+
+Rejected as the only workflow because a static HTML document needs one action,
+not project-specific command configuration. The explicit development server
+remains available for non-static applications.
+
+### Open a Browser panel as the primary desktop result
+
+Rejected because a file-preview action should preserve the user's active file
+context and match the existing source/preview mental model. The Browser panel
+remains available as an explicit secondary action for broader browsing and
+inspection workflows.
diff --git a/docs/decisions/INDEX.md b/docs/decisions/INDEX.md
index 940785c790..dd42edbea2 100644
--- a/docs/decisions/INDEX.md
+++ b/docs/decisions/INDEX.md
@@ -250,3 +250,5 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
| 2026-09-06-opt-in-external-e2e-runners | [Opt in selected Linux CI jobs to external runners](2026-09-06-opt-in-external-e2e-runners.md) | accepted | infra, workflow, security | 2026-09-06 |
| 2026-09-06-explicit-pr-auto-fix-outcomes | [Require Explicit Outcomes for PR Auto-Fix Attempts](2026-09-06-explicit-pr-auto-fix-outcomes.md) | proposed | backend, frontend, protocol, workflow, GitHub | 2026-09-06 |
| 2026-09-05-bounded-progressive-storage-analysis | [Use bounded progressive storage analysis](2026-09-05-bounded-progressive-storage-analysis.md) | accepted | backend, frontend, protocol | 2026-09-05 |
+| 2026-09-05-script-capable-html-preview-isolation | [Capability-Free Runtime for Script-Capable HTML Preview](2026-09-05-script-capable-html-preview-isolation.md) | proposed | frontend, desktop, security | 2026-09-05 |
+| 2026-09-05-trusted-browser-html-preview | [Treat HTML Preview as Trusted Workspace Code](2026-09-05-trusted-browser-html-preview.md) | accepted | frontend, backend, agentctl, security | 2026-09-05 |
diff --git a/docs/plans/native-html-preview/plan.md b/docs/plans/native-html-preview/plan.md
new file mode 100644
index 0000000000..b6be69ae1e
--- /dev/null
+++ b/docs/plans/native-html-preview/plan.md
@@ -0,0 +1,215 @@
+---
+created: 2026-09-05
+status: complete
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+legacy_specs: []
+---
+
+# Implementation Plan: Trusted Browser HTML File Preview
+
+## Overview
+
+Replace the QuickJS and virtual-DOM implementation with a one-click static
+preview server in agentctl. Publish the current editor buffer as an in-memory
+entry document. Serve relative workspace assets from disk. Desktop and mobile
+replace the active file editor body with the focused iframe. The Browser panel
+remains an explicit secondary desktop action.
+
+The product owner selected the trusted native-browser model. Implementation and
+documentation must state that previewing executes trusted workspace code and
+must not claim hostile-content isolation.
+
+## Scope
+
+### In scope
+
+- An ephemeral loopback static server owned by each agentctl instance.
+- Bounded in-memory overlays for current, unsaved HTML buffers.
+- Workspace-rooted relative asset serving with traversal and symlink-escape
+ protection.
+- A session-authorized backend publish endpoint and agentctl client contract.
+- Existing session port-proxy routing for every supported executor.
+- Desktop in-editor iframe, `Show code`, refresh, and optional Browser-panel
+ behavior.
+- A full-height mobile iframe with the same recovery and preserved editor state.
+- Localized trusted-code, progress, error, and retry copy in all locales.
+- Removal of the preview-specific QuickJS and direct parse5 dependencies, the
+ virtual DOM, worker code, and obsolete tests.
+- Public documentation and focused backend, frontend, E2E, and desktop checks.
+
+### Out of scope
+
+- An untrusted-content security boundary or dedicated preview origin.
+- Framework builds, HMR, server-side routes, or package installation.
+- Review-diff previews, source mapping, and new Browser-panel inspector features.
+- Replacing explicit development-server commands for non-static applications.
+
+## Technical approach
+
+### Workspace preview server
+
+Add a concurrency-safe manager to agentctl. It binds one loopback ephemeral port
+per selected workspace or repository root. The manager publishes up to 32
+current-buffer overlays of at most 5 MiB each. It stops all servers with the
+agentctl instance. Exact overlay paths win over disk content. All responses use
+`Cache-Control: no-store`.
+
+The server resolves request paths with the existing repository path helpers and
+rejects traversal, encoded traversal, and symlink targets outside the selected
+root. It does not rewrite document content or emulate browser behavior.
+
+### Session publish contract
+
+Add a task-session endpoint that authenticates the session, ensures agentctl is
+ready, bounds the request, and forwards `{repo, path, content}`. Agentctl repeats
+validation and returns `{port, path, version}`. The frontend builds the existing
+session port-proxy URL and appends the version as a cache buster.
+
+### Responsive UI
+
+Desktop and mobile keep the file tab identity and replace the source body with
+`HtmlPreviewContent` after publication. Kandev-owned chrome provides `Show
+code`, refresh, errors, retry, and an optional desktop Browser-panel action.
+Preview state is scoped to the active session and file identity and is not
+restored across reloads.
+
+Before execution, user-facing copy identifies the action as trusted workspace
+code. The iframe uses the existing Browser-panel sandbox policy. The feature
+does not add an isolation claim or browser capability filter.
+
+### Migration and documentation
+
+Delete the worker runtime, virtual DOM, Shadow DOM renderer, navigation
+normalizer, and preview-only QuickJS/direct parse5 dependencies. Keep Markdown
+behavior and explicit development-server controls unchanged. Update public
+guidance to distinguish the zero-configuration static preview from application
+development servers and to state the trust consequence.
+
+## Acceptance evidence
+
+| Acceptance criteria | Evidence |
+| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `.1`, `.2`, `.3`, `.7`, `.8`, `.9`, `.10` | Frontend API/component tests plus desktop and mobile E2E for current buffers, in-editor toggling, explicit Browser-panel opening, trust copy, source preservation, and recovery |
+| `.4`, `.5`, `.11` | Agentctl server tests and browser E2E for native scripts, browser APIs, relative assets, bounds, path containment, eviction, and shutdown |
+| `.6` | Backend handler/client tests plus existing session port-proxy regression coverage |
+
+## Work orders
+
+### Trusted Browser delivery
+
+- [done] [Task 10: Build the agentctl workspace preview server](task-10-agentctl-workspace-preview-server.md)
+- [done] [Task 11: Add the session publish contract](task-11-session-preview-publish-contract.md)
+- [done] [Task 12: Replace the virtual runtime with Browser-panel UI](task-12-browser-panel-preview-ui.md)
+- [done] [Task 13: Document and prove browser-fidelity preview](task-13-browser-fidelity-docs-and-e2e.md)
+
+### In-editor preview revision
+
+- [done] [Task 14: Move desktop HTML preview into the file editor](task-14-in-editor-html-preview.md)
+- [done] [Task 15: Revise preview guidance and browser E2E](task-15-in-editor-preview-docs-e2e.md)
+
+### Superseded implementation work
+
+Tasks 01 through 04 describe the original scriptless prototype. Tasks 05
+through 09 describe the QuickJS and virtual-DOM implementation. They are
+retained for traceability but cancelled because the selected product contract
+is native browser fidelity:
+
+- [cancelled] [Task 01: Establish preview state and sandbox contract](task-01-preview-state-and-sandbox.md)
+- [cancelled] [Task 02: Add responsive HTML preview surfaces](task-02-responsive-html-preview.md)
+- [cancelled] [Task 03: Publish HTML preview guidance](task-03-public-html-preview-guidance.md)
+- [cancelled] [Task 04: Prove responsive HTML preview flows](task-04-responsive-html-preview-e2e.md)
+- [cancelled] [Task 05: Build the capability-free preview runtime](task-05-script-capable-preview-runtime.md)
+- [cancelled] [Task 06: Integrate the runtime with preview state and renderer](task-06-preview-state-and-renderer.md)
+- [cancelled] [Task 07: Wire responsive preview surfaces](task-07-responsive-preview-surfaces.md)
+- [cancelled] [Task 08: Publish script-capable preview guidance](task-08-script-capable-preview-guidance.md)
+- [cancelled] [Task 09: Prove script execution and isolation](task-09-script-capable-preview-e2e.md)
+
+## Dependency order
+
+```text
+Task 10 agentctl server
+ |
+ v
+Task 11 session contract
+ |
+ v
+Task 12 responsive UI and runtime removal
+ |
+ v
+Task 13 docs, E2E, and final verification
+ |
+ v
+Task 14 in-editor desktop preview
+ |
+ v
+Task 15 revised docs and E2E
+```
+
+Tasks 10 and 11 establish the serving and authorization boundary before any UI
+depends on it. Task 12 removes the old runtime only after the end-to-end URL can
+be produced. Task 13 verified the first shipped flow. Tasks 14 and 15 revise the
+desktop presentation without changing the serving boundary, then update its
+documentation and browser evidence.
+
+## Risks
+
+- Same-origin Browser-panel content can exercise Kandev/browser authority. This
+ is an accepted product tradeoff and must remain visible to users.
+- Agentctl server teardown and overlay locking must cover stop, restart, and
+ executor failure paths without leaking listeners or data.
+- Proxy rewriting and capability cookies must preserve relative, root-relative,
+ redirect, module, and fetch requests.
+- Multi-repository roots and symlinks can accidentally widen filesystem access
+ unless existing canonical path helpers are used consistently.
+- In-editor preview state must not outlive its session or file identity, and
+ stale publish completions must not replace another file's source body.
+- Removing runtime packages changes the shared lockfile and generated license
+ catalog. The Markdown renderer still owns transitive parse5 licenses, so the
+ verification must distinguish those from the removed preview dependency.
+
+## Completion gate
+
+The package is ready to merge after all required checks pass. These checks cover
+the backend, frontend, documentation, localization, E2E, desktop, dependencies,
+licenses, and PR CI. Every review thread must have a disposition before merge.
+
+## Verification results
+
+- Agentctl API tests and race tests passed, including deterministic concurrent
+ publish/read, per-root ports, and HEAD/405 method coverage.
+- Full backend tests passed with the task-host internal config overrides cleared.
+- Focused frontend tests passed: 13 files and 69 tests, including stale
+ session completion guards.
+- Agentctl client and task-handler tests passed for typed 400/413/503
+ propagation, malformed responses, oversized requests, and unavailable
+ sessions. The unused lifecycle forwarding layer was removed.
+- Web typecheck, lint, i18n checks, i18n ratchet, license generation, and Vite
+ E2E build passed.
+- Public-doc validation passed: 61 tests and 43 published pages.
+- Raw desktop Chromium and mobile Chrome preview suites passed: 2 tests each.
+- Desktop shell E2E passed.
+- Fresh desktop and mobile screenshots were captured, inspected, and compressed
+ for the pull request.
+- Reproduced and fixed the CI-only frontend failure in the saved-repository
+ picker by synchronizing the Radix menu dismissal and asynchronous discovery
+ assertions.
+- Disabled Happy DOM main-frame and child-frame fixture navigation, and isolated
+ Vitest-importable E2E utilities from Playwright runtime imports to remove the
+ unhandled loopback `ECONNREFUSED` failures.
+- The full frontend suite passed after the fix: 1,871 test files, 15,869
+ passed, and 4 skipped, with no unhandled network errors.
+- Re-ran desktop and mobile native preview E2E locally: 2 tests passed in each
+ project. Web lint, direct web typecheck, i18n checks, and formatting checks
+ also passed.
+- Revised the desktop presentation so `Preview HTML` replaces the active editor
+ body instead of automatically opening a Browser panel. Focused frontend
+ coverage passed 6 files and 22 tests, including source recovery, latest-buffer
+ refresh, explicit Browser-panel opening, and file/session identity resets.
+- Revised desktop and mobile preview E2E passed 2 tests in each project on the
+ first attempt. Specification validation passed all 30 tests, public-doc
+ validation passed 61 tests across 46 pages, and desktop shell E2E passed.
+- The final full frontend suite passed 1,872 test files and 15,902 tests, with
+ four intentional skips and no failures.
diff --git a/docs/plans/native-html-preview/task-01-preview-state-and-sandbox.md b/docs/plans/native-html-preview/task-01-preview-state-and-sandbox.md
new file mode 100644
index 0000000000..990e75474e
--- /dev/null
+++ b/docs/plans/native-html-preview/task-01-preview-state-and-sandbox.md
@@ -0,0 +1,79 @@
+---
+id: "01-preview-state-and-sandbox"
+title: "Establish preview state and sandbox contract"
+status: cancelled
+wave: 1
+depends_on: []
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.6
+ - AC-UI-NATIVE-HTML-PREVIEW-001.7
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 01: Establish preview state and sandbox contract
+
+## Summary
+
+This work order described the first static-only prototype. Its empty iframe
+and inert-script contract was superseded when the owner reaffirmed the
+inline-JavaScript requirement. It is retained as history and is not evidence
+for the current plan.
+
+## In scope
+
+- The previously implemented format-neutral preview state and legacy Markdown
+ restoration are reusable inputs for the revised work.
+- The previous static document builder and navigation normalizer are reusable
+ only as defense in depth.
+
+## Out of scope
+
+- Treating an empty sandbox or `script-src 'none'` as the final preview
+ execution boundary.
+- Shipping the static-only HTML preview contract.
+
+## Acceptance
+
+- Cancelled. The original acceptance conditions required scripts to remain
+ inert and therefore conflict with the approved script-capable contract.
+
+## Verification
+
+Not applicable. Use [Task 05](task-05-script-capable-preview-runtime.md) and
+[Task 06](task-06-preview-state-and-renderer.md) for the replacement work.
+
+## Files likely touched
+
+- `apps/web/lib/utils/file-types.ts`
+- `apps/web/lib/html-preview/`
+- `apps/web/hooks/use-file-editors.ts`
+
+## Dependencies
+
+Replaced by Tasks 05 and 06.
+
+## Risks
+
+Treating the old static tests as final evidence can conceal the missing inline
+JavaScript capability.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- The superseded static prototype commit and its test results.
+- The revised requirements and system design.
+
+## Results
+
+Cancelled after review. No production contract is accepted from this work
+order.
diff --git a/docs/plans/native-html-preview/task-02-responsive-html-preview.md b/docs/plans/native-html-preview/task-02-responsive-html-preview.md
new file mode 100644
index 0000000000..e0f6816d05
--- /dev/null
+++ b/docs/plans/native-html-preview/task-02-responsive-html-preview.md
@@ -0,0 +1,79 @@
+---
+id: "02-responsive-html-preview"
+title: "Add responsive HTML preview surfaces"
+status: cancelled
+wave: 2
+depends_on:
+ - "01-preview-state-and-sandbox"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.6
+ - AC-UI-NATIVE-HTML-PREVIEW-001.8
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 02: Add responsive HTML preview surfaces
+
+## Summary
+
+This work order wired the HTML preview to a static `srcDoc` iframe. The owner
+selected script-capable execution, so the implementation is superseded by the
+runtime-backed renderer work in Tasks 06 and 07.
+
+## In scope
+
+- Existing toolbar, file-state, localization, desktop, and mobile wiring may be
+ reused after the runtime-backed renderer contract is implemented.
+
+## Out of scope
+
+- Shipping `HtmlPreviewContent` with `sandbox=""` and inert scripts as the
+ final behavior.
+- Treating component tests for iframe attributes as proof of script isolation.
+
+## Acceptance
+
+- Cancelled. The original acceptance conditions describe the static-only
+ renderer and do not satisfy inline JavaScript execution.
+
+## Verification
+
+Not applicable. Use [Task 06](task-06-preview-state-and-renderer.md) and
+[Task 07](task-07-responsive-preview-surfaces.md) for the replacement work.
+
+## Files likely touched
+
+- `apps/web/components/task/html-preview-content.tsx`
+- `apps/web/components/task/file-editor-content.tsx`
+- `apps/web/components/task/mobile/mobile-file-viewer-panel.tsx`
+
+## Dependencies
+
+Replaced by Tasks 06 and 07.
+
+## Risks
+
+Keeping the old iframe contract in a passing component test can hide a missing
+runtime boundary.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- The superseded static prototype and existing Markdown preview surfaces.
+- The revised runtime and renderer system design.
+
+## Results
+
+Cancelled after review. Existing UI changes remain non-ready until the
+script-capable renderer is implemented.
diff --git a/docs/plans/native-html-preview/task-03-public-html-preview-guidance.md b/docs/plans/native-html-preview/task-03-public-html-preview-guidance.md
new file mode 100644
index 0000000000..44d8c4320e
--- /dev/null
+++ b/docs/plans/native-html-preview/task-03-public-html-preview-guidance.md
@@ -0,0 +1,71 @@
+---
+id: "03-public-html-preview-guidance"
+title: "Publish HTML preview guidance"
+status: cancelled
+wave: 3
+depends_on:
+ - "02-responsive-html-preview"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 03: Publish HTML preview guidance
+
+## Summary
+
+This work order documented the static-only preview prototype. Its public text
+must not be treated as the final contract because the approved behavior now
+includes isolated inline JavaScript.
+
+## In scope
+
+- The existing Files and editor guidance is an input to Task 08.
+
+## Out of scope
+
+- Publishing claims that scripts are inert as the final product behavior.
+- Describing the current `srcDoc` iframe as a complete security boundary.
+
+## Acceptance
+
+- Cancelled. Public guidance will be rewritten after the runtime-backed
+ behavior and its browser evidence are complete.
+
+## Verification
+
+Not applicable. Use [Task 08](task-08-script-capable-preview-guidance.md).
+
+## Files likely touched
+
+- `docs/public/developer-tools.md`
+
+## Dependencies
+
+Replaced by Task 08, after the runtime and user-facing surfaces are complete.
+
+## Risks
+
+Premature public guidance can promise either unsupported script execution or a
+static-only behavior that the owner rejected.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- The revised requirements and system design.
+- The existing Files and editor integrations guide.
+
+## Results
+
+Cancelled after review. No public documentation result from this work order is
+accepted for the revised contract.
diff --git a/docs/plans/native-html-preview/task-04-responsive-html-preview-e2e.md b/docs/plans/native-html-preview/task-04-responsive-html-preview-e2e.md
new file mode 100644
index 0000000000..66fc97fce3
--- /dev/null
+++ b/docs/plans/native-html-preview/task-04-responsive-html-preview-e2e.md
@@ -0,0 +1,74 @@
+---
+id: "04-responsive-html-preview-e2e"
+title: "Prove responsive HTML preview flows"
+status: cancelled
+wave: 3
+depends_on:
+ - "02-responsive-html-preview"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.6
+ - AC-UI-NATIVE-HTML-PREVIEW-001.7
+ - AC-UI-NATIVE-HTML-PREVIEW-001.8
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 04: Prove responsive HTML preview flows
+
+## Summary
+
+This work order proved the static-only browser flow and asserted that scripts
+did not execute. Those assertions contradict the approved script-capable
+contract and are superseded by Task 09.
+
+## In scope
+
+- Existing desktop and mobile fixtures may be reused after the runtime-backed
+ flow is implemented.
+
+## Out of scope
+
+- Treating a passing static-only browser test as evidence for script execution
+ or network isolation.
+
+## Acceptance
+
+- Cancelled. The old test proves the wrong behavior.
+
+## Verification
+
+Not applicable. Use [Task 09](task-09-script-capable-preview-e2e.md).
+
+## Files likely touched
+
+- `apps/web/e2e/tests/chat/html-preview.spec.ts`
+- `apps/web/e2e/tests/task/mobile-html-preview.spec.ts`
+
+## Dependencies
+
+Replaced by Task 09 after Tasks 06 and 07.
+
+## Risks
+
+Negative-only tests can pass while the required inline script capability is
+missing.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- The superseded static browser scenarios.
+- The runtime-backed verification strategy in the system design.
+
+## Results
+
+Cancelled after review. Its previous results remain historical only.
diff --git a/docs/plans/native-html-preview/task-05-script-capable-preview-runtime.md b/docs/plans/native-html-preview/task-05-script-capable-preview-runtime.md
new file mode 100644
index 0000000000..9031d55a9c
--- /dev/null
+++ b/docs/plans/native-html-preview/task-05-script-capable-preview-runtime.md
@@ -0,0 +1,118 @@
+---
+id: "05-script-capable-preview-runtime"
+title: "Build the capability-free preview runtime"
+status: cancelled
+wave: 1
+depends_on: []
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.9
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 05: Build the capability-free preview runtime
+
+## Summary
+
+Implement the isolated script execution boundary selected by the revised HTML
+preview design. Inline source JavaScript runs in a dedicated worker-hosted
+ECMAScript VM with a virtual DOM and no host imports. The worker emits only
+data-only snapshots and diagnostics through a structured-clone protocol.
+
+## In scope
+
+- Select, pin, and license-check the ECMAScript engine and virtual-DOM support
+ needed for the first supported capability set.
+- Implement worker lifecycle, load, event-dispatch, snapshot, diagnostics, and
+ disposal messages.
+- Implement virtual DOM mutation, inline script order, virtual event handlers,
+ bounded timers, console diagnostics, and runtime-owned blob tokens.
+- Deny browser globals, Kandev data, credentials, storage, filesystem,
+ network, navigation, downloads, and window-opening APIs.
+- Enforce instruction, wall-clock, virtual-heap, timer, event-queue, and
+ snapshot limits with fail-closed termination.
+- Add unit tests for capability absence, script execution, event dispatch,
+ dynamic resource filtering, navigation no-ops, and budget failure.
+
+## Out of scope
+
+- File-tab state or toolbar changes.
+- Responsive surface composition and localization.
+- Public documentation.
+- Full browser API compatibility, remote resources, or multi-file sites.
+
+## Acceptance
+
+- A representative inline script mutates the virtual document and responds to
+ a sanitized user event without executing in the native browser context.
+- Attempts to use fetch, XHR, WebSocket, EventSource, location, history,
+ parent/top, forms, downloads, or window APIs fail closed and cannot produce a
+ browser request or navigation.
+- Infinite loops, oversized output, unsupported APIs, malformed messages, and
+ runtime exceptions terminate or reject the generation without falling back to
+ native script execution.
+
+## Verification
+
+```bash
+cd apps/web
+pnpm exec vitest run lib/html-preview/preview-runtime.test.ts lib/html-preview/preview-resource-policy.test.ts
+pnpm run typecheck
+```
+
+## Files likely touched
+
+- `apps/web/lib/html-preview/preview-runtime.ts`
+- `apps/web/lib/html-preview/preview-runtime.worker.ts`
+- `apps/web/lib/html-preview/preview-runtime-types.ts`
+- `apps/web/lib/html-preview/preview-resource-policy.ts`
+- `apps/web/lib/html-preview/preview-runtime.test.ts`
+- `apps/web/lib/html-preview/preview-resource-policy.test.ts`
+- `apps/web/package.json`
+- `apps/pnpm-lock.yaml`
+
+## Dependencies
+
+None. This boundary must exist before UI integration can claim script support.
+
+## Risks
+
+- The engine may not support the required ECMAScript or virtual-DOM behavior in
+ all supported browsers and WebViews.
+- A host import, native function escape, or unbounded callback can invalidate
+ the security boundary.
+- Runtime limits that are too strict can break ordinary interactive documents.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- `REQ-UI-NATIVE-HTML-PREVIEW-001.3` through `.6` and `.9`.
+- The runtime boundary, message contract, execution limits, and security
+ invariants in the system design.
+- The capability-free preview isolation design that this cancelled work order
+ originally implemented.
+
+## Results
+
+Implemented the worker-hosted QuickJS runtime with a virtual document, bounded
+event and timer execution, deny-by-default resource policy, runtime-owned blob
+tokens, structured-clone snapshots, and fail-closed lifecycle errors.
+Load and interaction execution each receive an independent wall-clock and
+interrupt budget, so an idle preview cannot consume the next user interaction's
+runtime allowance.
+
+Verification completed:
+
+```text
+pnpm exec vitest run lib/html-preview/preview-runtime.test.ts lib/html-preview/preview-runtime-client.test.ts lib/html-preview/preview-resource-policy.test.ts
+pnpm exec tsc --noEmit --pretty false
+```
diff --git a/docs/plans/native-html-preview/task-06-preview-state-and-renderer.md b/docs/plans/native-html-preview/task-06-preview-state-and-renderer.md
new file mode 100644
index 0000000000..2fb791992a
--- /dev/null
+++ b/docs/plans/native-html-preview/task-06-preview-state-and-renderer.md
@@ -0,0 +1,117 @@
+---
+id: "06-preview-state-and-renderer"
+title: "Integrate the runtime with preview state and renderer"
+status: cancelled
+wave: 2
+depends_on:
+ - "05-script-capable-preview-runtime"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.6
+ - AC-UI-NATIVE-HTML-PREVIEW-001.9
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 06: Integrate the runtime with preview state and renderer
+
+## Summary
+
+Replace the static `srcDoc` execution path with the worker-backed runtime and a
+scriptless snapshot renderer. Preserve the existing format-neutral file state,
+legacy Markdown restoration, source buffer, and preview identity behavior.
+
+## In scope
+
+- Reuse or complete generic HTML preview eligibility and session-state
+ migration.
+- Replace source-controlled HTML sinks and iframe execution assumptions with
+ the controlled snapshot renderer and event bridge.
+- Apply static and dynamic resource, navigation, meta-refresh, form, and SVG
+ link policies.
+- Keep the current navigation-neutralization work as defense in depth.
+- Add component and renderer tests for snapshots, events, failures, generation
+ disposal, source restoration, and Markdown preservation.
+
+## Out of scope
+
+- Toolbar placement changes beyond the renderer contract.
+- Public documentation.
+- Desktop WebView smoke and broad browser suites.
+
+## Acceptance
+
+- HTML preview displays runtime snapshots and supports script-driven DOM
+ updates without placing source scripts or inline event handlers in the native
+ browser DOM.
+- Static and dynamically-created URLs cannot cause a browser request,
+ navigation, popup, download, form submission, or access to Kandev content.
+- Runtime errors, budget failures, file identity changes, and source toggles
+ dispose the generation safely and preserve the existing source/editor state.
+
+## Verification
+
+```bash
+cd apps/web
+pnpm exec vitest run lib/html-preview/html-preview-document.test.ts lib/html-preview/preview-surface.test.ts components/task/html-preview-content.test.tsx components/task/file-editor-content.test.tsx components/task/file-tab-content.test.tsx components/task/mobile/mobile-file-viewer-panel.test.tsx
+pnpm run typecheck
+pnpm run i18n:check
+pnpm run i18n:ratchet
+```
+
+## Files likely touched
+
+- `apps/web/lib/html-preview/html-preview-document.ts`
+- `apps/web/lib/html-preview/html-preview-document.test.ts`
+- `apps/web/lib/html-preview/preview-surface.ts`
+- `apps/web/lib/html-preview/preview-surface.test.ts`
+- `apps/web/components/task/html-preview-content.tsx`
+- `apps/web/components/task/html-preview-content.test.tsx`
+- `apps/web/components/task/file-editor-content.tsx`
+- `apps/web/components/task/file-tab-content.tsx`
+- `apps/web/components/task/mobile/mobile-file-viewer-panel.tsx`
+- Existing file-state and storage files from the superseded prototype.
+
+## Dependencies
+
+Task 05 supplies the runtime message contract and capability policy.
+
+## Risks
+
+- A renderer that uses `innerHTML`, `dangerouslySetInnerHTML`, or native event
+ attributes can turn a safe VM into an unsafe browser execution path.
+- CSS URL, `srcset`, SVG, or mutation paths can bypass a policy that checks
+ only initial HTML attributes.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- All requirement criteria in the revised system design.
+- Existing Markdown renderer, generic preview state, and mobile viewer patterns.
+- The current local navigation-neutralization change.
+
+## Results
+
+Replaced the iframe `srcDoc` path with the worker-backed runtime client and a
+scriptless Shadow DOM snapshot renderer. The component keeps the current
+buffer, generation-safe disposal, source recovery, localized runtime failures,
+and the existing generic file preview state.
+
+Verification completed:
+
+```text
+pnpm exec vitest run lib/html-preview/html-preview-document.test.ts lib/html-preview/preview-surface.test.ts components/task/html-preview-content.test.tsx components/task/file-editor-content.test.tsx components/task/file-tab-content.test.tsx components/task/mobile/mobile-file-viewer-panel.test.tsx
+pnpm exec tsc --noEmit --pretty false
+pnpm run i18n:check
+pnpm run i18n:ratchet
+```
diff --git a/docs/plans/native-html-preview/task-07-responsive-preview-surfaces.md b/docs/plans/native-html-preview/task-07-responsive-preview-surfaces.md
new file mode 100644
index 0000000000..b0ddd4ff43
--- /dev/null
+++ b/docs/plans/native-html-preview/task-07-responsive-preview-surfaces.md
@@ -0,0 +1,109 @@
+---
+id: "07-responsive-preview-surfaces"
+title: "Wire responsive preview surfaces"
+status: cancelled
+wave: 3
+depends_on:
+ - "06-preview-state-and-renderer"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.7
+ - AC-UI-NATIVE-HTML-PREVIEW-001.8
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 07: Wire responsive preview surfaces
+
+## Summary
+
+Connect the runtime-backed renderer to every existing HTML preview entry point.
+Reuse the Markdown toolbar and focused mobile viewer composition without
+creating a second execution path or changing unrelated file actions.
+
+## In scope
+
+- Monaco, CodeMirror, Dockview, center-panel, and mobile file-viewer preview
+ entry points.
+- Localized preview, source, runtime-failure, and unsupported-capability copy
+ in all shipped locales.
+- Mobile touch sizing, full-height layout, identity reset, and overflow
+ containment.
+- Component coverage for source restoration, dirty buffers, Markdown parity,
+ and runtime failure recovery.
+
+## Out of scope
+
+- Runtime engine or virtual-DOM implementation.
+- Public documentation.
+- Review-diff preview and Browser-panel behavior.
+
+## Acceptance
+
+- Every eligible text-file surface uses the same runtime-backed HTML preview,
+ and returning to source preserves buffer and dirty state.
+- Mobile exposes a touch-sized action and the scriptless preview surface does
+ not add page-level horizontal overflow.
+- Markdown, binary, unsupported-file, download, delete, comment, and external
+ editor behavior remains unchanged.
+
+## Verification
+
+```bash
+cd apps/web
+pnpm exec vitest run components/task/file-editor-content.test.tsx components/task/file-tab-content.test.tsx components/task/mobile/mobile-file-viewer-panel.test.tsx components/editors/monaco/monaco-editor-toolbar.test.tsx components/editors/codemirror/codemirror-code-editor.preview.test.tsx
+pnpm run typecheck
+pnpm run i18n:check
+pnpm run i18n:ratchet
+```
+
+## Files likely touched
+
+- `apps/web/components/task/file-editor-content.tsx`
+- `apps/web/components/task/file-editor-panel.tsx`
+- `apps/web/components/task/file-tab-content.tsx`
+- `apps/web/components/task/task-center-panel.tsx`
+- `apps/web/components/task/mobile/mobile-file-viewer-panel.tsx`
+- Monaco and CodeMirror toolbar files and tests.
+- Five shipped locale files and generated pseudo-locale data.
+
+## Dependencies
+
+Task 06 supplies the runtime-backed renderer and stable failure contract.
+
+## Risks
+
+- Multiple desktop file-editor paths can accidentally create independent worker
+ generations or omit disposal on unmount.
+- Mobile and Dockview may mount duplicate preview controls, so selectors and
+ lifecycle ownership must remain scoped to the active surface.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- The responsive contract in the system design.
+- Existing Markdown preview toolbar and mobile file-viewer tests.
+- `/mobile-parity` guidance for native mobile interaction patterns.
+
+## Results
+
+Verified that Monaco, CodeMirror, Dockview, center-panel tabs, and the
+focused mobile file viewer all select the same runtime-backed HTML component.
+Touch controls retain 44-pixel targets, preview state resets on file identity
+changes, and the existing Markdown and file action paths remain separate.
+
+Verification completed:
+
+```text
+pnpm exec vitest run components/task/file-editor-content.test.tsx components/task/file-tab-content.test.tsx components/task/mobile/mobile-file-viewer-panel.test.tsx components/editors/monaco/monaco-editor-toolbar.test.tsx components/editors/codemirror/codemirror-code-editor.preview.test.tsx
+pnpm exec tsc --noEmit --pretty false
+pnpm run i18n:check
+pnpm run i18n:ratchet
+```
diff --git a/docs/plans/native-html-preview/task-08-script-capable-preview-guidance.md b/docs/plans/native-html-preview/task-08-script-capable-preview-guidance.md
new file mode 100644
index 0000000000..b263b6e052
--- /dev/null
+++ b/docs/plans/native-html-preview/task-08-script-capable-preview-guidance.md
@@ -0,0 +1,96 @@
+---
+id: "08-script-capable-preview-guidance"
+title: "Publish script-capable preview guidance"
+status: cancelled
+wave: 4
+depends_on:
+ - "07-responsive-preview-surfaces"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.9
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 08: Publish script-capable preview guidance
+
+## Summary
+
+Update the Files and editor guide after the runtime-backed behavior is shipped.
+Explain how inline scripts work inside the restricted preview runtime and make
+the resource, browser-API, navigation, and multi-file limitations explicit.
+
+## In scope
+
+- Replace the static-only preview description in
+ `docs/public/developer-tools.md`.
+- Explain current-buffer rendering, unsaved edits, `Show code`, embedded
+ resources, isolated inline scripts, and blocked remote/workspace resources.
+- Explain when a development server and Browser panel are required.
+- Run public-doc validators and the docs diff check.
+
+## Out of scope
+
+- Architecture rationale, VM package details, or internal message shapes.
+- Public claims before the browser and WebView evidence is complete.
+- New navigation entries or a separate public page.
+
+## Acceptance
+
+- A user can preview an eligible file and understands that inline scripts run
+ in a restricted runtime without Kandev credentials or network access.
+- The guide does not describe the preview as a general site server or promise
+ unsupported browser APIs and external resources.
+
+## Verification
+
+```bash
+node --test scripts/validate-public-docs.test.mjs
+node scripts/validate-public-docs.mjs
+git diff --check -- docs/public
+```
+
+## Files likely touched
+
+- `docs/public/developer-tools.md`
+
+## Dependencies
+
+Task 07 supplies final user-facing labels and runtime behavior.
+
+## Risks
+
+Publishing a security claim before browser and WebView tests pass can create a
+false guarantee for users.
+
+## Parallelism
+
+`parallel-safe` with Task 09 after Task 07.
+
+## Inputs
+
+- The final requirements and public behavior from the system design.
+- The existing Files and editor integrations guide.
+- `/docs-maintainer` validation rules.
+
+## Results
+
+Updated `docs/public/developer-tools.md` with the restricted runtime contract,
+current-buffer and unsaved-edit behavior, embedded-resource policy, blocked
+browser capabilities, navigation behavior, source recovery, and Browser-panel
+guidance for multi-file applications.
+
+Verification completed:
+
+```text
+node --test scripts/validate-public-docs.test.mjs: 61 passed
+node scripts/validate-public-docs.mjs: Validated 43 published docs pages.
+git diff --check -- docs/public: passed
+```
diff --git a/docs/plans/native-html-preview/task-09-script-capable-preview-e2e.md b/docs/plans/native-html-preview/task-09-script-capable-preview-e2e.md
new file mode 100644
index 0000000000..acb29c007f
--- /dev/null
+++ b/docs/plans/native-html-preview/task-09-script-capable-preview-e2e.md
@@ -0,0 +1,112 @@
+---
+id: "09-script-capable-preview-e2e"
+title: "Prove script execution and isolation in browsers"
+status: cancelled
+wave: 4
+depends_on:
+ - "07-responsive-preview-surfaces"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.6
+ - AC-UI-NATIVE-HTML-PREVIEW-001.7
+ - AC-UI-NATIVE-HTML-PREVIEW-001.8
+ - AC-UI-NATIVE-HTML-PREVIEW-001.9
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 09: Prove script execution and isolation in browsers
+
+## Summary
+
+Replace the static-only browser assertions with causal evidence that inline
+JavaScript executes in the preview runtime while network, credential, and
+navigation capabilities remain unavailable. Repeat the user flow on desktop
+and mobile, then add the required desktop WebView smoke evidence.
+
+## In scope
+
+- Chromium desktop coverage for inline script output, event-driven mutation,
+ runtime failure, source recovery, and same-session restoration.
+- Negative request and navigation assertions for fetch, XHR, WebSocket,
+ dynamic links, meta refresh, forms, location, history, downloads, and
+ window opening.
+- Mobile Chrome coverage for runtime output, touch target, source recovery, and
+ containment.
+- Desktop WebView smoke coverage for the same request and navigation policy.
+
+## Out of scope
+
+- Broad E2E suite execution.
+- Other browser engines and executor types.
+- Review-diff, multi-file, remote-resource, or Browser-panel tests.
+
+## Acceptance
+
+- Desktop Chromium proves a script changes visible output and a user event
+ reaches the virtual runtime.
+- Causal request and frame-state assertions prove blocked network and
+ navigation attempts do not leave the preview or contact an external origin.
+- Mobile Chrome and desktop WebView prove the same safe entry, recovery, and
+ responsive behavior.
+
+## Verification
+
+```bash
+make build-web
+cd apps/web
+pnpm e2e:raw --project=chromium tests/chat/html-preview.spec.ts
+pnpm e2e:raw --project=mobile-chrome tests/task/mobile-html-preview.spec.ts
+cd ../desktop
+pnpm e2e
+```
+
+## Files likely touched
+
+- `apps/web/e2e/tests/chat/html-preview.spec.ts`
+- `apps/web/e2e/tests/task/mobile-html-preview.spec.ts`
+- `apps/web/e2e/pages/session-page.ts`
+- `apps/desktop/e2e/desktop-launch-smoke.mjs` or a focused WebView test.
+
+## Dependencies
+
+Task 07 supplies the runtime-backed responsive surface and stable selectors.
+
+## Risks
+
+- A test that only observes the final DOM can miss a request made before the
+ runtime snapshot arrives. Request listeners and frame-state assertions must
+ be causal and installed before preview activation.
+- Chromium and WebView may differ in worker, blob, and resource behavior.
+
+## Parallelism
+
+`parallel-safe` with Task 08 after Task 07.
+
+## Inputs
+
+- The revised acceptance criteria `.3` through `.9`.
+- The browser verification strategy and security invariants in the system
+ design.
+- Existing desktop and mobile HTML preview fixtures from the superseded work.
+
+## Results
+
+Replaced the static iframe assertions with runtime-backed Chromium and mobile
+coverage. The tests prove inline script output, event-driven mutation, inert
+links and forms, blocked dynamic resources and navigation, source recovery,
+same-session saved-source restoration, touch sizing, and page containment.
+
+Verification completed:
+
+```text
+pnpm e2e:raw --project=chromium tests/chat/html-preview.spec.ts: 1 passed
+pnpm e2e:raw --project=mobile-chrome tests/task/mobile-html-preview.spec.ts: 1 passed
+pnpm e2e (desktop): build and WebView launch smoke passed
+```
diff --git a/docs/plans/native-html-preview/task-10-agentctl-workspace-preview-server.md b/docs/plans/native-html-preview/task-10-agentctl-workspace-preview-server.md
new file mode 100644
index 0000000000..a06a223887
--- /dev/null
+++ b/docs/plans/native-html-preview/task-10-agentctl-workspace-preview-server.md
@@ -0,0 +1,109 @@
+---
+id: "10-agentctl-workspace-preview-server"
+title: "Build the agentctl workspace preview server"
+status: done
+wave: 1
+depends_on: []
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.11
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 10: Build the agentctl workspace preview server
+
+## Summary
+
+Add the ephemeral static server and bounded current-buffer overlay to agentctl.
+It must serve browser-ready HTML and relative workspace assets without exposing
+paths outside the selected repository root.
+
+## In scope
+
+- Lazy per-root loopback listener creation, reuse, shutdown, and structured
+ lifecycle logging.
+- A concurrency-safe 32-entry overlay with 5 MiB per-document bounds,
+ replacement versions, and least-recently-published eviction.
+- Exact overlay serving, disk fallback, MIME types, no-store headers, normal
+ 404 responses, and method handling.
+- Repository-aware canonical paths and traversal, encoded-traversal, and
+ symlink-escape rejection.
+- Agentctl publish route, request/response types, and unit tests.
+
+## Out of scope
+
+- Backend task-session routing.
+- Frontend Browser-panel behavior.
+- Browser-script isolation or content rewriting.
+
+## Acceptance
+
+- Publishing an unsaved entry document returns a stable live port for its
+ workspace or repository root, a canonical path, and an increasing version.
+ A request for that path returns the overlay.
+- Relative and root-relative requests serve the correct workspace files with
+ browser-appropriate content types.
+- No request path or symlink can read outside the selected root.
+- Bounds, eviction, concurrent publish/read, and shutdown behavior are tested.
+
+## Verification
+
+```bash
+cd apps/backend
+go test ./internal/agentctl/server/api
+go test -race ./internal/agentctl/server/api
+```
+
+## Files likely touched
+
+- `apps/backend/internal/agentctl/server/api/server.go`
+- `apps/backend/internal/agentctl/server/api/workspace_preview.go`
+- `apps/backend/internal/agentctl/server/api/workspace_preview_test.go`
+- Agentctl instance lifecycle files that own API server shutdown.
+
+## Dependencies
+
+None.
+
+## Risks
+
+- URL and filesystem canonicalization can disagree after escaping or symlink
+ resolution.
+- A listener or overlay can outlive the agentctl instance if teardown ownership
+ is incomplete.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- Acceptance criteria `.4`, `.5`, and `.11`.
+- Workspace preview manager, overlay bounds, trust, and lifecycle sections in
+ the system design.
+
+## Results
+
+Implemented the agentctl-owned workspace preview server.
+
+- Added one loopback static server per canonical workspace or repository root.
+- Added bounded current-buffer overlays with replacement versions and oldest
+ eviction across roots.
+- Added MIME handling, no-store responses, GET and HEAD support, containment
+ checks, traversal rejection, symlink escape protection, and teardown cleanup.
+- Added focused tests for publishing, disk assets, replacement, bounds,
+ eviction, traversal, symlinks, shutdown, concurrent publish/read,
+ per-repository roots and ports, and HEAD/405 method handling.
+
+Verification:
+
+- `go test ./internal/agentctl/server/api` passed.
+- `go test -race ./internal/agentctl/server/api` passed.
+- The API package passed 462 tests under the race detector, including the
+ deterministic concurrent publish/read and per-root method coverage.
+- `rtk make -C apps/backend test` passed with task-host internal config
+ overrides cleared.
diff --git a/docs/plans/native-html-preview/task-11-session-preview-publish-contract.md b/docs/plans/native-html-preview/task-11-session-preview-publish-contract.md
new file mode 100644
index 0000000000..fd2a669daf
--- /dev/null
+++ b/docs/plans/native-html-preview/task-11-session-preview-publish-contract.md
@@ -0,0 +1,108 @@
+---
+id: "11-session-preview-publish-contract"
+title: "Add the session preview publish contract"
+status: done
+wave: 2
+depends_on:
+ - "10-agentctl-workspace-preview-server"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.6
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 11: Add the session preview publish contract
+
+## Summary
+
+Expose the agentctl publish operation through an authorized task-session API.
+Add the frontend client that obtains a session port-proxy URL.
+
+## In scope
+
+- Agentctl client request and response methods.
+- Session-scoped backend route, authorization, readiness checks, 5 MiB body
+ bound, validation, forwarding, and error mapping.
+- Frontend domain API types, request helper, and proxy URL construction.
+- Backend and frontend tests for success, authorization, limits, invalid agentctl
+ responses, unavailable sessions, and retryable failures.
+
+## Out of scope
+
+- Toolbar, Browser panel, and mobile rendering changes.
+- A new gateway route or direct browser-to-agentctl access.
+
+## Acceptance
+
+- An authorized live session can publish `{repo, path, content}` and receive a
+ validated `{port, path, version}` result.
+- Unauthorized, stopped, malformed, and oversized requests fail without storing
+ source content in the backend.
+- The frontend produces the same session proxy URL shape as existing
+ development-server Browser panels.
+
+## Verification
+
+```bash
+cd apps/backend
+go test ./internal/agent/runtime/agentctl ./internal/task/handlers
+cd ../web
+pnpm exec vitest run lib/api/domains/process-api.test.ts
+pnpm run typecheck
+```
+
+## Files likely touched
+
+- `apps/backend/internal/agent/runtime/agentctl/client.go`
+- `apps/backend/internal/task/handlers/process_handlers.go`
+- Focused backend handler and client tests.
+- `apps/web/lib/api/domains/process-api.ts`
+- `apps/web/lib/api/domains/process-api.test.ts`
+
+## Dependencies
+
+Task 10 defines the agentctl route and response contract.
+
+## Risks
+
+- Generic error translation can hide whether retry requires restarting agentctl
+ or only republishing.
+- Duplicating existing proxy URL logic can produce inconsistent remote-executor
+ behavior.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- Acceptance criteria `.2`, `.5`, `.6`, and `.10`.
+- Browser-to-backend and backend-to-agentctl contracts in the system design.
+
+## Results
+
+Implemented the authorized session publish path and stable upstream status
+mapping.
+
+- Added the agentctl client for current HTML buffers. Non-success agentctl
+ responses now use a typed status error without retaining response content.
+- Added session authorization, readiness, body bounds, validation, and stable
+ 400/413/503 error mapping in the task-session handler. Unexpected upstream
+ failures and malformed success responses remain 502.
+- Added the frontend process API and versioned session port-proxy URL builder.
+- Removed the unused lifecycle forwarding layer so the handler has one client
+ acquisition path.
+- Added backend and frontend success, authorization, malformed/oversized,
+ unavailable, invalid-response, status-propagation, limit, and failure tests.
+
+Verification:
+
+- `go test ./internal/agent/runtime/agentctl ./internal/agent/runtime/lifecycle ./internal/task/handlers` passed.
+- Focused agentctl and task-handler race coverage passed: 15 tests.
+- `pnpm exec vitest run lib/api/domains/process-api.test.ts` passed.
+- `pnpm run typecheck` passed.
diff --git a/docs/plans/native-html-preview/task-12-browser-panel-preview-ui.md b/docs/plans/native-html-preview/task-12-browser-panel-preview-ui.md
new file mode 100644
index 0000000000..196222b8c4
--- /dev/null
+++ b/docs/plans/native-html-preview/task-12-browser-panel-preview-ui.md
@@ -0,0 +1,128 @@
+---
+id: "12-browser-panel-preview-ui"
+title: "Replace the virtual runtime with Browser-panel UI"
+status: done
+wave: 3
+depends_on:
+ - "11-session-preview-publish-contract"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.7
+ - AC-UI-NATIVE-HTML-PREVIEW-001.8
+ - AC-UI-NATIVE-HTML-PREVIEW-001.9
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 12: Replace the virtual runtime with Browser-panel UI
+
+## Summary
+
+Wire `Preview HTML` to publish the current buffer. Desktop opens the resulting
+URL in the existing Browser panel. Mobile opens it in the focused iframe. Remove
+the QuickJS and virtual-DOM path after the server flow works.
+
+## In scope
+
+- Desktop toolbar activation, progress/error state, trusted-code affordance,
+ Browser-panel focus/reuse, cache-busted refresh, and dirty-buffer preservation.
+- Mobile publication, full-height iframe, 44-pixel target, `Show code`, retry,
+ and repository-plus-path identity reset.
+- Shared localized copy in English, Portuguese, Simplified Chinese, and
+ Traditional Chinese catalogs.
+- Unit/component tests across Monaco, CodeMirror, Dockview, center-panel, and
+ mobile paths as applicable.
+- Removal of the QuickJS worker, virtual DOM, renderer, navigation normalizer,
+ obsolete state, preview-only packages, and obsolete tests.
+- Regeneration of the lock and license artifacts.
+- No behavior changes to Markdown or explicit development-server controls.
+
+## Out of scope
+
+- Public documentation and browser E2E.
+- New Browser-panel inspector or console features.
+
+## Acceptance
+
+- Desktop publishes the unsaved buffer, keeps source open and dirty, and reuses
+ the existing Browser panel on repeated activation.
+- Mobile renders the same proxy URL in a contained focused viewer and reliably
+ returns to unchanged source.
+- Trust and recovery copy is localized and available outside preview content.
+- QuickJS and the preview-only direct parse5 dependency no longer ship. The
+ Markdown renderer may retain parse5 as a transitive license entry.
+
+## Verification
+
+```bash
+cd apps/web
+pnpm exec vitest run components/task/dev-server-preview-button.test.tsx components/task/html-preview-content.test.tsx
+pnpm run typecheck
+pnpm run lint
+pnpm run i18n:check
+pnpm run i18n:ratchet
+cd ..
+pnpm --filter @kandev/web licenses:gen
+```
+
+## Files likely touched
+
+- `apps/web/components/task/html-preview-content.tsx`
+- HTML preview entry points in Monaco, CodeMirror, Dockview, and mobile files.
+- `apps/web/lib/state/dockview-panel-actions.ts`
+- `apps/web/src/locales/**`
+- `apps/web/lib/html-preview/**` (removed)
+- `apps/web/package.json`
+- `apps/pnpm-lock.yaml`
+- Generated dependency/license artifacts.
+
+## Dependencies
+
+Task 11 supplies the publish API and proxy URL.
+
+## Risks
+
+- Generic Browser-panel reuse can replace unrelated content.
+- Async publish completion can target a file that changed identity while the
+ request was in flight.
+- Removing generic preview state can regress Markdown restoration.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- Acceptance criteria `.1` through `.3` and `.7` through `.10`.
+- Desktop/mobile control flow and responsive contract in the system design.
+- Existing development-server button and mobile Markdown preview as exemplars.
+
+## Results
+
+Replaced the preview-specific QuickJS and virtual-DOM path with the native
+Browser-panel and mobile focused-viewer flows.
+
+- Desktop publishes the current buffer, preserves the source tab, and reuses
+ the shared Browser panel with versioned refresh URLs.
+- Mobile publishes the current buffer into a full-height iframe with a
+ touch-sized control, trust warning, Show code, retry, and identity reset.
+- Desktop task-center and file-editor publication now invalidate requests on
+ session replacement and unmount, so stale success, error, and cleanup
+ callbacks cannot affect the replacement session.
+- Markdown preview and explicit development-server behavior remain unchanged.
+- Removed the preview-specific runtime, worker, direct QuickJS dependency, and
+ direct parse5 dependency. Markdown keeps its transitive parse5 dependency.
+- Added localized trust, loading, retry, and unavailable copy in all catalogs.
+
+Verification:
+
+- Focused Vitest suite passed: 13 files and 69 tests, including deferred
+ stale-session success/error coverage for task-center and file-editor hooks.
+- `pnpm run typecheck`, `pnpm run lint`, `pnpm run i18n:check`, and
+ `pnpm run i18n:ratchet` passed.
+- `pnpm --filter @kandev/web licenses:gen` completed successfully.
diff --git a/docs/plans/native-html-preview/task-13-browser-fidelity-docs-and-e2e.md b/docs/plans/native-html-preview/task-13-browser-fidelity-docs-and-e2e.md
new file mode 100644
index 0000000000..03f80f6d25
--- /dev/null
+++ b/docs/plans/native-html-preview/task-13-browser-fidelity-docs-and-e2e.md
@@ -0,0 +1,129 @@
+---
+id: "13-browser-fidelity-docs-and-e2e"
+title: "Document and prove browser-fidelity preview"
+status: done
+wave: 4
+depends_on:
+ - "12-browser-panel-preview-ui"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.5
+ - AC-UI-NATIVE-HTML-PREVIEW-001.6
+ - AC-UI-NATIVE-HTML-PREVIEW-001.7
+ - AC-UI-NATIVE-HTML-PREVIEW-001.8
+ - AC-UI-NATIVE-HTML-PREVIEW-001.9
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+ - AC-UI-NATIVE-HTML-PREVIEW-001.11
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+# Task 13: Document and prove browser-fidelity preview
+
+## Summary
+
+Update public guidance for the trusted static preview. Replace isolation E2E
+assertions with browser-fidelity, relative-asset, lifecycle, and responsive
+evidence.
+
+## In scope
+
+- Public documentation for the one-click action, current-buffer behavior,
+ relative assets, browser APIs, and trusted-code warning.
+- Public documentation for static-server limits, recovery, and the explicit
+ development-server alternative.
+- Desktop Chromium E2E for native scripts, a browser API absent from the old
+ virtual runtime, relative CSS/JavaScript/image assets, unsaved republish,
+ Browser-panel reuse, errors, and source preservation.
+- Mobile Chrome E2E for the shared proxied page, touch size, containment, retry,
+ `Show code`, and unchanged source.
+- Desktop build/WebView smoke and final targeted regression verification.
+
+## Out of scope
+
+- Claiming hostile-content isolation.
+- Broad executor-matrix E2E beyond existing port-proxy coverage.
+
+## Acceptance
+
+- Documentation accurately distinguishes trusted static preview from configured
+ application development servers.
+- Desktop and mobile tests causally prove native browser execution, relative
+ asset loading, current-buffer refresh, responsive behavior, and recovery.
+- Specification, documentation, dependency, backend, frontend, and desktop
+ validation commands pass.
+
+## Verification
+
+```bash
+python3 scripts/lint-spec-files.py --all
+node --test scripts/validate-public-docs.test.mjs
+node scripts/validate-public-docs.mjs
+make -C apps/backend test
+cd apps/web
+pnpm e2e:raw --project=chromium tests/chat/html-preview.spec.ts
+pnpm e2e:raw --project=mobile-chrome tests/task/mobile-html-preview.spec.ts
+cd ../desktop
+pnpm e2e
+```
+
+## Files likely touched
+
+- `docs/public/developer-tools.md`
+- `apps/web/e2e/tests/chat/html-preview.spec.ts`
+- `apps/web/e2e/tests/task/mobile-html-preview.spec.ts`
+- Focused E2E fixtures and page objects.
+
+## Dependencies
+
+Task 12 supplies the complete user flow and removes the old runtime.
+
+## Risks
+
+- E2E fixtures can accidentally prove saved disk content instead of the current
+ unsaved overlay.
+- Relative asset assertions can pass through cached data unless request paths and
+ version changes are observed.
+- Documentation can overstate safety unless the trusted-code language stays
+ explicit.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- All acceptance criteria and the final verification strategy.
+- The accepted trusted Browser HTML preview ADR.
+
+## Results
+
+Updated the public developer-tools guidance to document current-buffer static
+preview, relative assets, native browser behavior, trusted workspace code,
+bounded server behavior, recovery, and the explicit development-server
+alternative. Replaced the old isolation-oriented E2E assertions with native
+browser fidelity coverage on desktop and mobile.
+
+Verification:
+
+- `python3 scripts/lint-spec-files.py --all` passed.
+- `node --test scripts/validate-public-docs.test.mjs` passed: 61 tests.
+- `node scripts/validate-public-docs.mjs` validated 43 published pages.
+- `make -C apps/backend test` passed with task-host internal config overrides
+ cleared.
+- Desktop Chromium preview E2E passed: 2 tests.
+- Mobile Chrome preview E2E passed: 2 tests.
+- Desktop shell E2E passed.
+- Fresh desktop and mobile PR screenshots were captured and compressed.
+- Reproduced and fixed the frontend CI failure caused by a synchronous
+ assertion racing Radix repository-menu dismissal, and removed unhandled
+ Happy DOM loopback requests caused by Playwright runtime imports in Vitest
+ helper tests.
+- Full frontend Vitest verification passed after the fix: 1,871 test files,
+ 15,869 passed, and 4 skipped, with no unhandled network errors. Direct web
+ typecheck, lint, i18n, and formatting checks passed as well.
diff --git a/docs/plans/native-html-preview/task-14-in-editor-html-preview.md b/docs/plans/native-html-preview/task-14-in-editor-html-preview.md
new file mode 100644
index 0000000000..7ffd1a72dc
--- /dev/null
+++ b/docs/plans/native-html-preview/task-14-in-editor-html-preview.md
@@ -0,0 +1,110 @@
+---
+id: "14-in-editor-html-preview"
+title: "Move desktop HTML preview into the file editor"
+status: done
+wave: 5
+depends_on:
+ - "13-browser-fidelity-docs-and-e2e"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.7
+ - AC-UI-NATIVE-HTML-PREVIEW-001.8
+ - AC-UI-NATIVE-HTML-PREVIEW-001.9
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+
+# Task 14: Move desktop HTML preview into the file editor
+
+## Summary
+
+Replace automatic Browser-panel opening with the same focused iframe used by
+the mobile file viewer. Preserve the file tab, unsaved buffer, dirty state,
+source recovery, and explicit access to the Browser panel.
+
+## In scope
+
+- Shared desktop publication state scoped to session, repository, and path.
+- In-editor loading, iframe, failure, retry, refresh, and `Show code` states.
+- An explicit secondary action that opens the published URL in a Browser panel.
+- Monaco, CodeMirror, Dockview file-panel, and task-center file-tab wiring.
+- Removal of obsolete desktop publish hooks and shared global busy state.
+- Focused component and controller tests, including stale completion guards.
+
+## Out of scope
+
+- Agentctl server or task-session API changes.
+- Markdown preview behavior.
+- Automatic shutdown of the session-scoped shared preview server.
+
+## Acceptance
+
+- Activating `Preview HTML` replaces the active editor body without creating a
+ Browser panel and publishes the current unsaved buffer.
+- `Show code` restores the same file buffer and dirty state; refresh republishes
+ the latest buffer and retry recovers from publication failure.
+- Preview state resets on file or session identity change, and only the explicit
+ secondary action opens the Browser panel.
+
+## Verification
+
+```bash
+cd apps/web
+pnpm exec vitest run components/task/file-editor-content.test.tsx components/task/file-tab-content.test.tsx components/task/html-preview-content.test.tsx hooks/use-html-preview-publisher.test.ts components/editors/monaco/monaco-editor-toolbar.test.tsx components/editors/codemirror/codemirror-code-editor.preview.test.tsx
+pnpm run typecheck
+pnpm run lint
+pnpm run i18n:check
+pnpm run i18n:ratchet
+```
+
+## Files likely touched
+
+- `apps/web/components/task/file-editor-content.tsx`
+- `apps/web/components/task/html-preview-content.tsx`
+- `apps/web/components/task/file-editor-panel.tsx`
+- `apps/web/components/task/file-tab-content.tsx`
+- `apps/web/components/task/task-center-panel.tsx`
+- `apps/web/hooks/use-html-preview-publisher.ts`
+- Obsolete desktop HTML-preview hooks and tests.
+- `apps/web/src/locales/**`
+
+## Dependencies
+
+Task 13 supplies the trusted static server, publish API, iframe surface, and
+browser-fidelity coverage.
+
+## Risks
+
+- A stale response can reveal preview for a previous file or session.
+- Replacing the editor body can accidentally discard unsaved parent state.
+- Shared toolbar changes can regress Markdown preview controls.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- Acceptance criteria `.1`, `.2`, `.3`, and `.7` through `.10`.
+- Desktop control flow and responsive contract in the system design.
+- Existing Markdown toggle and mobile HTML preview as shipped exemplars.
+
+## Results
+
+- Desktop `Preview HTML` now replaces the active editor body with the shared
+ native-browser iframe while preserving the current unsaved buffer and dirty
+ state. `Show code` restores the same editor state, and refresh republishes the
+ latest buffer.
+- Preview identity is scoped to session, repository, and path. File or session
+ changes immediately hide stale previews, reset the request generation, and
+ prevent late responses from opening or mutating the new file.
+- Browser-panel opening is now an explicit secondary action. The obsolete
+ automatic-open hooks and global publish-busy plumbing were removed.
+- Focused Vitest coverage passed: 6 files and 22 tests. Web typecheck, lint,
+ i18n checks, and the changed-line i18n ratchet also passed.
diff --git a/docs/plans/native-html-preview/task-15-in-editor-preview-docs-e2e.md b/docs/plans/native-html-preview/task-15-in-editor-preview-docs-e2e.md
new file mode 100644
index 0000000000..429c482e2c
--- /dev/null
+++ b/docs/plans/native-html-preview/task-15-in-editor-preview-docs-e2e.md
@@ -0,0 +1,107 @@
+---
+id: "15-in-editor-preview-docs-e2e"
+title: "Revise preview guidance and browser E2E"
+status: done
+wave: 6
+depends_on:
+ - "14-in-editor-html-preview"
+plan: "plan.md"
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-NATIVE-HTML-PREVIEW-001.1
+ - AC-UI-NATIVE-HTML-PREVIEW-001.2
+ - AC-UI-NATIVE-HTML-PREVIEW-001.3
+ - AC-UI-NATIVE-HTML-PREVIEW-001.4
+ - AC-UI-NATIVE-HTML-PREVIEW-001.7
+ - AC-UI-NATIVE-HTML-PREVIEW-001.8
+ - AC-UI-NATIVE-HTML-PREVIEW-001.10
+system_design:
+ - ../../specs/ui/system-design/native-html-preview.md
+---
+
+# Task 15: Revise preview guidance and browser E2E
+
+## Summary
+
+Update public guidance and browser tests so they describe and prove the
+in-editor source/preview workflow while retaining native browser fidelity and
+the focused mobile composition.
+
+## In scope
+
+- Public documentation for in-editor preview, source recovery, refresh, and the
+ optional Browser-panel action.
+- Desktop Chromium E2E proving no automatic Browser panel, iframe replacement,
+ unsaved republish, native scripts and assets, `Show code`, and dirty state.
+- Mobile Chrome regression coverage for the shared focused iframe behavior.
+- Final specification, documentation, frontend, E2E, and desktop verification.
+
+## Out of scope
+
+- Changes to the trusted-code boundary or server lifecycle.
+- New executor-matrix E2E beyond existing port-proxy coverage.
+
+## Acceptance
+
+- Documentation describes the in-editor workflow without claiming automatic
+ Browser-panel behavior.
+- Desktop and mobile tests prove the same current-buffer browser value with
+ viewport-appropriate controls and source recovery.
+- Task-defined validation passes without retries or flakes.
+
+## Verification
+
+```bash
+python3 scripts/lint-spec-files.py --all
+node --test scripts/validate-public-docs.test.mjs
+node scripts/validate-public-docs.mjs
+cd apps/web
+pnpm e2e:run tests/chat/html-preview.spec.ts
+pnpm e2e:run --project mobile-chrome tests/task/mobile-html-preview.spec.ts
+cd ../desktop
+pnpm e2e
+```
+
+## Files likely touched
+
+- `docs/public/developer-tools.md`
+- `apps/web/e2e/tests/chat/html-preview.spec.ts`
+- `apps/web/e2e/tests/task/mobile-html-preview.spec.ts`
+- Focused E2E page objects or helpers.
+
+## Dependencies
+
+Task 14 supplies the revised in-editor implementation.
+
+## Risks
+
+- An E2E test can accidentally assert the old Browser-panel iframe.
+- Production-build E2E can exercise stale assets unless the managed runner
+ rebuilds after frontend changes.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- All applicable acceptance criteria and the revised responsive contract.
+- The amended trusted native-browser HTML preview ADR.
+
+## Results
+
+- Public guidance now documents the in-editor desktop and focused mobile
+ workflow, explicit Browser-panel access, and the session-scoped server
+ lifecycle. Closing any preview view unmounts that view; agentctl teardown
+ stops the bounded shared server.
+- Desktop Chromium E2E passed 2 of 2 tests on the first attempt. It proves no
+ automatic Browser panel, native scripts and browser APIs, relative assets,
+ current-buffer refresh, explicit Browser-panel opening, source recovery, and
+ retry after publication failure.
+- Mobile Chrome E2E passed 2 of 2 tests on the first attempt, preserving the
+ focused-viewer interaction and source recovery at phone width.
+- Specification validation passed all 30 tests. Public-documentation validation
+ passed 61 tests across 46 published pages. Desktop shell E2E passed.
+- The full frontend suite passed 1,872 test files and 15,902 tests, with four
+ intentional skips and no failures.
diff --git a/docs/public/developer-tools.md b/docs/public/developer-tools.md
index 574177d388..0e9ed8a4da 100644
--- a/docs/public/developer-tools.md
+++ b/docs/public/developer-tools.md
@@ -138,10 +138,10 @@ at your cursor; Kandev's own composer still owns the draft and the send.
The plugin offers three engines, chosen per user under **Settings > Plugins > Voice Mode**:
-| Engine | Where recognition happens | Requirements and data flow |
-| --- | --- | --- |
-| **Browser speech** | The browser's own recognizer | No audio reaches your Kandev server. Chromium only, and the browser vendor's own handling applies. |
-| **In-browser Whisper** | On the device | Downloads and caches an ONNX model from Hugging Face (about 40 MB, 75 MB or 240 MB), then runs locally in a worker. No audio leaves the device. |
+| Engine | Where recognition happens | Requirements and data flow |
+| ------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Browser speech** | The browser's own recognizer | No audio reaches your Kandev server. Chromium only, and the browser vendor's own handling applies. |
+| **In-browser Whisper** | On the device | Downloads and caches an ONNX model from Hugging Face (about 40 MB, 75 MB or 240 MB), then runs locally in a worker. No audio leaves the device. |
| **Server transcription** | Your Kandev server, relaying to OpenAI | Requires an operator to save an OpenAI key in the plugin's settings. The key stays on the server; the request requires a signed-in Kandev user. |
`Automatic` picks the first engine the browser can run, in that order. Engine choice is capability
@@ -169,6 +169,16 @@ For an idle, non-archived repository-backed task, **Files → Workspace actions
The task **Files** panel browses, searches, opens, and edits task-worktree files. Kandev rejects file paths that escape the resolved worktree. A session with one worktree opens that worktree directly in a host editor. When a session has several worktrees, the editor button asks which repository or worktree to open, and each configured editor in the adjacent menu expands to the same repository-and-branch picker. Check that selection before launching an editor from a multi-repository task. Older API clients that omit `worktree_id` retain the first-worktree fallback.
+### Preview an HTML file
+
+Open an `.html` or `.htm` file in **Files**, then select **Preview HTML**. Kandev publishes the current editor buffer to an ephemeral static server, so the preview includes unsaved changes while the source file stays dirty and unchanged on disk. On desktop, the preview replaces the editor body. On mobile, it replaces the source in the focused full-height viewer. Select **Show code** to return to the same source buffer. Select **Refresh HTML preview** to publish the latest buffer again, or use the Browser button to open the published page in a separate **Browser** panel.
+
+The preview uses the native browser engine. HTML, CSS, JavaScript, inline event handlers, browser APIs, forms, popups, and normal browser navigation behave as they do in a regular page. A visible warning identifies the preview as trusted workspace code. Preview only HTML you trust because this feature does not claim to isolate hostile content from the browser or task environment.
+
+Relative and root-relative URLs resolve from the selected task repository or workspace root. Static files use their normal browser content types, and the current entry document is held in memory. The static server does not persist the overlay, run a build, provide HMR, or proxy an application backend. It bounds one entry document to 5 MiB and keeps at most 32 recently published overlays per agentctl instance.
+
+Closing the preview, file tab, focused viewer, or optional Browser panel removes that view but does not stop the shared static server. One bounded server is reused for the agentctl session and stops when that task runtime is torn down. If the page needs a build pipeline, HMR, backend routes, or project services, start a development server and open it in the **Browser** panel instead. If the task session stops, publish the file again or select **Retry** after the session becomes available. Preview URLs and in-editor preview state are not restored as durable file state.
+
Open the context menu on any file or folder in the Files tree: right-click on desktop or long-press on touch to see **Open in \**, which launches your default editor at that path instead of at the worktree root. When more than one editor is configured, **Open in other editor** lists the rest. When the tree is rooted above the worktrees (a multi-worktree task or any task that has had sources attached), Kandev resolves the clicked path back to its own worktree, so no picker is needed. The action is hidden for entries that belong to no worktree, such as an attached plain folder, because the editor launch is resolved against a worktree. It is also hidden while several files are selected, because it applies to a single path.
**VS Code (Embedded)** runs code-server inside the active task environment and displays it in a workbench panel. Opening it starts code-server independently of the agent process. It launches with `--auth none` and binds `0.0.0.0` on a random port inside that runtime; network/firewall isolation is therefore important, especially for Local, Kubernetes, and SSH environments. If the binary is absent, agentctl attempts to download it into `~/.kandev/tools/code-server`; first use needs a supported execution platform and access to the [code-server release](https://github.com/coder/code-server/releases/tag/v4.130.0). Native Windows Local and Worktree sessions do not support this integration. Linux-backed Local Docker, Kubernetes, Sprites, and supported SSH sessions can offer it even when the Kandev app runs on Windows. The task-detail topbar follows the active session's executor capability, never the visitor's browser platform; other configured editors remain available. Use the panel error and task-environment logs when installation, startup, or proxying fails.
diff --git a/docs/specs/ui/README.md b/docs/specs/ui/README.md
index 483c46a715..60512a63f7 100644
--- a/docs/specs/ui/README.md
+++ b/docs/specs/ui/README.md
@@ -54,6 +54,7 @@ UI owns responsive behavior; other systems own behavior/state.
- [GitHub PR Review Actions](requirements/github-pr-review-actions.md)
- [GitHub Saved-Query Default Views](requirements/github-saved-query-defaults.md)
- [Kandev MCP Tool Results](requirements/kandev-mcp-tool-results.md)
+- [Native HTML File Preview](requirements/native-html-preview.md)
- [Auto-hide empty workflow steps](requirements/kanban-auto-hide-empty-columns.md)
- [Repair last-prompt transcript pinning](requirements/last-prompt-pinning-regressions.md)
- [Repair merge commit details](requirements/merge-commit-details.md)
@@ -162,6 +163,7 @@ UI owns responsive behavior; other systems own behavior/state.
- [File Tree Chat](system-design/file-tree-chat-context.md)
- [MCP results](system-design/kandev-mcp-tool-results.md)
- [Message queue row controls](system-design/message-queue-row-controls.md)
+- [Native HTML File Preview](system-design/native-html-preview.md)
- [Mobile Task Chrome](system-design/mobile-task-chrome.md)
- [Persistent status motion](system-design/persistent-status-motion.md)
- [Repository Groups](system-design/sidebar-repository-grouping.md)
diff --git a/docs/specs/ui/requirements/native-html-preview.md b/docs/specs/ui/requirements/native-html-preview.md
new file mode 100644
index 0000000000..ef60851808
--- /dev/null
+++ b/docs/specs/ui/requirements/native-html-preview.md
@@ -0,0 +1,103 @@
+---
+status: active
+system: ui
+created: 2026-09-04
+owners:
+ - kandev
+---
+
+# Native HTML File Preview Requirements
+
+## Overview
+
+Kandev shall open an `.html` or `.htm` file in a real browser runtime. The user
+does not need to start a development server. The preview shall use the current
+editor buffer as its entry document and shall resolve relative assets from the
+task workspace. Desktop and phone surfaces replace the active file editor body
+with the same proxied page while preserving a direct return to source.
+
+HTML preview is a trusted-workspace-code feature. Previewed scripts have native
+browser behavior and the authority already granted to Browser-panel content.
+Kandev does not claim that this feature safely executes untrusted HTML. Users
+must trust a document before they preview it.
+
+The UI system owns the reusable file-viewer interaction. Agentctl owns the
+ephemeral static server that makes current-buffer HTML and workspace files
+available to the browser. The existing session port proxy owns routing and
+session authorization across local and remote executors.
+
+## Terminology
+
+- **Entry document:** The current, possibly unsaved, editor buffer selected for
+ preview.
+- **Workspace preview server:** A loopback HTTP server inside agentctl that
+ serves the entry document from memory and relative assets from the workspace.
+- **Preview URL:** The session-scoped port-proxy URL used by the in-editor
+ preview iframe or an explicitly opened Browser panel.
+- **Trusted workspace code:** Content the user permits to run with the existing
+ Browser-panel sandbox and origin policy. It is not isolated as hostile code.
+
+## Requirements
+
+### REQ-UI-NATIVE-HTML-PREVIEW-001: Preview HTML with browser fidelity
+
+**Intent:** Let users inspect a generated or edited HTML document with normal
+browser rendering, relative assets, and browser APIs. A configured
+development-server command is not necessary.
+
+#### Acceptance criteria
+
+- **AC-UI-NATIVE-HTML-PREVIEW-001.1:** When an editable text file ends in
+ `.html` or `.htm`, each file editor shall expose an accessible `Preview HTML`
+ action. The action shall use each location that has the Markdown preview
+ action.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.2:** Activating `Preview HTML` shall publish
+ the current editor buffer without saving it. The source file shall remain
+ open and retain its content and dirty state.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.3:** On desktop, activation shall replace the
+ active file editor body with an iframe at the preview URL without opening a
+ Browser panel. The preview shall provide `Show code`, refresh the current
+ unsaved buffer on request, and offer an explicit secondary action to open the
+ same URL in a Browser panel.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.4:** The preview shall use the native browser
+ engine for markup, CSS, JavaScript, DOM events, and relative workspace assets.
+ Existing Browser-panel iframe, browser, and deployment policies shall control
+ links, forms, media, modules, and network APIs.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.5:** Relative URLs shall resolve from the
+ entry document's workspace directory. The server shall not expose paths
+ outside the selected task workspace or repository scope, including through
+ traversal or symlink escape.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.6:** The preview shall use the existing
+ session-authorized port-proxy path. This path shall support local, Docker,
+ SSH, and Kubernetes task sessions without a second public routing mechanism.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.7:** Before activation, the UI shall make the
+ trusted-code consequence clear. Preview source shall execute only in the
+ Kandev-owned preview iframe or an explicitly opened Browser panel and shall
+ never be inserted directly into the Kandev parent document.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.8:** On phone and coarse-pointer surfaces, the
+ focused file viewer shall expose the preview action. The active touch
+ dimension shall be at least 44 pixels. Preview shall occupy the full-height
+ viewer, provide `Show code`, and avoid document-level horizontal overflow.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.9:** Adding HTML preview shall not change
+ Markdown sanitization, source editing, saving, downloading, deleting,
+ commenting, external-editor actions, or explicit development-server flows.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.10:** If the task session or agentctl is not
+ available, the buffer is too large, or the server cannot start, Kandev shall
+ show localized recovery copy. The UI shall preserve source access and permit
+ retry. It shall not silently use another execution model.
+- **AC-UI-NATIVE-HTML-PREVIEW-001.11:** Preview overlays and their server shall
+ be bounded and ephemeral. They shall end with the agentctl instance and shall
+ not persist file contents to Kandev storage. A stale preview URL after an
+ executor restart shall be recoverable by activating `Preview HTML` again.
+
+## Out of scope
+
+- Safely executing hostile or untrusted HTML.
+- A dedicated untrusted-content origin, network egress filtering, or browser
+ capability emulation.
+- Framework build pipelines, hot-module replacement, package installation, or
+ replacing an application's configured development-server command.
+- Rendering HTML reconstructed from a Review diff.
+- Mapping rendered elements to source lines, inspector annotations, or console
+ forwarding beyond existing Browser-panel behavior.
+- Publishing the preview server as a production website or a durable URL.
diff --git a/docs/specs/ui/system-design/native-html-preview.md b/docs/specs/ui/system-design/native-html-preview.md
new file mode 100644
index 0000000000..8863e6e8d4
--- /dev/null
+++ b/docs/specs/ui/system-design/native-html-preview.md
@@ -0,0 +1,328 @@
+---
+status: current
+system: ui
+requirements:
+ - REQ-UI-NATIVE-HTML-PREVIEW-001
+---
+
+# Native HTML File Preview System Design
+
+## Purpose and boundaries
+
+Native HTML preview is a one-click static-server workflow built on Kandev's
+existing browser iframe policy and session port proxy. It favors browser
+fidelity over hostile-content isolation. Agentctl serves the current editor
+buffer as an in-memory entry document and serves relative assets from the
+selected task workspace. The active file viewer replaces its source body with
+an iframe that renders the page with normal HTML, CSS, and JavaScript behavior.
+
+The feature does not create a virtual browser, transform user scripts, or
+introduce a second executor-routing protocol. It also does not make a security
+claim for arbitrary workspace HTML. The user-selected trust model is recorded
+in [ADR-2026-09-05-trusted-browser-html-preview](../../../decisions/2026-09-05-trusted-browser-html-preview.md).
+
+## Requirement mapping
+
+| Requirement | Design sections |
+| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `REQ-UI-NATIVE-HTML-PREVIEW-001` | [Architecture](#architecture), [Contracts](#contracts), [Control flow](#control-flow), [Trust and security](#trust-and-security), [Responsive contract](#responsive-contract), [Failure and recovery](#failure-and-recovery), [Verification strategy](#verification-strategy) |
+
+## Architecture
+
+```text
+File editor current buffer
+ |
+ | POST session-scoped preview request
+ v
+Kandev backend task handler
+ |
+ | authenticated agentctl request
+ v
+Agentctl workspace preview manager
+ |-- in-memory entry-document overlay
+ |-- workspace-rooted static files
+ `-- loopback ephemeral HTTP port
+ |
+ | existing session port proxy
+ v
+ Desktop or mobile file-preview iframe
+```
+
+The existing gateway port proxy remains responsible for session authorization,
+executor reachability, capability cookies for iframe subresources, response
+rewriting, and Browser-panel compatibility. The preview feature adds no public
+listener and no direct browser-to-agentctl connection.
+
+## Components and responsibilities
+
+### Agentctl
+
+- A workspace preview manager starts one loopback HTTP server per selected root
+ when it receives the first request. The manager closes every server during
+ agentctl instance teardown. A port identifies one static root, including for
+ multi-repository tasks.
+- The manager stores entry-document overlays by canonical repository-relative
+ path. Publishing the same path replaces its previous buffer and increments a
+ version token.
+- The static handler serves an overlay for an exact entry-document match and
+ otherwise serves files from the selected workspace or repository root.
+- Existing repository path resolution remains authoritative. Request paths are
+ URL-decoded once, cleaned, joined through the workspace path helper, and
+ rejected if the resolved path or symlink target escapes the selected root.
+- Responses use standard MIME detection and `Cache-Control: no-store` so a
+ republished current buffer and edited assets are visible on refresh.
+
+### Backend
+
+- A task-session handler authorizes the requested session and validates the
+ request. It also makes sure that agentctl is ready. Then it forwards the
+ request through the existing agentctl client.
+- The agentctl client preserves non-success status codes in a typed error
+ without retaining the response body. The task-session handler maps agentctl
+ validation, size, and availability responses to 400, 413, and 503; other
+ upstream failures and malformed success responses become 502.
+- The backend does not store HTML or proxy asset bytes itself.
+- The response contains the agentctl port, scoped entry path, and version needed
+ to construct a session port-proxy URL.
+
+### Frontend
+
+- Preview eligibility remains derived from editable `.html` and `.htm` file
+ paths after binary-file classification.
+- The frontend API publishes the current editor buffer and converts the result
+ into the same port-proxy URL form used by development servers.
+- Desktop renders the URL through `HtmlPreviewContent` in place of the active
+ source editor. `Show code` restores the source body, refresh republishes the
+ latest buffer, and an explicit secondary action can call
+ `openBrowserPanel(url)` without making that panel the primary outcome.
+- Mobile renders the same URL in the focused file viewer and uses the same
+ source-recovery, refresh, trust, loading, and error semantics.
+- Markdown keeps its sanitized in-place renderer and existing preview state.
+
+## Contracts
+
+### Browser-to-backend request
+
+```http
+POST /api/v1/task-sessions/{sessionID}/html-previews
+Content-Type: application/json
+
+{
+ "repo": "optional repository identity",
+ "path": "relative/path/index.html",
+ "content": "current editor buffer"
+}
+```
+
+The request body is capped at 5 MiB. `path` must be an editable `.html` or
+`.htm` file within the task workspace. `repo` uses the same optional multi-repo
+identity accepted by existing workspace-file operations.
+
+The successful response is:
+
+```json
+{
+ "port": 43127,
+ "path": "/relative/path/index.html",
+ "version": 4
+}
+```
+
+The frontend appends the version as a cache-busting query value when opening
+the session port-proxy URL. The port is an implementation detail scoped to the
+live task session, not a durable public identifier.
+
+### Backend-to-agentctl request
+
+```http
+POST /api/v1/workspace/html-previews
+Content-Type: application/json
+```
+
+The body and response use the same fields. Agentctl repeats size, extension,
+and path validation because the backend is not its trust boundary. Existing
+agentctl authentication applies.
+
+### Overlay bounds
+
+Each document is limited to 5 MiB. An agentctl instance retains at most 32
+entry-document overlays across its root servers. Replacing an existing path
+does not consume another slot. When the limit is reached, the least recently
+published overlay is evicted. The number of loopback servers is bounded by the
+task's configured workspace and repository roots. Overlay bytes, versions, and
+access metadata live only in memory.
+
+These limits constrain accidental memory growth. They are not a hostile-code
+sandbox and do not constrain resources consumed by browser scripts.
+
+## Control flow
+
+### Desktop
+
+1. The file editor derives HTML preview eligibility from the current path.
+2. The user activates `Preview HTML` after seeing the trusted-code affordance.
+3. The frontend sends the current buffer, path, and repository identity to the
+ task-session endpoint without saving the file.
+4. The backend authorizes the session and publishes the entry document through
+ agentctl.
+5. Agentctl starts or reuses its workspace preview server and returns its port,
+ path, and new version.
+6. The frontend constructs the existing session port-proxy URL and replaces the
+ source editor body with `HtmlPreviewContent` at that URL.
+7. The iframe loads the page. Relative subresources traverse the same proxy and
+ are read from the workspace by agentctl.
+8. `Refresh` republishes the latest unsaved buffer and loads the new versioned
+ URL. `Show code` restores source, while `Open in Browser panel` explicitly
+ opens the same URL as a secondary workflow.
+
+The file tab, current buffer, and dirty state remain owned by the source editor
+flow while the iframe is visible.
+
+### Mobile
+
+1. The focused file viewer exposes the same `Preview HTML` action.
+2. Publishing follows the desktop flow.
+3. The focused viewer replaces its code body with a full-height iframe at the
+ returned proxy URL and keeps a `Show code` action outside the iframe.
+4. `Show code` or file identity change restores source without changing its
+ buffer. A preview failure keeps source recovery and retry actions available.
+
+## Relative assets and browser fidelity
+
+For an entry document at `site/pages/index.html`, `../styles/site.css` resolves
+to `site/styles/site.css` and `/images/logo.svg` resolves from the selected
+workspace or repository root. Query strings and fragments do not participate
+in filesystem path resolution.
+
+The static server does not rewrite HTML, CSS, JavaScript, or import maps.
+Standard browser behavior covers scripts, modules, DOM APIs, fetch, media,
+forms, canvas, WebGL, and available Browser-panel APIs. An application that requires a build
+step, custom fallback routing, server-side endpoints, or HMR must continue to
+use the configured development-server workflow.
+
+## Trust and security
+
+HTML preview uses the existing Browser-panel iframe policy, including scripts
+and same-origin behavior. It is intentionally a trusted-code workflow.
+
+The following remain required:
+
+1. Preview HTML is never inserted into the Kandev parent DOM.
+2. Browser access always traverses the session-authorized port proxy.
+3. Static file reads stay within the selected workspace or repository root,
+ including after symlink evaluation.
+4. Agentctl listens only on loopback and stops the preview server with the
+ instance.
+5. The UI tells the user that previewing runs workspace code with Browser-panel
+ capabilities.
+6. Logs and errors do not include the HTML buffer.
+
+These controls protect routing and host filesystem scope. They do not isolate
+the user from malicious JavaScript. In deployments where Browser-panel content
+shares an origin with Kandev, previewed code can exercise the authority that
+origin and the Browser panel expose. Operators who cannot trust workspace HTML
+must not enable or use this action until a dedicated-origin mode exists.
+
+## Lifecycle and persistence
+
+- A root's preview server starts on first publish and is reused for the
+ agentctl instance lifetime.
+- Entry overlays are in-memory only and do not save or modify workspace files.
+- Leaving preview or closing the file tab unmounts the iframe but does not stop
+ the shared server. Agentctl teardown stops the server.
+- In-editor preview state and URLs are not persisted. Activating `Preview HTML`
+ after reload or executor restart publishes the current buffer to the live
+ instance and obtains a valid URL.
+- Preview state is scoped to the active session plus repository-and-path
+ identity and resets when that identity changes.
+
+## Responsive contract
+
+- **Desktop entry and outcome:** The existing file toolbar exposes `Preview
+HTML`. The action replaces the source body inside the same file tab with an
+ iframe and keeps `Show code`, refresh, and optional Browser-panel actions in
+ Kandev-owned chrome.
+- **Phone entry and outcome:** `MobileFileViewerPanel` exposes a minimum
+ 44-pixel action target. It shows a focused full-height iframe and puts `Show
+code` in Kandev-owned chrome.
+- **Nearest shipped exemplars:** Markdown preview supplies the in-place source
+ toggle and file identity behavior. The mobile HTML focused viewer supplies
+ the iframe toolbar, source recovery, and full-height composition. The
+ development-server preview keeps ownership of automatic Browser-panel use.
+- **Scroll ownership:** Browser content owns iframe scrolling. Kandev's focused
+ viewer owns its header and must not add page-level horizontal overflow.
+- **Shared behavior:** Eligibility, publication, errors, trust copy, proxy URL,
+ refresh, source recovery, and retry semantics are shared. Only toolbar
+ density and viewport composition differ.
+
+## Failure and recovery
+
+- Invalid paths, unsupported extensions, oversized buffers, and workspace-root
+ escapes return validation errors and do not start or update a preview.
+- A missing or stopped task session returns the existing session-unavailable
+ response. The UI preserves source and shows localized retry guidance.
+- An agentctl start or publish failure keeps the in-editor preview surface open
+ with localized retry and `Show code` actions. Retry republishes the latest
+ buffer.
+- A missing relative asset receives a normal HTTP 404 inside the preview so the
+ browser's native diagnostics remain meaningful.
+- A stale URL after agentctl restart is not restored as active preview state.
+ Republishing obtains the current ephemeral port and version.
+- The UI never falls back to `srcDoc`, QuickJS, a virtual DOM, or direct file
+ URLs when the server path fails.
+
+## Compatibility and migration
+
+Remove the QuickJS worker, virtual-DOM renderer, source-navigation normalizer,
+and their dependencies. Existing persisted `markdownPreview` compatibility only
+applies to Markdown. HTML in-place preview starts from source after reload and
+publishes only after the user activates it.
+
+The explicit development-server action remains unchanged. Native HTML preview
+is the zero-configuration static case, while the development-server path
+remains the correct choice for applications with build or backend behavior.
+
+## Observability
+
+Agentctl emits structured lifecycle logs for preview-server start, stop, and
+publish failures. Logs can include the canonical relative path, response status,
+and byte count, but not source content. Existing port-proxy logs and metrics
+continue to cover browser routing.
+
+## Verification strategy
+
+- Agentctl unit tests cover server start, reuse, shutdown, concurrent
+ publish/read, per-root ports, GET/HEAD/405 method handling, overlays, disk
+ assets, MIME types, no-store headers, path traversal, symlink escape, and
+ missing files.
+- Backend handler and client tests cover session authorization, agentctl
+ readiness, payload bounds, forwarding, typed 400/413/503 propagation,
+ malformed requests, unavailable sessions, malformed responses, and response
+ validation.
+- Frontend API and component tests cover unsaved-buffer publication, in-editor
+ source/preview toggling, explicit Browser-panel opening, versioned URLs,
+ trusted-code copy, errors, retry, refresh, source-state preservation, identity
+ reset, and stale publish completion guards.
+- Desktop Chromium E2E proves that an unsaved document runs native JavaScript
+ and loads relative CSS, JavaScript, and images. It also proves use of a native
+ browser API and refresh after a second publish.
+- Mobile Chrome E2E proves the same proxied content, 44-pixel action, full-height
+ containment, `Show code`, and source preservation.
+- Existing development-server and Browser-panel tests guard against regressions
+ in explicit server workflows and generic port-proxy behavior.
+
+## Open implementation risks
+
+- Every executor lifecycle path must own the agentctl preview server. This
+ ownership prevents ephemeral port leaks after stop or restart.
+- Root-relative and module subresource requests must retain the session proxy
+ capability cookie through redirects and rewritten URLs.
+- Multi-repository tasks must use the same repository identity and path helper
+ as existing workspace-file APIs.
+- Preview publication completion must remain scoped to the file and session
+ that initiated it so a stale response cannot replace another editor's body.
+
+## Related decisions
+
+- [ADR-2026-07-24-operator-owned-agent-launcher-settings](../../../decisions/2026-07-24-operator-owned-agent-launcher-settings.md)
+- [ADR-2026-09-05-trusted-browser-html-preview](../../../decisions/2026-09-05-trusted-browser-html-preview.md)