Skip to content

Commit 0a9ebc4

Browse files
joohwcursoragent
andcommitted
release(desktop): v0.1.1 with single-instance and tray UX fixes
Enforce single-instance startup, refresh tray menu before popup on Windows, and make the tray entry always show or focus the main window. Clear transient proxy/update status when leaving Settings, and bump the desktop shell to v0.1.1. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 05c83d2 commit 0a9ebc4

11 files changed

Lines changed: 128 additions & 61 deletions

File tree

electron/main.js

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ app.commandLine.appendSwitch("enable-features", "OverlayScrollbar,FluentOverlayS
2020
fs.mkdirSync(electronUserDataDir(), { recursive: true });
2121
app.setPath("userData", electronUserDataDir());
2222

23+
const gotSingleInstanceLock = app.requestSingleInstanceLock();
24+
if (!gotSingleInstanceLock) {
25+
app.quit();
26+
}
27+
2328
let mainWindow = null;
2429
let tray = null;
30+
let trayContextMenu = null;
2531
let runningProcess = null;
2632
let quitting = false;
2733
const THEME_STORAGE_KEY = "clovapi-theme";
@@ -105,6 +111,7 @@ function showMainWindow(options = {}) {
105111
mainWindow.show();
106112
if (process.platform === "darwin") app.dock?.show();
107113
mainWindow.focus();
114+
void updateTrayMenu();
108115
if (!eventPayload) return;
109116
const sendAppEvent = () => dispatchRendererEvent(eventPayload);
110117
if (created || mainWindow.webContents.isLoading()) {
@@ -117,14 +124,7 @@ function showMainWindow(options = {}) {
117124
function hideMainWindow() {
118125
if (!mainWindow || mainWindow.isDestroyed()) return;
119126
mainWindow.hide();
120-
}
121-
122-
function toggleMainWindow() {
123-
if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) {
124-
showMainWindow();
125-
return;
126-
}
127-
hideMainWindow();
127+
void updateTrayMenu();
128128
}
129129

130130
async function readTrayProxyState() {
@@ -214,7 +214,6 @@ async function updateTrayMenu() {
214214
if (!tray) return;
215215
const [state, desktop] = await Promise.all([readTrayProxyState(), readTrayDesktopState()]);
216216
const model = buildTrayMenuModel({
217-
visible: Boolean(mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()),
218217
running: state.running,
219218
port: state.port,
220219
external: state.external,
@@ -227,7 +226,7 @@ async function updateTrayMenu() {
227226
const template = [
228227
{
229228
label: model.windowLabel,
230-
click: () => toggleMainWindow(),
229+
click: () => showMainWindow(),
231230
},
232231
{
233232
label: model.profilesLabel,
@@ -300,13 +299,29 @@ async function updateTrayMenu() {
300299
},
301300
},
302301
];
303-
tray.setContextMenu(Menu.buildFromTemplate(template));
302+
trayContextMenu = Menu.buildFromTemplate(template);
303+
if (process.platform === "darwin") {
304+
tray.setContextMenu(trayContextMenu);
305+
}
306+
}
307+
308+
function openTrayContextMenu() {
309+
if (!tray || !trayContextMenu) return;
310+
tray.popUpContextMenu(trayContextMenu);
304311
}
305312

306313
function createTray() {
307314
if (tray) return tray;
308315
tray = new Tray(createTrayImage());
309-
tray.on("click", () => toggleMainWindow());
316+
tray.on("click", () => showMainWindow());
317+
if (process.platform !== "darwin") {
318+
tray.on("right-click", () => {
319+
void (async () => {
320+
await updateTrayMenu();
321+
openTrayContextMenu();
322+
})();
323+
});
324+
}
310325
void updateTrayMenu();
311326
return tray;
312327
}
@@ -1019,25 +1034,31 @@ ipcMain.handle("cli:tool-status", async () => {
10191034
return { ok: false, available: false, source: "none", path: "", error: "No bundled or system clovapi found" };
10201035
});
10211036

1022-
app.whenReady().then(async () => {
1023-
nativeTheme.themeSource = "light";
1024-
createWindow();
1025-
createTray();
1026-
watchCoreDevBinary();
1027-
try {
1028-
await proxyManager.autostartIfAllowed();
1029-
} catch {
1030-
// Non-fatal on startup
1031-
}
1032-
await updateTrayMenu();
1037+
if (gotSingleInstanceLock) {
1038+
app.on("second-instance", () => {
1039+
showMainWindow();
1040+
});
10331041

1034-
app.on("activate", () => {
1035-
if (BrowserWindow.getAllWindows().length === 0) {
1036-
createWindow();
1042+
app.whenReady().then(async () => {
1043+
nativeTheme.themeSource = "light";
1044+
createWindow();
1045+
createTray();
1046+
watchCoreDevBinary();
1047+
try {
1048+
await proxyManager.autostartIfAllowed();
1049+
} catch {
1050+
// Non-fatal on startup
10371051
}
1038-
showMainWindow();
1052+
await updateTrayMenu();
1053+
1054+
app.on("activate", () => {
1055+
if (BrowserWindow.getAllWindows().length === 0) {
1056+
createWindow();
1057+
}
1058+
showMainWindow();
1059+
});
10391060
});
1040-
});
1061+
}
10411062

10421063
app.on("before-quit", () => {
10431064
quitting = true;

electron/package-lock.json

Lines changed: 68 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "clovapi-desktop-minimal",
33
"private": true,
4-
"version": "0.1.0",
4+
"version": "0.1.1",
55
"description": "Minimal desktop CLI runner for ClovAPI",
66
"main": "main.js",
77
"type": "commonjs",

electron/tray-menu.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,12 @@ function resolveActiveBindings(state = {}) {
166166
}
167167

168168
function buildTrayMenuModel(state = {}) {
169-
const visible = Boolean(state.visible);
170169
const running = Boolean(state.running);
171170
const port = Number(state.port) || 27483;
172171
const bindings = resolveActiveBindings(state);
173172

174173
return {
175-
windowLabel: visible ? "Hide ClovAPI Switcher" : "Show ClovAPI Switcher",
174+
windowLabel: "Show ClovAPI Switcher",
176175
profilesLabel: "Open Profiles",
177176
settingsLabel: "Open Settings",
178177
logsLabel: "Open Call Logs",

electron/tray-menu.test.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ test("resolveActiveBindings maps active selections to vendor/model labels", () =
9696

9797
test("buildTrayMenuModel exposes safe proxy actions and active models", () => {
9898
const running = buildTrayMenuModel({
99-
visible: true,
10099
running: true,
101100
managed: true,
102101
port: 27483,
@@ -109,7 +108,7 @@ test("buildTrayMenuModel exposes safe proxy actions and active models", () => {
109108
],
110109
active: { hermes: { provider_id: "custom-api", model_id: "gpt-4.1" } },
111110
});
112-
assert.equal(running.windowLabel, "Hide ClovAPI Switcher");
111+
assert.equal(running.windowLabel, "Show ClovAPI Switcher");
113112
assert.equal(running.canStartProxy, false);
114113
assert.equal(running.startProxyLabel, "Start Proxy on :27483");
115114
assert.match(running.statusLabel, /Proxy running/);
@@ -125,7 +124,7 @@ test("buildTrayMenuModel exposes safe proxy actions and active models", () => {
125124
]);
126125
assert.equal("canStopProxy" in running, false);
127126

128-
const stopped = buildTrayMenuModel({ visible: false, running: false, port: 3000 });
127+
const stopped = buildTrayMenuModel({ running: false, port: 3000 });
129128
assert.equal(stopped.windowLabel, "Show ClovAPI Switcher");
130129
assert.equal(stopped.canStartProxy, true);
131130
assert.equal(stopped.startProxyLabel, "Start Proxy on :3000");

electron/ui/src/components/ListRow.svelte

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script lang="ts">
22
import type { Snippet } from "svelte";
33
import { cn } from "$lib/utils.js";
4-
import { i18n, t } from "../lib/i18n";
54
import type { ModelTestStatus } from "../global";
65
76
let {
@@ -10,7 +9,6 @@
109
showStatusDot = false,
1110
testStatus = "",
1211
testSummary = "",
13-
testDetail = "",
1412
muted = false,
1513
indent = false,
1614
linesNowrap = false,
@@ -24,7 +22,6 @@
2422
showStatusDot?: boolean;
2523
testStatus?: "" | ModelTestStatus;
2624
testSummary?: string;
27-
testDetail?: string;
2825
muted?: boolean;
2926
indent?: boolean;
3027
linesNowrap?: boolean;
@@ -53,10 +50,6 @@
5350
testStatus !== "pass" && testStatus !== "fail" && testStatus !== "testing" && "text-muted-foreground",
5451
),
5552
);
56-
const viewDetailsLabel = $derived.by(() => {
57-
void i18n.locale;
58-
return t("common.viewDetails");
59-
});
6053
</script>
6154

6255
{#snippet titleRow(clickable: boolean)}
@@ -121,16 +114,6 @@
121114
{line}
122115
</p>
123116
{/each}
124-
{#if testDetail && (testStatus === "pass" || testStatus === "fail")}
125-
<details class="group text-xs">
126-
<summary class="cursor-pointer text-muted-foreground hover:text-foreground">{viewDetailsLabel}</summary>
127-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
128-
<pre
129-
class="mt-1.5 max-h-48 overflow-auto rounded-md border border-border bg-muted/40 p-2 font-mono text-[11px] leading-relaxed break-words whitespace-pre-wrap"
130-
tabindex="0"
131-
>{testDetail}</pre>
132-
</details>
133-
{/if}
134117
</div>
135118
</div>
136119
<div class="flex shrink-0 flex-wrap items-center gap-2 sm:justify-end">

electron/ui/src/components/ProxyPanel.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282
lines={electronDev ? [copy.versionLine, copy.updateDisabledInDev] : [copy.versionLine]}
8383
testStatus={electronDev ? "" : rowTestStatus(coreUpdateCheck?.status)}
8484
testSummary={electronDev ? "" : coreUpdateCheck?.summary || ""}
85-
testDetail={electronDev ? "" : coreUpdateCheck?.detail || ""}
8685
>
8786
{#snippet actions()}
8887
{#if !electronDev}

electron/ui/src/lib/i18n/locales/en.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ const en = {
3737
empty: "(empty)",
3838
noOutput: "(no output)",
3939
loading: "Loading…",
40-
viewDetails: "View details",
4140
},
4241
vendor: {
4342
claudeSubscription: "Claude Subscription",
@@ -127,7 +126,6 @@ const en = {
127126
updateUpToDate: "Up to date",
128127
updateAvailable: "Update available · v{latest}",
129128
updateInstalled: "Updated to v{version}",
130-
updateInstalledPath: "Installed at {path}",
131129
updateDisabledInDev: "Update checks are disabled in dev mode",
132130
pathHint: "/{providerId}/{modelId}/{apiStyle}",
133131
},

electron/ui/src/lib/i18n/locales/zh.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const zh = {
3939
empty: "(空)",
4040
noOutput: "(无输出)",
4141
loading: "加载中…",
42-
viewDetails: "查看详情",
4342
},
4443
vendor: {
4544
claudeSubscription: "Claude 订阅",
@@ -125,7 +124,6 @@ const zh = {
125124
updateUpToDate: "已是最新版本",
126125
updateAvailable: "有可用更新 · v{latest}",
127126
updateInstalled: "已更新至 v{version}",
128-
updateInstalledPath: "安装路径 {path}",
129127
updateDisabledInDev: "开发模式不检查更新",
130128
pathHint: "/{providerId}/{modelId}/{apiStyle}",
131129
},

electron/ui/src/lib/store/navigation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export function setActiveTab(tab: TabId) {
3030
if (store.activeTab === "system-logs" && tab !== "system-logs") {
3131
store.proxySystemLogSelectedId = null;
3232
}
33+
if (store.activeTab === "settings" && tab !== "settings") {
34+
store.coreUpdateCheck = null;
35+
store.proxyHealthTest = null;
36+
}
3337
store.activeTab = tab;
3438
if (tab === "profiles") {
3539
void (async () => {

0 commit comments

Comments
 (0)