Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8170259b47 | |||
| 4e654acebf | |||
| 51ba10b444 | |||
| f13ed0579c | |||
| dd1d31da58 | |||
| fcfa9fc900 | |||
| 8771d4a4b5 |
@@ -33,21 +33,21 @@
|
||||
|
||||
### Beta Release
|
||||
|
||||
> ⚠️ Beta releases may contain bugs and breaking changes. [View all releases](https://github.com/AndyMik90/Auto-Claude/releases)
|
||||
> ⚠️ Beta releases may contain bugs and breaking changes. [View all releases](https://github.com/AndyMik90/Aperant/releases)
|
||||
|
||||
<!-- BETA_VERSION_BADGE -->
|
||||
[](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.8.0-beta.5)
|
||||
[](https://github.com/AndyMik90/Aperant/releases/tag/v2.8.0-beta.5)
|
||||
<!-- BETA_VERSION_BADGE_END -->
|
||||
|
||||
<!-- BETA_DOWNLOADS -->
|
||||
| Platform | Download |
|
||||
|----------|----------|
|
||||
| **Windows** | [Aperant-2.8.0-beta.5-win32-x64.exe](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-win32-x64.exe) |
|
||||
| **macOS (Apple Silicon)** | [Aperant-2.8.0-beta.5-darwin-arm64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-darwin-arm64.dmg) |
|
||||
| **macOS (Intel)** | [Aperant-2.8.0-beta.5-darwin-x64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-darwin-x64.dmg) |
|
||||
| **Linux** | [Aperant-2.8.0-beta.5-linux-x86_64.AppImage](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-x86_64.AppImage) |
|
||||
| **Linux (Debian)** | [Aperant-2.8.0-beta.5-linux-amd64.deb](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-amd64.deb) |
|
||||
| **Linux (Flatpak)** | [Aperant-2.8.0-beta.5-linux-x86_64.flatpak](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-x86_64.flatpak) |
|
||||
| **Windows** | [Aperant-2.8.0-beta.5-win32-x64.exe](https://github.com/AndyMik90/Aperant/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-win32-x64.exe) |
|
||||
| **macOS (Apple Silicon)** | [Aperant-2.8.0-beta.5-darwin-arm64.dmg](https://github.com/AndyMik90/Aperant/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-darwin-arm64.dmg) |
|
||||
| **macOS (Intel)** | [Aperant-2.8.0-beta.5-darwin-x64.dmg](https://github.com/AndyMik90/Aperant/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-darwin-x64.dmg) |
|
||||
| **Linux** | [Aperant-2.8.0-beta.5-linux-x86_64.AppImage](https://github.com/AndyMik90/Aperant/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-x86_64.AppImage) |
|
||||
| **Linux (Debian)** | [Aperant-2.8.0-beta.5-linux-amd64.deb](https://github.com/AndyMik90/Aperant/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-amd64.deb) |
|
||||
| **Linux (Flatpak)** | [Aperant-2.8.0-beta.5-linux-x86_64.flatpak](https://github.com/AndyMik90/Aperant/releases/download/v2.8.0-beta.5/Aperant-2.8.0-beta.5-linux-x86_64.flatpak) |
|
||||
<!-- BETA_DOWNLOADS_END -->
|
||||
|
||||
> All releases include SHA256 checksums and VirusTotal scan results for security verification.
|
||||
|
||||
+49
-49
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aperant",
|
||||
"version": "2.8.0-beta.1",
|
||||
"version": "2.8.0-beta.5",
|
||||
"type": "module",
|
||||
"description": "Autonomous multi-agent coding framework",
|
||||
"homepage": "https://github.com/AndyMik90/Aperant",
|
||||
@@ -50,27 +50,27 @@
|
||||
"typecheck": "tsc --noEmit --incremental"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "^4.0.77",
|
||||
"@ai-sdk/anthropic": "^3.0.58",
|
||||
"@ai-sdk/azure": "^3.0.42",
|
||||
"@ai-sdk/google": "^3.0.43",
|
||||
"@ai-sdk/groq": "^3.0.29",
|
||||
"@ai-sdk/mcp": "^1.0.25",
|
||||
"@ai-sdk/mistral": "^3.0.24",
|
||||
"@ai-sdk/openai": "^3.0.41",
|
||||
"@ai-sdk/openai-compatible": "^2.0.35",
|
||||
"@ai-sdk/xai": "^3.0.67",
|
||||
"@anthropic-ai/sdk": "^0.78.0",
|
||||
"@ai-sdk/amazon-bedrock": "^4.0.61",
|
||||
"@ai-sdk/anthropic": "^3.0.45",
|
||||
"@ai-sdk/azure": "^3.0.31",
|
||||
"@ai-sdk/google": "^3.0.29",
|
||||
"@ai-sdk/groq": "^3.0.24",
|
||||
"@ai-sdk/mcp": "^1.0.21",
|
||||
"@ai-sdk/mistral": "^2.0.28",
|
||||
"@ai-sdk/openai": "^3.0.30",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@ai-sdk/xai": "^3.0.57",
|
||||
"@anthropic-ai/sdk": "^0.71.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@libsql/client": "^0.17.0",
|
||||
"@lydell/node-pty": "^1.1.0",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@openrouter/ai-sdk-provider": "^2.3.1",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"@openrouter/ai-sdk-provider": "^2.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
@@ -84,78 +84,78 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@sentry/electron": "^7.10.0",
|
||||
"@sentry/electron": "^7.5.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-virtual": "^3.13.22",
|
||||
"@tanstack/react-virtual": "^3.13.13",
|
||||
"@tavily/core": "^0.7.2",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-serialize": "^0.14.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"ai": "^6.0.116",
|
||||
"ai": "^6.0.91",
|
||||
"chokidar": "^5.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-updater": "^6.8.3",
|
||||
"i18next": "^25.8.18",
|
||||
"lucide-react": "^0.577.0",
|
||||
"minimatch": "^10.2.4",
|
||||
"motion": "^12.36.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"i18next": "^25.7.3",
|
||||
"lucide-react": "^0.562.0",
|
||||
"minimatch": "^10.1.1",
|
||||
"motion": "^12.23.26",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^16.5.8",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"semver": "^7.7.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"semver": "^7.7.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"uuid": "^13.0.0",
|
||||
"web-tree-sitter": "^0.26.7",
|
||||
"xstate": "^5.28.0",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.11"
|
||||
"web-tree-sitter": "^0.26.5",
|
||||
"xstate": "^5.26.0",
|
||||
"zod": "^4.2.1",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.7",
|
||||
"@biomejs/biome": "2.3.11",
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@electron/rebuild": "^4.0.3",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@electron/rebuild": "^4.0.2",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/minimatch": "^6.0.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "40.0.0",
|
||||
"electron-builder": "^26.8.1",
|
||||
"electron-builder": "^26.4.0",
|
||||
"electron-vite": "^5.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.3.0",
|
||||
"lint-staged": "^16.4.0",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"lint-staged": "^16.2.7",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.7",
|
||||
"vitest": "^4.1.0"
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"overrides": {
|
||||
"electron-builder-squirrel-windows": "^26.0.12",
|
||||
"dmg-builder": "^26.0.12",
|
||||
"@electron/rebuild": "4.0.3"
|
||||
"@electron/rebuild": "4.0.2"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.aperant.app",
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
/**
|
||||
* Tests for ensureOnboardingComplete function in cli-integration-handler.ts
|
||||
*
|
||||
* Tests the exported ensureOnboardingComplete() which reads/writes .claude.json
|
||||
* to set hasCompletedOnboarding: true, suppressing Claude's onboarding wizard
|
||||
* for already-authenticated profiles.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
// ---- fs mock (sync only — the function uses fs, not fs/promises) ----
|
||||
const mockFiles: Map<string, string | Error> = new Map();
|
||||
|
||||
vi.mock('fs', () => {
|
||||
const readFileSync = vi.fn((filePath: string, _encoding?: string): string => {
|
||||
const entry = mockFiles.get(filePath);
|
||||
if (entry === undefined) {
|
||||
const err = new Error(`ENOENT: no such file or directory, open '${filePath}'`) as NodeJS.ErrnoException;
|
||||
err.code = 'ENOENT';
|
||||
throw err;
|
||||
}
|
||||
if (entry instanceof Error) {
|
||||
throw entry;
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
|
||||
const writeFileSync = vi.fn();
|
||||
const renameSync = vi.fn();
|
||||
|
||||
return { default: { readFileSync, writeFileSync, renameSync }, readFileSync, writeFileSync, renameSync };
|
||||
});
|
||||
|
||||
// ---- stubs for heavy transitive dependencies ----
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: { handle: vi.fn() },
|
||||
app: { getPath: vi.fn(() => os.tmpdir()), getAppPath: vi.fn(() => os.tmpdir()) },
|
||||
dialog: { showOpenDialog: vi.fn() },
|
||||
shell: { openExternal: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@electron-toolkit/utils', () => ({ is: { dev: true } }));
|
||||
|
||||
vi.mock('../../shared/constants', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../shared/constants')>('../../shared/constants');
|
||||
return { ...actual };
|
||||
});
|
||||
|
||||
vi.mock('../claude-profile-manager', () => ({
|
||||
getClaudeProfileManager: vi.fn(),
|
||||
initializeClaudeProfileManager: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../claude-profile/credential-utils', () => ({
|
||||
getFullCredentialsFromKeychain: vi.fn(),
|
||||
clearKeychainCache: vi.fn(),
|
||||
updateProfileSubscriptionMetadata: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../claude-profile/usage-monitor', () => ({
|
||||
getUsageMonitor: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../claude-profile/profile-utils', () => ({
|
||||
getEmailFromConfigDir: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../terminal/output-parser', () => ({}));
|
||||
vi.mock('../terminal/session-handler', () => ({}));
|
||||
|
||||
vi.mock('./pty-manager', () => ({
|
||||
writeToPty: vi.fn(),
|
||||
resizePty: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../ipc-handlers/utils', () => ({
|
||||
safeSendToRenderer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../shared/utils/debug-logger', () => ({
|
||||
debugLog: vi.fn(),
|
||||
debugError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../shared/utils/shell-escape', () => ({
|
||||
escapeShellArg: vi.fn((s: string) => s),
|
||||
escapeForWindowsDoubleQuote: vi.fn((s: string) => s),
|
||||
buildCdCommand: vi.fn((cwd: string) => `cd ${cwd}`),
|
||||
}));
|
||||
|
||||
vi.mock('../cli-utils', () => ({
|
||||
getClaudeCliInvocation: vi.fn(() => 'claude'),
|
||||
getClaudeCliInvocationAsync: vi.fn(async () => 'claude'),
|
||||
}));
|
||||
|
||||
vi.mock('../platform', () => ({
|
||||
isWindows: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
vi.mock('../settings-utils', () => ({
|
||||
readSettingsFileAsync: vi.fn(async () => ({})),
|
||||
readSettingsFile: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
// ---- import the function under test ----
|
||||
import { ensureOnboardingComplete } from '../terminal/cli-integration-handler';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// ---- helpers ----
|
||||
function claudeJsonPath(configDir: string): string {
|
||||
const expanded = configDir.startsWith('~')
|
||||
? configDir.replace(/^~/, os.homedir())
|
||||
: configDir;
|
||||
return path.join(path.resolve(expanded), '.claude.json');
|
||||
}
|
||||
|
||||
const TEST_DIR = '/tmp/test-profile';
|
||||
|
||||
describe('ensureOnboardingComplete', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockFiles.clear();
|
||||
});
|
||||
|
||||
// ---- ENOENT: file does not exist ----
|
||||
test('returns early (no write) when .claude.json does not exist', () => {
|
||||
// mockFiles is empty → readFileSync will throw ENOENT
|
||||
ensureOnboardingComplete(TEST_DIR);
|
||||
|
||||
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ---- already set ----
|
||||
test('returns early (no write) when hasCompletedOnboarding is already true', () => {
|
||||
const filePath = claudeJsonPath(TEST_DIR);
|
||||
mockFiles.set(filePath, JSON.stringify({ hasCompletedOnboarding: true }));
|
||||
|
||||
ensureOnboardingComplete(TEST_DIR);
|
||||
|
||||
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ---- missing flag → should write ----
|
||||
test('writes hasCompletedOnboarding: true when flag is absent', () => {
|
||||
const filePath = claudeJsonPath(TEST_DIR);
|
||||
mockFiles.set(filePath, JSON.stringify({ someOtherField: 'value' }));
|
||||
|
||||
ensureOnboardingComplete(TEST_DIR);
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledOnce();
|
||||
const written = JSON.parse((fs.writeFileSync as ReturnType<typeof vi.fn>).mock.calls[0][1] as string);
|
||||
expect(written.hasCompletedOnboarding).toBe(true);
|
||||
expect(written.someOtherField).toBe('value');
|
||||
});
|
||||
|
||||
// ---- flag is false → should write ----
|
||||
test('writes hasCompletedOnboarding: true when flag is false', () => {
|
||||
const filePath = claudeJsonPath(TEST_DIR);
|
||||
mockFiles.set(filePath, JSON.stringify({ hasCompletedOnboarding: false }));
|
||||
|
||||
ensureOnboardingComplete(TEST_DIR);
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledOnce();
|
||||
const written = JSON.parse((fs.writeFileSync as ReturnType<typeof vi.fn>).mock.calls[0][1] as string);
|
||||
expect(written.hasCompletedOnboarding).toBe(true);
|
||||
});
|
||||
|
||||
// ---- non-object JSON (string) → should return silently ----
|
||||
test('returns early (no write) when .claude.json contains a JSON string', () => {
|
||||
const filePath = claudeJsonPath(TEST_DIR);
|
||||
mockFiles.set(filePath, JSON.stringify('just a string'));
|
||||
|
||||
ensureOnboardingComplete(TEST_DIR);
|
||||
|
||||
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ---- array JSON → should return silently ----
|
||||
test('returns early (no write) when .claude.json contains a JSON array', () => {
|
||||
const filePath = claudeJsonPath(TEST_DIR);
|
||||
mockFiles.set(filePath, JSON.stringify([1, 2, 3]));
|
||||
|
||||
ensureOnboardingComplete(TEST_DIR);
|
||||
|
||||
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ---- corrupted / invalid JSON → outer catch swallows error ----
|
||||
test('handles corrupted JSON gracefully without throwing', () => {
|
||||
const filePath = claudeJsonPath(TEST_DIR);
|
||||
mockFiles.set(filePath, '{ invalid json }');
|
||||
|
||||
expect(() => ensureOnboardingComplete(TEST_DIR)).not.toThrow();
|
||||
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ---- tilde expansion ----
|
||||
test('expands leading tilde to home directory', () => {
|
||||
const tildeDir = '~/myprofile';
|
||||
const resolvedDir = path.resolve(tildeDir.replace(/^~/, os.homedir()));
|
||||
const filePath = path.join(resolvedDir, '.claude.json');
|
||||
|
||||
mockFiles.set(filePath, JSON.stringify({}));
|
||||
|
||||
ensureOnboardingComplete(tildeDir);
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledOnce();
|
||||
// Writes to a temp file (claudeJsonPath + UUID + .tmp), then renames to target
|
||||
const writtenPath = (fs.writeFileSync as ReturnType<typeof vi.fn>).mock.calls[0][0] as string;
|
||||
expect(writtenPath).toMatch(new RegExp(`^${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\..*\\.tmp$`));
|
||||
expect(fs.renameSync).toHaveBeenCalledWith(writtenPath, filePath);
|
||||
});
|
||||
|
||||
// ---- write error → outer catch swallows error ----
|
||||
test('handles write error gracefully without throwing', () => {
|
||||
const filePath = claudeJsonPath(TEST_DIR);
|
||||
mockFiles.set(filePath, JSON.stringify({}));
|
||||
|
||||
(fs.writeFileSync as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
||||
throw new Error('EACCES: permission denied');
|
||||
});
|
||||
|
||||
expect(() => ensureOnboardingComplete(TEST_DIR)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -147,7 +147,6 @@ export const MODEL_PROVIDER_MAP: Record<string, SupportedProvider> = {
|
||||
'llama-': 'groq',
|
||||
'grok-': 'xai',
|
||||
'glm-': 'zai',
|
||||
'mascarade-': 'mascarade',
|
||||
} as const;
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -156,24 +156,6 @@ function createProviderInstance(config: ProviderConfig) {
|
||||
});
|
||||
}
|
||||
|
||||
case SupportedProvider.Mascarade: {
|
||||
// Mascarade LLM orchestration engine — OpenAI-compatible API
|
||||
// Default: http://localhost:8100/v1 (Tower) or configurable
|
||||
let mascaradeBaseURL = baseURL ?? 'http://localhost:8100/v1';
|
||||
if (!mascaradeBaseURL.endsWith('/v1')) {
|
||||
mascaradeBaseURL = mascaradeBaseURL.replace(/\/+$/, '') + '/v1';
|
||||
}
|
||||
return createOpenAICompatible({
|
||||
name: 'mascarade',
|
||||
apiKey: apiKey ?? 'mascarade-local',
|
||||
baseURL: mascaradeBaseURL,
|
||||
headers: {
|
||||
...headers,
|
||||
'Authorization': `Bearer ${apiKey ?? 'mascarade-local'}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
default: {
|
||||
const _exhaustive: never = provider;
|
||||
throw new Error(`Unsupported provider: ${_exhaustive}`);
|
||||
|
||||
@@ -21,7 +21,6 @@ export const SupportedProvider = {
|
||||
OpenRouter: 'openrouter',
|
||||
ZAI: 'zai',
|
||||
Ollama: 'ollama',
|
||||
Mascarade: 'mascarade',
|
||||
} as const;
|
||||
|
||||
export type SupportedProvider = (typeof SupportedProvider)[keyof typeof SupportedProvider];
|
||||
|
||||
@@ -933,20 +933,6 @@ export function registerSettingsHandlers(
|
||||
try {
|
||||
const settings = readSettingsFile() ?? {};
|
||||
const accounts: ProviderAccount[] = (settings.providerAccounts as ProviderAccount[] | undefined) ?? [];
|
||||
|
||||
// Prevent duplicate: same email + provider already registered
|
||||
if (account.email) {
|
||||
const duplicate = accounts.find(
|
||||
(a) => a.provider === account.provider && a.email?.toLowerCase() === account.email!.toLowerCase()
|
||||
);
|
||||
if (duplicate) {
|
||||
return {
|
||||
success: false,
|
||||
error: `DUPLICATE_EMAIL:${duplicate.name}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const newAccount: ProviderAccount = {
|
||||
...account,
|
||||
|
||||
@@ -27,7 +27,8 @@ import type {
|
||||
TerminalProcess,
|
||||
WindowGetter,
|
||||
RateLimitEvent,
|
||||
OAuthTokenEvent
|
||||
OAuthTokenEvent,
|
||||
OnboardingCompleteEvent
|
||||
} from './types';
|
||||
|
||||
// ============================================================================
|
||||
@@ -563,17 +564,17 @@ export function handleOAuthToken(
|
||||
|
||||
console.warn('[ClaudeIntegration] Profile credentials verified via Keychain (not caching token):', profileId);
|
||||
|
||||
// Mark onboarding complete so future `claude` invocations skip the wizard.
|
||||
// `claude auth login` creates .claude.json but doesn't set this flag.
|
||||
if (profile.configDir) {
|
||||
ensureOnboardingComplete(profile.configDir);
|
||||
}
|
||||
// Set flag to watch for Claude's ready state (onboarding complete)
|
||||
terminal.awaitingOnboardingComplete = true;
|
||||
|
||||
// needsOnboarding: true tells the UI to show "complete setup" message
|
||||
// instead of "success" - user should finish Claude's onboarding before closing
|
||||
safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, {
|
||||
terminalId: terminal.id,
|
||||
profileId,
|
||||
email: emailFromOutput || keychainCreds.email || profile?.email,
|
||||
success: true,
|
||||
needsOnboarding: true,
|
||||
detectedAt: new Date().toISOString()
|
||||
} as OAuthTokenEvent);
|
||||
} else {
|
||||
@@ -584,16 +585,17 @@ export function handleOAuthToken(
|
||||
if (hasCredentials) {
|
||||
console.warn('[ClaudeIntegration] Profile credentials verified (no Keychain token):', profileId);
|
||||
|
||||
// Mark onboarding complete so future `claude` invocations skip the wizard
|
||||
if (profile.configDir) {
|
||||
ensureOnboardingComplete(profile.configDir);
|
||||
}
|
||||
// Set flag to watch for Claude's ready state (onboarding complete)
|
||||
terminal.awaitingOnboardingComplete = true;
|
||||
|
||||
// needsOnboarding: true tells the UI to show "complete setup" message
|
||||
// instead of "success" - user should finish Claude's onboarding before closing
|
||||
safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, {
|
||||
terminalId: terminal.id,
|
||||
profileId,
|
||||
email: emailFromOutput || profile?.email,
|
||||
success: true,
|
||||
needsOnboarding: true,
|
||||
detectedAt: new Date().toISOString()
|
||||
} as OAuthTokenEvent);
|
||||
} else {
|
||||
@@ -716,19 +718,135 @@ export function handleOAuthToken(
|
||||
|
||||
/**
|
||||
* Handle onboarding complete detection
|
||||
* Called when terminal output indicates Claude Code is ready after login/onboarding.
|
||||
* Called when terminal output indicates Claude Code is ready after login/onboarding
|
||||
*
|
||||
* Note: This is now a no-op. The onboarding flag is set proactively via
|
||||
* ensureOnboardingComplete() in handleOAuthToken() and executeProfileCommand(),
|
||||
* so awaitingOnboardingComplete is never set and this path is never reached.
|
||||
* Kept as a stub to satisfy the terminal-event-handler callback interface.
|
||||
* This detects the Claude Code welcome screen that appears after successful login,
|
||||
* which includes patterns like "Welcome back", "Claude Code v2.x", or subscription
|
||||
* tier info like "Claude Max". When detected, it notifies the frontend to auto-close
|
||||
* the auth terminal.
|
||||
*/
|
||||
export function handleOnboardingComplete(
|
||||
_terminal: TerminalProcess,
|
||||
_data: string,
|
||||
_getWindow: WindowGetter
|
||||
terminal: TerminalProcess,
|
||||
data: string,
|
||||
getWindow: WindowGetter
|
||||
): void {
|
||||
// No-op — onboarding is handled proactively in handleOAuthToken()
|
||||
// Only check if we're waiting for onboarding to complete
|
||||
if (!terminal.awaitingOnboardingComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if output shows Claude Code welcome screen (onboarding complete indicators)
|
||||
if (!OutputParser.isOnboardingCompleteOutput(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('[ClaudeIntegration] Onboarding complete detected for terminal:', terminal.id);
|
||||
|
||||
// Clear the flag
|
||||
terminal.awaitingOnboardingComplete = false;
|
||||
|
||||
// Extract profile ID from terminal ID pattern (claude-login-{profileId}-*)
|
||||
const profileId = extractProfileIdFromAuthTerminalId(terminal.id) || undefined;
|
||||
|
||||
// Try to extract email from the welcome screen (e.g., "user@example.com's Organization")
|
||||
// Note: extractEmail automatically strips ANSI escape codes internally
|
||||
let email = OutputParser.extractEmail(data);
|
||||
if (!email) {
|
||||
email = OutputParser.extractEmail(terminal.outputBuffer);
|
||||
}
|
||||
|
||||
// Fallback: If terminal extraction failed or might be corrupt, read directly from Claude's config file
|
||||
// This is the authoritative source and doesn't suffer from ANSI escape code issues
|
||||
const profileManager = getClaudeProfileManager();
|
||||
const profile = profileId ? profileManager.getProfile(profileId) : null;
|
||||
|
||||
if (!email && profile?.configDir) {
|
||||
const configEmail = getEmailFromConfigDir(profile.configDir);
|
||||
if (configEmail) {
|
||||
console.warn('[ClaudeIntegration] Email not found in terminal output, using config file:', maskEmail(configEmail));
|
||||
email = configEmail;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email looks correct (basic sanity check)
|
||||
// If terminal extraction gave us a truncated email but config file has the correct one, prefer config
|
||||
if (email && profile?.configDir) {
|
||||
const configEmail = getEmailFromConfigDir(profile.configDir);
|
||||
if (configEmail && configEmail !== email) {
|
||||
// Config file email is different - it's more authoritative
|
||||
console.warn('[ClaudeIntegration] Terminal email differs from config file, using config file:', {
|
||||
terminalEmail: maskEmail(email),
|
||||
configEmail: maskEmail(configEmail)
|
||||
});
|
||||
email = configEmail;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('[ClaudeIntegration] Email extraction attempt:', {
|
||||
profileId,
|
||||
foundEmail: maskEmail(email),
|
||||
dataLength: data.length,
|
||||
bufferLength: terminal.outputBuffer.length
|
||||
});
|
||||
|
||||
// Update profile with email and subscription metadata if found and profile exists
|
||||
// Always update - the newly extracted email from re-authentication should overwrite any stale/truncated email
|
||||
if (profileId && email && profile) {
|
||||
const previousEmail = profile.email;
|
||||
profile.email = email;
|
||||
// Also update subscription metadata from Keychain credentials
|
||||
updateProfileSubscriptionMetadata(profile, profile.configDir);
|
||||
profileManager.saveProfile(profile);
|
||||
if (previousEmail !== email) {
|
||||
console.warn('[ClaudeIntegration] Updated profile email from welcome screen:', profileId, maskEmail(email), '(was:', maskEmail(previousEmail), ')');
|
||||
}
|
||||
}
|
||||
|
||||
// Persist onboarding completion so future invocations skip the wizard
|
||||
if (profile?.configDir) {
|
||||
ensureOnboardingComplete(profile.configDir);
|
||||
}
|
||||
|
||||
safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, {
|
||||
terminalId: terminal.id,
|
||||
profileId,
|
||||
detectedAt: new Date().toISOString()
|
||||
} as OnboardingCompleteEvent);
|
||||
|
||||
// Trigger immediate usage fetch after successful re-authentication
|
||||
// This gives the user immediate feedback that their account is working
|
||||
if (profileId) {
|
||||
try {
|
||||
const usageMonitor = getUsageMonitor();
|
||||
if (usageMonitor) {
|
||||
// Clear any auth failure status for this profile since they just re-authenticated
|
||||
usageMonitor.clearAuthFailedProfile(profileId);
|
||||
|
||||
console.warn('[ClaudeIntegration] Triggering immediate usage fetch after re-authentication:', profileId);
|
||||
|
||||
// Switch to this profile if it's not already active, then fetch usage
|
||||
const profileManager = getClaudeProfileManager();
|
||||
|
||||
// Also clear the migration flag if this profile was migrated to an isolated directory
|
||||
// This prevents the auth failure modal from showing again on next startup
|
||||
if (profileManager.isProfileMigrated(profileId)) {
|
||||
profileManager.clearMigratedProfile(profileId);
|
||||
console.warn('[ClaudeIntegration] Cleared migration flag for re-authenticated profile:', profileId);
|
||||
}
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
if (activeProfile?.id !== profileId) {
|
||||
profileManager.setActiveProfile(profileId);
|
||||
}
|
||||
|
||||
// Small delay to allow profile switch to settle, then trigger usage fetch
|
||||
setTimeout(() => {
|
||||
usageMonitor.checkNow();
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ClaudeIntegration] Failed to trigger post-auth usage fetch:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -785,7 +903,7 @@ export function handleClaudeExit(
|
||||
* When CLAUDE_CONFIG_DIR is set, Claude Code reads .claude.json from that directory.
|
||||
* Without this flag, it triggers the onboarding wizard even for authenticated profiles.
|
||||
*/
|
||||
export function ensureOnboardingComplete(configDir: string): void {
|
||||
function ensureOnboardingComplete(configDir: string): void {
|
||||
try {
|
||||
const expandedDir = path.resolve(
|
||||
configDir.startsWith('~') ? configDir.replace(/^~/, os.homedir()) : configDir
|
||||
@@ -814,13 +932,7 @@ export function ensureOnboardingComplete(configDir: string): void {
|
||||
}
|
||||
|
||||
config.hasCompletedOnboarding = true;
|
||||
const updatedContent = JSON.stringify(config, null, 2);
|
||||
|
||||
// Write atomically via temp file + rename to avoid partial writes and satisfy CodeQL js/insecure-temporary-file.
|
||||
// crypto.randomUUID() ensures no collisions; mode 0o600 restricts to owner-only.
|
||||
const tmpPath = `${claudeJsonPath}.${crypto.randomUUID()}.tmp`;
|
||||
fs.writeFileSync(tmpPath, updatedContent, { encoding: 'utf-8', mode: 0o600 });
|
||||
fs.renameSync(tmpPath, claudeJsonPath);
|
||||
fs.writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2), { encoding: 'utf-8' });
|
||||
debugLog(`[ClaudeIntegration] Set hasCompletedOnboarding in ${claudeJsonPath}`);
|
||||
} catch (error) {
|
||||
// Non-fatal — worst case the user sees onboarding once
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface TerminalProcess {
|
||||
dangerouslySkipPermissions?: boolean;
|
||||
/** Shell type for Windows (affects command chaining syntax) */
|
||||
shellType?: WindowsShellType;
|
||||
/** Whether this terminal is waiting for Claude onboarding to complete (login flow) */
|
||||
awaitingOnboardingComplete?: boolean;
|
||||
/** Whether PTY has emitted exit; used to avoid writes/resizes on dead PTYs */
|
||||
hasExited?: boolean;
|
||||
}
|
||||
@@ -53,8 +55,19 @@ export interface OAuthTokenEvent {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
detectedAt: string;
|
||||
/** If true, user should complete onboarding in terminal before closing */
|
||||
needsOnboarding?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Onboarding complete event data
|
||||
* Sent when Claude Code shows its ready state after login/onboarding
|
||||
*/
|
||||
export interface OnboardingCompleteEvent {
|
||||
terminalId: string;
|
||||
profileId?: string;
|
||||
detectedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session capture result
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface TerminalAPI {
|
||||
onTerminalClaudeSession: (callback: (id: string, sessionId: string) => void) => () => void;
|
||||
onTerminalRateLimit: (callback: (info: RateLimitInfo) => void) => () => void;
|
||||
onTerminalOAuthToken: (
|
||||
callback: (info: { terminalId: string; profileId?: string; email?: string; success: boolean; message?: string; detectedAt: string }) => void
|
||||
callback: (info: { terminalId: string; profileId?: string; email?: string; success: boolean; message?: string; detectedAt: string; needsOnboarding?: boolean }) => void
|
||||
) => () => void;
|
||||
onTerminalAuthCreated: (
|
||||
callback: (info: { terminalId: string; profileId: string; profileName: string }) => void
|
||||
@@ -91,6 +91,9 @@ export interface TerminalAPI {
|
||||
submitOAuthCode: (terminalId: string, code: string) => Promise<IPCResult>;
|
||||
onTerminalClaudeBusy: (callback: (id: string, isBusy: boolean) => void) => () => void;
|
||||
onTerminalClaudeExit: (callback: (id: string) => void) => () => void;
|
||||
onTerminalOnboardingComplete: (
|
||||
callback: (info: { terminalId: string; profileId?: string; detectedAt: string }) => void
|
||||
) => () => void;
|
||||
onTerminalPendingResume: (callback: (id: string, sessionId?: string) => void) => () => void;
|
||||
onTerminalProfileChanged: (callback: (event: TerminalProfileChangedEvent) => void) => () => void;
|
||||
|
||||
@@ -385,6 +388,21 @@ export const createTerminalAPI = (): TerminalAPI => ({
|
||||
};
|
||||
},
|
||||
|
||||
onTerminalOnboardingComplete: (
|
||||
callback: (info: { terminalId: string; profileId?: string; detectedAt: string }) => void
|
||||
): (() => void) => {
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
info: { terminalId: string; profileId?: string; detectedAt: string }
|
||||
): void => {
|
||||
callback(info);
|
||||
};
|
||||
ipcRenderer.on(IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, handler);
|
||||
return () => {
|
||||
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, handler);
|
||||
};
|
||||
},
|
||||
|
||||
onTerminalPendingResume: (
|
||||
callback: (id: string, sessionId?: string) => void
|
||||
): (() => void) => {
|
||||
|
||||
@@ -33,15 +33,11 @@ export function ProjectTabBar({
|
||||
// Keyboard shortcuts for tab navigation
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Skip if in input fields (but NOT xterm's hidden textarea —
|
||||
// xterm already passes through Cmd/Ctrl+1-9 via attachCustomKeyEventHandler)
|
||||
const target = e.target as HTMLElement;
|
||||
const isXtermTextarea = target.classList?.contains('xterm-helper-textarea');
|
||||
// Skip if in input fields
|
||||
if (
|
||||
!isXtermTextarea &&
|
||||
(e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
target?.isContentEditable)
|
||||
(e.target as HTMLElement)?.isContentEditable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,20 +110,6 @@ export function AddAccountDialog({
|
||||
}
|
||||
}, [open, editAccount, provider, billingModelOverride]);
|
||||
|
||||
// Parse DUPLICATE_EMAIL error from backend and show user-friendly toast
|
||||
const handleDuplicateEmailError = useCallback((error: string): boolean => {
|
||||
if (error.startsWith('DUPLICATE_EMAIL:')) {
|
||||
const existingName = error.slice('DUPLICATE_EMAIL:'.length);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('providers.dialog.toast.error'),
|
||||
description: t('providers.dialog.toast.duplicateEmail', { existingName }),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [toast, t]);
|
||||
|
||||
const isOAuthOnly = (provider === 'anthropic' || provider === 'openai') && authType === 'oauth';
|
||||
const isCodexOAuth = provider === 'openai' && authType === 'oauth';
|
||||
|
||||
@@ -204,16 +190,10 @@ export function AddAccountDialog({
|
||||
: t('providers.dialog.toast.added'),
|
||||
description: name.trim(),
|
||||
});
|
||||
} else if (result.error && !handleDuplicateEmailError(result.error)) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('providers.dialog.toast.error'),
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
};
|
||||
autoSave();
|
||||
}, [oauthStatus, isCodexOAuth, accountSaved, name, provider, oauthProfileId, isEditing, editAccount, oauthEmail, addProviderAccount, updateProviderAccount, handleDuplicateEmailError, toast, t, refreshUsageData]);
|
||||
}, [oauthStatus, isCodexOAuth, accountSaved, name, provider, oauthProfileId, isEditing, editAccount, oauthEmail, addProviderAccount, updateProviderAccount, toast, t, refreshUsageData]);
|
||||
|
||||
const canSave = () => {
|
||||
if (!name.trim()) return false;
|
||||
@@ -284,14 +264,8 @@ export function AddAccountDialog({
|
||||
description: name.trim(),
|
||||
});
|
||||
await refreshUsageData();
|
||||
onOpenChange(false);
|
||||
} else if (saveResult.error && !handleDuplicateEmailError(saveResult.error)) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('providers.dialog.toast.error'),
|
||||
description: saveResult.error,
|
||||
});
|
||||
}
|
||||
onOpenChange(false);
|
||||
}, 800);
|
||||
} else {
|
||||
setOauthStatus('error');
|
||||
@@ -344,7 +318,7 @@ export function AddAccountDialog({
|
||||
setOauthStatus('error');
|
||||
setOauthError(err instanceof Error ? err.message : 'Unexpected error');
|
||||
}
|
||||
}, [name, t, toast, isCodexOAuth, isEditing, editAccount, provider, addProviderAccount, updateProviderAccount, handleDuplicateEmailError, onOpenChange, refreshUsageData]);
|
||||
}, [name, t, toast, isCodexOAuth, isEditing, editAccount, provider, addProviderAccount, updateProviderAccount, onOpenChange, refreshUsageData]);
|
||||
|
||||
const handleFallbackTerminal = useCallback(async () => {
|
||||
if (!name.trim()) {
|
||||
@@ -368,7 +342,7 @@ export function AddAccountDialog({
|
||||
createdAt: new Date(),
|
||||
});
|
||||
if (!profileResult.success || !profileResult.data) {
|
||||
toast({ variant: 'destructive', title: t('providers.dialog.toast.createProfileFailed') });
|
||||
toast({ variant: 'destructive', title: 'Failed to create profile' });
|
||||
return;
|
||||
}
|
||||
profileId = profileResult.data.id;
|
||||
@@ -378,7 +352,7 @@ export function AddAccountDialog({
|
||||
// Get terminal config for embedded AuthTerminal
|
||||
const authResult = await window.electronAPI.authenticateClaudeProfile(profileId);
|
||||
if (!authResult.success || !authResult.data) {
|
||||
toast({ variant: 'destructive', title: authResult.error ?? t('providers.dialog.toast.authPrepareFailed') });
|
||||
toast({ variant: 'destructive', title: authResult.error ?? 'Failed to prepare terminal' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -388,7 +362,7 @@ export function AddAccountDialog({
|
||||
} catch (err) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: err instanceof Error ? err.message : t('providers.dialog.toast.unexpectedError'),
|
||||
title: err instanceof Error ? err.message : 'Unexpected error',
|
||||
});
|
||||
}
|
||||
}, [name, oauthProfileId, t, toast]);
|
||||
@@ -447,7 +421,7 @@ export function AddAccountDialog({
|
||||
description: name.trim(),
|
||||
});
|
||||
onOpenChange(false);
|
||||
} else if (result.error && !handleDuplicateEmailError(result.error)) {
|
||||
} else {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('providers.dialog.toast.error'),
|
||||
|
||||
@@ -51,8 +51,9 @@ export function AuthTerminal({
|
||||
const loginSentRef = useRef(false); // Track if /login was already sent
|
||||
const loginTimeoutRef = useRef<NodeJS.Timeout | null>(null); // Track setTimeout for cleanup
|
||||
const successTimeoutRef = useRef<NodeJS.Timeout | null>(null); // Track success auto-close timeout for cleanup
|
||||
const authCompletedRef = useRef(false); // Track if auth has already completed to prevent race conditions
|
||||
|
||||
const [status, setStatus] = useState<'connecting' | 'ready' | 'success' | 'error'>('connecting');
|
||||
const [status, setStatus] = useState<'connecting' | 'ready' | 'onboarding' | 'success' | 'error'>('connecting');
|
||||
const [authEmail, setAuthEmail] = useState<string | undefined>();
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||
|
||||
@@ -234,6 +235,7 @@ export function AuthTerminal({
|
||||
thisTerminalId: terminalId,
|
||||
isMatch: info.terminalId === terminalId,
|
||||
success: info.success,
|
||||
needsOnboarding: info.needsOnboarding,
|
||||
email: info.email,
|
||||
currentStatus: statusRef.current,
|
||||
loginSent: loginSentRef.current
|
||||
@@ -241,9 +243,16 @@ export function AuthTerminal({
|
||||
if (info.terminalId === terminalId) {
|
||||
if (info.success) {
|
||||
setAuthEmail(info.email);
|
||||
debugLog('Setting status to success', { terminalId });
|
||||
setStatus('success');
|
||||
onAuthSuccess?.(info.email);
|
||||
// If needsOnboarding is true, user should complete setup in terminal
|
||||
// Otherwise, authentication is fully complete
|
||||
if (info.needsOnboarding) {
|
||||
debugLog('Setting status to onboarding', { terminalId });
|
||||
setStatus('onboarding');
|
||||
} else {
|
||||
debugLog('Setting status to success (no onboarding needed)', { terminalId });
|
||||
setStatus('success');
|
||||
onAuthSuccess?.(info.email);
|
||||
}
|
||||
} else {
|
||||
debugLog('OAuth failed', { terminalId, message: info.message });
|
||||
setStatus('error');
|
||||
@@ -263,13 +272,60 @@ export function AuthTerminal({
|
||||
terminalId,
|
||||
exitCode,
|
||||
currentStatus: statusRef.current,
|
||||
loginSent: loginSentRef.current
|
||||
loginSent: loginSentRef.current,
|
||||
willTransitionToSuccess: statusRef.current === 'onboarding' && exitCode === 0
|
||||
});
|
||||
// If we were in onboarding status and terminal exits with code 0,
|
||||
// that means the user completed the onboarding successfully
|
||||
if (statusRef.current === 'onboarding' && exitCode === 0) {
|
||||
// Prevent race condition with onboarding-complete handler
|
||||
if (authCompletedRef.current) {
|
||||
debugLog('SKIPPED exit handler - auth already completed', { terminalId });
|
||||
return;
|
||||
}
|
||||
authCompletedRef.current = true;
|
||||
debugLog('Transitioning from onboarding to success', { terminalId });
|
||||
setStatus('success');
|
||||
onAuthSuccess?.(authEmailRef.current);
|
||||
}
|
||||
// Don't close automatically - let user see any error messages
|
||||
}
|
||||
});
|
||||
cleanupFnsRef.current.push(unsubExit);
|
||||
|
||||
// Handle onboarding complete (Claude shows ready state after login)
|
||||
const unsubOnboardingComplete = window.electronAPI.onTerminalOnboardingComplete((info) => {
|
||||
if (info.terminalId === terminalId) {
|
||||
console.warn('[AuthTerminal] Onboarding complete:', info);
|
||||
debugLog('Onboarding complete event', {
|
||||
terminalId: info.terminalId,
|
||||
profileId: info.profileId,
|
||||
currentStatus: statusRef.current
|
||||
});
|
||||
// Only process if we're in onboarding status
|
||||
if (statusRef.current === 'onboarding') {
|
||||
// Prevent race condition with terminal exit handler
|
||||
if (authCompletedRef.current) {
|
||||
debugLog('SKIPPED onboarding-complete handler - auth already completed', { terminalId });
|
||||
return;
|
||||
}
|
||||
authCompletedRef.current = true;
|
||||
debugLog('Auto-closing terminal after onboarding complete', { terminalId });
|
||||
setStatus('success');
|
||||
onAuthSuccess?.(authEmailRef.current);
|
||||
// Auto-close after a brief delay to show success UI
|
||||
successTimeoutRef.current = setTimeout(() => {
|
||||
if (isCreatedRef.current) {
|
||||
window.electronAPI.destroyTerminal(terminalId).catch(console.error);
|
||||
isCreatedRef.current = false;
|
||||
}
|
||||
onClose();
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
});
|
||||
cleanupFnsRef.current.push(unsubOnboardingComplete);
|
||||
|
||||
return () => {
|
||||
debugLog('Cleaning up event listeners', { terminalId });
|
||||
inputDisposable.dispose();
|
||||
@@ -352,6 +408,9 @@ export function AuthTerminal({
|
||||
{status === 'ready' && (
|
||||
<div className="h-2 w-2 rounded-full bg-yellow-500 animate-pulse" />
|
||||
)}
|
||||
{status === 'onboarding' && (
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
|
||||
)}
|
||||
{status === 'success' && (
|
||||
<CheckCircle2 className="h-4 w-4 text-success" />
|
||||
)}
|
||||
@@ -361,6 +420,7 @@ export function AuthTerminal({
|
||||
<span className="text-sm font-medium">
|
||||
{status === 'connecting' && t('authTerminal.connecting')}
|
||||
{status === 'ready' && t('authTerminal.authenticate', { profileName })}
|
||||
{status === 'onboarding' && t('authTerminal.completeSetup', { profileName })}
|
||||
{status === 'success' && (authEmail ? t('authTerminal.authenticatedAs', { email: authEmail }) : t('authTerminal.authenticated'))}
|
||||
{status === 'error' && t('authTerminal.authError')}
|
||||
</span>
|
||||
@@ -386,6 +446,13 @@ export function AuthTerminal({
|
||||
/>
|
||||
|
||||
{/* Status bar */}
|
||||
{status === 'onboarding' && (
|
||||
<div className="px-3 py-2 border-t border-border bg-blue-500/10">
|
||||
<p className="text-sm text-blue-600 dark:text-blue-400">
|
||||
{t('authTerminal.onboardingMessage')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{status === 'success' && (
|
||||
<div className="px-3 py-2 border-t border-border bg-success/10">
|
||||
<p className="text-sm text-success">
|
||||
|
||||
@@ -104,6 +104,7 @@ export const terminalMock = {
|
||||
onTerminalAuthCreated: () => () => {},
|
||||
onTerminalClaudeBusy: () => () => {},
|
||||
onTerminalClaudeExit: () => () => {},
|
||||
onTerminalOnboardingComplete: () => () => {},
|
||||
onTerminalPendingResume: () => () => {},
|
||||
onTerminalProfileChanged: () => () => {},
|
||||
onTerminalOAuthCodeNeeded: () => () => {},
|
||||
|
||||
@@ -109,6 +109,7 @@ export const IPC_CHANNELS = {
|
||||
TERMINAL_OAUTH_CODE_SUBMIT: 'terminal:oauthCodeSubmit', // User submitted OAuth code to send to terminal
|
||||
TERMINAL_CLAUDE_BUSY: 'terminal:claudeBusy', // Claude Code busy state (for visual indicator)
|
||||
TERMINAL_CLAUDE_EXIT: 'terminal:claudeExit', // Claude Code exited (returned to shell)
|
||||
TERMINAL_ONBOARDING_COMPLETE: 'terminal:onboardingComplete', // Claude onboarding complete (ready for input after login)
|
||||
TERMINAL_PROFILE_CHANGED: 'terminal:profileChanged', // Profile changed, terminals need refresh (main -> renderer)
|
||||
|
||||
// Claude profile management (multi-account support)
|
||||
|
||||
@@ -71,12 +71,6 @@ export const ALL_AVAILABLE_MODELS: ModelOption[] = [
|
||||
{ value: 'glm-4.7', label: 'GLM-4.7', provider: 'zai', description: 'Previous flagship', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
|
||||
{ value: 'glm-4.6v', label: 'GLM-4.6V', provider: 'zai', description: 'Multimodal', capabilities: { thinking: false, tools: true, vision: true, contextWindow: 128000 } },
|
||||
{ value: 'glm-4.5-flash', label: 'GLM-4.5 Flash', provider: 'zai', description: 'Fast', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
|
||||
// Mascarade (LLM Orchestration Engine)
|
||||
{ value: 'mascarade-router', label: 'Mascarade Router', provider: 'mascarade', description: 'Multi-LLM routing (best/cheapest/fastest)', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
|
||||
{ value: 'mascarade-writer', label: 'Mascarade Writer', provider: 'mascarade', description: 'Content generation agent', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
|
||||
{ value: 'mascarade-coder', label: 'Mascarade Coder', provider: 'mascarade', description: 'Code review & generation', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
|
||||
{ value: 'mascarade-analyst', label: 'Mascarade Analyst', provider: 'mascarade', description: 'Data analysis agent', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
|
||||
{ value: 'mascarade-planner', label: 'Mascarade Planner', provider: 'mascarade', description: 'Project decomposition', capabilities: { thinking: false, tools: true, vision: false, contextWindow: 128000 } },
|
||||
];
|
||||
|
||||
// Maps model shorthand to actual Claude model IDs
|
||||
@@ -328,12 +322,6 @@ export const PROVIDER_PRESET_DEFINITIONS: Partial<Record<BuiltinProvider, Record
|
||||
balanced: { primaryModel: '', primaryThinking: 'low', phaseModels: { spec: '', planning: '', coding: '', qa: '' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
|
||||
quick: { primaryModel: '', primaryThinking: 'low', phaseModels: { spec: '', planning: '', coding: '', qa: '' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
|
||||
},
|
||||
mascarade: {
|
||||
auto: { primaryModel: 'mascarade-router', primaryThinking: 'low', phaseModels: { spec: 'mascarade-router', planning: 'mascarade-router', coding: 'mascarade-router', qa: 'mascarade-router' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
|
||||
complex: { primaryModel: 'mascarade-router', primaryThinking: 'low', phaseModels: { spec: 'mascarade-router', planning: 'mascarade-router', coding: 'mascarade-router', qa: 'mascarade-router' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
|
||||
balanced: { primaryModel: 'mascarade-router', primaryThinking: 'low', phaseModels: { spec: 'mascarade-router', planning: 'mascarade-router', coding: 'mascarade-router', qa: 'mascarade-router' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
|
||||
quick: { primaryModel: 'mascarade-router', primaryThinking: 'low', phaseModels: { spec: 'mascarade-router', planning: 'mascarade-router', coding: 'mascarade-router', qa: 'mascarade-router' }, phaseThinking: { spec: 'low', planning: 'low', coding: 'low', qa: 'low' } },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,11 +73,4 @@ export const PROVIDER_REGISTRY: ProviderInfo[] = [
|
||||
authMethods: ['api-key'], envVars: [],
|
||||
configFields: ['baseUrl'],
|
||||
},
|
||||
{
|
||||
id: 'mascarade', name: 'Mascarade', description: 'LLM Orchestration Engine — multi-provider routing with 19+ agents',
|
||||
category: 'local',
|
||||
authMethods: ['api-key'], envVars: ['MASCARADE_API_KEY'],
|
||||
configFields: ['baseUrl'],
|
||||
website: 'https://mascarade.saillant.cc',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -674,9 +674,11 @@
|
||||
"authFailed": "Authentication failed",
|
||||
"connecting": "Connecting...",
|
||||
"authenticate": "Authenticate: {{profileName}}",
|
||||
"completeSetup": "Complete setup: {{profileName}}",
|
||||
"authenticatedAs": "Authenticated as {{email}}",
|
||||
"authenticated": "Authenticated!",
|
||||
"authError": "Authentication Error",
|
||||
"onboardingMessage": "Token received! Complete the Claude Code setup below - this window will close automatically when done.",
|
||||
"successMessage": "Authentication successful! Closing..."
|
||||
},
|
||||
"profileCreated": {
|
||||
|
||||
@@ -810,11 +810,7 @@
|
||||
"toast": {
|
||||
"added": "Account added",
|
||||
"updated": "Account updated",
|
||||
"error": "Failed to save account",
|
||||
"duplicateEmail": "This email is already registered as \"{{existingName}}\"",
|
||||
"createProfileFailed": "Failed to create profile",
|
||||
"authPrepareFailed": "Failed to prepare terminal",
|
||||
"unexpectedError": "Unexpected error"
|
||||
"error": "Failed to save account"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
|
||||
@@ -674,9 +674,11 @@
|
||||
"authFailed": "Échec de l'authentification",
|
||||
"connecting": "Connexion...",
|
||||
"authenticate": "Authentifier : {{profileName}}",
|
||||
"completeSetup": "Terminer la configuration : {{profileName}}",
|
||||
"authenticatedAs": "Authentifié en tant que {{email}}",
|
||||
"authenticated": "Authentifié !",
|
||||
"authError": "Erreur d'authentification",
|
||||
"onboardingMessage": "Jeton reçu ! Terminez la configuration de Claude Code ci-dessous - cette fenêtre se fermera automatiquement une fois terminé.",
|
||||
"successMessage": "Authentification réussie ! Fermeture..."
|
||||
},
|
||||
"profileCreated": {
|
||||
|
||||
@@ -810,11 +810,7 @@
|
||||
"toast": {
|
||||
"added": "Compte ajouté",
|
||||
"updated": "Compte mis à jour",
|
||||
"error": "Échec de l'enregistrement du compte",
|
||||
"duplicateEmail": "Cet e-mail est déjà enregistré sous \"{{existingName}}\"",
|
||||
"createProfileFailed": "Échec de la création du profil",
|
||||
"authPrepareFailed": "Échec de la préparation du terminal",
|
||||
"unexpectedError": "Erreur inattendue"
|
||||
"error": "Échec de l'enregistrement du compte"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
|
||||
@@ -287,6 +287,8 @@ export interface ElectronAPI {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
detectedAt: string;
|
||||
/** If true, user should complete onboarding in terminal before closing */
|
||||
needsOnboarding?: boolean;
|
||||
}) => void) => () => void;
|
||||
/** Listen for auth terminal creation - allows UI to display the OAuth terminal */
|
||||
onTerminalAuthCreated: (callback: (info: {
|
||||
@@ -298,6 +300,12 @@ export interface ElectronAPI {
|
||||
onTerminalClaudeBusy: (callback: (id: string, isBusy: boolean) => void) => () => void;
|
||||
/** Listen for Claude exit (user closed Claude within terminal, returned to shell) */
|
||||
onTerminalClaudeExit: (callback: (id: string) => void) => () => void;
|
||||
/** Listen for onboarding complete (Claude shows ready state after login/onboarding) */
|
||||
onTerminalOnboardingComplete: (callback: (info: {
|
||||
terminalId: string;
|
||||
profileId?: string;
|
||||
detectedAt: string;
|
||||
}) => void) => () => void;
|
||||
/** Listen for pending Claude resume notifications (for deferred resume on tab activation) */
|
||||
onTerminalPendingResume: (callback: (id: string, sessionId?: string) => void) => () => void;
|
||||
/** Listen for profile change events - terminals need to be recreated with new profile env vars */
|
||||
|
||||
@@ -7,7 +7,7 @@ export type CredentialSource = 'oauth' | 'api-key' | 'env' | 'keychain';
|
||||
export type BuiltinProvider =
|
||||
| 'anthropic' | 'openai' | 'google' | 'amazon-bedrock' | 'azure'
|
||||
| 'mistral' | 'groq' | 'xai' | 'openrouter' | 'zai'
|
||||
| 'ollama' | 'openai-compatible' | 'mascarade';
|
||||
| 'ollama' | 'openai-compatible';
|
||||
|
||||
export type BillingModel = 'subscription' | 'pay-per-use';
|
||||
|
||||
|
||||
Generated
+947
-1121
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -45,6 +45,6 @@
|
||||
"lucide-react": "^0.562.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@electron/rebuild": "4.0.3"
|
||||
"@electron/rebuild": "4.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user