From d85e17b41df52e4753af3a2150f8149fa263dabf Mon Sep 17 00:00:00 2001 From: Ruud Andriessen Date: Sun, 8 Mar 2026 18:54:03 +0100 Subject: [PATCH] feat(desktop): add auto updater --- bun.lock | 12 ++ package.json | 7 +- packages/desktop-shell/src/app-updater.mts | 171 ++++++++++++++++++ packages/desktop-shell/src/index.mts | 17 +- packages/desktop-shell/src/preload.mts | 16 ++ src/components/desktop-app-updater-toasts.tsx | 71 ++++++++ src/components/ui/sonner.tsx | 27 +++ src/lib/clanki-desktop-bridge.ts | 78 ++++++++ src/lib/desktop-app-updater.ts | 17 ++ src/lib/desktop-runner.ts | 81 ++------- src/routes/__root.tsx | 4 + 11 files changed, 433 insertions(+), 68 deletions(-) create mode 100644 packages/desktop-shell/src/app-updater.mts create mode 100644 src/components/desktop-app-updater-toasts.tsx create mode 100644 src/components/ui/sonner.tsx create mode 100644 src/lib/clanki-desktop-bridge.ts create mode 100644 src/lib/desktop-app-updater.ts diff --git a/bun.lock b/bun.lock index 65f23eb..ef9af8b 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", + "electron-updater": "^6.8.3", "lucide-react": "^0.563.0", "motion": "^12.35.1", "nitro": "^3.0.1-alpha.2", @@ -27,6 +28,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "vite-tsconfig-paths": "^6.1.1", @@ -1297,6 +1299,8 @@ "electron-to-chromium": ["electron-to-chromium@1.5.283", "", {}, "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w=="], + "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], + "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1689,8 +1693,12 @@ "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], @@ -2213,6 +2221,8 @@ "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "sorted-btree": ["sorted-btree@1.8.1", "", {}, "sha512-395+XIP+wqNn3USkFSrNz7G3Ss/MXlZEqesxvzCRFwL14h6e8LukDHdLBePn5pwbm5OQ9vGu8mDyz2lLDIqamQ=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -2293,6 +2303,8 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], + "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], diff --git a/package.json b/package.json index 84bd9b7..c98f9f8 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", + "electron-updater": "^6.8.3", "lucide-react": "^0.563.0", "motion": "^12.35.1", "nitro": "^3.0.1-alpha.2", @@ -58,6 +59,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "vite-tsconfig-paths": "^6.1.1", @@ -115,18 +117,19 @@ "mac": { "category": "public.app-category.developer-tools", "target": [ + "dmg", "zip" ] }, "linux": { "category": "Development", "target": [ - "zip" + "AppImage" ] }, "win": { "target": [ - "zip" + "nsis" ] }, "publish": [ diff --git a/packages/desktop-shell/src/app-updater.mts b/packages/desktop-shell/src/app-updater.mts new file mode 100644 index 0000000..7a8e103 --- /dev/null +++ b/packages/desktop-shell/src/app-updater.mts @@ -0,0 +1,171 @@ +import { app, BrowserWindow } from "electron"; +import { autoUpdater } from "electron-updater"; + +const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; +const UPDATE_STATE_CHANNEL = "app-updater:state-changed"; + +export type AppUpdateState = { + availableVersion: string | null; + currentVersion: string; + status: + | "checking" + | "downloaded" + | "downloading" + | "error" + | "idle" + | "unavailable" + | "up-to-date" + | "update-available"; +}; + +type AppUpdaterController = { + getState: () => AppUpdateState; + quitAndInstall: () => void; + start: () => void; + stop: () => void; +}; + +export function createAppUpdaterController(): AppUpdaterController { + let checkForUpdatesPromise: Promise | null = null; + let hasStarted = false; + let intervalId: NodeJS.Timeout | null = null; + let state: AppUpdateState = { + availableVersion: null, + currentVersion: app.getVersion(), + status: app.isPackaged ? "idle" : "unavailable", + }; + + autoUpdater.autoDownload = true; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.allowPrerelease = app.getVersion().includes("-"); + + autoUpdater.on("checking-for-update", () => { + updateState({ + status: "checking", + }); + }); + + autoUpdater.on("update-available", (info) => { + updateState({ + availableVersion: info.version, + status: "update-available", + }); + }); + + autoUpdater.on("download-progress", () => { + updateState({ + status: "downloading", + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateState({ + availableVersion: info.version, + status: "downloaded", + }); + }); + + autoUpdater.on("update-not-available", () => { + updateState({ + availableVersion: null, + status: "up-to-date", + }); + }); + + autoUpdater.on("error", () => { + updateState({ + status: "error", + }); + }); + + function start(): void { + if (hasStarted) { + return; + } + + hasStarted = true; + + if (!app.isPackaged) { + broadcastState(); + return; + } + + void checkForUpdates(); + intervalId = setInterval(() => { + void checkForUpdates(); + }, UPDATE_CHECK_INTERVAL_MS); + } + + async function checkForUpdates(): Promise { + if (!app.isPackaged) { + updateState({ + status: "unavailable", + }); + return state; + } + + if (checkForUpdatesPromise) { + return await checkForUpdatesPromise; + } + + checkForUpdatesPromise = autoUpdater + .checkForUpdates() + .then(() => state) + .catch(() => { + updateState({ + status: "error", + }); + return state; + }) + .finally(() => { + checkForUpdatesPromise = null; + }); + + return await checkForUpdatesPromise; + } + + function quitAndInstall(): void { + if (state.status !== "downloaded") { + throw new Error("No downloaded update is ready to install."); + } + + autoUpdater.quitAndInstall(); + } + + function getState(): AppUpdateState { + return state; + } + + function stop(): void { + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } + } + + function updateState(nextPartialState: Partial): void { + state = { + ...state, + ...nextPartialState, + currentVersion: app.getVersion(), + }; + broadcastState(); + } + + function broadcastState(): void { + for (const window of BrowserWindow.getAllWindows()) { + if (window.isDestroyed()) { + continue; + } + + window.webContents.send(UPDATE_STATE_CHANNEL, state); + } + } + + return { + getState, + quitAndInstall, + start, + stop, + }; +} diff --git a/packages/desktop-shell/src/index.mts b/packages/desktop-shell/src/index.mts index 812d422..38eecaf 100644 --- a/packages/desktop-shell/src/index.mts +++ b/packages/desktop-shell/src/index.mts @@ -2,13 +2,16 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain, shell } from "electron"; import { createAppServerController } from "./app-server.mjs"; +import { createAppUpdaterController } from "./app-updater.mjs"; import { createDesktopRunnerController } from "./desktop-runner.mjs"; const currentDirectory = path.dirname(fileURLToPath(import.meta.url)); let appServerController: ReturnType | null = null; let desktopRunnerController: ReturnType | null = null; +const appUpdaterController = createAppUpdaterController(); let isQuitting = false; +let isInstallingUpdate = false; function resolveWorkspaceRoot(): string { if (app.isPackaged) { @@ -60,6 +63,16 @@ function registerIpcHandlers(): void { ipcMain.handle("desktop-runner:prompt-task", async (_event, args) => { return await getDesktopRunnerController().promptRunnerTask(args); }); + + ipcMain.handle("app-updater:get-state", () => { + return appUpdaterController.getState(); + }); + + ipcMain.handle("app-updater:quit-and-install", async () => { + isInstallingUpdate = true; + await disposeControllers(); + appUpdaterController.quitAndInstall(); + }); } function isExternalUrl(targetUrl: string, appUrl: string): boolean { @@ -151,6 +164,7 @@ async function createMainWindow(): Promise { } async function disposeControllers(): Promise { + appUpdaterController.stop(); await Promise.allSettled([appServerController?.stop(), desktopRunnerController?.stop()]); } @@ -159,6 +173,7 @@ registerIpcHandlers(); app .whenReady() .then(async () => { + appUpdaterController.start(); await createMainWindow(); app.on("activate", async () => { @@ -181,7 +196,7 @@ app.on("window-all-closed", () => { }); app.on("before-quit", (event) => { - if (isQuitting) { + if (isQuitting || isInstallingUpdate) { return; } diff --git a/packages/desktop-shell/src/preload.mts b/packages/desktop-shell/src/preload.mts index 2343d6d..fe1e83e 100644 --- a/packages/desktop-shell/src/preload.mts +++ b/packages/desktop-shell/src/preload.mts @@ -7,9 +7,22 @@ contextBridge.exposeInMainWorld("clankiDesktop", { deleteRunnerWorkspace(workspaceDirectory: string) { return ipcRenderer.invoke("desktop-runner:delete-workspace", { workspaceDirectory }); }, + getAppUpdateState() { + return ipcRenderer.invoke("app-updater:get-state"); + }, listRunnerModels(args: { directory: string }) { return ipcRenderer.invoke("desktop-runner:list-models", args); }, + onAppUpdateStateChange(listener: (state: unknown) => void) { + const wrappedListener = (_event: unknown, state: unknown) => { + listener(state); + }; + + ipcRenderer.on("app-updater:state-changed", wrappedListener); + return () => { + ipcRenderer.off("app-updater:state-changed", wrappedListener); + }; + }, openWorkspaceInEditor(args: { editor: "cursor" | "vscode" | "zed"; workspaceDirectory: string }) { return ipcRenderer.invoke("desktop-runner:open-workspace-in-editor", args); }, @@ -25,4 +38,7 @@ contextBridge.exposeInMainWorld("clankiDesktop", { }) { return ipcRenderer.invoke("desktop-runner:prompt-task", args); }, + quitAndInstallAppUpdate() { + return ipcRenderer.invoke("app-updater:quit-and-install"); + }, }); diff --git a/src/components/desktop-app-updater-toasts.tsx b/src/components/desktop-app-updater-toasts.tsx new file mode 100644 index 0000000..5fa37ae --- /dev/null +++ b/src/components/desktop-app-updater-toasts.tsx @@ -0,0 +1,71 @@ +import { useEffect, useRef } from "react"; +import { toast } from "sonner"; +import { + getDesktopAppUpdateState, + onDesktopAppUpdateStateChange, + quitAndInstallDesktopAppUpdate, + type DesktopAppUpdateState, +} from "@/lib/desktop-app-updater"; +import { isDesktopApp } from "@/lib/is-desktop-app"; + +const UPDATE_READY_TOAST_ID = "desktop-app-update-ready"; + +export function DesktopAppUpdaterToasts() { + const previousStatusRef = useRef(null); + + useEffect(() => { + if (!isDesktopApp()) { + return; + } + + let active = true; + + const handleUpdateState = (updateState: DesktopAppUpdateState) => { + if (!active) { + return; + } + + const previousStatus = previousStatusRef.current; + previousStatusRef.current = updateState.status; + + if (updateState.status === "downloaded" && previousStatus !== "downloaded") { + toast("Clanki update ready", { + action: { + label: "Update Now", + onClick: () => { + void quitAndInstallDesktopAppUpdate().catch((error) => { + toast.error( + error instanceof Error ? error.message : "Failed to install the desktop update", + ); + }); + }, + }, + description: updateState.availableVersion + ? `Version ${updateState.availableVersion} has been downloaded and is ready to install.` + : "A new Clanki version has been downloaded and is ready to install.", + duration: Number.POSITIVE_INFINITY, + id: UPDATE_READY_TOAST_ID, + }); + return; + } + + if (updateState.status !== "downloaded") { + toast.dismiss(UPDATE_READY_TOAST_ID); + } + }; + + void getDesktopAppUpdateState() + .then(handleUpdateState) + .catch(() => {}); + + const unsubscribe = onDesktopAppUpdateStateChange(handleUpdateState); + + return () => { + active = false; + toast.dismiss(UPDATE_READY_TOAST_ID); + unsubscribe(); + }; + }, []); + + return null; +} diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..f83a490 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,27 @@ +import { Toaster as Sonner, type ToasterProps } from "sonner"; +import { useTheme } from "@/components/theme-provider"; + +export function Toaster(props: ToasterProps) { + const { theme } = useTheme(); + + return ( + + ); +} diff --git a/src/lib/clanki-desktop-bridge.ts b/src/lib/clanki-desktop-bridge.ts new file mode 100644 index 0000000..ce210f5 --- /dev/null +++ b/src/lib/clanki-desktop-bridge.ts @@ -0,0 +1,78 @@ +type CreateDesktopRunnerSessionResponse = { + runnerType: string; + sessionId: string; + workspaceDirectory: string; +}; + +export type DesktopWorkspaceEditor = "cursor" | "vscode" | "zed"; + +export type DesktopRunnerModelSelection = { + model: string; + provider: string; +}; + +export type DesktopRunnerModelProvider = { + id: string; + models: Record; + name: string; +}; + +export type ListDesktopRunnerModelsResponse = { + connected: string[]; + default: Record; + providers: DesktopRunnerModelProvider[]; +}; + +export type DesktopAppUpdateState = { + availableVersion: string | null; + currentVersion: string; + status: + | "checking" + | "downloaded" + | "downloading" + | "error" + | "idle" + | "unavailable" + | "up-to-date" + | "update-available"; +}; + +export type ClankiDesktopBridge = { + createRunnerSession: ( + title: string, + repoUrl: string, + ) => Promise; + deleteRunnerWorkspace: (workspaceDirectory: string) => Promise; + getAppUpdateState: () => Promise; + listRunnerModels: (args: { directory: string }) => Promise; + onAppUpdateStateChange: (listener: (state: DesktopAppUpdateState) => void) => () => void; + openWorkspaceInEditor: (args: { + editor: DesktopWorkspaceEditor; + workspaceDirectory: string; + }) => Promise; + promptRunnerTask: (args: { + backendBaseUrl: string; + callbackToken: string; + directory: string; + executionId: string; + model?: string; + prompt: string; + provider?: string; + sessionId: string; + }) => Promise; + quitAndInstallAppUpdate: () => Promise; +}; + +declare global { + interface Window { + clankiDesktop?: ClankiDesktopBridge; + } +} + +export function getClankiDesktopBridge(): ClankiDesktopBridge { + if (typeof window === "undefined" || !window.clankiDesktop) { + throw new Error("The desktop bridge is only available in the Electron app."); + } + + return window.clankiDesktop; +} diff --git a/src/lib/desktop-app-updater.ts b/src/lib/desktop-app-updater.ts new file mode 100644 index 0000000..12419f1 --- /dev/null +++ b/src/lib/desktop-app-updater.ts @@ -0,0 +1,17 @@ +import { getClankiDesktopBridge, type DesktopAppUpdateState } from "@/lib/clanki-desktop-bridge"; + +export type { DesktopAppUpdateState }; + +export async function getDesktopAppUpdateState(): Promise { + return await getClankiDesktopBridge().getAppUpdateState(); +} + +export function onDesktopAppUpdateStateChange( + listener: (state: DesktopAppUpdateState) => void, +): () => void { + return getClankiDesktopBridge().onAppUpdateStateChange(listener); +} + +export async function quitAndInstallDesktopAppUpdate(): Promise { + await getClankiDesktopBridge().quitAndInstallAppUpdate(); +} diff --git a/src/lib/desktop-runner.ts b/src/lib/desktop-runner.ts index 9f770b6..9964f22 100644 --- a/src/lib/desktop-runner.ts +++ b/src/lib/desktop-runner.ts @@ -1,87 +1,38 @@ -type CreateDesktopRunnerSessionResponse = { - runnerType: string; - sessionId: string; - workspaceDirectory: string; -}; - -export type DesktopWorkspaceEditor = "cursor" | "vscode" | "zed"; - -export type DesktopRunnerModelSelection = { - model: string; - provider: string; -}; - -export type DesktopRunnerModelProvider = { - id: string; - models: Record; - name: string; -}; - -export type ListDesktopRunnerModelsResponse = { - connected: string[]; - default: Record; - providers: DesktopRunnerModelProvider[]; -}; - -type DesktopRunnerBridge = { - createRunnerSession: ( - title: string, - repoUrl: string, - ) => Promise; - deleteRunnerWorkspace: (workspaceDirectory: string) => Promise; - listRunnerModels: (args: { directory: string }) => Promise; - openWorkspaceInEditor: (args: { - editor: DesktopWorkspaceEditor; - workspaceDirectory: string; - }) => Promise; - promptRunnerTask: (args: { - backendBaseUrl: string; - callbackToken: string; - directory: string; - executionId: string; - model?: string; - prompt: string; - provider?: string; - sessionId: string; - }) => Promise; +import { + getClankiDesktopBridge, + type DesktopRunnerModelSelection, + type DesktopWorkspaceEditor, + type ListDesktopRunnerModelsResponse, +} from "@/lib/clanki-desktop-bridge"; + +export type { + DesktopRunnerModelSelection, + DesktopWorkspaceEditor, + ListDesktopRunnerModelsResponse, }; -declare global { - interface Window { - clankiDesktop?: DesktopRunnerBridge; - } -} - -function getDesktopRunnerBridge(): DesktopRunnerBridge { - if (typeof window === "undefined" || !window.clankiDesktop) { - throw new Error("The desktop runner API is only available in the Electron app."); - } - - return window.clankiDesktop; -} - export async function createDesktopRunnerSession( title: string, repoUrl: string, ): Promise<{ runnerType: string; sessionId: string; workspaceDirectory: string }> { - return await getDesktopRunnerBridge().createRunnerSession(title, repoUrl); + return await getClankiDesktopBridge().createRunnerSession(title, repoUrl); } export async function deleteDesktopRunnerWorkspace(workspaceDirectory: string): Promise { - await getDesktopRunnerBridge().deleteRunnerWorkspace(workspaceDirectory); + await getClankiDesktopBridge().deleteRunnerWorkspace(workspaceDirectory); } export async function listDesktopRunnerModels(args: { directory: string; }): Promise { - return await getDesktopRunnerBridge().listRunnerModels(args); + return await getClankiDesktopBridge().listRunnerModels(args); } export async function openDesktopWorkspaceInEditor(args: { editor: DesktopWorkspaceEditor; workspaceDirectory: string; }): Promise { - await getDesktopRunnerBridge().openWorkspaceInEditor(args); + await getClankiDesktopBridge().openWorkspaceInEditor(args); } export async function promptDesktopRunnerTask(args: { @@ -94,5 +45,5 @@ export async function promptDesktopRunnerTask(args: { provider?: string; sessionId: string; }): Promise { - await getDesktopRunnerBridge().promptRunnerTask(args); + await getClankiDesktopBridge().promptRunnerTask(args); } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index e2052a2..0391377 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,7 +1,9 @@ /// import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router"; import type { ReactNode } from "react"; +import { DesktopAppUpdaterToasts } from "@/components/desktop-app-updater-toasts"; import { ThemeProvider } from "@/components/theme-provider"; +import { Toaster } from "@/components/ui/sonner"; import { themeInitializationScript } from "@/lib/theme"; import appCss from "@/index.css?url"; @@ -32,6 +34,8 @@ function RootDocument({ children }: { children: ReactNode }) { {children} + +