Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/internal/buildinfo/buildinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "strings"

// Set at link time via -ldflags (see .goreleaser.yaml).
var (
Version = "dev0.1.42"
Version = "dev0.1.43"
Commit = "none"
Date = "unknown"
)
Expand Down
24 changes: 24 additions & 0 deletions electron/clovapi-desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,29 @@ function saveProfiles(payload) {
});
}

async function switchProviderModel(cliKind, providerId, modelId) {
const result = await runClovapiArgsAsync(
[
"switch",
"--cli",
String(cliKind || ""),
"--provider",
String(providerId || ""),
"--model",
String(modelId || ""),
],
{ timeout: 45000 },
);
if (result.error && result.error.code === "ETIMEDOUT") {
return { ok: false, error: "clovapi switch timed out" };
}
if (!result.ok) {
const message = String(result.stderr || result.stdout || "clovapi switch failed").trim();
return { ok: false, error: message || "clovapi switch failed" };
}
return { ok: true, stdout: result.stdout, stderr: result.stderr };
}

function listVendorModels(vendorName) {
return runDesktop(["vendor", "list-models", "--vendor", String(vendorName || "")], {
timeout: 45000,
Expand Down Expand Up @@ -122,6 +145,7 @@ module.exports = {
loadProxyConfig,
saveProxyConfig,
saveProfiles,
switchProviderModel,
listVendorModels,
testBinding,
modelAdapters,
Expand Down
33 changes: 9 additions & 24 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { createGoProxyManager } = require("./proxy-manager");
const proxyLogger = require("./proxy-logger");
const callLogsStore = require("./call-logs-store");
const clovapiDesktop = require("./clovapi-desktop");
const { applyTrayModelSwitch } = require("./tray-model-switch");
const {
coreDevStatePath,
resolveClovapiExecutable: resolveBundledClovapiExecutable,
Expand Down Expand Up @@ -232,31 +233,15 @@ async function readTrayDesktopState() {
}

async function switchTrayAgentModel(cliKind, providerId, modelId) {
const kind = String(cliKind || "").trim();
const provider = String(providerId || "").trim();
const model = String(modelId || "").trim();
if (!kind || !provider || !model) return;

const loaded = await clovapiDesktop.loadProfiles();
if (!loaded?.ok) {
emitOutput("stderr", `[tray] failed to load profiles: ${loaded?.error || "unknown error"}\n`);
return;
}

const active = loaded.active && typeof loaded.active === "object" ? { ...loaded.active } : {};
active[kind] = { provider_id: provider, model_id: model };
const saved = await clovapiDesktop.saveProfiles({
profiles: Array.isArray(loaded.profiles) ? loaded.profiles : [],
active,
proxy: loaded.proxy,
await applyTrayModelSwitch({
desktop: clovapiDesktop,
cliKind,
providerId,
modelId,
emitOutput,
dispatchRendererEvent,
updateTrayMenu,
});
if (!saved?.ok) {
emitOutput("stderr", `[tray] failed to switch ${kind} model: ${saved?.error || "unknown error"}\n`);
return;
}

dispatchRendererEvent({ type: "profiles-changed" });
await updateTrayMenu();
}

async function updateTrayMenu() {
Expand Down
2 changes: 1 addition & 1 deletion electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"build:icons": "node scripts/build-icons.mjs",
"build:mac": "npm run build:ui && npm run build:icons && electron-builder --mac dmg",
"build:win": "npm run build:ui && electron-builder --win nsis",
"test": "node --test proxy-manager.test.js clovapi-exec.test.js tray-menu.test.js desktop-update.test.js",
"test": "node --test proxy-manager.test.js clovapi-exec.test.js tray-menu.test.js tray-model-switch.test.js desktop-update.test.js",
"start": "npm run build:ui && electron ."
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions electron/proxy-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ module.exports = {
buildProxyStopArgs,
normalizeBindHost,
healthClientHost,
reachableLoopbackHost,
healthUrl,
redactSecrets,
createGoProxyManager,
Expand Down
32 changes: 32 additions & 0 deletions electron/tray-model-switch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
async function applyTrayModelSwitch(options = {}) {
const desktop = options.desktop;
const emitOutput = typeof options.emitOutput === "function" ? options.emitOutput : () => {};
const dispatchRendererEvent =
typeof options.dispatchRendererEvent === "function" ? options.dispatchRendererEvent : () => {};
const updateTrayMenu = typeof options.updateTrayMenu === "function" ? options.updateTrayMenu : async () => {};

const kind = String(options.cliKind || "").trim();
const provider = String(options.providerId || "").trim();
const model = String(options.modelId || "").trim();
if (!kind || !provider || !model) return { ok: false, skipped: true };
if (!desktop || typeof desktop.switchProviderModel !== "function") {
emitOutput("stderr", `[tray] failed to switch ${kind} model: clovapi switch is unavailable\n`);
return { ok: false, error: "clovapi switch is unavailable" };
}

const result = await desktop.switchProviderModel(kind, provider, model);
if (!result?.ok) {
const error = String(result?.error || "unknown error").trim() || "unknown error";
emitOutput("stderr", `[tray] failed to switch ${kind} model: ${error}\n`);
await updateTrayMenu();
return { ok: false, error };
}

dispatchRendererEvent({ type: "profiles-changed" });
await updateTrayMenu();
return { ok: true };
}

module.exports = {
applyTrayModelSwitch,
};
60 changes: 60 additions & 0 deletions electron/tray-model-switch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const assert = require("node:assert/strict");
const test = require("node:test");
const { applyTrayModelSwitch } = require("./tray-model-switch");

test("applyTrayModelSwitch delegates tray selections to clovapi switch", async () => {
const calls = [];
const result = await applyTrayModelSwitch({
desktop: {
async switchProviderModel(cliKind, providerId, modelId) {
calls.push({ cliKind, providerId, modelId });
return { ok: true };
},
async saveProfiles() {
throw new Error("tray switch must not save profiles directly");
},
},
cliKind: "codex",
providerId: "custom-api",
modelId: "gpt-5.5",
dispatchRendererEvent(payload) {
calls.push({ event: payload });
},
async updateTrayMenu() {
calls.push({ updateTrayMenu: true });
},
});

assert.equal(result.ok, true);
assert.deepEqual(calls, [
{ cliKind: "codex", providerId: "custom-api", modelId: "gpt-5.5" },
{ event: { type: "profiles-changed" } },
{ updateTrayMenu: true },
]);
});

test("applyTrayModelSwitch reports switch failures without profile change events", async () => {
const errors = [];
const events = [];
const result = await applyTrayModelSwitch({
desktop: {
async switchProviderModel() {
return { ok: false, error: "write failed" };
},
},
cliKind: "hermes",
providerId: "custom-api",
modelId: "claude-sonnet",
emitOutput(stream, message) {
errors.push({ stream, message });
},
dispatchRendererEvent(payload) {
events.push(payload);
},
});

assert.equal(result.ok, false);
assert.equal(result.error, "write failed");
assert.deepEqual(events, []);
assert.deepEqual(errors, [{ stream: "stderr", message: "[tray] failed to switch hermes model: write failed\n" }]);
});