Skip to content

Commit 83f7f68

Browse files
author
李杰
committed
release: v0.20.12
1 parent d0c0e64 commit 83f7f68

9 files changed

Lines changed: 158 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ All notable changes to Cockpit Tools will be documented in this file.
66

77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

9+
---
10+
## [0.20.12] - 2026-03-27
11+
12+
### Changed
13+
- **macOS tray interactions now align left-click and right-click behavior with native expectations while clearing stale menu highlight state**: left-click release now focuses and restores the main window, right-click press opens the tray context menu, and native menu teardown explicitly clears status-item highlight to avoid a stuck highlighted icon.
14+
- **Antigravity account store persistence now keeps only minimal account snapshots and recovers gracefully when localStorage quota is exceeded**: persisted token fields are sanitized, quota snapshots exclude heavy model payloads, and quota overflow now auto-cleans legacy/new cache keys instead of repeatedly failing writes.
15+
916
---
1017
## [0.20.11] - 2026-03-27
1118

CHANGELOG.zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
88

9+
---
10+
## [0.20.12] - 2026-03-27
11+
12+
### 变更
13+
- **macOS 托盘交互现已按原生习惯对齐左/右键行为,并在菜单关闭时清理残留高亮状态**:左键抬起会直接恢复并聚焦主窗口,右键按下会打开托盘上下文菜单,原生菜单收起时会显式清理状态栏图标高亮,避免图标长期停留在高亮态。
14+
- **Antigravity 账号持久化现已只保存最小快照,并在 localStorage 超限时自动恢复**:持久化 token 会做脱敏处理,quota 快照不再落盘大体积模型数据,存储配额超限时会自动清理新旧账号缓存键,避免写入持续失败。
15+
916
---
1017
## [0.20.11] - 2026-03-27
1118

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cockpit-tools",
33
"private": true,
4-
"version": "0.20.11",
4+
"version": "0.20.12",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cockpit-tools"
3-
version = "0.20.11"
3+
version = "0.20.12"
44
description = "Cockpit Tools"
55
authors = ["jlcodes"]
66
license = "CC-BY-NC-SA-4.0"

src-tauri/native/macos-native-menu/Sources/MacosNativeMenuSwift/NativeMenuPopoverController.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ final class NativeMenuPopoverController: NSObject, ObservableObject, NSMenuDeleg
127127
if self.statusItem?.menu === menu {
128128
self.statusItem?.menu = nil
129129
}
130+
self.clearStatusItemHighlight()
130131
}
131132

132133
var selectedPlatform: NativeMenuPlatform? {
@@ -320,6 +321,7 @@ final class NativeMenuPopoverController: NSObject, ObservableObject, NSMenuDeleg
320321
return
321322
}
322323

324+
self.clearStatusItemHighlight()
323325
statusItem.menu = menu
324326
statusItem.popUpMenu(menu)
325327
}
@@ -329,6 +331,16 @@ final class NativeMenuPopoverController: NSObject, ObservableObject, NSMenuDeleg
329331
if let menu, self.statusItem?.menu === menu {
330332
self.statusItem?.menu = nil
331333
}
334+
self.clearStatusItemHighlight()
335+
}
336+
337+
private func clearStatusItemHighlight() {
338+
guard let button = self.statusItem?.button else {
339+
return
340+
}
341+
button.highlight(false)
342+
button.needsDisplay = true
343+
button.displayIfNeeded()
332344
}
333345

