From ae643c9e8a7e999e168f00d72dd0713996f0e6d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:44:00 +0000 Subject: [PATCH 1/7] Initial plan From 577ed4462de0b864fb5c656fb39e6765d49985df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:51:44 +0000 Subject: [PATCH 2/7] Add throttled renderer set-is-active activity ping Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- emain/emain-ipc.ts | 5 ++++ emain/preload.ts | 1 + frontend/app/app.tsx | 8 +++-- frontend/app/store/global-model.test.ts | 40 +++++++++++++++++++++++++ frontend/app/store/global-model.ts | 14 ++++++++- frontend/types/custom.d.ts | 1 + 6 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 frontend/app/store/global-model.test.ts diff --git a/emain/emain-ipc.ts b/emain/emain-ipc.ts index aaf3736431..72498f15c1 100644 --- a/emain/emain-ipc.ts +++ b/emain/emain-ipc.ts @@ -17,6 +17,7 @@ import { incrementTermCommandsRemote, incrementTermCommandsRun, incrementTermCommandsWsl, + setWasActive, } from "./emain-activity"; import { createBuilderWindow, getAllBuilderWindows, getBuilderWindowByWebContentsId } from "./emain-builder"; import { callWithOriginalXdgCurrentDesktopAsync, unamePlatform } from "./emain-platform"; @@ -317,6 +318,10 @@ export function initIpcHandlers() { tabView?.setKeyboardChordMode(true); }); + electron.ipcMain.handle("set-is-active", () => { + setWasActive(true); + }); + const fac = new FastAverageColor(); electron.ipcMain.on("update-window-controls-overlay", async (event, rect: Dimensions) => { if (unamePlatform === "darwin") return; diff --git a/emain/preload.ts b/emain/preload.ts index 7acdf2e73a..823f99c4cd 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -71,6 +71,7 @@ contextBridge.exposeInMainWorld("api", { setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId), doRefresh: () => ipcRenderer.send("do-refresh"), saveTextFile: (fileName: string, content: string) => ipcRenderer.invoke("save-text-file", fileName, content), + setIsActive: () => ipcRenderer.invoke("set-is-active"), }); // Custom event for "new-window" diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 76ad557516..a9c093cd51 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -200,12 +200,16 @@ function AppFocusHandler() { const AppKeyHandlers = () => { useEffect(() => { const staticKeyDownHandler = keyutil.keydownWrapper(appHandleKeyDown); + const staticMouseDownHandler = (e: MouseEvent) => { + keyboardMouseDownHandler(e); + void GlobalModel.getInstance().setIsActive(); + }; document.addEventListener("keydown", staticKeyDownHandler); - document.addEventListener("mousedown", keyboardMouseDownHandler); + document.addEventListener("mousedown", staticMouseDownHandler); return () => { document.removeEventListener("keydown", staticKeyDownHandler); - document.removeEventListener("mousedown", keyboardMouseDownHandler); + document.removeEventListener("mousedown", staticMouseDownHandler); }; }, []); return null; diff --git a/frontend/app/store/global-model.test.ts b/frontend/app/store/global-model.test.ts new file mode 100644 index 0000000000..9557f339f6 --- /dev/null +++ b/frontend/app/store/global-model.test.ts @@ -0,0 +1,40 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { GlobalModel } from "./global-model"; + +const { setIsActiveMock } = vi.hoisted(() => ({ + setIsActiveMock: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("@/store/global", () => ({ + getApi: () => ({ + setIsActive: setIsActiveMock, + }), +})); + +describe("GlobalModel", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-01T00:00:00Z")); + vi.clearAllMocks(); + (GlobalModel as any).instance = null; + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("throttles setIsActive to once every 5 seconds", async () => { + const model = GlobalModel.getInstance(); + + await model.setIsActive(); + await model.setIsActive(); + expect(setIsActiveMock).toHaveBeenCalledTimes(1); + + vi.setSystemTime(new Date(Date.now() + 5000)); + await model.setIsActive(); + expect(setIsActiveMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/frontend/app/store/global-model.ts b/frontend/app/store/global-model.ts index 804e3a18f6..bf7e697b9e 100644 --- a/frontend/app/store/global-model.ts +++ b/frontend/app/store/global-model.ts @@ -3,14 +3,17 @@ import * as WOS from "@/app/store/wos"; import { ClientModel } from "@/app/store/client-model"; +import { getApi } from "@/store/global"; import { atom, Atom } from "jotai"; class GlobalModel { private static instance: GlobalModel; + private static readonly IsActiveThrottleMs = 5000; windowId: string; builderId: string; platform: NodeJS.Platform; + private lastSetIsActiveTs = 0; windowDataAtom!: Atom; workspaceAtom!: Atom; @@ -47,6 +50,15 @@ class GlobalModel { return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get); }); } + + async setIsActive(): Promise { + const now = Date.now(); + if (now - this.lastSetIsActiveTs < GlobalModel.IsActiveThrottleMs) { + return; + } + this.lastSetIsActiveTs = now; + await getApi().setIsActive(); + } } -export { GlobalModel }; \ No newline at end of file +export { GlobalModel }; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 74180391cc..25c40eefef 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -132,6 +132,7 @@ declare global { setBuilderWindowAppId: (appId: string) => void; // set-builder-window-appid doRefresh: () => void; // do-refresh saveTextFile: (fileName: string, content: string) => Promise; // save-text-file + setIsActive: () => Promise; // set-is-active }; type ElectronContextMenuItem = { From 7d0281d4021352692d46f391362f7ff5d60667d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:32:42 +0000 Subject: [PATCH 3/7] Remove private model fields and drop throttle test Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/app/store/global-model.test.ts | 40 ------------------------- frontend/app/store/global-model.ts | 4 +-- 2 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 frontend/app/store/global-model.test.ts diff --git a/frontend/app/store/global-model.test.ts b/frontend/app/store/global-model.test.ts deleted file mode 100644 index 9557f339f6..0000000000 --- a/frontend/app/store/global-model.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { GlobalModel } from "./global-model"; - -const { setIsActiveMock } = vi.hoisted(() => ({ - setIsActiveMock: vi.fn().mockResolvedValue(undefined), -})); - -vi.mock("@/store/global", () => ({ - getApi: () => ({ - setIsActive: setIsActiveMock, - }), -})); - -describe("GlobalModel", () => { - beforeEach(() => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-01-01T00:00:00Z")); - vi.clearAllMocks(); - (GlobalModel as any).instance = null; - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it("throttles setIsActive to once every 5 seconds", async () => { - const model = GlobalModel.getInstance(); - - await model.setIsActive(); - await model.setIsActive(); - expect(setIsActiveMock).toHaveBeenCalledTimes(1); - - vi.setSystemTime(new Date(Date.now() + 5000)); - await model.setIsActive(); - expect(setIsActiveMock).toHaveBeenCalledTimes(2); - }); -}); diff --git a/frontend/app/store/global-model.ts b/frontend/app/store/global-model.ts index bf7e697b9e..baa52c79cc 100644 --- a/frontend/app/store/global-model.ts +++ b/frontend/app/store/global-model.ts @@ -8,12 +8,12 @@ import { atom, Atom } from "jotai"; class GlobalModel { private static instance: GlobalModel; - private static readonly IsActiveThrottleMs = 5000; + static readonly IsActiveThrottleMs = 5000; windowId: string; builderId: string; platform: NodeJS.Platform; - private lastSetIsActiveTs = 0; + lastSetIsActiveTs = 0; windowDataAtom!: Atom; workspaceAtom!: Atom; From 2ee337c17dd656553750a6850d0053f6a98195a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:52:13 +0000 Subject: [PATCH 4/7] Use fireAndForget for mousedown and swallow setIsActive telemetry errors Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/app/app.tsx | 2 +- frontend/app/store/global-model.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index a9c093cd51..574f7fce88 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -202,7 +202,7 @@ const AppKeyHandlers = () => { const staticKeyDownHandler = keyutil.keydownWrapper(appHandleKeyDown); const staticMouseDownHandler = (e: MouseEvent) => { keyboardMouseDownHandler(e); - void GlobalModel.getInstance().setIsActive(); + util.fireAndForget(() => GlobalModel.getInstance().setIsActive()); }; document.addEventListener("keydown", staticKeyDownHandler); document.addEventListener("mousedown", staticMouseDownHandler); diff --git a/frontend/app/store/global-model.ts b/frontend/app/store/global-model.ts index baa52c79cc..38c65d90d0 100644 --- a/frontend/app/store/global-model.ts +++ b/frontend/app/store/global-model.ts @@ -57,7 +57,11 @@ class GlobalModel { return; } this.lastSetIsActiveTs = now; - await getApi().setIsActive(); + try { + await getApi().setIsActive(); + } catch (e) { + console.log("setIsActive error", e); + } } } From f015b8b8e95624bd5d52a2315aff3ffac45a61b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:23:44 +0000 Subject: [PATCH 5/7] Restructure setIsActive to sync wrapper with internal fireAndForget Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/app/app.tsx | 2 +- frontend/app/store/global-model.test.ts | 66 +++++++++++++++++++++++++ frontend/app/store/global-model.ts | 15 +++--- 3 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 frontend/app/store/global-model.test.ts diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 574f7fce88..0970b476a1 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -202,7 +202,7 @@ const AppKeyHandlers = () => { const staticKeyDownHandler = keyutil.keydownWrapper(appHandleKeyDown); const staticMouseDownHandler = (e: MouseEvent) => { keyboardMouseDownHandler(e); - util.fireAndForget(() => GlobalModel.getInstance().setIsActive()); + GlobalModel.getInstance().setIsActive(); }; document.addEventListener("keydown", staticKeyDownHandler); document.addEventListener("mousedown", staticMouseDownHandler); diff --git a/frontend/app/store/global-model.test.ts b/frontend/app/store/global-model.test.ts new file mode 100644 index 0000000000..b1a3f0b166 --- /dev/null +++ b/frontend/app/store/global-model.test.ts @@ -0,0 +1,66 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; + +describe("GlobalModel.setIsActive", () => { + it("calls fireAndForget once and throttles repeated mousedown activity", async () => { + const setIsActive = vi.fn().mockResolvedValue(undefined); + const fireAndForget = vi.fn((f: () => Promise) => { + void f(); + }); + + vi.resetModules(); + vi.doMock("@/store/global", () => ({ + getApi: () => ({ setIsActive }), + })); + vi.doMock("@/util/util", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fireAndForget, + }; + }); + + const { GlobalModel } = await import("./global-model"); + const model = GlobalModel.getInstance(); + + const result = model.setIsActive(); + model.setIsActive(); + + expect(result).toBeUndefined(); + expect(fireAndForget).toHaveBeenCalledTimes(1); + expect(setIsActive).toHaveBeenCalledTimes(1); + }); + + it("logs and swallows setIsActive telemetry errors", async () => { + const error = new Error("telemetry failed"); + const setIsActive = vi.fn().mockRejectedValue(error); + const fireAndForget = vi.fn((f: () => Promise) => { + void f(); + }); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + vi.resetModules(); + vi.doMock("@/store/global", () => ({ + getApi: () => ({ setIsActive }), + })); + vi.doMock("@/util/util", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fireAndForget, + }; + }); + + const { GlobalModel } = await import("./global-model"); + const model = GlobalModel.getInstance(); + model.setIsActive(); + await Promise.resolve(); + + expect(fireAndForget).toHaveBeenCalledTimes(1); + expect(logSpy).toHaveBeenCalledWith("setIsActive error", error); + + logSpy.mockRestore(); + }); +}); diff --git a/frontend/app/store/global-model.ts b/frontend/app/store/global-model.ts index 38c65d90d0..e4a5515ed9 100644 --- a/frontend/app/store/global-model.ts +++ b/frontend/app/store/global-model.ts @@ -4,6 +4,7 @@ import * as WOS from "@/app/store/wos"; import { ClientModel } from "@/app/store/client-model"; import { getApi } from "@/store/global"; +import * as util from "@/util/util"; import { atom, Atom } from "jotai"; class GlobalModel { @@ -51,17 +52,19 @@ class GlobalModel { }); } - async setIsActive(): Promise { + setIsActive(): void { const now = Date.now(); if (now - this.lastSetIsActiveTs < GlobalModel.IsActiveThrottleMs) { return; } this.lastSetIsActiveTs = now; - try { - await getApi().setIsActive(); - } catch (e) { - console.log("setIsActive error", e); - } + util.fireAndForget(async () => { + try { + await getApi().setIsActive(); + } catch (e) { + console.log("setIsActive error", e); + } + }); } } From 7fb04f50268ee549eeeb71f3e4417153e6f39ce1 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 4 Mar 2026 14:42:13 -0800 Subject: [PATCH 6/7] fix fireandforget --- frontend/app/store/global-model.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/app/store/global-model.ts b/frontend/app/store/global-model.ts index e4a5515ed9..05e84e3774 100644 --- a/frontend/app/store/global-model.ts +++ b/frontend/app/store/global-model.ts @@ -58,13 +58,7 @@ class GlobalModel { return; } this.lastSetIsActiveTs = now; - util.fireAndForget(async () => { - try { - await getApi().setIsActive(); - } catch (e) { - console.log("setIsActive error", e); - } - }); + util.fireAndForget(() => getApi().setIsActive()); } } From 4cf62215d8840c4400e2249c4034f13ab42b9152 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 4 Mar 2026 16:24:01 -0800 Subject: [PATCH 7/7] remove test... not worth it --- frontend/app/store/global-model.test.ts | 66 ------------------------- 1 file changed, 66 deletions(-) delete mode 100644 frontend/app/store/global-model.test.ts diff --git a/frontend/app/store/global-model.test.ts b/frontend/app/store/global-model.test.ts deleted file mode 100644 index b1a3f0b166..0000000000 --- a/frontend/app/store/global-model.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { describe, expect, it, vi } from "vitest"; - -describe("GlobalModel.setIsActive", () => { - it("calls fireAndForget once and throttles repeated mousedown activity", async () => { - const setIsActive = vi.fn().mockResolvedValue(undefined); - const fireAndForget = vi.fn((f: () => Promise) => { - void f(); - }); - - vi.resetModules(); - vi.doMock("@/store/global", () => ({ - getApi: () => ({ setIsActive }), - })); - vi.doMock("@/util/util", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - fireAndForget, - }; - }); - - const { GlobalModel } = await import("./global-model"); - const model = GlobalModel.getInstance(); - - const result = model.setIsActive(); - model.setIsActive(); - - expect(result).toBeUndefined(); - expect(fireAndForget).toHaveBeenCalledTimes(1); - expect(setIsActive).toHaveBeenCalledTimes(1); - }); - - it("logs and swallows setIsActive telemetry errors", async () => { - const error = new Error("telemetry failed"); - const setIsActive = vi.fn().mockRejectedValue(error); - const fireAndForget = vi.fn((f: () => Promise) => { - void f(); - }); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - - vi.resetModules(); - vi.doMock("@/store/global", () => ({ - getApi: () => ({ setIsActive }), - })); - vi.doMock("@/util/util", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - fireAndForget, - }; - }); - - const { GlobalModel } = await import("./global-model"); - const model = GlobalModel.getInstance(); - model.setIsActive(); - await Promise.resolve(); - - expect(fireAndForget).toHaveBeenCalledTimes(1); - expect(logSpy).toHaveBeenCalledWith("setIsActive error", error); - - logSpy.mockRestore(); - }); -});