mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(ui): refresh Browser when sidebar tab activates (#126058)
This commit is contained in:
committed by
GitHub
parent
a8bc60a119
commit
d1f327a4e4
@@ -120,6 +120,8 @@ export class BrowserPanelController implements ReactiveController {
|
||||
this.input.resetCaptureState();
|
||||
this.setState("inspected", null);
|
||||
this.setState("inspectPointer", null);
|
||||
this.urlDraftEditing = false;
|
||||
this.setState("urlDraft", "");
|
||||
this.setState("pendingNewTab", false);
|
||||
// Re-probe per connection: another gateway may have evaluate enabled.
|
||||
this.setState("evaluateUnavailable", false);
|
||||
|
||||
@@ -207,9 +207,10 @@ describe("normalizeBrowserUrlDraft", () => {
|
||||
expect(panel.browserPanelIsOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("treats an embedded panel as open while the side panel owns visibility", async () => {
|
||||
it("treats an embedded panel as open only while it is presented", async () => {
|
||||
const panel = document.createElement("openclaw-browser-panel") as unknown as HTMLElement & {
|
||||
embedded: boolean;
|
||||
presented: boolean;
|
||||
browserPanelIsOpen: () => boolean;
|
||||
updateComplete: Promise<unknown>;
|
||||
};
|
||||
@@ -217,19 +218,27 @@ describe("normalizeBrowserUrlDraft", () => {
|
||||
document.body.append(panel);
|
||||
await panel.updateComplete;
|
||||
|
||||
expect(panel.browserPanelIsOpen()).toBe(false);
|
||||
panel.presented = true;
|
||||
await panel.updateComplete;
|
||||
expect(panel.browserPanelIsOpen()).toBe(true);
|
||||
panel.presented = false;
|
||||
await panel.updateComplete;
|
||||
expect(panel.browserPanelIsOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("starts a fresh browser tab draft when an embedded panel receives a new-tab request", async () => {
|
||||
const panel = document.createElement("openclaw-browser-panel") as unknown as HTMLElement & {
|
||||
available: boolean;
|
||||
embedded: boolean;
|
||||
presented: boolean;
|
||||
handleToggleRequest: (event: Event) => void;
|
||||
renderRoot: ShadowRoot;
|
||||
updateComplete: Promise<unknown>;
|
||||
};
|
||||
panel.available = true;
|
||||
panel.embedded = true;
|
||||
panel.presented = true;
|
||||
document.body.append(panel);
|
||||
await panel.updateComplete;
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ class OpenClawBrowserPanel extends OpenClawLitElement implements BrowserPanelCon
|
||||
@property({ attribute: false }) authToken: string | null = null;
|
||||
/** Hosted by the chat side panel, which owns visibility and geometry. */
|
||||
@property({ type: Boolean }) embedded = false;
|
||||
/** This embedded instance is the active pane's visible Browser presenter. */
|
||||
@property({ type: Boolean }) presented = false;
|
||||
|
||||
private readonly browserPanelController = new BrowserPanelController(this);
|
||||
private readonly dockLayout = new DockLayoutController(this, {
|
||||
@@ -59,7 +61,6 @@ class OpenClawBrowserPanel extends OpenClawLitElement implements BrowserPanelCon
|
||||
isAvailable: () => this.available,
|
||||
});
|
||||
private readonly onToggleRequest = (event: Event) => this.handleToggleRequest(event);
|
||||
private embeddedRefreshTimer: number | null = null;
|
||||
private viewportResizeObserver: ResizeObserver | null = null;
|
||||
private observedViewportElement: Element | null = null;
|
||||
|
||||
@@ -78,13 +79,12 @@ class OpenClawBrowserPanel extends OpenClawLitElement implements BrowserPanelCon
|
||||
// A settings takeover can already own the viewport when the panel mounts.
|
||||
// Suppress before the restored open state refreshes a dock nobody can see.
|
||||
this.dockLayout.setSuppressed(this.suppressed);
|
||||
if (this.dockLayout.open) {
|
||||
if (!this.embedded && this.dockLayout.open) {
|
||||
void this.browserPanelController.refreshAll();
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
this.clearEmbeddedRefresh();
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(BROWSER_PANEL_TOGGLE_EVENT, this.onToggleRequest);
|
||||
this.viewportResizeObserver?.disconnect();
|
||||
@@ -100,18 +100,26 @@ class OpenClawBrowserPanel extends OpenClawLitElement implements BrowserPanelCon
|
||||
window.addEventListener(BROWSER_PANEL_TOGGLE_EVENT, this.onToggleRequest);
|
||||
}
|
||||
}
|
||||
if (changed.has("suppressed") && this.dockLayout.setSuppressed(this.suppressed)) {
|
||||
if (
|
||||
changed.has("suppressed") &&
|
||||
this.dockLayout.setSuppressed(this.suppressed) &&
|
||||
this.browserPanelIsOpen()
|
||||
) {
|
||||
void this.browserPanelController.refreshAll();
|
||||
}
|
||||
const gatewayAvailabilityChanged = changed.has("client") || changed.has("available");
|
||||
const presentationChanged =
|
||||
this.embedded && (changed.has("embedded") || changed.has("presented"));
|
||||
const refreshedForClientChange = this.browserPanelController.synchronizeHostProperties(changed);
|
||||
if (
|
||||
this.embedded &&
|
||||
this.available &&
|
||||
!refreshedForClientChange &&
|
||||
(changed.has("embedded") || changed.has("client") || changed.has("available"))
|
||||
) {
|
||||
this.scheduleEmbeddedRefresh();
|
||||
} else if (changed.has("client") || changed.has("available")) {
|
||||
if (this.embedded) {
|
||||
if (!this.presented || !this.available || !this.client) {
|
||||
if (presentationChanged || gatewayAvailabilityChanged) {
|
||||
this.browserPanelController.resetBrowserState();
|
||||
}
|
||||
} else if (!refreshedForClientChange && (presentationChanged || gatewayAvailabilityChanged)) {
|
||||
void this.browserPanelController.refreshAll();
|
||||
}
|
||||
} else if (gatewayAvailabilityChanged) {
|
||||
if (!this.available && this.dockLayout.open) {
|
||||
// Surface disappeared (disconnect/scope loss): hide without persisting
|
||||
// so the open preference survives a reconnect.
|
||||
@@ -146,27 +154,7 @@ class OpenClawBrowserPanel extends OpenClawLitElement implements BrowserPanelCon
|
||||
}
|
||||
|
||||
browserPanelIsOpen(): boolean {
|
||||
return this.embedded || this.dockLayout.open;
|
||||
}
|
||||
|
||||
private scheduleEmbeddedRefresh(): void {
|
||||
if (this.embeddedRefreshTimer !== null) {
|
||||
return;
|
||||
}
|
||||
this.embeddedRefreshTimer = window.setTimeout(() => {
|
||||
this.embeddedRefreshTimer = null;
|
||||
if (this.isConnected && this.embedded && this.available) {
|
||||
void this.browserPanelController.refreshAll();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private clearEmbeddedRefresh(): void {
|
||||
if (this.embeddedRefreshTimer === null) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.embeddedRefreshTimer);
|
||||
this.embeddedRefreshTimer = null;
|
||||
return this.embedded ? this.presented : this.dockLayout.open;
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
@@ -182,13 +170,12 @@ class OpenClawBrowserPanel extends OpenClawLitElement implements BrowserPanelCon
|
||||
}
|
||||
|
||||
handleToggleRequest(event: Event): void {
|
||||
this.clearEmbeddedRefresh();
|
||||
const detail =
|
||||
event instanceof CustomEvent && typeof event.detail === "object" && event.detail !== null
|
||||
? (event.detail as BrowserPanelToggleDetail)
|
||||
: null;
|
||||
if (this.embedded) {
|
||||
if (detail?.open === false || !this.available) {
|
||||
if (!this.presented || detail?.open === false || !this.available) {
|
||||
return;
|
||||
}
|
||||
const normalizedRequestedUrl =
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
installMockGateway,
|
||||
type ControlUiMockGatewayScenario,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
import { openChatSidePanelType } from "./chat-side-panel.test-support.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({
|
||||
@@ -338,6 +339,109 @@ suite.define(() => {
|
||||
}
|
||||
});
|
||||
|
||||
it("refreshes retained Browser state when its sidebar tab becomes active", async () => {
|
||||
const context = await suite.newBrowserContext({ serviceWorkers: "block" });
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
await page.route("**/__openclaw__/assistant-media?*", (route) =>
|
||||
route.fulfill({ body: ONE_PIXEL_PNG, contentType: "image/png" }),
|
||||
);
|
||||
const gateway = await installMockGateway(page, {
|
||||
featureMethods: ["browser.request", "chat.metadata", "chat.startup"],
|
||||
methodResponses: {
|
||||
"browser.request": {
|
||||
cases: [
|
||||
{
|
||||
match: { method: "GET", path: "/tabs" },
|
||||
response: { running: true, tabs: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
await waitForControlUiGatewayReady(page);
|
||||
await openChatSidePanelType(page, "Browser");
|
||||
const browser = page.locator("openclaw-browser-panel");
|
||||
await browser.locator("openclaw-panel-empty-state").waitFor();
|
||||
|
||||
const initialRequests = await gateway.getRequests("browser.request");
|
||||
expect(initialRequests.map((request) => request.params)).toEqual([
|
||||
{ method: "GET", path: "/tabs" },
|
||||
]);
|
||||
|
||||
await openChatSidePanelType(page, "Files");
|
||||
expect(await browser.evaluate((element) => element.isConnected)).toBe(true);
|
||||
const hiddenRequestCount = (await gateway.getRequests("browser.request")).length;
|
||||
await gateway.setMethodResponse("browser.request", {
|
||||
cases: [
|
||||
{
|
||||
match: { method: "GET", path: "/tabs" },
|
||||
response: {
|
||||
running: true,
|
||||
tabs: [
|
||||
{
|
||||
targetId: "blacksmith-target",
|
||||
tabId: "blacksmith-tab",
|
||||
title: "Blacksmith",
|
||||
url: "https://blacksmith.sh/",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { method: "POST", path: "/screenshot" },
|
||||
response: {
|
||||
path: "/proof/blacksmith.png",
|
||||
targetId: "blacksmith-target",
|
||||
url: "https://blacksmith.sh/",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
}),
|
||||
);
|
||||
expect((await gateway.getRequests("browser.request")).length).toBe(hiddenRequestCount);
|
||||
|
||||
await page
|
||||
.locator(".side-panel__header .tabstrip-tab")
|
||||
.filter({ hasText: "Browser" })
|
||||
.click();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const requests = await gateway.getRequests("browser.request");
|
||||
return requests.filter((request) => {
|
||||
const params = request.params as { method?: string; path?: string };
|
||||
return params.method === "GET" && params.path === "/tabs";
|
||||
}).length;
|
||||
})
|
||||
.toBe(2);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
(await gateway.getRequests("browser.request")).map((request) => request.params),
|
||||
)
|
||||
.toContainEqual({
|
||||
body: { targetId: "blacksmith-tab", type: "png" },
|
||||
method: "POST",
|
||||
path: "/screenshot",
|
||||
});
|
||||
|
||||
await browser.locator(".bp-shot").waitFor();
|
||||
expect(await browser.locator(".bp-shot").getAttribute("src")).toMatch(
|
||||
/^data:image\/png;base64,/,
|
||||
);
|
||||
expect(await browser.locator(".bp-url").inputValue()).toBe("https://blacksmith.sh/");
|
||||
expect(await browser.locator(".bp-loading").count()).toBe(0);
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves the production header-action shapes for Side chat and Discussion", async () => {
|
||||
const context = await suite.newBrowserContext({ serviceWorkers: "block" });
|
||||
const page = await context.newPage();
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { SidebarSlotId } from "./sidebar-layout-types.ts";
|
||||
type SidebarPanelDefinitionParams = {
|
||||
state: ChatPageHost;
|
||||
agentId: string | null;
|
||||
browserPresented: boolean;
|
||||
desktopAvailable: boolean;
|
||||
hasBoard: boolean;
|
||||
chat: TemplateResult;
|
||||
@@ -100,6 +101,7 @@ export function sidebarPanelDefinitions(
|
||||
data-chat-autotype-exempt
|
||||
.client=${state.connected ? state.client : null}
|
||||
.available=${state.browserPanelAvailable}
|
||||
.presented=${params?.browserPresented ?? false}
|
||||
.basePath=${state.basePath}
|
||||
.authToken=${resolveAssistantAttachmentAuthToken(state)}
|
||||
></openclaw-browser-panel>`
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import type { SidebarFullMessageLoader } from "./components/chat-sidebar.ts";
|
||||
import {
|
||||
SIDEBAR_NARROW_BREAKPOINT_PX,
|
||||
isSidebarSlotVisible,
|
||||
type SidebarLayout,
|
||||
type SidebarSlotId,
|
||||
} from "./sidebar-layout.ts";
|
||||
@@ -89,9 +90,12 @@ export abstract class ChatPaneLayoutRender extends ChatPaneBrowserAnnotationRend
|
||||
const discussion = this.buildSessionDiscussionPanel(state, state.sessionKey.trim());
|
||||
const desktopAvailable = isDesktopPanelAvailable(this.context.gateway.snapshot);
|
||||
const companionThread = this.sessionCompanionThreads.view(state.sessionKey, currentAgentId);
|
||||
const browserPresented =
|
||||
this.active && this.presented && isSidebarSlotVisible(sidebarLayout, "browser");
|
||||
const panelDefinitions = sidebarPanelDefinitions({
|
||||
state,
|
||||
agentId: currentAgentId,
|
||||
browserPresented,
|
||||
desktopAvailable,
|
||||
hasBoard: board.hasBoard,
|
||||
chat,
|
||||
|
||||
Reference in New Issue
Block a user