From 87e900bb64094b6fa365d47333c30b926ed62ce6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 31 Jul 2026 19:51:17 -0700 Subject: [PATCH 1/2] fix(teams): restore the official square product icon (#117170) Co-authored-by: Peter Steinberger --- extensions/teams-meetings/openclaw.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/teams-meetings/openclaw.plugin.json b/extensions/teams-meetings/openclaw.plugin.json index 62412c1c185c..425bd7a7fc69 100644 --- a/extensions/teams-meetings/openclaw.plugin.json +++ b/extensions/teams-meetings/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "teams-meetings", "name": "Microsoft Teams meetings", "description": "Join Microsoft Teams meetings as a Chrome browser guest.", - "icon": "https://cdn.simpleicons.org/microsoftteams", + "icon": "https://res.cdn.office.net/files/fabric-cdn-prod_20230815.001/assets/brand-icons/product/svg/teams_48x1.svg", "enabledByDefault": true, "commandAliases": [{ "name": "teamsmeetings" }], "activation": { From 4a27e2d3aab5d27cb63c1d6affdb01011b09fd62 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 31 Jul 2026 19:55:29 -0700 Subject: [PATCH 2/2] fix(ui): keep canvas lease renewals lifecycle-owned and timer-safe (#117173) Co-authored-by: Peter Steinberger --- ui/src/app/canvas-surface-lease.runtime.ts | 27 ++++--- ui/src/app/canvas-surface-lease.test.ts | 90 ++++++++++++++++++++++ 2 files changed, 108 insertions(+), 9 deletions(-) diff --git a/ui/src/app/canvas-surface-lease.runtime.ts b/ui/src/app/canvas-surface-lease.runtime.ts index f4880e42b4af..c030b493a9a1 100644 --- a/ui/src/app/canvas-surface-lease.runtime.ts +++ b/ui/src/app/canvas-surface-lease.runtime.ts @@ -1,4 +1,6 @@ // Loaded after hello so capability renewal does not inflate the startup chunk. +import { resolveSafeTimeoutDelayMs } from "@openclaw/gateway-client/browser"; + const RENEWAL_LEAD_MS = 15_000; const MIN_RENEWAL_DELAY_MS = 1_000; // Used only when a refresh response omits expiry; an early one-minute refresh @@ -45,6 +47,7 @@ export function createCanvasSurfaceLease< let consecutiveFailures = 0; let generation = 0; let started = false; + const ownsGeneration = (expected: number) => started && generation === expected; const clearScheduledRenewal = () => { if (timer !== null) { @@ -54,17 +57,24 @@ export function createCanvasSurfaceLease< }; const schedule = (delayMs: number, expectedGeneration: number) => { + if (!ownsGeneration(expectedGeneration)) { + return; + } clearScheduledRenewal(); - timer = setTimer(() => { - timer = null; - if (started && generation === expectedGeneration) { + timer = setTimer( + () => { + if (!ownsGeneration(expectedGeneration)) { + return; + } + timer = null; renew(expectedGeneration); - } - }, delayMs); + }, + resolveSafeTimeoutDelayMs(delayMs, { minMs: MIN_RENEWAL_DELAY_MS }), + ); }; const handleFailure = (expectedGeneration: number) => { - if (!started || generation !== expectedGeneration) { + if (!ownsGeneration(expectedGeneration)) { return; } consecutiveFailures += 1; @@ -75,8 +85,7 @@ export function createCanvasSurfaceLease< const renew = (expectedGeneration: number) => { if ( inFlight?.generation === expectedGeneration || - !started || - generation !== expectedGeneration || + !ownsGeneration(expectedGeneration) || !currentUrl ) { return; @@ -85,7 +94,7 @@ export function createCanvasSurfaceLease< const request = Promise.resolve() .then(() => params.request("plugin.surface.refresh", { surface: "canvas", observedUrl })) .then((response) => { - if (!started || generation !== expectedGeneration || currentUrl !== observedUrl) { + if (!ownsGeneration(expectedGeneration) || currentUrl !== observedUrl) { return; } const refreshed = parseCanvasSurfaceRefresh(response); diff --git a/ui/src/app/canvas-surface-lease.test.ts b/ui/src/app/canvas-surface-lease.test.ts index 41caedac674d..56f72c38ab9e 100644 --- a/ui/src/app/canvas-surface-lease.test.ts +++ b/ui/src/app/canvas-surface-lease.test.ts @@ -185,6 +185,96 @@ describe("createCanvasSurfaceLease", () => { expect(clock.nextDelayMs).toBe(60_000); }); + it.each(["stops", "reconnects without a canvas"] as const)( + "does not schedule a retired generation when publishing a refreshed URL %s", + async (transition) => { + const clock = new FakeClock(); + const changes: Array = []; + const request = vi.fn(async () => ({ + surface: "canvas", + pluginSurfaceUrls: { canvas: "https://canvas.test/__openclaw__/cap/refreshed" }, + expiresAtMs: 120_000, + })); + const lease = createCanvasSurfaceLease({ + request, + onChange: (url) => { + changes.push(url); + if (url === "https://canvas.test/__openclaw__/cap/refreshed") { + if (transition === "stops") { + lease.stop(); + } else { + lease.start(undefined); + } + } + }, + now: clock.now, + setTimer: clock.setTimer, + clearTimer: clock.clearTimer, + }); + + lease.start("https://canvas.test/__openclaw__/cap/original"); + await flushPromises(); + + expect(request).toHaveBeenCalledOnce(); + expect(changes).toEqual([ + "https://canvas.test/__openclaw__/cap/original", + "https://canvas.test/__openclaw__/cap/refreshed", + null, + ]); + expect(clock.pendingCount).toBe(0); + }, + ); + + it("keeps the reconnect renewal owned when a retired timer callback arrives late", async () => { + const request = vi + .fn<(method: string, params: unknown) => Promise>() + .mockResolvedValueOnce({ + surface: "canvas", + pluginSurfaceUrls: { canvas: "https://canvas.test/__openclaw__/cap/first" }, + expiresAtMs: 120_000, + }) + .mockResolvedValueOnce({ + surface: "canvas", + pluginSurfaceUrls: { canvas: "https://canvas.test/__openclaw__/cap/reconnected" }, + expiresAtMs: 180_000, + }); + const { clock, lease } = createLeaseHarness(request); + + lease.start("https://canvas.test/__openclaw__/cap/original"); + await flushPromises(); + const retiredCallback = clock.takeNextCallback(); + expect(retiredCallback).toBeDefined(); + + lease.stop(); + lease.start("https://canvas.test/__openclaw__/cap/reconnect"); + await flushPromises(); + expect(clock.pendingCount).toBe(1); + + retiredCallback?.(); + expect(request).toHaveBeenCalledTimes(2); + lease.stop(); + + expect(clock.pendingCount).toBe(0); + }); + + it.each([Number.MAX_SAFE_INTEGER, 2 ** 32 + 115_000])( + "clamps an advertised canvas expiry of %d to a browser-safe native timer", + async (expiresAtMs) => { + const request = vi.fn(async () => ({ + surface: "canvas", + pluginSurfaceUrls: { canvas: "https://canvas.test/__openclaw__/cap/refreshed" }, + expiresAtMs, + })); + const { clock, lease } = createLeaseHarness(request); + + lease.start("https://canvas.test/__openclaw__/cap/original"); + await flushPromises(); + + expect(clock.nextDelayMs).toBe(2_147_483_647); + lease.stop(); + }, + ); + it("stop clears timers, ignores an in-flight result, and publishes null once", async () => { const pending = deferred(); const { changes, clock, connectionChanges, lease } = createLeaseHarness(() => pending.promise);