mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
fix(browser): extend existing-session manage timeouts
This commit is contained in:
@@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Discord/subagents: preserve thread-bound completion delivery by keeping the requester-agent announce path primary and falling back to direct thread sends only when the announce produces no visible output. (#71064) Thanks @DolencLuka.
|
||||
- Browser/tool: give Chrome MCP existing-session manage calls a longer default timeout, pass explicit tool timeouts through tab management, and recover stale selected-page MCP sessions instead of forcing a manual reset. Thanks @steipete.
|
||||
- Gateway/sessions: recover main-agent turns interrupted by a gateway restart from stale transcript-lock evidence, avoiding stuck `status: "running"` sessions without broad post-boot transcript scans. Fixes #70555. Thanks @bitloi.
|
||||
- Codex approvals: keep command approval responses within Codex app-server `availableDecisions`, including deny/cancel fallbacks for prompts that do not offer `decline`. (#71338) Thanks @Lucenx9.
|
||||
- Plugins/Google Meet: include live Chrome-node readiness in `googlemeet setup` and document the Parallels recovery checks, so stale node tokens or disconnected VM browsers are visible before an agent opens a meeting. Thanks @steipete.
|
||||
|
||||
@@ -209,19 +209,21 @@ function withRoleRefsFallback<T extends { refs?: "aria" | "role" }>(
|
||||
export async function executeTabsAction(params: {
|
||||
baseUrl?: string;
|
||||
profile?: string;
|
||||
timeoutMs?: number;
|
||||
proxyRequest: BrowserProxyRequest | null;
|
||||
}): Promise<AgentToolResult<unknown>> {
|
||||
const { baseUrl, profile, proxyRequest } = params;
|
||||
const { baseUrl, profile, timeoutMs, proxyRequest } = params;
|
||||
if (proxyRequest) {
|
||||
const result = await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/tabs",
|
||||
profile,
|
||||
timeoutMs,
|
||||
});
|
||||
const tabs = (result as { tabs?: unknown[] }).tabs ?? [];
|
||||
return formatTabsToolResult(tabs);
|
||||
}
|
||||
const tabs = await browserToolActionDeps.browserTabs(baseUrl, { profile });
|
||||
const tabs = await browserToolActionDeps.browserTabs(baseUrl, { profile, timeoutMs });
|
||||
return formatTabsToolResult(tabs);
|
||||
}
|
||||
|
||||
|
||||
@@ -379,7 +379,69 @@ describe("browser tool snapshot maxChars", () => {
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", { action: "profiles" });
|
||||
|
||||
expect(browserClientMocks.browserProfiles).toHaveBeenCalledWith(undefined);
|
||||
expect(browserClientMocks.browserProfiles).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({ timeoutMs: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a longer default timeout for existing-session profile status through node proxy", async () => {
|
||||
mockSingleBrowserProxyNode();
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", { action: "status", profile: "user", target: "node" });
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
{ timeoutMs: 50_000 },
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
method: "GET",
|
||||
path: "/",
|
||||
profile: "user",
|
||||
timeoutMs: 45_000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes top-level timeoutMs through to existing-session open", async () => {
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", {
|
||||
action: "open",
|
||||
profile: "user",
|
||||
url: "https://example.com",
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
|
||||
expect(browserClientMocks.browserOpenTab).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
"https://example.com",
|
||||
expect.objectContaining({ profile: "user", timeoutMs: 60_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes top-level timeoutMs through to close without targetId", async () => {
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", {
|
||||
action: "close",
|
||||
profile: "user",
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
|
||||
expect(browserActionsMocks.browserAct).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
{ kind: "close" },
|
||||
expect.objectContaining({ profile: "user", timeoutMs: 60_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes refs mode through to browser snapshot", async () => {
|
||||
@@ -750,7 +812,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
{ timeoutMs: 25000 },
|
||||
{ timeoutMs: 50_000 },
|
||||
expect.objectContaining({
|
||||
nodeId: "node-1",
|
||||
command: "browser.proxy",
|
||||
@@ -758,7 +820,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
profile: "user",
|
||||
path: "/",
|
||||
method: "GET",
|
||||
timeoutMs: 20000,
|
||||
timeoutMs: 45_000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -809,7 +871,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
{ timeoutMs: 25000 },
|
||||
{ timeoutMs: 50_000 },
|
||||
expect.objectContaining({
|
||||
nodeId: "node-1",
|
||||
command: "browser.proxy",
|
||||
@@ -817,6 +879,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
profile: "user",
|
||||
path: "/",
|
||||
method: "GET",
|
||||
timeoutMs: 45_000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -833,7 +896,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
{ timeoutMs: 25000 },
|
||||
{ timeoutMs: 50_000 },
|
||||
expect.objectContaining({
|
||||
nodeId: "node-1",
|
||||
command: "browser.proxy",
|
||||
@@ -841,6 +904,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
profile: "user",
|
||||
path: "/",
|
||||
method: "GET",
|
||||
timeoutMs: 45_000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -372,6 +372,43 @@ function shouldPreferHostForProfile(profileName: string | undefined) {
|
||||
return capabilities.usesChromeMcp;
|
||||
}
|
||||
|
||||
const DEFAULT_EXISTING_SESSION_MANAGE_TIMEOUT_MS = 45_000;
|
||||
const EXISTING_SESSION_MANAGE_ACTIONS = new Set([
|
||||
"status",
|
||||
"start",
|
||||
"stop",
|
||||
"profiles",
|
||||
"tabs",
|
||||
"open",
|
||||
"focus",
|
||||
"close",
|
||||
]);
|
||||
|
||||
function usesExistingSessionManageFlow(params: { action: string; profileName?: string }) {
|
||||
if (!EXISTING_SESSION_MANAGE_ACTIONS.has(params.action)) {
|
||||
return false;
|
||||
}
|
||||
const cfg = browserToolDeps.loadConfig();
|
||||
const resolved = resolveBrowserConfig(cfg.browser, cfg);
|
||||
const profile = resolveProfile(resolved, params.profileName ?? resolved.defaultProfile);
|
||||
if (profile && getBrowserProfileCapabilities(profile).usesChromeMcp) {
|
||||
return true;
|
||||
}
|
||||
if (params.action !== "profiles") {
|
||||
return false;
|
||||
}
|
||||
return Object.keys(resolved.profiles).some((name) => {
|
||||
const candidate = resolveProfile(resolved, name);
|
||||
return candidate ? getBrowserProfileCapabilities(candidate).usesChromeMcp : false;
|
||||
});
|
||||
}
|
||||
|
||||
function readToolTimeoutMs(params: Record<string, unknown>) {
|
||||
return typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||
? Math.max(1, Math.floor(params.timeoutMs))
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function createBrowserTool(opts?: {
|
||||
sandboxBridgeUrl?: string;
|
||||
allowHostControl?: boolean;
|
||||
@@ -402,6 +439,7 @@ export function createBrowserTool(opts?: {
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const profile = readStringParam(params, "profile");
|
||||
const requestedNode = readStringParam(params, "node");
|
||||
const requestedTimeoutMs = readToolTimeoutMs(params);
|
||||
let target = readStringParam(params, "target") as "sandbox" | "host" | "node" | undefined;
|
||||
const configuredNode = browserToolDeps.loadConfig().gateway?.nodes?.browser?.node?.trim();
|
||||
|
||||
@@ -469,6 +507,11 @@ export function createBrowserTool(opts?: {
|
||||
return proxy.result;
|
||||
}
|
||||
: null;
|
||||
const toolTimeoutMs =
|
||||
requestedTimeoutMs ??
|
||||
(usesExistingSessionManageFlow({ action, profileName: profile })
|
||||
? DEFAULT_EXISTING_SESSION_MANAGE_TIMEOUT_MS
|
||||
: undefined);
|
||||
|
||||
switch (action) {
|
||||
case "doctor":
|
||||
@@ -489,55 +532,74 @@ export function createBrowserTool(opts?: {
|
||||
method: "GET",
|
||||
path: "/",
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return jsonResult(await browserToolDeps.browserStatus(baseUrl, { profile }));
|
||||
return jsonResult(
|
||||
await browserToolDeps.browserStatus(baseUrl, { profile, timeoutMs: toolTimeoutMs }),
|
||||
);
|
||||
case "start":
|
||||
if (proxyRequest) {
|
||||
await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/start",
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
return jsonResult(
|
||||
await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/",
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await browserToolDeps.browserStart(baseUrl, { profile });
|
||||
return jsonResult(await browserToolDeps.browserStatus(baseUrl, { profile }));
|
||||
await browserToolDeps.browserStart(baseUrl, { profile, timeoutMs: toolTimeoutMs });
|
||||
return jsonResult(
|
||||
await browserToolDeps.browserStatus(baseUrl, { profile, timeoutMs: toolTimeoutMs }),
|
||||
);
|
||||
case "stop":
|
||||
if (proxyRequest) {
|
||||
await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/stop",
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
return jsonResult(
|
||||
await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/",
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await browserToolDeps.browserStop(baseUrl, { profile });
|
||||
return jsonResult(await browserToolDeps.browserStatus(baseUrl, { profile }));
|
||||
await browserToolDeps.browserStop(baseUrl, { profile, timeoutMs: toolTimeoutMs });
|
||||
return jsonResult(
|
||||
await browserToolDeps.browserStatus(baseUrl, { profile, timeoutMs: toolTimeoutMs }),
|
||||
);
|
||||
case "profiles":
|
||||
if (proxyRequest) {
|
||||
const result = await proxyRequest({
|
||||
method: "GET",
|
||||
path: "/profiles",
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
return jsonResult({ profiles: await browserToolDeps.browserProfiles(baseUrl) });
|
||||
return jsonResult({
|
||||
profiles: await browserToolDeps.browserProfiles(baseUrl, { timeoutMs: toolTimeoutMs }),
|
||||
});
|
||||
case "tabs":
|
||||
return await executeTabsAction({ baseUrl, profile, proxyRequest });
|
||||
return await executeTabsAction({
|
||||
baseUrl,
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
proxyRequest,
|
||||
});
|
||||
case "open": {
|
||||
const targetUrl = readTargetUrlParam(params);
|
||||
const label = normalizeOptionalString(params.label);
|
||||
@@ -547,12 +609,14 @@ export function createBrowserTool(opts?: {
|
||||
path: "/tabs/open",
|
||||
profile,
|
||||
body: { url: targetUrl, ...(label ? { label } : {}) },
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
const opened = await browserToolDeps.browserOpenTab(baseUrl, targetUrl, {
|
||||
profile,
|
||||
label,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
browserToolDeps.trackSessionBrowserTab({
|
||||
sessionKey: opts?.agentSessionKey,
|
||||
@@ -572,10 +636,14 @@ export function createBrowserTool(opts?: {
|
||||
path: "/tabs/focus",
|
||||
profile,
|
||||
body: { targetId },
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
await browserToolDeps.browserFocusTab(baseUrl, targetId, { profile });
|
||||
await browserToolDeps.browserFocusTab(baseUrl, targetId, {
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "close": {
|
||||
@@ -586,17 +654,22 @@ export function createBrowserTool(opts?: {
|
||||
method: "DELETE",
|
||||
path: `/tabs/${encodeURIComponent(targetId)}`,
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
})
|
||||
: await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/act",
|
||||
profile,
|
||||
body: { kind: "close" },
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
if (targetId) {
|
||||
await browserToolDeps.browserCloseTab(baseUrl, targetId, { profile });
|
||||
await browserToolDeps.browserCloseTab(baseUrl, targetId, {
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
});
|
||||
browserToolDeps.untrackSessionBrowserTab({
|
||||
sessionKey: opts?.agentSessionKey,
|
||||
targetId,
|
||||
@@ -604,7 +677,14 @@ export function createBrowserTool(opts?: {
|
||||
profile,
|
||||
});
|
||||
} else {
|
||||
await browserToolDeps.browserAct(baseUrl, { kind: "close" }, { profile });
|
||||
await browserToolDeps.browserAct(
|
||||
baseUrl,
|
||||
{ kind: "close" },
|
||||
{
|
||||
profile,
|
||||
timeoutMs: toolTimeoutMs,
|
||||
},
|
||||
);
|
||||
}
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
|
||||
@@ -157,14 +157,17 @@ export async function browserDownload(
|
||||
export async function browserAct(
|
||||
baseUrl: string | undefined,
|
||||
req: BrowserActRequest,
|
||||
opts?: { profile?: string },
|
||||
opts?: { profile?: string; timeoutMs?: number },
|
||||
): Promise<BrowserActResponse> {
|
||||
const q = buildProfileQuery(opts?.profile);
|
||||
return await fetchBrowserJson<BrowserActResponse>(withBaseUrl(baseUrl, `/act${q}`), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(req),
|
||||
timeoutMs: 20000,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 20000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,11 +69,14 @@ export type SnapshotResult =
|
||||
|
||||
export async function browserStatus(
|
||||
baseUrl?: string,
|
||||
opts?: { profile?: string },
|
||||
opts?: { profile?: string; timeoutMs?: number },
|
||||
): Promise<BrowserStatus> {
|
||||
const q = buildProfileQuery(opts?.profile);
|
||||
return await fetchBrowserJson<BrowserStatus>(withBaseUrl(baseUrl, `/${q}`), {
|
||||
timeoutMs: 1500,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 1500,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,29 +90,47 @@ export async function browserDoctor(
|
||||
});
|
||||
}
|
||||
|
||||
export async function browserProfiles(baseUrl?: string): Promise<ProfileStatus[]> {
|
||||
export async function browserProfiles(
|
||||
baseUrl?: string,
|
||||
opts?: { timeoutMs?: number },
|
||||
): Promise<ProfileStatus[]> {
|
||||
const res = await fetchBrowserJson<{ profiles: ProfileStatus[] }>(
|
||||
withBaseUrl(baseUrl, `/profiles`),
|
||||
{
|
||||
timeoutMs: 3000,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 3000,
|
||||
},
|
||||
);
|
||||
return res.profiles ?? [];
|
||||
}
|
||||
|
||||
export async function browserStart(baseUrl?: string, opts?: { profile?: string }): Promise<void> {
|
||||
export async function browserStart(
|
||||
baseUrl?: string,
|
||||
opts?: { profile?: string; timeoutMs?: number },
|
||||
): Promise<void> {
|
||||
const q = buildProfileQuery(opts?.profile);
|
||||
await fetchBrowserJson(withBaseUrl(baseUrl, `/start${q}`), {
|
||||
method: "POST",
|
||||
timeoutMs: 15000,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function browserStop(baseUrl?: string, opts?: { profile?: string }): Promise<void> {
|
||||
export async function browserStop(
|
||||
baseUrl?: string,
|
||||
opts?: { profile?: string; timeoutMs?: number },
|
||||
): Promise<void> {
|
||||
const q = buildProfileQuery(opts?.profile);
|
||||
await fetchBrowserJson(withBaseUrl(baseUrl, `/stop${q}`), {
|
||||
method: "POST",
|
||||
timeoutMs: 15000,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,12 +207,17 @@ export async function browserDeleteProfile(
|
||||
|
||||
export async function browserTabs(
|
||||
baseUrl?: string,
|
||||
opts?: { profile?: string },
|
||||
opts?: { profile?: string; timeoutMs?: number },
|
||||
): Promise<BrowserTab[]> {
|
||||
const q = buildProfileQuery(opts?.profile);
|
||||
const res = await fetchBrowserJson<{ running: boolean; tabs: BrowserTab[] }>(
|
||||
withBaseUrl(baseUrl, `/tabs${q}`),
|
||||
{ timeoutMs: 3000 },
|
||||
{
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 3000,
|
||||
},
|
||||
);
|
||||
return res.tabs ?? [];
|
||||
}
|
||||
@@ -199,40 +225,49 @@ export async function browserTabs(
|
||||
export async function browserOpenTab(
|
||||
baseUrl: string | undefined,
|
||||
url: string,
|
||||
opts?: { profile?: string; label?: string },
|
||||
opts?: { profile?: string; label?: string; timeoutMs?: number },
|
||||
): Promise<BrowserTab> {
|
||||
const q = buildProfileQuery(opts?.profile);
|
||||
return await fetchBrowserJson<BrowserTab>(withBaseUrl(baseUrl, `/tabs/open${q}`), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url, ...(opts?.label ? { label: opts.label } : {}) }),
|
||||
timeoutMs: 15000,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function browserFocusTab(
|
||||
baseUrl: string | undefined,
|
||||
targetId: string,
|
||||
opts?: { profile?: string },
|
||||
opts?: { profile?: string; timeoutMs?: number },
|
||||
): Promise<void> {
|
||||
const q = buildProfileQuery(opts?.profile);
|
||||
await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/focus${q}`), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ targetId }),
|
||||
timeoutMs: 5000,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function browserCloseTab(
|
||||
baseUrl: string | undefined,
|
||||
targetId: string,
|
||||
opts?: { profile?: string },
|
||||
opts?: { profile?: string; timeoutMs?: number },
|
||||
): Promise<void> {
|
||||
const q = buildProfileQuery(opts?.profile);
|
||||
await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/${encodeURIComponent(targetId)}${q}`), {
|
||||
method: "DELETE",
|
||||
timeoutMs: 5000,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user