mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-10-19 17:03:59 +00:00
Split app packaging commands by platform
This commit is contained in:
parent
77208a6da9
commit
585ff6ddd3
12 changed files with 232 additions and 208 deletions
|
|
@ -151,18 +151,19 @@ npm install
|
|||
npm run dev
|
||||
npm run typecheck
|
||||
npm run build:assets
|
||||
npm run build:app
|
||||
npm run build:app:mac
|
||||
npm run build:app:win
|
||||
```
|
||||
|
||||
`npm run build:assets` compiles the Electron main process and renderer assets into `dist/`.
|
||||
|
||||
`npm run build` packages the app for the current platform and writes installer artifacts to `release/`.
|
||||
|
||||
`npm run build:app` packages macOS and Windows artifacts with `electron-builder --mac --win`. You can also run `npm run build:app:mac` or `npm run build:app:win` for a single platform. Linux AppImage packaging is configured in `electron-builder.json`.
|
||||
`npm run build:app:mac` and `npm run build:app:win` package platform-specific app artifacts. Linux AppImage packaging is configured in `electron-builder.json`.
|
||||
|
||||
`npm run build:app:mac` creates a local macOS test package in `release-local/` using ad-hoc signing. It is useful with a free Apple Account or Apple Development certificate, but it is not suitable for public distribution because downloaded copies will not pass Gatekeeper notarization checks.
|
||||
|
||||
macOS release builds are signed and notarized for distribution. Before running `npm run build:app` or `npm run build:app:mac:release`, the build machine must have a `Developer ID Application` certificate available through the keychain or `CSC_LINK`/`CSC_KEY_PASSWORD`, full Xcode selected with `xcode-select`, and one notarization credential set:
|
||||
macOS release builds are signed and notarized for distribution. Before running `npm run build:app:mac:release`, the build machine must have a `Developer ID Application` certificate available through the keychain or `CSC_LINK`/`CSC_KEY_PASSWORD`, full Xcode selected with `xcode-select`, and one notarization credential set:
|
||||
|
||||
- `APPLE_API_KEY`, `APPLE_API_KEY_ID`, and `APPLE_API_ISSUER`
|
||||
- `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID`
|
||||
|
|
|
|||
|
|
@ -151,18 +151,19 @@ npm install
|
|||
npm run dev
|
||||
npm run typecheck
|
||||
npm run build:assets
|
||||
npm run build:app
|
||||
npm run build:app:mac
|
||||
npm run build:app:win
|
||||
```
|
||||
|
||||
`npm run build:assets` 会把 Electron main process 和 renderer assets 编译到 `dist/`。
|
||||
|
||||
`npm run build` 会为当前平台打包应用,并把安装包写入 `release/`。
|
||||
|
||||
`npm run build:app` 会通过 `electron-builder --mac --win` 打包 macOS 和 Windows 产物。你也可以使用 `npm run build:app:mac` 或 `npm run build:app:win` 单独打包一个平台。Linux AppImage 打包配置在 `electron-builder.json` 中。
|
||||
`npm run build:app:mac` 和 `npm run build:app:win` 会分别打包对应平台的应用产物。Linux AppImage 打包配置在 `electron-builder.json` 中。
|
||||
|
||||
`npm run build:app:mac` 会在 `release-local/` 生成本地测试用 macOS 包,使用 ad-hoc 签名。它适合免费 Apple Account 或只有 Apple Development 证书的本机测试,但不适合公开分发,因为用户下载后仍无法通过 Gatekeeper 公证检查。
|
||||
|
||||
macOS 发布包会使用 Developer ID 签名并提交 Apple 公证。运行 `npm run build:app` 或 `npm run build:app:mac:release` 前,打包机器必须具备:可用的 `Developer ID Application` 证书(在 keychain 中,或通过 `CSC_LINK`/`CSC_KEY_PASSWORD` 提供)、已通过 `xcode-select` 选择完整 Xcode,以及下面任意一组公证凭据:
|
||||
macOS 发布包会使用 Developer ID 签名并提交 Apple 公证。运行 `npm run build:app:mac:release` 前,打包机器必须具备:可用的 `Developer ID Application` 证书(在 keychain 中,或通过 `CSC_LINK`/`CSC_KEY_PASSWORD` 提供)、已通过 `xcode-select` 选择完整 Xcode,以及下面任意一组公证凭据:
|
||||
|
||||
- `APPLE_API_KEY`、`APPLE_API_KEY_ID`、`APPLE_API_ISSUER`
|
||||
- `APPLE_ID`、`APPLE_APP_SPECIFIC_PASSWORD`、`APPLE_TEAM_ID`
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
"dev": "node build/dev.mjs",
|
||||
"build": "npm run build:assets && electron-builder",
|
||||
"build:assets": "node build/build.mjs",
|
||||
"build:app": "node build/macos-release-preflight.mjs && npm run build:assets && electron-builder --mac --win",
|
||||
"build:app:mac": "npm run build:app:mac:local",
|
||||
"build:app:mac:local": "npm run build:assets && electron-builder --config build/electron-builder.local.cjs --mac --publish never",
|
||||
"build:app:mac:release": "node build/macos-release-preflight.mjs && npm run build:assets && electron-builder --mac --publish never",
|
||||
|
|
@ -21,17 +20,17 @@
|
|||
"rebuild:sqlite3": "electron-rebuild -f -w better-sqlite3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@the-next-ai/ai-gateway": "^1.0.0",
|
||||
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"electron-updater": "^6.8.9",
|
||||
"node-forge": "^1.4.0"
|
||||
"node-forge": "^1.4.0",
|
||||
"undici": "^7.27.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"undici": "^7.27.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@the-next-ai/ai-gateway": "^1.0.0",
|
||||
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
|
||||
"@tailwindcss/cli": "^4.3.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^22.10.2",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { app, dialog, Menu, type BrowserWindow, type MenuItemConstructorOptions } from "electron";
|
||||
import { APP_NAME, IPC_CHANNELS } from "./constants";
|
||||
import { appUpdateService } from "./update-service";
|
||||
import windowsManager from "./windows";
|
||||
import type { AppUpdateStatus } from "../shared/app";
|
||||
|
||||
export function setupApplicationMenu(): void {
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(createMenuTemplate()));
|
||||
|
|
@ -16,6 +18,7 @@ function createMenuTemplate(): MenuItemConstructorOptions[] {
|
|||
{ label: `About ${APP_NAME}`, click: showAboutPanel },
|
||||
{ type: "separator" },
|
||||
{ label: "Settings...", accelerator: "CmdOrCtrl+,", click: openSettings },
|
||||
{ label: "Check for Updates...", click: checkForUpdatesFromMenu },
|
||||
{ type: "separator" },
|
||||
{ role: "services" },
|
||||
{ type: "separator" },
|
||||
|
|
@ -102,6 +105,8 @@ function createMenuTemplate(): MenuItemConstructorOptions[] {
|
|||
template.push({
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{ label: "Check for Updates...", click: checkForUpdatesFromMenu },
|
||||
{ type: "separator" },
|
||||
{ label: `About ${APP_NAME}`, click: showAboutPanel }
|
||||
]
|
||||
});
|
||||
|
|
@ -126,6 +131,125 @@ function showAboutPanel(): void {
|
|||
void (window ? dialog.showMessageBox(window, options) : dialog.showMessageBox(options));
|
||||
}
|
||||
|
||||
function checkForUpdatesFromMenu(): void {
|
||||
const window = windowsManager.showMainWindow();
|
||||
void checkForUpdatesWithDialog(window);
|
||||
}
|
||||
|
||||
async function checkForUpdatesWithDialog(window: BrowserWindow): Promise<void> {
|
||||
try {
|
||||
await presentUpdateStatus(window, await appUpdateService.checkForUpdates());
|
||||
} catch (error) {
|
||||
await showUpdateDialog(window, {
|
||||
detail: formatError(error),
|
||||
message: "Update check failed.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function presentUpdateStatus(window: BrowserWindow, status: AppUpdateStatus): Promise<void> {
|
||||
if (status.state === "not-available") {
|
||||
await showUpdateDialog(window, {
|
||||
detail: `Version ${status.currentVersion} is currently installed.`,
|
||||
message: `${APP_NAME} is up to date.`,
|
||||
type: "info"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.state === "available") {
|
||||
const result = await showUpdateDialog(window, {
|
||||
buttons: ["Download", "Later"],
|
||||
cancelId: 1,
|
||||
detail: status.availableVersion
|
||||
? `Version ${status.availableVersion} is available.`
|
||||
: "An update is available.",
|
||||
message: "Update available.",
|
||||
type: "info"
|
||||
});
|
||||
if (result.response === 0) {
|
||||
await downloadUpdateFromMenu(window);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.state === "downloaded") {
|
||||
await promptInstallUpdate(window);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.state === "error") {
|
||||
await showUpdateDialog(window, {
|
||||
detail: status.lastError || "Unable to check for updates.",
|
||||
message: "Update check failed.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadUpdateFromMenu(window: BrowserWindow): Promise<void> {
|
||||
try {
|
||||
const status = await appUpdateService.downloadUpdate();
|
||||
if (status.state === "downloaded" || status.canInstall) {
|
||||
await promptInstallUpdate(window);
|
||||
return;
|
||||
}
|
||||
if (status.state === "error") {
|
||||
await showUpdateDialog(window, {
|
||||
detail: status.lastError || "Unable to download the update.",
|
||||
message: "Update download failed.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await showUpdateDialog(window, {
|
||||
detail: formatError(error),
|
||||
message: "Update download failed.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function promptInstallUpdate(window: BrowserWindow): Promise<void> {
|
||||
const result = await showUpdateDialog(window, {
|
||||
buttons: ["Install and Restart", "Later"],
|
||||
cancelId: 1,
|
||||
detail: "The update has been downloaded and is ready to install.",
|
||||
message: "Update ready to install.",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
if (result.response !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await appUpdateService.installUpdate();
|
||||
} catch (error) {
|
||||
await showUpdateDialog(window, {
|
||||
detail: formatError(error),
|
||||
message: "Update install failed.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showUpdateDialog(
|
||||
window: BrowserWindow,
|
||||
options: { buttons?: string[]; cancelId?: number; detail: string; message: string; type: "error" | "info" }
|
||||
): Promise<Electron.MessageBoxReturnValue> {
|
||||
return dialog.showMessageBox(window, {
|
||||
buttons: options.buttons ?? ["OK"],
|
||||
cancelId: options.cancelId,
|
||||
defaultId: 0,
|
||||
detail: options.detail,
|
||||
message: options.message,
|
||||
title: "Software Update",
|
||||
type: options.type
|
||||
});
|
||||
}
|
||||
|
||||
function sendWhenReady(window: BrowserWindow, channel: string): void {
|
||||
if (window.isDestroyed() || window.webContents.isDestroyed()) {
|
||||
return;
|
||||
|
|
@ -144,3 +268,7 @@ function sendWhenReady(window: BrowserWindow, channel: string): void {
|
|||
|
||||
send();
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ function resolveUserPath(value: string): string {
|
|||
if (trimmed === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
if (trimmed.startsWith("~/")) {
|
||||
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
||||
return path.join(os.homedir(), trimmed.slice(2));
|
||||
}
|
||||
return path.resolve(trimmed || ".");
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import path from "node:path";
|
|||
import type { AppConfig, ProfileOpenSurface } from "../shared/app";
|
||||
import { botGatewayProfileEnv } from "./bot-gateway-env";
|
||||
import { launchCodexAppProfile, launchZcodeAppProfile } from "./codex-app-launch";
|
||||
import { buildProfileLaunchPlan, findProfileForOpen, resolveProfileOpenSurface } from "./profile-launch-core";
|
||||
import { buildProfileLaunchPlan, findProfileForOpen, profileLaunchSpawnCommand, resolveProfileOpenSurface } from "./profile-launch-core";
|
||||
|
||||
type CliOptions = {
|
||||
agentArgs: string[];
|
||||
|
|
@ -55,7 +55,8 @@ async function main(): Promise<void> {
|
|||
};
|
||||
delete childEnv.ELECTRON_RUN_AS_NODE;
|
||||
|
||||
const child = spawn(plan.command, plan.args, {
|
||||
const launch = profileLaunchSpawnCommand(plan);
|
||||
const child = spawn(launch.command, launch.args, {
|
||||
env: childEnv,
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ export type ProfileLaunchPlan = {
|
|||
surface: ProfileOpenSurface;
|
||||
};
|
||||
|
||||
export type ProfileLaunchSpawnCommand = {
|
||||
args: string[];
|
||||
command: string;
|
||||
};
|
||||
|
||||
export function findProfileForOpen(config: Pick<AppConfig, "profile">, profileRef: string): ProfileConfig {
|
||||
const needle = profileRef.trim();
|
||||
if (!needle) {
|
||||
|
|
@ -86,6 +91,25 @@ export function buildProfileLaunchPlan(
|
|||
return buildClaudeCodeLaunchPlan(configDir, profile, resolvedSurface, extraArgs);
|
||||
}
|
||||
|
||||
export function profileLaunchSpawnCommand(plan: Pick<ProfileLaunchPlan, "args" | "command">): ProfileLaunchSpawnCommand {
|
||||
if (!isWindowsCommandScript(plan.command)) {
|
||||
return {
|
||||
args: plan.args,
|
||||
command: plan.command
|
||||
};
|
||||
}
|
||||
return {
|
||||
args: [
|
||||
"/d",
|
||||
"/s",
|
||||
"/v:off",
|
||||
"/c",
|
||||
windowsCommandScriptInvocation(plan.command, plan.args)
|
||||
],
|
||||
command: process.env.ComSpec || process.env.COMSPEC || "cmd.exe"
|
||||
};
|
||||
}
|
||||
|
||||
export function ccrManagedProfileDir(configDir: string, profile: ProfileConfig): string {
|
||||
const slug = sanitizePathSegment(profile.id || profile.name || profile.agent);
|
||||
const baseDir = path.join(configDir, "profiles", slug || "profile");
|
||||
|
|
@ -194,7 +218,7 @@ function resolveUserPath(value: string): string {
|
|||
if (trimmed === "~") {
|
||||
return homeDir();
|
||||
}
|
||||
if (trimmed.startsWith("~/")) {
|
||||
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
||||
return path.join(homeDir(), trimmed.slice(2));
|
||||
}
|
||||
return path.resolve(trimmed || ".");
|
||||
|
|
@ -228,3 +252,23 @@ function windowsCommandQuote(value: string): string {
|
|||
? normalized
|
||||
: `"${normalized.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function isWindowsCommandScript(command: string): boolean {
|
||||
return process.platform === "win32" && /\.(?:bat|cmd)$/i.test(path.basename(command));
|
||||
}
|
||||
|
||||
function windowsCommandScriptInvocation(command: string, args: string[]): string {
|
||||
return [
|
||||
"call",
|
||||
windowsCommandInvocationArg(command),
|
||||
...args.map(windowsCommandInvocationArg)
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function windowsCommandInvocationArg(value: string): string {
|
||||
const normalized = value.replace(/\r?\n/g, " ");
|
||||
if (!normalized) {
|
||||
return "\"\"";
|
||||
}
|
||||
return `"${normalized.replace(/[\^"%&|<>()]/g, "^$&")}"`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { launchCodexAppProfile, launchZcodeAppProfile, refreshCodexCompatibleApp
|
|||
import { codexCliMiddlewareRuntimeScript } from "./codex-cli-middleware-runtime";
|
||||
import { CONFIGDIR } from "./constants";
|
||||
import { gatewayService } from "../server/gateway/service";
|
||||
import { buildProfileLaunchPlan, findProfileForOpen, profileOpenCommand, resolveClaudeCodeSettingsFile, resolveProfileOpenSurface } from "./profile-launch-core";
|
||||
import { buildProfileLaunchPlan, findProfileForOpen, profileLaunchSpawnCommand, profileOpenCommand, resolveClaudeCodeSettingsFile, resolveProfileOpenSurface } from "./profile-launch-core";
|
||||
import { applyProfileConfig } from "./profile-service";
|
||||
|
||||
const ccrPathBlockStart = "# >>> Claude Code Router CLI >>>";
|
||||
|
|
@ -64,7 +64,8 @@ export async function openProfileFromCcr(config: AppConfig, request: ProfileOpen
|
|||
throw new Error(`Profile launcher was not found: ${plan.command}. Re-save the profile and try again.`);
|
||||
}
|
||||
|
||||
const child = spawn(plan.command, plan.args, {
|
||||
const launch = profileLaunchSpawnCommand(plan);
|
||||
const child = spawn(launch.command, launch.args, {
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
|
|
|
|||
|
|
@ -961,7 +961,7 @@ function resolveUserPath(value: string): string {
|
|||
if (trimmed === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
if (trimmed.startsWith("~/")) {
|
||||
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
||||
return path.join(os.homedir(), trimmed.slice(2));
|
||||
}
|
||||
return path.resolve(trimmed || ".");
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ import windowsManager from "./windows";
|
|||
import type { AppUpdateStatus } from "../shared/app";
|
||||
|
||||
type InstallPreparation = () => Promise<void>;
|
||||
type UpdateCheckOptions = {
|
||||
silentFailure?: boolean;
|
||||
};
|
||||
|
||||
const startupCheckDelayMs = 12_000;
|
||||
|
||||
class AppUpdateService {
|
||||
private activeSilentCheckFailureRestoreStatus?: AppUpdateStatus;
|
||||
private initialized = false;
|
||||
private installingUpdate = false;
|
||||
private prepareInstall?: InstallPreparation;
|
||||
|
|
@ -49,7 +53,7 @@ class AppUpdateService {
|
|||
return this.installingUpdate;
|
||||
}
|
||||
|
||||
async checkForUpdates(): Promise<AppUpdateStatus> {
|
||||
async checkForUpdates(options: UpdateCheckOptions = {}): Promise<AppUpdateStatus> {
|
||||
this.start();
|
||||
if (!this.isUpdaterSupported()) {
|
||||
return this.publishStatus({
|
||||
|
|
@ -58,6 +62,8 @@ class AppUpdateService {
|
|||
});
|
||||
}
|
||||
|
||||
const restoreStatus = options.silentFailure ? this.status : undefined;
|
||||
this.activeSilentCheckFailureRestoreStatus = restoreStatus;
|
||||
this.publishStatus({
|
||||
lastError: undefined,
|
||||
progress: undefined,
|
||||
|
|
@ -96,11 +102,18 @@ class AppUpdateService {
|
|||
progress: undefined
|
||||
});
|
||||
} catch (error) {
|
||||
if (options.silentFailure && restoreStatus) {
|
||||
return this.publishSilentCheckFailure(error, restoreStatus);
|
||||
}
|
||||
return this.publishStatus({
|
||||
lastError: formatError(error),
|
||||
progress: undefined,
|
||||
state: "error"
|
||||
});
|
||||
} finally {
|
||||
if (this.activeSilentCheckFailureRestoreStatus === restoreStatus) {
|
||||
this.activeSilentCheckFailureRestoreStatus = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -232,6 +245,10 @@ class AppUpdateService {
|
|||
});
|
||||
});
|
||||
autoUpdater.on("error", (error) => {
|
||||
if (this.activeSilentCheckFailureRestoreStatus) {
|
||||
this.publishSilentCheckFailure(error, this.activeSilentCheckFailureRestoreStatus);
|
||||
return;
|
||||
}
|
||||
this.publishStatus({
|
||||
lastError: formatError(error),
|
||||
progress: undefined,
|
||||
|
|
@ -246,11 +263,26 @@ class AppUpdateService {
|
|||
}
|
||||
this.startupCheckTimer = setTimeout(() => {
|
||||
this.startupCheckTimer = undefined;
|
||||
void this.checkForUpdates();
|
||||
void this.checkForUpdates({ silentFailure: true });
|
||||
}, readEnvNumber("CCR_UPDATE_STARTUP_DELAY_MS") ?? startupCheckDelayMs);
|
||||
this.startupCheckTimer.unref?.();
|
||||
}
|
||||
|
||||
private publishSilentCheckFailure(error: unknown, restoreStatus: AppUpdateStatus): AppUpdateStatus {
|
||||
const state = restoreStatus.state === "checking" || restoreStatus.state === "error" ? "idle" : restoreStatus.state;
|
||||
return this.publishStatus({
|
||||
availableVersion: restoreStatus.availableVersion,
|
||||
downloadedAt: restoreStatus.downloadedAt,
|
||||
lastCheckedAt: restoreStatus.lastCheckedAt,
|
||||
lastError: formatError(error),
|
||||
progress: restoreStatus.progress,
|
||||
releaseDate: restoreStatus.releaseDate,
|
||||
releaseName: restoreStatus.releaseName,
|
||||
releaseNotes: restoreStatus.releaseNotes,
|
||||
state
|
||||
});
|
||||
}
|
||||
|
||||
private publishUpdateInfo(
|
||||
state: AppUpdateStatus["state"],
|
||||
info: UpdateInfo | UpdateDownloadedEvent,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
AddApiKeyDraft, AddProfileDraft, AddProviderDraft, AddRoutingRuleDraft, AgentAnalysisSessionSelection, AgentAnalysisSnapshot, AgentFilterValue,
|
||||
ApiKeyConfig, AppConfig, appCopy, AppI18nContext, AppInfo, AppUpdateStatus,
|
||||
ApiKeyConfig, AppConfig, appCopy, AppI18nContext, AppInfo,
|
||||
AppLanguagePreference, applyProviderProbeResult, AppToast, BotGatewaySavedConfig, buildExtensionList, claudeDesignRoutingConfigFromDraft,
|
||||
buildRouterConditionPath,
|
||||
ClaudeDesignRoutingDraft, ClaudeDesignRoutingRuleDraft, cloneConfig, createApiKeyDraft, createApiKeyEditDraft,
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
enforceSingleEnabledGlobalProfilePerAgent,
|
||||
ExtensionConfigTarget, ExtensionDeleteTarget, ExtensionInstallDraft, ExtensionSource, fallbackAgentAnalysis, fallbackConfig,
|
||||
fallbackGatewayStatus, fallbackInfo, fallbackProxyCertificateStatus, fallbackProxyNetworkSnapshot, fallbackProxyStatus, fallbackRequestLogPage,
|
||||
fallbackUpdateStatus, fallbackUsageStats, formatJson, formatProxyCertificateInstallMessage, GatewayProviderConfig,
|
||||
fallbackUsageStats, formatJson, formatProxyCertificateInstallMessage, GatewayProviderConfig,
|
||||
fusionCustomMcpServerFromDraft, fusionCustomToolConfigFromProfile,
|
||||
GatewayProviderProbeResult, gatewayServiceMessage, GatewayStatus, getDefaultOnboardingStep, isClaudeDesignPluginConfig, isClaudeDesignRoutingDraftValid,
|
||||
isCursorProxyPluginConfig, isMacPlatform, isPlainRecord, isProfileDraftSubmittable, isProviderNameDuplicate, isProviderProbeCandidateReady,
|
||||
|
|
@ -52,8 +52,6 @@ type ProfileActionBusy = {
|
|||
surface: ProfileOpenSurface;
|
||||
};
|
||||
|
||||
type UpdateActionBusy = "" | "check" | "download" | "install";
|
||||
|
||||
const providerNamePlaceholder = "__CCR_PROVIDER_NAME__";
|
||||
const providerNameSlugPlaceholder = "__CCR_PROVIDER_NAME_SLUG__";
|
||||
const providerInternalNamePlaceholder = "__CCR_PROVIDER_INTERNAL_NAME__";
|
||||
|
|
@ -162,8 +160,6 @@ function App() {
|
|||
const [proxyCertificateStatus, setProxyCertificateStatus] = useState<ProxyCertificateStatus>(fallbackProxyCertificateStatus);
|
||||
const [proxyNetworkSnapshot, setProxyNetworkSnapshot] = useState<ProxyNetworkSnapshot>(fallbackProxyNetworkSnapshot);
|
||||
const [proxyStatus, setProxyStatus] = useState<ProxyStatus>(fallbackProxyStatus);
|
||||
const [updateStatus, setUpdateStatus] = useState<AppUpdateStatus>(fallbackUpdateStatus);
|
||||
const [updateActionBusy, setUpdateActionBusy] = useState<UpdateActionBusy>("");
|
||||
const [actionBusy, setActionBusy] = useState<ServerActionBusy>("");
|
||||
const [gatewayActionBusy, setGatewayActionBusy] = useState(false);
|
||||
const [actionMessage, setActionMessage] = useState("");
|
||||
|
|
@ -308,9 +304,7 @@ function App() {
|
|||
.finally(() => setOnboardingStatusLoaded(true));
|
||||
void window.ccr.getPluginMarketplace().then(setPluginMarketplace).catch(() => setPluginMarketplace([]));
|
||||
void window.ccr.getProxyCertificateStatus().then(setProxyCertificateStatus);
|
||||
void window.ccr.getUpdateStatus().then(setUpdateStatus).catch(() => setUpdateStatus(fallbackUpdateStatus));
|
||||
const unsubscribeOpenSettings = window.ccr.onOpenSettingsRequest(openSettingsDialog);
|
||||
const unsubscribeUpdateStatus = window.ccr.onUpdateStatusChanged(setUpdateStatus);
|
||||
const refreshRuntimeStatus = () => {
|
||||
void window.ccr?.getGatewayStatus().then(setGatewayStatus);
|
||||
void window.ccr?.getProxyStatus().then(setProxyStatus);
|
||||
|
|
@ -321,7 +315,6 @@ function App() {
|
|||
return () => {
|
||||
window.clearInterval(timer);
|
||||
unsubscribeOpenSettings();
|
||||
unsubscribeUpdateStatus();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -801,62 +794,6 @@ function App() {
|
|||
}, 1800);
|
||||
}
|
||||
|
||||
async function checkAppUpdate() {
|
||||
if (!window.ccr) {
|
||||
showToast(t("Updates are only available in packaged builds."));
|
||||
return;
|
||||
}
|
||||
setUpdateActionBusy("check");
|
||||
try {
|
||||
const status = await window.ccr.updateCheck();
|
||||
setUpdateStatus(status);
|
||||
if (status.state === "error" && status.lastError) {
|
||||
showToast(status.lastError);
|
||||
} else if (status.state === "not-available") {
|
||||
showToast(t("No updates available"));
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(formatUnknownError(error));
|
||||
} finally {
|
||||
setUpdateActionBusy("");
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAppUpdate() {
|
||||
if (!window.ccr) {
|
||||
showToast(t("Updates are only available in packaged builds."));
|
||||
return;
|
||||
}
|
||||
setUpdateActionBusy("download");
|
||||
try {
|
||||
const status = await window.ccr.updateDownload();
|
||||
setUpdateStatus(status);
|
||||
if (status.state === "error" && status.lastError) {
|
||||
showToast(status.lastError);
|
||||
} else if (status.state === "not-available") {
|
||||
showToast(t("No updates available"));
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(formatUnknownError(error));
|
||||
} finally {
|
||||
setUpdateActionBusy("");
|
||||
}
|
||||
}
|
||||
|
||||
async function installAppUpdate() {
|
||||
if (!window.ccr) {
|
||||
showToast(t("Updates are only available in packaged builds."));
|
||||
return;
|
||||
}
|
||||
setUpdateActionBusy("install");
|
||||
try {
|
||||
await window.ccr.updateInstall();
|
||||
} catch (error) {
|
||||
showToast(formatUnknownError(error));
|
||||
setUpdateActionBusy("");
|
||||
}
|
||||
}
|
||||
|
||||
function updateConfig(mutator: (config: AppConfig) => AppConfig) {
|
||||
setDraftConfig((current) => {
|
||||
const next = normalizeConfig(mutator(cloneConfig(current)));
|
||||
|
|
@ -2689,9 +2626,6 @@ function App() {
|
|||
isMac={isMac}
|
||||
needsTrafficLightSafeArea={needsTrafficLightSafeArea}
|
||||
networkCaptureEnabled={networkCaptureEnabled}
|
||||
onCheckUpdate={checkAppUpdate}
|
||||
onDownloadUpdate={downloadAppUpdate}
|
||||
onInstallUpdate={installAppUpdate}
|
||||
onOpenSettings={openSettingsDialog}
|
||||
onSelectNavigationItem={selectNavigationItem}
|
||||
onToggleSidebar={() => setSidebarOpen((current) => !current)}
|
||||
|
|
@ -2699,8 +2633,6 @@ function App() {
|
|||
shouldReduceMotion={shouldReduceMotion}
|
||||
sidebarOpen={sidebarOpen}
|
||||
toggleGatewayService={toggleGatewayService}
|
||||
updateActionBusy={updateActionBusy}
|
||||
updateStatus={updateStatus}
|
||||
visibleNavigation={visibleNavigation}
|
||||
viewProps={{
|
||||
apiKeys: {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { ComponentProps } from "react";
|
||||
import {
|
||||
AnimatedIconSwap, AnimatePresence, AppConfig, AppCopy, AppUpdateStatus, Button, Check, CircleAlert, cn, EndpointTitleBar,
|
||||
AnimatedIconSwap, AnimatePresence, AppConfig, AppCopy, Button, cn, EndpointTitleBar,
|
||||
GatewayStatus, listSpringTransition, LucideIcon, motion, motionEase,
|
||||
LoaderCircle, NavigationId, PanelLeftClose, PanelLeftOpen,
|
||||
reducedMotionTransition, RefreshCw, ServiceControlButton, Settings, ViewId,
|
||||
NavigationId, PanelLeftClose, PanelLeftOpen,
|
||||
reducedMotionTransition, ServiceControlButton, Settings, ViewId,
|
||||
ViewMotionShell, viewUsesInternalScroll
|
||||
} from "../shared";
|
||||
import { ApiKeysView } from "./api-keys";
|
||||
|
|
@ -22,8 +22,6 @@ type MainNavigationItem = {
|
|||
id: NavigationId;
|
||||
};
|
||||
|
||||
type UpdateActionBusy = "" | "check" | "download" | "install";
|
||||
|
||||
type MainViewProps = {
|
||||
apiKeys: ComponentProps<typeof ApiKeysView>;
|
||||
extensions: ComponentProps<typeof ExtensionsView>;
|
||||
|
|
@ -65,17 +63,12 @@ export function MainLayout({
|
|||
needsTrafficLightSafeArea,
|
||||
agentAnalysisEnabled,
|
||||
networkCaptureEnabled,
|
||||
onCheckUpdate,
|
||||
onDownloadUpdate,
|
||||
onInstallUpdate,
|
||||
onOpenSettings,
|
||||
onSelectNavigationItem,
|
||||
onToggleSidebar,
|
||||
shouldReduceMotion,
|
||||
sidebarOpen,
|
||||
toggleGatewayService,
|
||||
updateActionBusy,
|
||||
updateStatus,
|
||||
viewProps,
|
||||
requestLogsEnabled,
|
||||
visibleNavigation
|
||||
|
|
@ -90,23 +83,17 @@ export function MainLayout({
|
|||
isMac: boolean;
|
||||
needsTrafficLightSafeArea: boolean;
|
||||
networkCaptureEnabled: boolean;
|
||||
onCheckUpdate: () => Promise<void>;
|
||||
onDownloadUpdate: () => Promise<void>;
|
||||
onInstallUpdate: () => Promise<void>;
|
||||
onOpenSettings: () => void;
|
||||
onSelectNavigationItem: (id: NavigationId) => void;
|
||||
onToggleSidebar: () => void;
|
||||
shouldReduceMotion: boolean | null;
|
||||
sidebarOpen: boolean;
|
||||
toggleGatewayService: () => void;
|
||||
updateActionBusy: UpdateActionBusy;
|
||||
updateStatus: AppUpdateStatus;
|
||||
viewProps: MainViewProps;
|
||||
requestLogsEnabled: boolean;
|
||||
visibleNavigation: MainNavigationItem[];
|
||||
}) {
|
||||
const windowControlSafeAreaWidth = isMac ? 152 : 88;
|
||||
const showUpdateButton = isUpdateButtonVisible(updateStatus);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -159,19 +146,6 @@ export function MainLayout({
|
|||
<div className="app-drag min-w-0 flex-1" />
|
||||
</div>
|
||||
|
||||
{showUpdateButton ? (
|
||||
<div className="shrink-0 px-2 pb-2">
|
||||
<SidebarUpdateButton
|
||||
actionBusy={updateActionBusy}
|
||||
copy={copy}
|
||||
onCheck={onCheckUpdate}
|
||||
onDownload={onDownloadUpdate}
|
||||
onInstall={onInstallUpdate}
|
||||
status={updateStatus}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<nav className="flex min-h-0 flex-1 flex-col gap-1 px-2 py-3 max-[720px]:flex-none max-[720px]:flex-row max-[720px]:overflow-x-auto max-[720px]:py-2" aria-label={copy.sidebar.primaryNavigation}>
|
||||
{visibleNavigation.map((item) => (
|
||||
<Button
|
||||
|
|
@ -256,95 +230,6 @@ export function MainLayout({
|
|||
);
|
||||
}
|
||||
|
||||
function SidebarUpdateButton({
|
||||
actionBusy,
|
||||
copy,
|
||||
onCheck,
|
||||
onDownload,
|
||||
onInstall,
|
||||
status
|
||||
}: {
|
||||
actionBusy: UpdateActionBusy;
|
||||
copy: AppCopy;
|
||||
onCheck: () => Promise<void>;
|
||||
onDownload: () => Promise<void>;
|
||||
onInstall: () => Promise<void>;
|
||||
status: AppUpdateStatus;
|
||||
}) {
|
||||
const t = (value: string) => copy.text[value] ?? value;
|
||||
const busy = Boolean(actionBusy) || status.state === "checking" || status.state === "downloading" || status.state === "installing";
|
||||
const canInstall = status.canInstall || status.state === "downloaded";
|
||||
const canDownload = status.canDownload || status.state === "available";
|
||||
const canCheck = status.canCheck || status.state === "error" || status.state === "not-available" || status.state === "idle";
|
||||
const label = updateButtonLabel(status, t);
|
||||
const detail = status.state === "downloading" && typeof status.progress?.percent === "number"
|
||||
? `${Math.max(0, Math.min(100, status.progress.percent)).toFixed(0)}%`
|
||||
: status.state === "error" ? t("Check for updates")
|
||||
: status.state === "not-available" ? t("No updates available")
|
||||
: status.availableVersion ? `v${status.availableVersion}` : "";
|
||||
const title = status.state === "error" && status.lastError ? `${label}: ${status.lastError}` : detail ? `${label} ${detail}` : label;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
"flex min-h-10 w-full min-w-0 items-center gap-2 rounded-md border px-2 py-2 text-left text-[12px] font-medium transition-colors disabled:cursor-default disabled:opacity-80",
|
||||
status.state === "error"
|
||||
? "border-destructive/25 bg-destructive/10 text-destructive hover:bg-destructive/15"
|
||||
: status.state === "not-available" || status.state === "idle"
|
||||
? "border-border bg-muted/55 text-muted-foreground hover:bg-muted"
|
||||
: "border-primary/20 bg-primary/10 text-primary hover:bg-primary/15"
|
||||
)}
|
||||
disabled={busy || (!canDownload && !canInstall && !canCheck)}
|
||||
onClick={() => {
|
||||
if (canInstall) {
|
||||
void onInstall();
|
||||
return;
|
||||
}
|
||||
if (canDownload) {
|
||||
void onDownload();
|
||||
return;
|
||||
}
|
||||
void onCheck();
|
||||
}}
|
||||
title={title}
|
||||
type="button"
|
||||
unstyled
|
||||
>
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-current/10">
|
||||
<AnimatedIconSwap iconKey={busy ? "busy" : status.state === "error" ? "error" : canInstall ? "install" : "refresh"}>
|
||||
{busy ? <LoaderCircle className="h-3.5 w-3.5 animate-spin" /> : status.state === "error" ? <CircleAlert className="h-3.5 w-3.5" /> : canInstall ? <Check className="h-3.5 w-3.5" /> : <RefreshCw className="h-3.5 w-3.5" />}
|
||||
</AnimatedIconSwap>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{label}</span>
|
||||
{detail ? <span className="max-w-[92px] shrink-0 truncate text-[11px] opacity-75">{detail}</span> : null}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function isUpdateButtonVisible(status: AppUpdateStatus): boolean {
|
||||
return (
|
||||
status.supported ||
|
||||
status.state === "available" ||
|
||||
status.state === "downloaded" ||
|
||||
status.state === "downloading" ||
|
||||
status.state === "installing" ||
|
||||
status.state === "error" ||
|
||||
status.canDownload ||
|
||||
status.canCheck ||
|
||||
status.canInstall
|
||||
);
|
||||
}
|
||||
|
||||
function updateButtonLabel(status: AppUpdateStatus, t: (value: string) => string): string {
|
||||
if (status.state === "checking") return t("Checking for updates");
|
||||
if (status.state === "downloading") return t("Downloading update");
|
||||
if (status.state === "installing") return t("Install and restart");
|
||||
if (status.canInstall || status.state === "downloaded") return t("Install and restart");
|
||||
if (status.canDownload || status.state === "available") return t("Download update");
|
||||
if (status.state === "error") return t("Update failed");
|
||||
return t("Check for updates");
|
||||
}
|
||||
|
||||
function MainViewSwitch({
|
||||
activeView,
|
||||
agentAnalysisEnabled,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue