feat(settings): manual Check for updates + Update button

Auto-update only checked at launch, so users with auto-updates off (or who
just want it now) had no way to update from the app. Add an on-demand flow:
checkForUpdatesNow/downloadUpdateNow/quitAndInstallUpdate in auto-updater
(works regardless of the opt-in; not-packaged dev surfaces 'unsupported'),
forwarded to the renderer as a live UpdateStatus over updates:status. The
Updates section gains a Check for updates button and an Update button that
downloads then installs (Restart & install), with checking/available/
downloading%/downloaded/error status. Refs #2207.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Harshit Singh Bhandari 2026-06-27 20:35:14 +05:30
parent 7f5beed70e
commit 77d1cf9ef8
No known key found for this signature in database
9 changed files with 492 additions and 190 deletions

View File

@ -11,8 +11,20 @@ import {
WebContentsView,
type OpenDialogOptions,
} from "electron";
import { startAutoUpdates, ensureUpdatePrefs } from "./main/auto-updater";
import { readUpdateSettings, writeUpdateSettings, type UpdateSettings } from "./main/update-settings";
import {
startAutoUpdates,
ensureUpdatePrefs,
checkForUpdatesNow,
downloadUpdateNow,
quitAndInstallUpdate,
getUpdateStatus,
} from "./main/auto-updater";
import {
readUpdateSettings,
writeUpdateSettings,
type UpdateSettings,
type UpdateStatus,
} from "./main/update-settings";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { existsSync } from "node:fs";
import { readFile, rm } from "node:fs/promises";
@ -818,6 +830,19 @@ ipcMain.handle("updateSettings:set", async (_event, settings: UpdateSettings) =>
await writeUpdateSettings(path.dirname(runFile), settings);
});
ipcMain.handle("updates:getStatus", (): UpdateStatus => getUpdateStatus());
ipcMain.handle("updates:check", async () => {
const runFile = runFilePath();
if (!runFile) return;
await checkForUpdatesNow(path.dirname(runFile));
});
ipcMain.handle("updates:download", async () => {
await downloadUpdateNow();
});
ipcMain.handle("updates:install", () => {
quitAndInstallUpdate();
});
ipcMain.handle("notifications:show", (_event, notification: { id: string; title: string; body?: string }) => {
if (!notification.id || !notification.title || !ElectronNotification.isSupported()) return;
const toast = new ElectronNotification({

View File

@ -1,8 +1,14 @@
import { autoUpdater } from "electron-updater";
import { dialog } from "electron";
import { app, BrowserWindow, dialog } from "electron";
import { existsSync } from "node:fs";
import path from "node:path";
import { readUpdateSettings, writeUpdateSettings, UPDATE_SETTINGS_FILE_NAME } from "./update-settings";
import {
readUpdateSettings,
writeUpdateSettings,
UPDATE_SETTINGS_FILE_NAME,
type UpdateChannel,
type UpdateStatus,
} from "./update-settings";
// Default release repo, mirroring backend cli.releaseRepo. Override via env for
// fork test builds (AO_RELEASE_REPO=owner/repo).
@ -15,6 +21,50 @@ function repo(): { owner: string; name: string } {
return { owner: defOwner, name: defName };
}
// configureFeed points electron-updater at the GitHub Releases feed for the
// chosen channel. Shared by the launch auto-check and the manual Settings flow.
function configureFeed(channel: UpdateChannel): void {
const { owner, name } = repo();
autoUpdater.setFeedURL({ provider: "github", owner, repo: name });
autoUpdater.channel = channel; // "latest" | "nightly"
autoUpdater.allowDowngrade = true; // permits a nightly -> stable channel switch
}
let lastStatus: UpdateStatus = { state: "idle" };
let eventsWired = false;
// broadcast pushes the latest update status to every renderer window so the
// Global Settings Updates section can reflect check/download progress live.
function broadcast(status: UpdateStatus): void {
lastStatus = status;
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) win.webContents.send("updates:status", status);
}
}
// wireUpdaterEvents registers electron-updater listeners once and forwards each
// to the renderer as an UpdateStatus. Idempotent: safe to call on every entry
// point (launch auto-check and manual check).
function wireUpdaterEvents(): void {
if (eventsWired) return;
eventsWired = true;
autoUpdater.on("checking-for-update", () => broadcast({ state: "checking" }));
autoUpdater.on("update-available", (info) => broadcast({ state: "available", version: info?.version }));
autoUpdater.on("update-not-available", () => broadcast({ state: "not-available" }));
autoUpdater.on("download-progress", (p) =>
broadcast({ state: "downloading", percent: Math.max(0, Math.min(100, Math.round(p?.percent ?? 0))) }),
);
autoUpdater.on("update-downloaded", (info) => broadcast({ state: "downloaded", version: info?.version }));
autoUpdater.on("error", (err) => {
// Never crash on update failure (offline, unsigned macOS, etc.).
broadcast({ state: "error", message: err?.message ?? String(err) });
});
}
export function getUpdateStatus(): UpdateStatus {
return lastStatus;
}
// startAutoUpdates configures electron-updater from the user's ~/.ao settings.
// It is a thin shell: all policy (channel, opt-in) comes from update-settings.
// Caller guards on app.isPackaged.
@ -22,18 +72,11 @@ export async function startAutoUpdates(stateDir: string): Promise<void> {
const settings = await readUpdateSettings(stateDir);
if (!settings.enabled) return;
const { owner, name } = repo();
autoUpdater.setFeedURL({ provider: "github", owner, repo: name });
autoUpdater.channel = settings.channel; // "latest" | "nightly"
autoUpdater.allowDowngrade = true; // permits a nightly -> stable channel switch
wireUpdaterEvents();
configureFeed(settings.channel);
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on("error", (err) => {
// Never crash on update failure (offline, unsigned macOS, etc.).
console.error("auto-update error:", err?.message ?? err);
});
try {
await autoUpdater.checkForUpdates();
} catch (err) {
@ -41,6 +84,50 @@ export async function startAutoUpdates(stateDir: string): Promise<void> {
}
}
// checkForUpdatesNow runs a manual update check regardless of the auto-update
// opt-in, so a user who never enabled auto-updates can still pull the latest
// build from Settings. It does NOT auto-download — the user clicks Update — and
// reports progress via the broadcast status. Updates only work in the packaged,
// signed app; in dev electron-updater has no feed, so surface that plainly.
export async function checkForUpdatesNow(stateDir: string): Promise<void> {
wireUpdaterEvents();
if (!app.isPackaged) {
broadcast({ state: "unsupported", message: "Updates are only available in the installed app." });
return;
}
const settings = await readUpdateSettings(stateDir);
configureFeed(settings.channel);
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
broadcast({ state: "checking" });
try {
await autoUpdater.checkForUpdates();
} catch (err) {
broadcast({ state: "error", message: (err as Error)?.message ?? "Update check failed" });
}
}
// downloadUpdateNow starts downloading the update found by checkForUpdatesNow.
export async function downloadUpdateNow(): Promise<void> {
wireUpdaterEvents();
if (!app.isPackaged) {
broadcast({ state: "unsupported", message: "Updates are only available in the installed app." });
return;
}
try {
await autoUpdater.downloadUpdate();
} catch (err) {
broadcast({ state: "error", message: (err as Error)?.message ?? "Download failed" });
}
}
// quitAndInstallUpdate installs a downloaded update and relaunches. isSilent
// false keeps the installer UI on Windows; isForceRunAfter relaunches the app.
export function quitAndInstallUpdate(): void {
if (!app.isPackaged) return;
autoUpdater.quitAndInstall(false, true);
}
// ensureUpdatePrefs prompts once (first run, before any settings file exists)
// for auto-update opt-in + channel, with a nightly instability disclaimer.
export async function ensureUpdatePrefs(stateDir: string): Promise<void> {

View File

@ -9,6 +9,25 @@ export interface UpdateSettings {
nightlyAck: boolean;
}
// Live state of a manual update check/download, streamed to the renderer so the
// Global Settings "Check for updates" / "Update" buttons can reflect progress.
export type UpdateState =
| "idle"
| "checking"
| "available"
| "not-available"
| "downloading"
| "downloaded"
| "error"
| "unsupported";
export interface UpdateStatus {
state: UpdateState;
version?: string;
percent?: number;
message?: string;
}
/** File holding the user's auto-update preferences under the ~/.ao state dir. */
export const UPDATE_SETTINGS_FILE_NAME = "update-settings.json";

View File

@ -3,7 +3,7 @@ import type { BrowserNavState, BrowserRect } from "./main/browser-view-host";
import type { DaemonStatus } from "./shared/daemon-status";
import type { TelemetryBootstrap } from "./shared/telemetry";
import type { MigrationState } from "./main/app-state";
import type { UpdateSettings } from "./main/update-settings";
import type { UpdateSettings, UpdateStatus } from "./main/update-settings";
export type BrowserBoundsInput = {
viewId: string;
@ -79,6 +79,19 @@ const api = {
get: () => ipcRenderer.invoke("updateSettings:get") as Promise<UpdateSettings>,
set: (settings: UpdateSettings) => ipcRenderer.invoke("updateSettings:set", settings) as Promise<void>,
},
updates: {
getStatus: () => ipcRenderer.invoke("updates:getStatus") as Promise<UpdateStatus>,
check: () => ipcRenderer.invoke("updates:check") as Promise<void>,
download: () => ipcRenderer.invoke("updates:download") as Promise<void>,
install: () => ipcRenderer.invoke("updates:install") as Promise<void>,
onStatus: (listener: (status: UpdateStatus) => void) => {
const wrapped = (_event: Electron.IpcRendererEvent, status: UpdateStatus) => listener(status);
ipcRenderer.on("updates:status", wrapped);
return () => {
ipcRenderer.off("updates:status", wrapped);
};
},
},
};
contextBridge.exposeInMainWorld("ao", api);

View File

@ -1,16 +1,33 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { GlobalSettingsForm } from "./GlobalSettingsForm";
const { getMock, postMock, getMigration, setMigration, getUpdate, setUpdate } = vi.hoisted(() => ({
const {
getMock,
postMock,
getMigration,
setMigration,
getUpdate,
setUpdate,
updGetStatus,
updCheck,
updDownload,
updInstall,
updOnStatus,
} = vi.hoisted(() => ({
getMock: vi.fn(),
postMock: vi.fn(),
getMigration: vi.fn(),
setMigration: vi.fn(),
getUpdate: vi.fn(),
setUpdate: vi.fn(),
updGetStatus: vi.fn(),
updCheck: vi.fn(),
updDownload: vi.fn(),
updInstall: vi.fn(),
updOnStatus: vi.fn(),
}));
vi.mock("../lib/api-client", () => ({
@ -22,6 +39,13 @@ vi.mock("../lib/bridge", () => ({
aoBridge: {
appState: { getMigration, setMigration },
updateSettings: { get: getUpdate, set: setUpdate },
updates: {
getStatus: updGetStatus,
check: updCheck,
download: updDownload,
install: updInstall,
onStatus: updOnStatus,
},
},
}));
@ -43,6 +67,11 @@ beforeEach(() => {
setMigration.mockResolvedValue(undefined);
getUpdate.mockResolvedValue({ enabled: true, channel: "latest", nightlyAck: false });
setUpdate.mockResolvedValue(undefined);
updGetStatus.mockResolvedValue({ state: "idle" });
updCheck.mockResolvedValue(undefined);
updDownload.mockResolvedValue(undefined);
updInstall.mockResolvedValue(undefined);
updOnStatus.mockReturnValue(() => undefined);
});
describe("GlobalSettingsForm", () => {
@ -100,6 +129,41 @@ describe("GlobalSettingsForm", () => {
expect(screen.getByRole("button", { name: "Run migration" })).toBeDisabled();
});
it("Check for updates triggers a manual check", async () => {
renderForm();
const btn = await screen.findByRole("button", { name: "Check for updates" });
await userEvent.click(btn);
expect(updCheck).toHaveBeenCalled();
});
it("offers an Update button when an update is available and downloads it", async () => {
let emit: (s: { state: string; version?: string }) => void = () => undefined;
updOnStatus.mockImplementation((cb: (s: unknown) => void) => {
emit = cb as typeof emit;
return () => undefined;
});
renderForm();
await screen.findByRole("button", { name: "Check for updates" });
act(() => emit({ state: "available", version: "1.2.3" }));
const updateBtn = await screen.findByRole("button", { name: "Update to v1.2.3" });
await userEvent.click(updateBtn);
expect(updDownload).toHaveBeenCalled();
});
it("offers Restart & install once downloaded and installs it", async () => {
let emit: (s: { state: string; version?: string }) => void = () => undefined;
updOnStatus.mockImplementation((cb: (s: unknown) => void) => {
emit = cb as typeof emit;
return () => undefined;
});
renderForm();
await screen.findByRole("button", { name: "Check for updates" });
act(() => emit({ state: "downloaded", version: "1.2.3" }));
const installBtn = await screen.findByRole("button", { name: /Restart & install/ });
await userEvent.click(installBtn);
expect(updInstall).toHaveBeenCalled();
});
it("a failed import surfaces the error and marks failed", async () => {
postMock.mockResolvedValue({ data: undefined, error: { message: "disk full" } });
renderForm();

View File

@ -1,7 +1,8 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { Loader2 } from "lucide-react";
import { aoBridge } from "../lib/bridge";
import type { UpdateChannel, UpdateSettings } from "../../main/update-settings";
import type { UpdateChannel, UpdateSettings, UpdateStatus } from "../../main/update-settings";
import { Button } from "./ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { Label } from "./ui/label";
@ -104,11 +105,85 @@ export function UpdatesSection() {
)}
{savedAt && !save.isPending && !save.isError && <span className="text-[12px] text-success">Saved.</span>}
</div>
<UpdateActions />
</CardContent>
</Card>
);
}
// UpdateActions is the on-demand update control: a Check for updates button plus
// an Update button that downloads then installs. It works even when automatic
// updates are off, so users who never opted in can still pull the latest build.
function UpdateActions() {
const [status, setStatus] = useState<UpdateStatus>({ state: "idle" });
useEffect(() => {
let live = true;
void aoBridge.updates.getStatus().then((s) => {
if (live) setStatus(s);
});
const off = aoBridge.updates.onStatus(setStatus);
return () => {
live = false;
off?.();
};
}, []);
const checking = status.state === "checking";
const downloading = status.state === "downloading";
const busy = checking || downloading;
return (
<div className="flex flex-col gap-2 border-t border-border pt-4">
<div className="flex items-center gap-3">
<Button type="button" variant="outline" onClick={() => void aoBridge.updates.check()} disabled={busy}>
{checking && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Check for updates
</Button>
{status.state === "available" && (
<Button type="button" variant="primary" onClick={() => void aoBridge.updates.download()}>
Update to {status.version ? `v${status.version}` : "latest"}
</Button>
)}
{status.state === "downloaded" && (
<Button type="button" variant="primary" onClick={() => void aoBridge.updates.install()}>
Restart &amp; install
</Button>
)}
<UpdateStatusLine status={status} />
</div>
</div>
);
}
function UpdateStatusLine({ status }: { status: UpdateStatus }) {
switch (status.state) {
case "checking":
return <span className="text-[12px] text-muted-foreground">Checking for updates</span>;
case "available":
return (
<span className="text-[12px] text-muted-foreground">
Update available{status.version ? ` (v${status.version})` : ""}.
</span>
);
case "downloading":
return <span className="text-[12px] text-muted-foreground">Downloading {status.percent ?? 0}%</span>;
case "downloaded":
return <span className="text-[12px] text-success">Downloaded. Restart to finish updating.</span>;
case "not-available":
return <span className="text-[12px] text-muted-foreground">You're on the latest version.</span>;
case "unsupported":
return <span className="text-[12px] text-passive">{status.message ?? "Updates need the installed app."}</span>;
case "error":
return <span className="text-[12px] text-error">{status.message ?? "Update failed."}</span>;
default:
return null;
}
}
function EnabledSelect({ id, value, onChange }: { id: string; value: boolean; onChange: (value: boolean) => void }) {
return (
<Select value={value ? "on" : "off"} onValueChange={(v) => onChange(v === "on")}>

View File

@ -100,4 +100,11 @@ export const aoBridge: AoBridge =
get: async () => ({ enabled: false, channel: "latest", nightlyAck: false }),
set: async () => undefined,
},
updates: {
getStatus: async () => ({ state: "idle" }),
check: async () => undefined,
download: async () => undefined,
install: async () => undefined,
onStatus: () => () => undefined,
},
} satisfies AoBridge);

View File

@ -8,204 +8,209 @@
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from "./routes/__root";
import { Route as ShellRouteImport } from "./routes/_shell";
import { Route as ShellIndexRouteImport } from "./routes/_shell.index";
import { Route as ShellSettingsRouteImport } from "./routes/_shell.settings";
import { Route as ShellPrsRouteImport } from "./routes/_shell.prs";
import { Route as ShellSessionsSessionIdRouteImport } from "./routes/_shell.sessions.$sessionId";
import { Route as ShellProjectsProjectIdRouteImport } from "./routes/_shell.projects.$projectId";
import { Route as ShellProjectsProjectIdSettingsRouteImport } from "./routes/_shell.projects.$projectId_.settings";
import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from "./routes/_shell.projects.$projectId_.sessions.$sessionId";
import { Route as rootRouteImport } from './routes/__root'
import { Route as ShellRouteImport } from './routes/_shell'
import { Route as ShellIndexRouteImport } from './routes/_shell.index'
import { Route as ShellSettingsRouteImport } from './routes/_shell.settings'
import { Route as ShellPrsRouteImport } from './routes/_shell.prs'
import { Route as ShellSessionsSessionIdRouteImport } from './routes/_shell.sessions.$sessionId'
import { Route as ShellProjectsProjectIdRouteImport } from './routes/_shell.projects.$projectId'
import { Route as ShellProjectsProjectIdSettingsRouteImport } from './routes/_shell.projects.$projectId_.settings'
import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from './routes/_shell.projects.$projectId_.sessions.$sessionId'
const ShellRoute = ShellRouteImport.update({
id: "/_shell",
getParentRoute: () => rootRouteImport,
} as any);
id: '/_shell',
getParentRoute: () => rootRouteImport,
} as any)
const ShellIndexRoute = ShellIndexRouteImport.update({
id: "/",
path: "/",
getParentRoute: () => ShellRoute,
} as any);
id: '/',
path: '/',
getParentRoute: () => ShellRoute,
} as any)
const ShellSettingsRoute = ShellSettingsRouteImport.update({
id: "/settings",
path: "/settings",
getParentRoute: () => ShellRoute,
} as any);
id: '/settings',
path: '/settings',
getParentRoute: () => ShellRoute,
} as any)
const ShellPrsRoute = ShellPrsRouteImport.update({
id: "/prs",
path: "/prs",
getParentRoute: () => ShellRoute,
} as any);
id: '/prs',
path: '/prs',
getParentRoute: () => ShellRoute,
} as any)
const ShellSessionsSessionIdRoute = ShellSessionsSessionIdRouteImport.update({
id: "/sessions/$sessionId",
path: "/sessions/$sessionId",
getParentRoute: () => ShellRoute,
} as any);
id: '/sessions/$sessionId',
path: '/sessions/$sessionId',
getParentRoute: () => ShellRoute,
} as any)
const ShellProjectsProjectIdRoute = ShellProjectsProjectIdRouteImport.update({
id: "/projects/$projectId",
path: "/projects/$projectId",
getParentRoute: () => ShellRoute,
} as any);
const ShellProjectsProjectIdSettingsRoute = ShellProjectsProjectIdSettingsRouteImport.update({
id: "/projects/$projectId_/settings",
path: "/projects/$projectId/settings",
getParentRoute: () => ShellRoute,
} as any);
const ShellProjectsProjectIdSessionsSessionIdRoute = ShellProjectsProjectIdSessionsSessionIdRouteImport.update({
id: "/projects/$projectId_/sessions/$sessionId",
path: "/projects/$projectId/sessions/$sessionId",
getParentRoute: () => ShellRoute,
} as any);
id: '/projects/$projectId',
path: '/projects/$projectId',
getParentRoute: () => ShellRoute,
} as any)
const ShellProjectsProjectIdSettingsRoute =
ShellProjectsProjectIdSettingsRouteImport.update({
id: '/projects/$projectId_/settings',
path: '/projects/$projectId/settings',
getParentRoute: () => ShellRoute,
} as any)
const ShellProjectsProjectIdSessionsSessionIdRoute =
ShellProjectsProjectIdSessionsSessionIdRouteImport.update({
id: '/projects/$projectId_/sessions/$sessionId',
path: '/projects/$projectId/sessions/$sessionId',
getParentRoute: () => ShellRoute,
} as any)
export interface FileRoutesByFullPath {
"/": typeof ShellIndexRoute;
"/prs": typeof ShellPrsRoute;
"/settings": typeof ShellSettingsRoute;
"/projects/$projectId": typeof ShellProjectsProjectIdRoute;
"/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
"/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute;
"/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
'/': typeof ShellIndexRoute
'/prs': typeof ShellPrsRoute
'/settings': typeof ShellSettingsRoute
'/projects/$projectId': typeof ShellProjectsProjectIdRoute
'/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
'/projects/$projectId/settings': typeof ShellProjectsProjectIdSettingsRoute
'/projects/$projectId/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
}
export interface FileRoutesByTo {
"/prs": typeof ShellPrsRoute;
"/settings": typeof ShellSettingsRoute;
"/": typeof ShellIndexRoute;
"/projects/$projectId": typeof ShellProjectsProjectIdRoute;
"/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
"/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute;
"/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
'/prs': typeof ShellPrsRoute
'/settings': typeof ShellSettingsRoute
'/': typeof ShellIndexRoute
'/projects/$projectId': typeof ShellProjectsProjectIdRoute
'/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
'/projects/$projectId/settings': typeof ShellProjectsProjectIdSettingsRoute
'/projects/$projectId/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport;
"/_shell": typeof ShellRouteWithChildren;
"/_shell/prs": typeof ShellPrsRoute;
"/_shell/settings": typeof ShellSettingsRoute;
"/_shell/": typeof ShellIndexRoute;
"/_shell/projects/$projectId": typeof ShellProjectsProjectIdRoute;
"/_shell/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
"/_shell/projects/$projectId_/settings": typeof ShellProjectsProjectIdSettingsRoute;
"/_shell/projects/$projectId_/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
__root__: typeof rootRouteImport
'/_shell': typeof ShellRouteWithChildren
'/_shell/prs': typeof ShellPrsRoute
'/_shell/settings': typeof ShellSettingsRoute
'/_shell/': typeof ShellIndexRoute
'/_shell/projects/$projectId': typeof ShellProjectsProjectIdRoute
'/_shell/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
'/_shell/projects/$projectId_/settings': typeof ShellProjectsProjectIdSettingsRoute
'/_shell/projects/$projectId_/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths:
| "/"
| "/prs"
| "/settings"
| "/projects/$projectId"
| "/sessions/$sessionId"
| "/projects/$projectId/settings"
| "/projects/$projectId/sessions/$sessionId";
fileRoutesByTo: FileRoutesByTo;
to:
| "/prs"
| "/settings"
| "/"
| "/projects/$projectId"
| "/sessions/$sessionId"
| "/projects/$projectId/settings"
| "/projects/$projectId/sessions/$sessionId";
id:
| "__root__"
| "/_shell"
| "/_shell/prs"
| "/_shell/settings"
| "/_shell/"
| "/_shell/projects/$projectId"
| "/_shell/sessions/$sessionId"
| "/_shell/projects/$projectId_/settings"
| "/_shell/projects/$projectId_/sessions/$sessionId";
fileRoutesById: FileRoutesById;
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/prs'
| '/settings'
| '/projects/$projectId'
| '/sessions/$sessionId'
| '/projects/$projectId/settings'
| '/projects/$projectId/sessions/$sessionId'
fileRoutesByTo: FileRoutesByTo
to:
| '/prs'
| '/settings'
| '/'
| '/projects/$projectId'
| '/sessions/$sessionId'
| '/projects/$projectId/settings'
| '/projects/$projectId/sessions/$sessionId'
id:
| '__root__'
| '/_shell'
| '/_shell/prs'
| '/_shell/settings'
| '/_shell/'
| '/_shell/projects/$projectId'
| '/_shell/sessions/$sessionId'
| '/_shell/projects/$projectId_/settings'
| '/_shell/projects/$projectId_/sessions/$sessionId'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
ShellRoute: typeof ShellRouteWithChildren;
ShellRoute: typeof ShellRouteWithChildren
}
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/_shell": {
id: "/_shell";
path: "";
fullPath: "/";
preLoaderRoute: typeof ShellRouteImport;
parentRoute: typeof rootRouteImport;
};
"/_shell/": {
id: "/_shell/";
path: "/";
fullPath: "/";
preLoaderRoute: typeof ShellIndexRouteImport;
parentRoute: typeof ShellRoute;
};
"/_shell/settings": {
id: "/_shell/settings";
path: "/settings";
fullPath: "/settings";
preLoaderRoute: typeof ShellSettingsRouteImport;
parentRoute: typeof ShellRoute;
};
"/_shell/prs": {
id: "/_shell/prs";
path: "/prs";
fullPath: "/prs";
preLoaderRoute: typeof ShellPrsRouteImport;
parentRoute: typeof ShellRoute;
};
"/_shell/sessions/$sessionId": {
id: "/_shell/sessions/$sessionId";
path: "/sessions/$sessionId";
fullPath: "/sessions/$sessionId";
preLoaderRoute: typeof ShellSessionsSessionIdRouteImport;
parentRoute: typeof ShellRoute;
};
"/_shell/projects/$projectId": {
id: "/_shell/projects/$projectId";
path: "/projects/$projectId";
fullPath: "/projects/$projectId";
preLoaderRoute: typeof ShellProjectsProjectIdRouteImport;
parentRoute: typeof ShellRoute;
};
"/_shell/projects/$projectId_/settings": {
id: "/_shell/projects/$projectId_/settings";
path: "/projects/$projectId/settings";
fullPath: "/projects/$projectId/settings";
preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport;
parentRoute: typeof ShellRoute;
};
"/_shell/projects/$projectId_/sessions/$sessionId": {
id: "/_shell/projects/$projectId_/sessions/$sessionId";
path: "/projects/$projectId/sessions/$sessionId";
fullPath: "/projects/$projectId/sessions/$sessionId";
preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport;
parentRoute: typeof ShellRoute;
};
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/_shell': {
id: '/_shell'
path: ''
fullPath: '/'
preLoaderRoute: typeof ShellRouteImport
parentRoute: typeof rootRouteImport
}
'/_shell/': {
id: '/_shell/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof ShellIndexRouteImport
parentRoute: typeof ShellRoute
}
'/_shell/settings': {
id: '/_shell/settings'
path: '/settings'
fullPath: '/settings'
preLoaderRoute: typeof ShellSettingsRouteImport
parentRoute: typeof ShellRoute
}
'/_shell/prs': {
id: '/_shell/prs'
path: '/prs'
fullPath: '/prs'
preLoaderRoute: typeof ShellPrsRouteImport
parentRoute: typeof ShellRoute
}
'/_shell/sessions/$sessionId': {
id: '/_shell/sessions/$sessionId'
path: '/sessions/$sessionId'
fullPath: '/sessions/$sessionId'
preLoaderRoute: typeof ShellSessionsSessionIdRouteImport
parentRoute: typeof ShellRoute
}
'/_shell/projects/$projectId': {
id: '/_shell/projects/$projectId'
path: '/projects/$projectId'
fullPath: '/projects/$projectId'
preLoaderRoute: typeof ShellProjectsProjectIdRouteImport
parentRoute: typeof ShellRoute
}
'/_shell/projects/$projectId_/settings': {
id: '/_shell/projects/$projectId_/settings'
path: '/projects/$projectId/settings'
fullPath: '/projects/$projectId/settings'
preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport
parentRoute: typeof ShellRoute
}
'/_shell/projects/$projectId_/sessions/$sessionId': {
id: '/_shell/projects/$projectId_/sessions/$sessionId'
path: '/projects/$projectId/sessions/$sessionId'
fullPath: '/projects/$projectId/sessions/$sessionId'
preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport
parentRoute: typeof ShellRoute
}
}
}
interface ShellRouteChildren {
ShellPrsRoute: typeof ShellPrsRoute;
ShellSettingsRoute: typeof ShellSettingsRoute;
ShellIndexRoute: typeof ShellIndexRoute;
ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute;
ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute;
ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute;
ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute;
ShellPrsRoute: typeof ShellPrsRoute
ShellSettingsRoute: typeof ShellSettingsRoute
ShellIndexRoute: typeof ShellIndexRoute
ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute
ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute
ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute
ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute
}
const ShellRouteChildren: ShellRouteChildren = {
ShellPrsRoute: ShellPrsRoute,
ShellSettingsRoute: ShellSettingsRoute,
ShellIndexRoute: ShellIndexRoute,
ShellProjectsProjectIdRoute: ShellProjectsProjectIdRoute,
ShellSessionsSessionIdRoute: ShellSessionsSessionIdRoute,
ShellProjectsProjectIdSettingsRoute: ShellProjectsProjectIdSettingsRoute,
ShellProjectsProjectIdSessionsSessionIdRoute: ShellProjectsProjectIdSessionsSessionIdRoute,
};
ShellPrsRoute: ShellPrsRoute,
ShellSettingsRoute: ShellSettingsRoute,
ShellIndexRoute: ShellIndexRoute,
ShellProjectsProjectIdRoute: ShellProjectsProjectIdRoute,
ShellSessionsSessionIdRoute: ShellSessionsSessionIdRoute,
ShellProjectsProjectIdSettingsRoute: ShellProjectsProjectIdSettingsRoute,
ShellProjectsProjectIdSessionsSessionIdRoute:
ShellProjectsProjectIdSessionsSessionIdRoute,
}
const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren);
const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren)
const rootRouteChildren: RootRouteChildren = {
ShellRoute: ShellRouteWithChildren,
};
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
ShellRoute: ShellRouteWithChildren,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

View File

@ -145,5 +145,12 @@ if (typeof window !== "undefined") {
get: async () => ({ enabled: false, channel: "latest", nightlyAck: false }),
set: async () => undefined,
},
updates: {
getStatus: async () => ({ state: "idle" }),
check: async () => undefined,
download: async () => undefined,
install: async () => undefined,
onStatus: () => () => undefined,
},
};
} // end if (typeof window !== "undefined")