334346
private func makeHostingItem<Content: View>(_ view: Content) -> NSMenuItem {

src-tauri/src/modules/tray.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3269,9 +3269,16 @@ fn handle_tray_event<R: Runtime>(tray: &TrayIcon<R>, event: TrayIconEvent) {
32693269
} => {
32703270
#[cfg(target_os = "macos")]
32713271
{
3272-
if button_state == MouseButtonState::Down
3273-
&& matches!(button, MouseButton::Left | MouseButton::Right)
3274-
{
3272+
if button == MouseButton::Left && button_state == MouseButtonState::Up {
3273+
if let Some(window) = tray.app_handle().get_webview_window("main") {
3274+
let _ = window.show();
3275+
let _ = window.unminimize();
3276+
let _ = window.set_focus();
3277+
}
3278+
return;
3279+
}
3280+
3281+
if button == MouseButton::Right && button_state == MouseButtonState::Down {
32753282
let app = tray.app_handle().clone();
32763283
let app_for_menu = app.clone();
32773284
let _ = app.run_on_main_thread(move || {
@@ -3293,6 +3300,12 @@ fn handle_tray_event<R: Runtime>(tray: &TrayIcon<R>, event: TrayIconEvent) {
32933300
button: MouseButton::Left,
32943301
..
32953302
} => {
3303+
#[cfg(target_os = "macos")]
3304+
{
3305+
return;
3306+
}
3307+
3308+
#[cfg(not(target_os = "macos"))]
32963309
if let Some(window) = tray.app_handle().get_webview_window("main") {
32973310
let _ = window.show();
32983311
let _ = window.unminimize();

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Cockpit Tools",
4-
"version": "0.20.11",
4+
"version": "0.20.12",
55
"identifier": "com.jlcodes.cockpit-tools",
66
"build": {
77
"beforeDevCommand": "npm run dev",

src/stores/useAccountStore.ts

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,112 @@
11
import { create } from 'zustand';
22
import { persist, createJSONStorage } from 'zustand/middleware';
3-
import { Account, RefreshStats } from '../types/account';
3+
import { Account, QuotaData, RefreshStats, TokenData } from '../types/account';
44
import * as accountService from '../services/accountService';
55
import { emitAccountsChanged, emitCurrentAccountChanged } from '../utils/accountSyncEvents';
66

77
const ACCOUNTS_STORE_KEY = 'agtools.accounts.store.v1';
8+
const LEGACY_ACCOUNTS_CACHE_KEY = 'agtools.accounts.cache';
9+
const LEGACY_CURRENT_ACCOUNT_CACHE_KEY = 'agtools.accounts.current';
10+
11+
let accountStoreQuotaCleanupScheduled = false;
12+
let accountStoreQuotaWarned = false;
13+
14+
function isQuotaExceededError(error: unknown): boolean {
15+
if (error instanceof DOMException) {
16+
return (
17+
error.name === 'QuotaExceededError' ||
18+
error.code === 22 ||
19+
error.code === 1014
20+
);
21+
}
22+
const message = String(error);
23+
return message.includes('QuotaExceededError') || message.includes('quota');
24+
}
25+
26+
function scheduleAccountStoreQuotaRecovery(storageKey: string) {
27+
if (typeof window === 'undefined' || accountStoreQuotaCleanupScheduled) return;
28+
accountStoreQuotaCleanupScheduled = true;
29+
setTimeout(() => {
30+
try {
31+
localStorage.removeItem(storageKey);
32+
localStorage.removeItem(LEGACY_ACCOUNTS_CACHE_KEY);
33+
localStorage.removeItem(LEGACY_CURRENT_ACCOUNT_CACHE_KEY);
34+
} catch (error) {
35+
console.warn('[AccountStore] 清理超限缓存失败:', error);
36+
} finally {
37+
accountStoreQuotaCleanupScheduled = false;
38+
}
39+
}, 0);
40+
}
41+
42+
const accountStoreStorage = createJSONStorage(() => ({
43+
getItem: (name: string) => {
44+
try {
45+
return localStorage.getItem(name);
46+
} catch (error) {
47+
console.warn(`[AccountStore] 读取持久化数据失败: ${name}`, error);
48+
return null;
49+
}
50+
},
51+
setItem: (name: string, value: string) => {
52+
try {
53+
localStorage.setItem(name, value);
54+
} catch (error) {
55+
if (isQuotaExceededError(error)) {
56+
if (!accountStoreQuotaWarned) {
57+
console.warn(
58+
'[AccountStore] 本地缓存空间不足,已自动清理账号缓存并回退为仅内存态。',
59+
error
60+
);
61+
accountStoreQuotaWarned = true;
62+
}
63+
scheduleAccountStoreQuotaRecovery(name);
64+
return;
65+
}
66+
console.warn(`[AccountStore] 写入持久化数据失败: ${name}`, error);
67+
}
68+
},
69+
removeItem: (name: string) => {
70+
try {
71+
localStorage.removeItem(name);
72+
} catch (error) {
73+
console.warn(`[AccountStore] 删除持久化数据失败: ${name}`, error);
74+
}
75+
},
76+
}));
77+
78+
function toPersistedTokenSnapshot(token: TokenData): TokenData {
79+
return {
80+
access_token: '',
81+
refresh_token: '',
82+
expires_in: 0,
83+
expiry_timestamp: 0,
84+
token_type: token.token_type || 'Bearer',
85+
email: token.email,
86+
project_id: token.project_id,
87+
is_gcp_tos: token.is_gcp_tos,
88+
session_id: token.session_id,
89+
};
90+
}
91+
92+
function toPersistedQuotaSnapshot(quota?: QuotaData): QuotaData | undefined {
93+
if (!quota) return undefined;
94+
return {
95+
models: [],
96+
last_updated: quota.last_updated ?? 0,
97+
is_forbidden: quota.is_forbidden,
98+
subscription_tier: quota.subscription_tier,
99+
tier_id: quota.tier_id,
100+
};
101+
}
102+
103+
function toPersistedAccountSnapshot(account: Account): Account {
104+
return {
105+
...account,
106+
token: toPersistedTokenSnapshot(account.token),
107+
quota: toPersistedQuotaSnapshot(account.quota),
108+
};
109+
}
8110

9111
// 防抖状态(在 store 外部维护,避免触发 re-render)
10112
let fetchAccountsPromise: Promise<void> | null = null;
@@ -256,18 +358,20 @@ export const useAccountStore = create<AccountState>()(
256358
}),
257359
{
258360
name: ACCOUNTS_STORE_KEY,
259-
storage: createJSONStorage(() => localStorage),
361+
storage: accountStoreStorage,
260362
partialize: (state) => ({
261-
accounts: state.accounts,
262-
currentAccount: state.currentAccount,
363+
accounts: state.accounts.map(toPersistedAccountSnapshot),
364+
currentAccount: state.currentAccount
365+
? toPersistedAccountSnapshot(state.currentAccount)
366+
: null,
263367
}),
264368
onRehydrateStorage: () => (state) => {
265369
// Migrate from old ACCOUNTS_CACHE_KEY if the new state is empty
266370
if (state && state.accounts.length === 0 && typeof window !== 'undefined') {
267371
setTimeout(() => {
268372
try {
269-
const oldAccountsRaw = localStorage.getItem('agtools.accounts.cache');
270-
const oldCurrentRaw = localStorage.getItem('agtools.accounts.current');
373+
const oldAccountsRaw = localStorage.getItem(LEGACY_ACCOUNTS_CACHE_KEY);
374+
const oldCurrentRaw = localStorage.getItem(LEGACY_CURRENT_ACCOUNT_CACHE_KEY);
271375
let hasMigrated = false;
272376

273377
if (oldAccountsRaw) {
@@ -287,8 +391,8 @@ export const useAccountStore = create<AccountState>()(
287391

288392
// Cleanup the old keys if we migrated successfully
289393
if (hasMigrated) {
290-
localStorage.removeItem('agtools.accounts.cache');
291-
localStorage.removeItem('agtools.accounts.current');
394+
localStorage.removeItem(LEGACY_ACCOUNTS_CACHE_KEY);
395+
localStorage.removeItem(LEGACY_CURRENT_ACCOUNT_CACHE_KEY);
292396
}
293397
} catch (error) {
294398
// ignore migration errors
@@ -298,4 +402,3 @@ export const useAccountStore = create<AccountState>()(
298402
},
299403
}
300404
));
301-

0 commit comments

Comments
 (0)