Compare commits

..
8 Commits
Author SHA1 Message Date
Draculabo f5c44c549f chore: release v0.2.0
Publish Release / publish (ubuntu-latest) (push) Failing after 6m12s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-16 22:26:55 +08:00
Draculabo f37127d385 build: conditionally include plugins based on start command in forge.config.ts 2026-01-16 22:00:33 +08:00
Draculabo f636dd61a5 feat: enhance cloudHandler to inject minimal auth state when database entry is missing 2026-01-16 20:30:49 +08:00
Draculabo 42973561fb chore: upgrade electron from 32.3.3 to 37.3.1 2026-01-16 02:58:26 +08:00
Draculabo 5772e2e90e feat: implement stability fixes and enhance error handling 2026-01-16 02:51:29 +08:00
Draculabo b0ec96d4a6 fix: Converting circular structure to JSON 2026-01-15 22:58:05 +08:00
Draculabo 5a004d9d16 docs: add curly brace constraints for conditional statements 2026-01-15 22:54:23 +08:00
Draculabo ccc5c91969 docs: fix incorrect reference documentation name 2026-01-15 22:37:35 +08:00
26 changed files with 1324 additions and 140 deletions

No files matched your search

+1
View File
@@ -263,6 +263,7 @@ export enum BudgetType {
- 信任既定前置条件,避免对承诺不变量的过度防御;若出现冲突,更新方案而非额外兜底。
- 老代码重构保守推进,新功能实现可采用更现代方式。
- 避免过早优化:先以简单直接的实现达成功能,确需时再优化(避免先行加缓存/抖动或拆分成过多文件)。
- 对于 if,while 等条件、循环语句必须加上花括号,不能与return、break、continue 等语句同级。
### 错误处理 (Error Handling)
+17
View File
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.0] - 2026-01-16
### New Features
- Enhanced cloudHandler to inject minimal auth state when database entry is missing, improving onboarding reliability.
- Implemented stability fixes and enhanced error handling across the application.
### Improvements
- Upgraded Electron from 32.3.3 to 37.3.1 for improved performance and security.
- Conditionally include plugins based on start command in forge.config.ts for better build flexibility.
### Bug Fixes
- Fixed "Converting circular structure to JSON" error.
### Documentation
- Added curly brace constraints for conditional statements.
- Fixed incorrect reference documentation name.
## [0.1.1] - 2026-01-11
### Bug Fixes
+1 -1
View File
@@ -1 +1 @@
请始终参考 @AGENT.md 文件
请始终参考 @AGENTS.md 文件
+16 -11
View File
@@ -12,6 +12,8 @@ import * as path from 'path';
const nativeModules = ['better-sqlite3', 'keytar', 'bindings', 'file-uri-to-path'];
const isStartCommand = process.argv.some((arg) => arg.includes('start'));
const config: ForgeConfig = {
packagerConfig: {
asar: {
@@ -80,7 +82,6 @@ const config: ForgeConfig = {
},
],
plugins: [
new AutoUnpackNativesPlugin({}),
new VitePlugin({
build: [
{
@@ -101,16 +102,20 @@ const config: ForgeConfig = {
},
],
}),
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
...(!isStartCommand
? [
new AutoUnpackNativesPlugin({}),
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
]
: []),
],
};
+369 -71
View File
@@ -1,13 +1,13 @@
{
"name": "antigravity-manager",
"version": "0.0.1",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "antigravity-manager",
"version": "0.1.0",
"license": "MIT",
"version": "0.1.1",
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
@@ -52,6 +52,7 @@
"i18next": "^25.6.3",
"i18next-browser-languagedetector": "^8.2.0",
"keytar": "^7.9.0",
"lodash-es": "^4.17.22",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
@@ -89,13 +90,14 @@
"@types/better-sqlite3": "^7.6.12",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/eslint-config-prettier": "^6.11.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^4.7.0",
"babel-plugin-react-compiler": "1.0.0",
"code-inspector-plugin": "^1.3.3",
"electron": "^32.3.3",
"electron": "^37.3.1",
"electron-devtools-installer": "^4.0.0",
"electron-playwright-helpers": "^1.8.2",
"eslint": "^9.39.1",
@@ -945,9 +947,9 @@
}
},
"node_modules/@electron-forge/cli": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.10.2.tgz",
"integrity": "sha512-X1RtS5IqNgzGDS2rr1q0Y74wU/m3DbU4vSgllNun1ZQv1BfMpDcKLhnKi3aeetoA0huLTpMVU9eWJ7bziI9fxA==",
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.11.1.tgz",
"integrity": "sha512-pk8AoLsr7t7LBAt0cFD06XFA6uxtPdvtLx06xeal7O9o7GHGCbj29WGwFoJ8Br/ENM0Ho868S3PrAn1PtBXt5g==",
"dev": true,
"funding": [
{
@@ -961,9 +963,9 @@
],
"license": "MIT",
"dependencies": {
"@electron-forge/core": "7.10.2",
"@electron-forge/core-utils": "7.10.2",
"@electron-forge/shared-types": "7.10.2",
"@electron-forge/core": "7.11.1",
"@electron-forge/core-utils": "7.11.1",
"@electron-forge/shared-types": "7.11.1",
"@electron/get": "^3.0.0",
"@inquirer/prompts": "^6.0.1",
"@listr2/prompt-adapter-inquirer": "^2.0.22",
@@ -984,10 +986,39 @@
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/cli/node_modules/@electron-forge/shared-types": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/tracer": "7.11.1",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/cli/node_modules/@electron-forge/tracer": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/core": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.10.2.tgz",
"integrity": "sha512-HAIuOtpOfGjA0cd55tbEV2gAv+A7tSZg9bonmVDYFEe6dBgbLk8a3+/1fJUdWW8fyFkg1wa8zK7pjP751bAXsA==",
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.11.1.tgz",
"integrity": "sha512-YtuPLzggPKPabFAD2rOZFE0s7f4KaUTpGRduhSMbZUqpqD1TIPyfoDBpYiZvao3Ht8pyZeOJjbzcC0LpFs9gIQ==",
"dev": true,
"funding": [
{
@@ -1001,17 +1032,17 @@
],
"license": "MIT",
"dependencies": {
"@electron-forge/core-utils": "7.10.2",
"@electron-forge/maker-base": "7.10.2",
"@electron-forge/plugin-base": "7.10.2",
"@electron-forge/publisher-base": "7.10.2",
"@electron-forge/shared-types": "7.10.2",
"@electron-forge/template-base": "7.10.2",
"@electron-forge/template-vite": "7.10.2",
"@electron-forge/template-vite-typescript": "7.10.2",
"@electron-forge/template-webpack": "7.10.2",
"@electron-forge/template-webpack-typescript": "7.10.2",
"@electron-forge/tracer": "7.10.2",
"@electron-forge/core-utils": "7.11.1",
"@electron-forge/maker-base": "7.11.1",
"@electron-forge/plugin-base": "7.11.1",
"@electron-forge/publisher-base": "7.11.1",
"@electron-forge/shared-types": "7.11.1",
"@electron-forge/template-base": "7.11.1",
"@electron-forge/template-vite": "7.11.1",
"@electron-forge/template-vite-typescript": "7.11.1",
"@electron-forge/template-webpack": "7.11.1",
"@electron-forge/template-webpack-typescript": "7.11.1",
"@electron-forge/tracer": "7.11.1",
"@electron/get": "^3.0.0",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
@@ -1041,13 +1072,13 @@
}
},
"node_modules/@electron-forge/core-utils": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.10.2.tgz",
"integrity": "sha512-JXrk2hWR4q8KgZFABpojjuqql3tYeVIH6qmtbkNEkZEQq7YIxajJBCct7J7bWfNQTmHotsQ3k5KLknhyhTaBMw==",
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.11.1.tgz",
"integrity": "sha512-9UxRWVsfcziBsbAA2MS0Oz4yYovQCO2BhnGIfsbKNTBtMc/RcVSxAS0NMyymce44i43p1ZC/FqWhnt1XqYw3bQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.10.2",
"@electron-forge/shared-types": "7.11.1",
"@electron/rebuild": "^3.7.0",
"@malept/cross-spawn-promise": "^2.0.0",
"chalk": "^4.0.0",
@@ -1062,6 +1093,105 @@
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/core-utils/node_modules/@electron-forge/shared-types": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/tracer": "7.11.1",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/core-utils/node_modules/@electron-forge/tracer": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/core/node_modules/@electron-forge/maker-base": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.11.1.tgz",
"integrity": "sha512-yhZrCGoN6bDeiB5DHFaueZ1h84AReElEj+f0hl2Ph4UbZnO0cnLpbx+Bs+XfMLAiA+beC8muB5UDK5ysfuT9BQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.11.1",
"fs-extra": "^10.0.0",
"which": "^2.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/core/node_modules/@electron-forge/plugin-base": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.11.1.tgz",
"integrity": "sha512-lKpSOV1GA3FoYiD9k05i6v4KaQVmojnRgCr7d6VL1bFp13QOtXSaAWhFI9mtSY7rGElOacX6Zt7P7rPoB8T9eQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.11.1"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/core/node_modules/@electron-forge/publisher-base": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-7.11.1.tgz",
"integrity": "sha512-rXE9oMFGMtdQrixnumWYH5TTGsp99iPHZb3jI74YWq518ctCh6DlIgWlhf6ok2X0+lhWovcIb45KJucUFAQ13w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.11.1"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/core/node_modules/@electron-forge/shared-types": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/tracer": "7.11.1",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/core/node_modules/@electron-forge/tracer": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/maker-base": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.10.2.tgz",
@@ -1262,14 +1392,14 @@
}
},
"node_modules/@electron-forge/template-base": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.10.2.tgz",
"integrity": "sha512-D9DbEx3rtikIhUyn4tcz2pJqHNU/+FXKNnzSvmrJoJ9LusR3C42OU9GtbU8oT3nawpnCGgPFIOGXrzexFPp6DA==",
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.11.1.tgz",
"integrity": "sha512-XpTaEf+EfQw+0BlSAtSpZKYIKYvKu4raNzSGHZZoSYHp+HDC7R+MlpFQmSJiGdYQzQ14C+uxO42tVjgM0DMbpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/core-utils": "7.10.2",
"@electron-forge/shared-types": "7.10.2",
"@electron-forge/core-utils": "7.11.1",
"@electron-forge/shared-types": "7.11.1",
"@malept/cross-spawn-promise": "^2.0.0",
"debug": "^4.3.1",
"fs-extra": "^10.0.0",
@@ -1280,15 +1410,44 @@
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/template-vite": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.10.2.tgz",
"integrity": "sha512-hR9HBOM902yq7zhFl8bO3w5ufMgitdd5ZwDzAdKITFh2ttZemHy9ha5S0K+R+4GoXHz8t7hUTHk8+iPy09qrpA==",
"node_modules/@electron-forge/template-base/node_modules/@electron-forge/shared-types": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.10.2",
"@electron-forge/template-base": "7.10.2",
"@electron-forge/tracer": "7.11.1",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/template-base/node_modules/@electron-forge/tracer": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/template-vite": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.11.1.tgz",
"integrity": "sha512-Or8Lxf4awoeUZoMTKJEw5KQDIhqOFs24WhVka3yZXxc6VgVWN79KmYKYM6uM/YMQttmafhsBhY2t1Lxo1WR/ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.11.1",
"@electron-forge/template-base": "7.11.1",
"fs-extra": "^10.0.0"
},
"engines": {
@@ -1296,29 +1455,87 @@
}
},
"node_modules/@electron-forge/template-vite-typescript": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.10.2.tgz",
"integrity": "sha512-df7rpxxIOIyZn0RfQ1GIlLW7dXhxkerc9uZ3ozO4C7zfvip3z0Mg+wS1synktPfr4WISaPktIdnj3mVu6Uu7Mw==",
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.11.1.tgz",
"integrity": "sha512-Us4AHXFb+4z+gXgZImSqMBS63oKnsQWLOhqRg321xiDzu2UcQPlwgWNb4rAEKNVC1e7LXrUNDHuBiTrQkvWXbg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.10.2",
"@electron-forge/template-base": "7.10.2",
"@electron-forge/shared-types": "7.11.1",
"@electron-forge/template-base": "7.11.1",
"fs-extra": "^10.0.0"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/template-webpack": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.10.2.tgz",
"integrity": "sha512-VIUXA+XHM5SLjg7fIpOOmBsgi0LstkjrEz4gUzVL0AaITM7e+BCziIHld1ceXLbQ1FnKtrUGnQ9X/cHYxYvhHg==",
"node_modules/@electron-forge/template-vite-typescript/node_modules/@electron-forge/shared-types": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.10.2",
"@electron-forge/template-base": "7.10.2",
"@electron-forge/tracer": "7.11.1",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/template-vite-typescript/node_modules/@electron-forge/tracer": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/template-vite/node_modules/@electron-forge/shared-types": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/tracer": "7.11.1",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/template-vite/node_modules/@electron-forge/tracer": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/template-webpack": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.11.1.tgz",
"integrity": "sha512-15lbXxi+er461MPk6sbwAOyjofAHwmQjTvxNCiNpaU2naEwbj3t0SlLq/BMr5HxnVOaMmA7+lKV9afkIom+d4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.11.1",
"@electron-forge/template-base": "7.11.1",
"fs-extra": "^10.0.0"
},
"engines": {
@@ -1326,14 +1543,14 @@
}
},
"node_modules/@electron-forge/template-webpack-typescript": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.10.2.tgz",
"integrity": "sha512-JtrLUAFbxxWJ1kU7b8MNyL5SO9/rY5UeNz1b9hvMvilW8GxyMWUen58dafgdnx3OpKLNZnhOOhgRagNppEzJOA==",
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.11.1.tgz",
"integrity": "sha512-6ExfFnFkHBz8rvRFTFg5HVGTC12uJpbVk4q8DVg0R8rhhxhqiVNh8lF2UPtZ2yT2UtGWjXNVlyP3Y3T6q6E3GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.10.2",
"@electron-forge/template-base": "7.10.2",
"@electron-forge/shared-types": "7.11.1",
"@electron-forge/template-base": "7.11.1",
"fs-extra": "^10.0.0",
"typescript": "~5.4.5",
"webpack": "^5.69.1"
@@ -1342,6 +1559,35 @@
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/template-webpack-typescript/node_modules/@electron-forge/shared-types": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/tracer": "7.11.1",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/template-webpack-typescript/node_modules/@electron-forge/tracer": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/template-webpack-typescript/node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
@@ -1356,6 +1602,35 @@
"node": ">=14.17"
}
},
"node_modules/@electron-forge/template-webpack/node_modules/@electron-forge/shared-types": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz",
"integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/tracer": "7.11.1",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/template-webpack/node_modules/@electron-forge/tracer": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz",
"integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/tracer": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.10.2.tgz",
@@ -8110,6 +8385,23 @@
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/mute-stream": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz",
@@ -8562,9 +8854,9 @@
}
},
"node_modules/@vscode/sudo-prompt": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz",
"integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==",
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz",
"integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==",
"dev": true,
"license": "MIT"
},
@@ -10738,9 +11030,9 @@
"license": "MIT"
},
"node_modules/diff": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
"integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -10820,15 +11112,15 @@
"license": "MIT"
},
"node_modules/electron": {
"version": "32.3.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-32.3.3.tgz",
"integrity": "sha512-7FT8tDg+MueAw8dBn5LJqDvlM4cZkKJhXfgB3w7P5gvSoUQVAY6LIQcXJxgL+vw2rIRY/b9ak7ZBFbCMF2Bk4w==",
"version": "37.10.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz",
"integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^20.9.0",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -11374,9 +11666,9 @@
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "20.19.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"version": "22.19.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15618,6 +15910,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.22",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -20523,9 +20821,9 @@
}
},
"node_modules/undici": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
"integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
+4 -8
View File
@@ -1,7 +1,7 @@
{
"name": "antigravity-manager",
"productName": "Antigravity Manager",
"version": "0.1.1",
"version": "0.2.0",
"description": "Antigravity Manager - Electron App",
"main": ".vite/build/main.js",
"private": true,
@@ -47,13 +47,14 @@
"@types/better-sqlite3": "^7.6.12",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/eslint-config-prettier": "^6.11.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^4.7.0",
"babel-plugin-react-compiler": "1.0.0",
"code-inspector-plugin": "^1.3.3",
"electron": "^32.3.3",
"electron": "^37.3.1",
"electron-devtools-installer": "^4.0.0",
"electron-playwright-helpers": "^1.8.2",
"eslint": "^9.39.1",
@@ -120,6 +121,7 @@
"i18next": "^25.6.3",
"i18next-browser-languagedetector": "^8.2.0",
"keytar": "^7.9.0",
"lodash-es": "^4.17.22",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
@@ -132,11 +134,5 @@
"update-electron-app": "^3.1.1",
"uuid": "^13.0.0",
"zod": "^4.1.12"
},
"ts-node": {
"transpileOnly": true,
"compilerOptions": {
"module": "commonjs"
}
}
}
+29 -1
View File
@@ -313,7 +313,35 @@ export class CloudAccountRepo {
.get('jetskiStateSync.agentManagerInitState') as { value: string } | undefined;
if (!row || !row.value) {
throw new Error('jetskiStateSync.agentManagerInitState not found in database');
logger.warn(
'jetskiStateSync.agentManagerInitState not found. ' +
'Injecting minimal auth state only. User may need to complete onboarding in the IDE first.',
);
// Inject minimal state: auth status and onboarding flag only
const authStatus = {
name: account.name || account.email,
email: account.email,
apiKey: account.token.access_token,
};
db.prepare('INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)').run(
'antigravityAuthStatus',
JSON.stringify(authStatus),
);
db.prepare('INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)').run(
'antigravityOnboarding',
'true',
);
db.prepare('DELETE FROM ItemTable WHERE key = ?').run('google.antigravity');
logger.info(
`Injected minimal auth state for ${account.email} (no protobuf state available)`,
);
return; // Early return - skip protobuf manipulation
}
// 1. Decode Base64
+2 -1
View File
@@ -7,6 +7,7 @@ import {
Usage,
GroundingMetadata,
} from './types';
import { decodeSignature } from './signature-utils';
/**
* Non-streaming response processor (Gemini -> Claude)
@@ -55,7 +56,7 @@ class NonStreamingProcessor {
}
private processPart(part: GeminiPart) {
const signature = part.thoughtSignature || null;
const signature = decodeSignature(part.thoughtSignature) || null;
// 1. Handle FunctionCall
if (part.functionCall) {
+55 -2
View File
@@ -1,5 +1,6 @@
import { GeminiPart, Usage, UsageMetadata, GroundingMetadata } from './types';
import { GeminiPart, Usage, UsageMetadata } from './types';
import { SignatureStore } from './SignatureStore';
import { decodeSignature } from './signature-utils';
type BlockType = 'None' | 'Text' | 'Thinking' | 'Function';
@@ -43,6 +44,8 @@ export class StreamingState {
public webSearchQuery: string | null = null;
public groundingChunks: any[] | null = null;
private parseErrorCount: number = 0;
constructor() {}
public emit(eventType: string, data: any): string {
@@ -242,6 +245,56 @@ export class StreamingState {
public storeSignature(signature?: string) {
this.signatures.store(signature);
}
public handleParseError(rawData: string): string[] {
const chunks: string[] = [];
this.parseErrorCount++;
console.warn(
`[SSE-Parser] Parse error #${this.parseErrorCount}. Raw data length: ${rawData.length}`,
);
// Safely close current block
if (this.blockType !== 'None') {
chunks.push(...this.endBlock());
}
// Emit error event if too many errors
if (this.parseErrorCount > 3) {
console.error(
`[SSE-Parser] High error rate (${this.parseErrorCount} errors). Stream may be corrupted.`,
);
chunks.push(
this.emit('error', {
type: 'error',
error: {
type: 'network_error',
message: 'Unstable network connection. Please check your network or proxy settings.',
code: 'stream_decode_error',
details: {
error_count: this.parseErrorCount,
suggestion: 'Check network connection',
},
},
}),
);
}
return chunks;
}
/**
* Reset error state (call after recovery)
*/
public resetErrorState(): void {
this.parseErrorCount = 0;
}
/**
* Get current error count (for monitoring)
*/
public getErrorCount(): number {
return this.parseErrorCount;
}
}
/**
@@ -252,7 +305,7 @@ export class PartProcessor {
public process(part: GeminiPart): string[] {
const chunks: string[] = [];
const signature = part.thoughtSignature || undefined;
const signature = decodeSignature(part.thoughtSignature);
// 1. Handle FunctionCall
if (part.functionCall) {
+6 -1
View File
@@ -126,7 +126,12 @@ export class QuotaService {
} catch (error: any) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const text = JSON.stringify(error.response?.data || '');
let text = '';
try {
text = JSON.stringify(error.response?.data || '');
} catch {
text = '[Unable to serialize response data]';
}
// ✅ Handle 403 Forbidden specifically - return immediately, do not retry
if (status === 403) {
+49
View File
@@ -0,0 +1,49 @@
/** Jitter factor: ±20% */
const JITTER_FACTOR = 0.2;
/** Maximum retry delay in milliseconds (30 seconds) */
const MAX_DELAY_MS = 30000;
/**
* Apply jitter to a delay value to prevent thundering herd
* Returns delay ± JITTER_FACTOR (e.g., 1000ms ± 20% = 800-1200ms)
*
* @param delayMs - Base delay in milliseconds
* @returns Jittered delay (minimum 1ms)
*/
export function applyJitter(delayMs: number): number {
if (delayMs <= 0) {
return 1;
}
const jitterRange = delayMs * JITTER_FACTOR;
const jitter = (Math.random() * 2 - 1) * jitterRange;
return Math.max(1, Math.round(delayMs + jitter));
}
/**
* Calculate retry delay with exponential backoff and jitter
*
* @param attempt - Current attempt number (0-indexed)
* @param baseMs - Base delay for first retry (default 1000ms)
* @returns Delay in milliseconds with jitter applied
*/
export function calculateRetryDelay(attempt: number, baseMs: number = 1000): number {
// Exponential backoff: 1s, 2s, 4s, 8s, ...
const exponentialDelay = baseMs * Math.pow(2, attempt);
// Cap at maximum delay
const cappedDelay = Math.min(exponentialDelay, MAX_DELAY_MS);
// Apply jitter
return applyJitter(cappedDelay);
}
/**
* Sleep for specified milliseconds
*
* @param ms - Milliseconds to sleep
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
+32
View File
@@ -0,0 +1,32 @@
import { isEmpty } from 'lodash-es';
/**
* Decode Base64-encoded signature if present
* Gemini sends Base64, Claude expects raw string
*
* @param signature - The signature string (may be Base64 or raw)
* @returns Decoded signature or undefined if input is empty
*/
export function decodeSignature(signature: string | undefined | null): string | undefined {
if (isEmpty(signature)) {
return undefined;
}
try {
const decoded = Buffer.from(signature!, 'base64').toString('utf-8');
// Validate: Check if it looks like valid UTF-8 text
// If decoding produces replacement characters, it wasn't valid Base64
if (decoded && !decoded.includes('\ufffd') && decoded.length > 0) {
// Additional check: re-encode and compare to detect false positives
const reEncoded = Buffer.from(decoded).toString('base64');
if (reEncoded === signature) {
return decoded;
}
}
} catch {
// Not valid Base64, return as-is
}
// Return original if not Base64
return signature!;
}
+75
View File
@@ -0,0 +1,75 @@
/**
* Stream Error Utilities
* Classifies network errors and formats user-friendly messages
*
* Problem: Technical error messages confuse users
* Solution: Classify errors and provide localized, actionable messages
*/
export interface ClassifiedError {
type: string;
message: string;
}
/**
* Classify a stream error and return user-friendly message
*
* @param error - The error object
* @returns Classified error with type and localized message
*/
export function classifyStreamError(error: Error): ClassifiedError {
const msg = error.message.toLowerCase();
if (msg.includes('timeout') || msg.includes('timedout')) {
return {
type: 'timeout_error',
message: 'Request timed out. Please check your network connection or try again later.',
};
}
if (msg.includes('econnrefused') || msg.includes('connect')) {
return {
type: 'connection_error',
message: 'Unable to connect to server. Please check your network.',
};
}
if (msg.includes('enotfound') || msg.includes('network') || msg.includes('dns')) {
return {
type: 'network_error',
message: 'Network connection failed. Please check your network settings.',
};
}
if (msg.includes('socket') || msg.includes('aborted') || msg.includes('reset')) {
return {
type: 'stream_error',
message: 'Data transfer interrupted. Please try again later.',
};
}
return {
type: 'unknown_error',
message: `Unknown error occurred: ${error.message.substring(0, 100)}`,
};
}
/**
* Format error as SSE event for Claude protocol
*
* @param errorType - Error type string
* @param message - User-friendly message
* @returns SSE-formatted error event string
*/
export function formatErrorForSSE(errorType: string, message: string): string {
const errorEvent = {
type: 'error',
error: {
type: errorType,
message: message,
code: 'stream_error',
},
};
return `event: error\ndata: ${JSON.stringify(errorEvent)}\n\n`;
}
+116
View File
@@ -0,0 +1,116 @@
/**
* Thinking Block Utilities
* Handles validation and graceful degradation of thinking blocks
*
* Problem: Invalid signatures cause thinking blocks to be dropped, losing content
* Solution: Validate and convert invalid blocks to text instead of dropping
*/
/** Minimum length for a valid thought signature */
const MIN_SIGNATURE_LENGTH = 10;
export interface ThinkingBlock {
type: 'thinking';
thinking: string;
signature?: string;
}
export interface TextBlock {
type: 'text';
text: string;
}
export type ContentBlock = ThinkingBlock | TextBlock | { type: string; [key: string]: any };
/**
* Check if a thinking block has a valid signature
*
* Rules:
* - Empty thinking + any signature = valid (trailing signature case)
* - Non-empty thinking + signature >= MIN_SIGNATURE_LENGTH = valid
* - Non-empty thinking + no/short signature = invalid
* - Non-thinking blocks = always valid
*
* @param block - Content block to validate
* @returns true if block is valid or not a thinking block
*/
export function hasValidSignature(block: ContentBlock): boolean {
if (block.type !== 'thinking') {
return true; // Non-thinking blocks are always valid
}
const thinkingBlock = block as ThinkingBlock;
// Empty thinking + any signature = valid (trailing signature)
if (!thinkingBlock.thinking || thinkingBlock.thinking.length === 0) {
return (
thinkingBlock.signature !== undefined &&
thinkingBlock.signature !== null &&
thinkingBlock.signature.length >= MIN_SIGNATURE_LENGTH
);
}
// Non-empty thinking needs a long enough signature
return (
thinkingBlock.signature !== undefined &&
thinkingBlock.signature !== null &&
thinkingBlock.signature.length >= MIN_SIGNATURE_LENGTH
);
}
/**
* Gracefully degrade an invalid thinking block
*
* - Valid blocks: return as-is
* - Invalid with content: convert to text block
* - Invalid without content: return null (should be dropped)
*
* @param block - Content block to process
* @returns Processed block or null if should be dropped
*/
export function gracefulDegradeThinking(block: ContentBlock): ContentBlock | null {
if (block.type !== 'thinking') {
return block;
}
const thinkingBlock = block as ThinkingBlock;
if (hasValidSignature(block)) {
return block;
}
// Invalid signature - check if we have content to preserve
if (thinkingBlock.thinking && thinkingBlock.thinking.length > 0) {
// Convert to text block to preserve content
console.info(
`[Thinking-Utils] Converting invalid thinking block to text. Content length: ${thinkingBlock.thinking.length}`,
);
return {
type: 'text',
text: thinkingBlock.thinking,
};
}
// Empty content + invalid signature = drop
console.debug('[Thinking-Utils] Dropping empty thinking block with invalid signature');
return null;
}
/**
* Process an array of content blocks, applying graceful degradation
*
* @param blocks - Array of content blocks
* @returns Processed array with invalid thinking blocks handled
*/
export function processThinkingBlocks(blocks: ContentBlock[]): ContentBlock[] {
const result: ContentBlock[] = [];
for (const block of blocks) {
const processed = gracefulDegradeThinking(block);
if (processed !== null) {
result.push(processed);
}
}
return result;
}
+23 -3
View File
@@ -20,9 +20,28 @@ import { ConfigManager } from './ipc/config/manager';
const packetLogPath = path.join(app.getPath('userData'), 'orpc_packets.log');
/**
* Safely stringify an object, handling circular references
*/
function safeStringifyPacket(obj: unknown): string {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
});
}
function logPacket(data: any) {
try {
fs.appendFileSync(packetLogPath, `[${new Date().toISOString()}] ${JSON.stringify(data)}\n`);
fs.appendFileSync(
packetLogPath,
`[${new Date().toISOString()}] ${safeStringifyPacket(data)}\n`,
);
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
@@ -120,8 +139,9 @@ function createWindow() {
logger.info('Page finished loading successfully');
});
mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
logger.info(`[Renderer Console] ${message} (${sourceId}:${line})`);
mainWindow.webContents.on('console-message', (details) => {
const { level, message, lineNumber, sourceId } = details;
logger.info(`[Renderer Console][${level}] ${message} (${sourceId}:${lineNumber})`);
});
}
@@ -27,7 +27,8 @@ export class GeminiClient {
this.logger.error(`Gemini Stream API Error: ${error.message}`);
throw new Error(error.response?.data?.error?.message || error.message);
}
throw error;
// Re-throw as clean Error to avoid circular reference issues
throw error instanceof Error ? new Error(error.message) : new Error(String(error));
}
}
@@ -50,11 +51,12 @@ export class GeminiClient {
} catch (error) {
if (axios.isAxiosError(error)) {
this.logger.error(
`Gemini API Error: ${error.message} - ${JSON.stringify(error.response?.data)}`,
`Gemini API Error: ${error.message} - ${this.safeStringify(error.response?.data)}`,
);
throw new Error(error.response?.data?.error?.message || error.message);
}
throw error;
// Re-throw as clean Error to avoid circular reference issues
throw error instanceof Error ? new Error(error.message) : new Error(String(error));
}
}
@@ -77,7 +79,6 @@ export class GeminiClient {
return response.data;
} catch (error) {
this.handleAxiosError(error, 'StreamInternal');
throw error;
}
}
@@ -96,17 +97,38 @@ export class GeminiClient {
return response.data.response || response.data;
} catch (error) {
this.handleAxiosError(error, 'GenerateInternal');
throw error;
}
}
private handleAxiosError(error: any, context: string) {
if (axios.isAxiosError(error)) {
this.logger.error(
`Gemini ${context} API Error: ${error.message} - ${JSON.stringify(error.response?.data)}`,
`Gemini ${context} API Error: ${error.message} - ${this.safeStringify(error.response?.data)}`,
);
throw new Error(error.response?.data?.error?.message || error.message);
}
throw error;
// Re-throw as clean Error to avoid circular reference issues
throw error instanceof Error ? new Error(error.message) : new Error(String(error));
}
/**
* Safely stringify an object, handling circular references
*/
private safeStringify(obj: unknown): string {
if (obj === undefined || obj === null) {
return String(obj);
}
try {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
});
} catch {
return '[Unserializable]';
}
}
}
+43 -2
View File
@@ -7,6 +7,11 @@ import { transformClaudeRequestIn } from '../../../lib/antigravity/ClaudeRequest
import { transformResponse } from '../../../lib/antigravity/ClaudeResponseMapper';
import { StreamingState, PartProcessor } from '../../../lib/antigravity/ClaudeStreamingMapper';
import { ClaudeRequest } from '../../../lib/antigravity/types';
import { calculateRetryDelay, sleep } from '../../../lib/antigravity/retry-utils';
import {
classifyStreamError,
formatErrorForSSE,
} from '../../../lib/antigravity/stream-error-utils';
import {
OpenAIChatRequest,
AnthropicChatRequest,
@@ -45,6 +50,13 @@ export class ProxyService {
const maxRetries = 3;
for (let i = 0; i < maxRetries; i++) {
// Fix #6: Add jitter delay for retries (skip first attempt)
if (i > 0) {
const delay = calculateRetryDelay(i - 1);
this.logger.log(`Retry attempt ${i + 1}/${maxRetries}, waiting ${delay}ms (jittered)`);
await sleep(delay);
}
const token = await this.tokenManager.getNextToken();
if (!token) {
throw new Error('No available accounts');
@@ -172,7 +184,11 @@ export class ProxyService {
let lastFinishReason: string | undefined;
let lastUsageMetadata: any | undefined;
// Fix #1: Track if we received any valid data
let receivedData = false;
upstreamStream.on('data', (chunk: Buffer) => {
receivedData = true; // Mark that we got data
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
@@ -205,19 +221,42 @@ export class ProxyService {
const chunks = processor.process(part as any);
chunks.forEach((c) => subscriber.next(c));
}
// Reset error state on successful parse
state.resetErrorState();
} catch (e) {
// Fix #3: Use handleParseError for graceful degradation
this.logger.error('Stream parse error', e);
const errorChunks = state.handleParseError(dataStr);
errorChunks.forEach((c) => subscriber.next(c));
}
}
});
upstreamStream.on('end', () => {
// Fix #1: Check for empty stream
if (!receivedData) {
this.logger.warn('Empty response stream detected');
subscriber.error(new Error('Empty response stream'));
return;
}
const finishChunks = state.emitFinish(lastFinishReason, lastUsageMetadata);
finishChunks.forEach((c) => subscriber.next(c));
subscriber.complete();
});
upstreamStream.on('error', (err: any) => subscriber.error(err));
upstreamStream.on('error', (err: any) => {
// Fix #2: Classify error and send friendly message
const cleanError = err instanceof Error ? err : new Error(String(err));
const { type, message } = classifyStreamError(cleanError);
this.logger.error(`Stream error: ${type} - ${cleanError.message}`);
// Send SSE error event before closing
subscriber.next(formatErrorForSSE(type, message));
subscriber.error(cleanError);
});
});
}
@@ -372,7 +411,9 @@ export class ProxyService {
});
upstreamStream.on('error', (err: any) => {
subscriber.error(err);
// Convert to clean Error to avoid circular reference issues (socket objects)
const cleanError = err instanceof Error ? new Error(err.message) : new Error(String(err));
subscriber.error(cleanError);
});
});
}
+92
View File
@@ -0,0 +1,92 @@
import { describe, it, expect, vi } from 'vitest';
import { EventEmitter } from 'events';
import { ProxyService } from '../../server/modules/proxy/proxy.service';
import { Observable } from 'rxjs';
// Mock dependencies
const mockTokenManager = { getNextToken: vi.fn(), markAsRateLimited: vi.fn() };
const mockGeminiClient = { streamGenerateInternal: vi.fn(), generateInternal: vi.fn() };
// Subclass to access private method
class TestableProxyService extends ProxyService {
constructor() {
super(mockTokenManager as any, mockGeminiClient as any);
}
public testProcessStream(stream: any, model: string = 'model'): Observable<string> {
// Access private method using type assertion
return (this as any).processAnthropicInternalStream(stream, model);
}
}
describe('ProxyService Empty Stream Retry Logic', () => {
it('should emit error when stream ends without data', async () => {
const service = new TestableProxyService();
const stream = new EventEmitter();
const resultObservable = service.testProcessStream(stream);
let errorReceived: Error | undefined;
const promise = new Promise<void>((resolve) => {
resultObservable.subscribe({
next: () => {},
error: (err) => {
errorReceived = err;
resolve();
},
complete: () => resolve(),
});
});
// Simulate empty stream: straight to end
setTimeout(() => stream.emit('end'), 10);
await promise;
expect(errorReceived).toBeDefined();
expect(errorReceived?.message).toBe('Empty response stream');
});
it('should NOT emit error when stream has data', async () => {
const service = new TestableProxyService();
const stream = new EventEmitter();
const resultObservable = service.testProcessStream(stream);
let errorReceived: Error | undefined;
const receivedChunks: string[] = [];
const promise = new Promise<void>((resolve) => {
resultObservable.subscribe({
next: (c) => receivedChunks.push(c),
error: (err) => {
errorReceived = err;
resolve();
},
complete: () => resolve(),
});
});
// Simulate valid data stream
setTimeout(() => {
const validJson = JSON.stringify({
candidates: [
{
content: { parts: [{ text: 'hello' }] },
finishReason: 'STOP',
},
],
});
stream.emit('data', Buffer.from(`data: ${validJson}\n\n`));
stream.emit('end');
}, 10);
await promise;
expect(errorReceived).toBeUndefined();
// It should produce chunks (though exact number depends on mapper logic, at least it shouldn't error)
// Actually our mapper might produce "message_start", "content_block_start" etc.
// We just care that it didn't error with "Empty response stream"
});
});
+59
View File
@@ -0,0 +1,59 @@
import { describe, it, expect } from 'vitest';
import { applyJitter, calculateRetryDelay } from '../../lib/antigravity/retry-utils';
describe('applyJitter', () => {
it('should return values within ±20% range', () => {
const baseDelay = 1000;
const results: number[] = [];
for (let i = 0; i < 100; i++) {
results.push(applyJitter(baseDelay));
}
// All values should be within 800-1200 range (±20%)
results.forEach((r) => {
expect(r).toBeGreaterThanOrEqual(800);
expect(r).toBeLessThanOrEqual(1200);
});
});
it('should produce different values (randomness)', () => {
const results = new Set<number>();
for (let i = 0; i < 20; i++) {
results.add(applyJitter(1000));
}
// 20 calls should produce multiple distinct values
expect(results.size).toBeGreaterThan(5);
});
it('should guarantee minimum value of 1 for small inputs', () => {
expect(applyJitter(1)).toBeGreaterThanOrEqual(1);
expect(applyJitter(0)).toBe(1);
});
});
describe('calculateRetryDelay', () => {
it('should implement exponential backoff', () => {
// First attempt (index 0): 1s ± 20% = 800-1200ms
const delay0 = calculateRetryDelay(0);
expect(delay0).toBeGreaterThanOrEqual(800);
expect(delay0).toBeLessThanOrEqual(1200);
// Second attempt (index 1): 2s ± 20% = 1600-2400ms
const delay1 = calculateRetryDelay(1);
expect(delay1).toBeGreaterThanOrEqual(1600);
expect(delay1).toBeLessThanOrEqual(2400);
});
it('should have maximum delay limit', () => {
// Even with high retry count, should not exceed 30s + 20% jitter = 36s
const delay = calculateRetryDelay(10);
expect(delay).toBeLessThanOrEqual(36000);
});
it('should accept custom base delay', () => {
const delay = calculateRetryDelay(0, 500);
expect(delay).toBeGreaterThanOrEqual(400); // 500 - 20%
expect(delay).toBeLessThanOrEqual(600); // 500 + 20%
});
});
+48
View File
@@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest';
import { decodeSignature } from '../../lib/antigravity/signature-utils';
describe('decodeSignature', () => {
describe('Base64 decoding', () => {
it('should correctly decode valid Base64 signature', () => {
const original = 'valid-thinking-signature-12345';
const base64 = Buffer.from(original).toString('base64');
expect(decodeSignature(base64)).toBe(original);
});
it('should decode signature with special characters', () => {
const original = 'sig+with/special=chars==';
const base64 = Buffer.from(original).toString('base64');
expect(decodeSignature(base64)).toBe(original);
});
it('should decode UTF-8 Chinese signature', () => {
const original = '思考签名-测试';
const base64 = Buffer.from(original).toString('base64');
expect(decodeSignature(base64)).toBe(original);
});
});
describe('Non-Base64 passthrough', () => {
it('should return non-Base64 strings as-is', () => {
expect(decodeSignature('plain-text-signature')).toBe('plain-text-signature');
});
it('should handle strings with invalid Base64 characters', () => {
expect(decodeSignature('not!valid@base64#')).toBe('not!valid@base64#');
});
});
describe('Empty value handling', () => {
it('should return undefined for null input', () => {
expect(decodeSignature(null)).toBeUndefined();
});
it('should return undefined for undefined input', () => {
expect(decodeSignature(undefined)).toBeUndefined();
});
it('should return undefined for empty string', () => {
expect(decodeSignature('')).toBeUndefined();
});
});
});
+61
View File
@@ -0,0 +1,61 @@
import { describe, it, expect } from 'vitest';
import { classifyStreamError, formatErrorForSSE } from '../../lib/antigravity/stream-error-utils';
describe('classifyStreamError', () => {
it('should identify timeout errors', () => {
const error = new Error('timeout of 30000ms exceeded');
const { type, message } = classifyStreamError(error);
expect(type).toBe('timeout_error');
expect(message).toContain('timed out');
});
it('should identify connection errors', () => {
const error = new Error('connect ECONNREFUSED 127.0.0.1:8080');
const { type, message } = classifyStreamError(error);
expect(type).toBe('connection_error');
expect(message).toContain('connect');
});
it('should identify network DNS errors', () => {
const error = new Error('getaddrinfo ENOTFOUND googleapis.com');
const { type, message } = classifyStreamError(error);
expect(type).toBe('network_error');
expect(message).toContain('Network');
});
it('should identify socket errors', () => {
const error = new Error('socket hang up');
const { type, message } = classifyStreamError(error);
expect(type).toBe('stream_error');
expect(message).toContain('interrupted');
});
it('should return generic message for unknown errors', () => {
const error = new Error('some random error');
const { type, message } = classifyStreamError(error);
expect(type).toBe('unknown_error');
expect(message).toBeTruthy();
expect(message).toContain('some random error');
});
});
describe('formatErrorForSSE', () => {
it('should format as Claude SSE error event', () => {
const result = formatErrorForSSE('network_error', 'Network connection failed');
expect(result).toContain('event: error');
expect(result).toContain('"type":"error"');
expect(result).toContain('network_error');
expect(result).toContain('Network connection failed');
});
it('should end with newlines', () => {
const result = formatErrorForSSE('test', 'test');
expect(result.endsWith('\n\n')).toBe(true);
});
it('should be valid SSE format', () => {
const result = formatErrorForSSE('timeout_error', 'Request timed out');
expect(result.startsWith('event: ')).toBe(true);
expect(result).toContain('\ndata: ');
});
});
+61
View File
@@ -0,0 +1,61 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { StreamingState } from '../../lib/antigravity/ClaudeStreamingMapper';
describe('StreamingState', () => {
let state: StreamingState;
beforeEach(() => {
state = new StreamingState();
});
describe('handleParseError', () => {
it('should return empty array on first error', () => {
const chunks = state.handleParseError('invalid json');
expect(chunks).toEqual([]);
});
it('should emit error event when error count exceeds 3', () => {
// Simulate 4 parse errors
state.handleParseError('error 1');
state.handleParseError('error 2');
state.handleParseError('error 3');
const chunks = state.handleParseError('error 4');
expect(chunks.length).toBeGreaterThan(0);
expect(chunks[0]).toContain('network_error');
expect(chunks[0]).toContain('Unstable network');
});
it('should safely close active block on error', () => {
// Start a text block first
state.startBlock('Text', { type: 'text', text: '' });
const chunks = state.handleParseError('error during block');
// Should contain content_block_stop event
expect(chunks.some((c) => c.includes('content_block_stop'))).toBe(true);
});
});
describe('resetErrorState', () => {
it('should reset error counter', () => {
state.handleParseError('error 1');
state.handleParseError('error 2');
state.resetErrorState();
// After reset, should start counting from 0
const chunks = state.handleParseError('error after reset');
expect(chunks).toEqual([]);
});
});
describe('getErrorCount', () => {
it('should return current error count', () => {
expect(state.getErrorCount()).toBe(0);
state.handleParseError('error 1');
expect(state.getErrorCount()).toBe(1);
state.handleParseError('error 2');
expect(state.getErrorCount()).toBe(2);
});
});
});
+90
View File
@@ -0,0 +1,90 @@
import { describe, it, expect } from 'vitest';
import {
hasValidSignature,
gracefulDegradeThinking,
processThinkingBlocks,
} from '../../lib/antigravity/thinking-utils';
describe('hasValidSignature', () => {
it('should return true for empty thinking + valid signature', () => {
const block = { type: 'thinking' as const, thinking: '', signature: 'valid-signature-long' };
expect(hasValidSignature(block)).toBe(true);
});
it('should return true for content + long signature', () => {
const block = {
type: 'thinking' as const,
thinking: 'some thought',
signature: 'long-enough-signature',
};
expect(hasValidSignature(block)).toBe(true);
});
it('should return false for content + short signature', () => {
const block = { type: 'thinking' as const, thinking: 'some thought', signature: 'short' };
expect(hasValidSignature(block)).toBe(false);
});
it('should return false for content + no signature', () => {
const block = { type: 'thinking' as const, thinking: 'some thought', signature: undefined };
expect(hasValidSignature(block)).toBe(false);
});
it('should return true for non-thinking types', () => {
const block = { type: 'text' as const, text: 'hello' };
expect(hasValidSignature(block)).toBe(true);
});
});
describe('gracefulDegradeThinking', () => {
it('should convert invalid signature thinking to text', () => {
const block = { type: 'thinking' as const, thinking: 'important content', signature: 'bad' };
const result = gracefulDegradeThinking(block);
expect(result?.type).toBe('text');
expect((result as any).text).toBe('important content');
});
it('should keep valid thinking block unchanged', () => {
const block = {
type: 'thinking' as const,
thinking: 'content',
signature: 'valid-long-signature',
};
const result = gracefulDegradeThinking(block);
expect(result?.type).toBe('thinking');
});
it('should drop empty content + invalid signature block', () => {
const block = { type: 'thinking' as const, thinking: '', signature: 'bad' };
const result = gracefulDegradeThinking(block);
expect(result).toBeNull();
});
it('should keep empty content + valid signature trailing block', () => {
const block = { type: 'thinking' as const, thinking: '', signature: 'any-valid-sig' };
const result = gracefulDegradeThinking(block);
expect(result?.type).toBe('thinking');
});
});
describe('processThinkingBlocks', () => {
it('should filter out invalid empty thinking blocks', () => {
const blocks = [
{ type: 'text' as const, text: 'hello' },
{ type: 'thinking' as const, thinking: '', signature: 'bad' },
{ type: 'text' as const, text: 'world' },
];
const result = processThinkingBlocks(blocks);
expect(result.length).toBe(2);
expect(result[0].type).toBe('text');
expect(result[1].type).toBe('text');
});
it('should convert content with invalid signature to text', () => {
const blocks = [{ type: 'thinking' as const, thinking: 'keep me', signature: 'x' }];
const result = processThinkingBlocks(blocks);
expect(result.length).toBe(1);
expect(result[0].type).toBe('text');
expect((result[0] as any).text).toBe('keep me');
});
});
+28 -1
View File
@@ -4,6 +4,33 @@ import { getAgentDir } from './paths';
export type LogLevel = 'info' | 'warn' | 'error' | 'debug';
/**
* Safely stringify an object, handling circular references
* This prevents "Converting circular structure to JSON" errors
* when logging objects like axios errors that contain socket references
*/
function safeStringify(obj: unknown): string {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
// Handle Error objects specially
if (value instanceof Error) {
return {
name: value.name,
message: value.message,
stack: value.stack,
};
}
// Handle circular references
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
});
}
class Logger {
private logFilePath: string;
@@ -22,7 +49,7 @@ class Logger {
private formatMessage(level: LogLevel, message: string, ...args: unknown[]): string {
const timestamp = new Date().toISOString();
const formattedArgs = args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.map((arg) => (typeof arg === 'object' ? safeStringify(arg) : String(arg)))
.join(' ');
return `[${timestamp}] [${level.toUpperCase()}] ${message} ${formattedArgs}`;
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(process.cwd(), './src'),
// Mock native modules that fail to load in test environment
keytar: path.resolve(process.cwd(), './src/mocks/empty.ts'),
'better-sqlite3': path.resolve(process.cwd(), './src/mocks/empty.ts'),
},
},
test: {
globals: true,
environment: 'happy-dom',
include: ['src/tests/unit/**/*.test.ts'],
},
});
-31
View File
@@ -1,31 +0,0 @@
import * as path from 'path';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react() as any],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
dir: './src/tests/unit',
globals: true,
environment: 'node',
setupFiles: './src/tests/unit/setup.ts',
css: true,
reporters: ['verbose'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*'],
exclude: [],
},
server: {
deps: {
inline: ['jsdom'],
},
},
},
});