Merge branch 'v0.9.0rc' into remove-sandbox-as-it-is-broken

This commit is contained in:
webdevcody
2026-01-07 15:01:31 -05:00
115 changed files with 8734 additions and 401 deletions

View File

@@ -33,9 +33,31 @@ export const DEFAULT_MODEL = 'claude-opus-4-5-20251101';
* Formats a model name for display
*/
export function formatModelName(model: string): string {
// Claude models
if (model.includes('opus')) return 'Opus 4.5';
if (model.includes('sonnet')) return 'Sonnet 4.5';
if (model.includes('haiku')) return 'Haiku 4.5';
// Codex/GPT models
if (model === 'gpt-5.2') return 'GPT-5.2';
if (model === 'gpt-5.1-codex-max') return 'GPT-5.1 Max';
if (model === 'gpt-5.1-codex') return 'GPT-5.1 Codex';
if (model === 'gpt-5.1-codex-mini') return 'GPT-5.1 Mini';
if (model === 'gpt-5.1') return 'GPT-5.1';
if (model.startsWith('gpt-')) return model.toUpperCase();
if (model.match(/^o\d/)) return model.toUpperCase(); // o1, o3, etc.
// Cursor models
if (model === 'cursor-auto' || model === 'auto') return 'Cursor Auto';
if (model === 'cursor-composer-1' || model === 'composer-1') return 'Composer 1';
if (model.startsWith('cursor-sonnet')) return 'Cursor Sonnet';
if (model.startsWith('cursor-opus')) return 'Cursor Opus';
if (model.startsWith('cursor-gpt')) return model.replace('cursor-', '').replace('gpt-', 'GPT-');
if (model.startsWith('cursor-gemini'))
return model.replace('cursor-', 'Cursor ').replace('gemini', 'Gemini');
if (model.startsWith('cursor-grok')) return 'Cursor Grok';
// Default: split by dash and capitalize
return model.split('-').slice(1, 3).join(' ');
}

View File

@@ -0,0 +1,86 @@
import { type CodexCreditsSnapshot, type CodexPlanType } from '@/store/app-store';
const WINDOW_DEFAULT_LABEL = 'Usage window';
const RESET_LABEL = 'Resets';
const UNKNOWN_LABEL = 'Unknown';
const UNAVAILABLE_LABEL = 'Unavailable';
const UNLIMITED_LABEL = 'Unlimited';
const AVAILABLE_LABEL = 'Available';
const NONE_LABEL = 'None';
const DAY_UNIT = 'day';
const HOUR_UNIT = 'hour';
const MINUTE_UNIT = 'min';
const WINDOW_SUFFIX = 'window';
const MINUTES_PER_HOUR = 60;
const MINUTES_PER_DAY = 24 * MINUTES_PER_HOUR;
const MILLISECONDS_PER_SECOND = 1000;
const SESSION_HOURS = 5;
const DAYS_PER_WEEK = 7;
const SESSION_WINDOW_MINS = SESSION_HOURS * MINUTES_PER_HOUR;
const WEEKLY_WINDOW_MINS = DAYS_PER_WEEK * MINUTES_PER_DAY;
const SESSION_TITLE = 'Session Usage';
const SESSION_SUBTITLE = '5-hour rolling window';
const WEEKLY_TITLE = 'Weekly';
const WEEKLY_SUBTITLE = 'All models';
const FALLBACK_TITLE = 'Usage Window';
const PLAN_TYPE_LABELS: Record<CodexPlanType, string> = {
free: 'Free',
plus: 'Plus',
pro: 'Pro',
team: 'Team',
business: 'Business',
enterprise: 'Enterprise',
edu: 'Education',
unknown: UNKNOWN_LABEL,
};
export function formatCodexWindowDuration(minutes: number | null): string {
if (!minutes || minutes <= 0) return WINDOW_DEFAULT_LABEL;
if (minutes % MINUTES_PER_DAY === 0) {
const days = minutes / MINUTES_PER_DAY;
return `${days} ${DAY_UNIT}${days === 1 ? '' : 's'} ${WINDOW_SUFFIX}`;
}
if (minutes % MINUTES_PER_HOUR === 0) {
const hours = minutes / MINUTES_PER_HOUR;
return `${hours} ${HOUR_UNIT}${hours === 1 ? '' : 's'} ${WINDOW_SUFFIX}`;
}
return `${minutes} ${MINUTE_UNIT} ${WINDOW_SUFFIX}`;
}
export type CodexWindowLabel = {
title: string;
subtitle: string;
isPrimary: boolean;
};
export function getCodexWindowLabel(windowDurationMins: number | null): CodexWindowLabel {
if (windowDurationMins === SESSION_WINDOW_MINS) {
return { title: SESSION_TITLE, subtitle: SESSION_SUBTITLE, isPrimary: true };
}
if (windowDurationMins === WEEKLY_WINDOW_MINS) {
return { title: WEEKLY_TITLE, subtitle: WEEKLY_SUBTITLE, isPrimary: false };
}
return {
title: FALLBACK_TITLE,
subtitle: formatCodexWindowDuration(windowDurationMins),
isPrimary: false,
};
}
export function formatCodexResetTime(resetsAt: number | null): string | null {
if (!resetsAt) return null;
const date = new Date(resetsAt * MILLISECONDS_PER_SECOND);
return `${RESET_LABEL} ${date.toLocaleString()}`;
}
export function formatCodexPlanType(plan: CodexPlanType | null): string {
if (!plan) return UNKNOWN_LABEL;
return PLAN_TYPE_LABELS[plan] ?? plan;
}
export function formatCodexCredits(snapshot: CodexCreditsSnapshot | null): string {
if (!snapshot) return UNAVAILABLE_LABEL;
if (snapshot.unlimited) return UNLIMITED_LABEL;
if (snapshot.balance) return snapshot.balance;
return snapshot.hasCredits ? AVAILABLE_LABEL : NONE_LABEL;
}

View File

@@ -568,6 +568,7 @@ export interface ElectronAPI {
mimeType: string,
projectPath?: string
) => Promise<SaveImageResult>;
isElectron?: boolean;
checkClaudeCli?: () => Promise<{
success: boolean;
status?: string;
@@ -614,79 +615,43 @@ export interface ElectronAPI {
error?: string;
}>;
};
setup?: {
getClaudeStatus: () => Promise<{
success: boolean;
status?: string;
installed?: boolean;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasCredentialsFile?: boolean;
hasToken?: boolean;
hasStoredOAuthToken?: boolean;
hasStoredApiKey?: boolean;
hasEnvApiKey?: boolean;
hasEnvOAuthToken?: boolean;
};
error?: string;
}>;
installClaude: () => Promise<{
success: boolean;
message?: string;
error?: string;
}>;
authClaude: () => Promise<{
success: boolean;
token?: string;
requiresManualAuth?: boolean;
terminalOpened?: boolean;
command?: string;
error?: string;
message?: string;
output?: string;
}>;
storeApiKey: (
provider: string,
apiKey: string
) => Promise<{ success: boolean; error?: string }>;
deleteApiKey: (
provider: string
) => Promise<{ success: boolean; error?: string; message?: string }>;
getApiKeys: () => Promise<{
success: boolean;
hasAnthropicKey: boolean;
hasGoogleKey: boolean;
}>;
getPlatform: () => Promise<{
success: boolean;
platform: string;
arch: string;
homeDir: string;
isWindows: boolean;
isMac: boolean;
isLinux: boolean;
}>;
verifyClaudeAuth: (authMethod?: 'cli' | 'api_key') => Promise<{
success: boolean;
authenticated: boolean;
error?: string;
}>;
getGhStatus?: () => Promise<{
success: boolean;
installed: boolean;
authenticated: boolean;
version: string | null;
path: string | null;
user: string | null;
error?: string;
}>;
onInstallProgress?: (callback: (progress: any) => void) => () => void;
onAuthProgress?: (callback: (progress: any) => void) => () => void;
templates?: {
clone: (
repoUrl: string,
projectName: string,
parentDir: string
) => Promise<{ success: boolean; projectPath?: string; error?: string }>;
};
backlogPlan?: {
generate: (
projectPath: string,
prompt: string,
model?: string
) => Promise<{ success: boolean; error?: string }>;
stop: () => Promise<{ success: boolean; error?: string }>;
status: () => Promise<{ success: boolean; isRunning?: boolean; error?: string }>;
apply: (
projectPath: string,
plan: {
changes: Array<{
type: 'add' | 'update' | 'delete';
featureId?: string;
feature?: Record<string, unknown>;
reason: string;
}>;
summary: string;
dependencyUpdates: Array<{
featureId: string;
removedDependencies: string[];
addedDependencies: string[];
}>;
}
) => Promise<{ success: boolean; appliedChanges?: string[]; error?: string }>;
onEvent: (callback: (data: unknown) => void) => () => void;
};
// Setup API surface is implemented by the main process and mirrored by HttpApiClient.
// Keep this intentionally loose to avoid tight coupling between front-end and server types.
setup?: any;
agent?: {
start: (
sessionId: string,
@@ -791,11 +756,13 @@ export const isElectron = (): boolean => {
return false;
}
if ((window as any).isElectron === true) {
const w = window as any;
if (w.isElectron === true) {
return true;
}
return window.electronAPI?.isElectron === true;
return !!w.electronAPI?.isElectron;
};
// Check if backend server is available

View File

@@ -422,6 +422,7 @@ export const checkSandboxEnvironment = async (): Promise<{
try {
const response = await fetch(`${getServerUrl()}/api/health/environment`, {
method: 'GET',
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
@@ -1237,6 +1238,52 @@ export class HttpApiClient implements ElectronAPI {
`/api/setup/cursor-permissions/example${profileId ? `?profileId=${profileId}` : ''}`
),
// Codex CLI methods
getCodexStatus: (): Promise<{
success: boolean;
status?: string;
installed?: boolean;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasAuthFile?: boolean;
hasOAuthToken?: boolean;
hasApiKey?: boolean;
hasStoredApiKey?: boolean;
hasEnvApiKey?: boolean;
};
error?: string;
}> => this.get('/api/setup/codex-status'),
installCodex: (): Promise<{
success: boolean;
message?: string;
error?: string;
}> => this.post('/api/setup/install-codex'),
authCodex: (): Promise<{
success: boolean;
token?: string;
requiresManualAuth?: boolean;
terminalOpened?: boolean;
command?: string;
error?: string;
message?: string;
output?: string;
}> => this.post('/api/setup/auth-codex'),
verifyCodexAuth: (
authMethod: 'cli' | 'api_key',
apiKey?: string
): Promise<{
success: boolean;
authenticated: boolean;
error?: string;
}> => this.post('/api/setup/verify-codex-auth', { authMethod, apiKey }),
onInstallProgress: (callback: (progress: unknown) => void) => {
return this.subscribeToEvent('agent:stream', callback);
},

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import type { ModelAlias } from '@/store/app-store';
import type { ModelAlias, ModelProvider } from '@/store/app-store';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -14,6 +14,33 @@ export function modelSupportsThinking(_model?: ModelAlias | string): boolean {
return true;
}
/**
* Determine the provider from a model string
* Mirrors the logic in apps/server/src/providers/provider-factory.ts
*/
export function getProviderFromModel(model?: string): ModelProvider {
if (!model) return 'claude';
// Check for Cursor models (cursor- prefix)
if (model.startsWith('cursor-') || model.startsWith('cursor:')) {
return 'cursor';
}
// Check for Codex/OpenAI models (gpt- prefix or o-series)
const CODEX_MODEL_PREFIXES = ['gpt-'];
const OPENAI_O_SERIES_PATTERN = /^o\d/;
if (
CODEX_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix)) ||
OPENAI_O_SERIES_PATTERN.test(model) ||
model.startsWith('codex:')
) {
return 'codex';
}
// Default to Claude
return 'claude';
}
/**
* Get display name for a model
*/
@@ -22,6 +49,15 @@ export function getModelDisplayName(model: ModelAlias | string): string {
haiku: 'Claude Haiku',
sonnet: 'Claude Sonnet',
opus: 'Claude Opus',
// Codex models
'gpt-5.2': 'GPT-5.2',
'gpt-5.1-codex-max': 'GPT-5.1 Codex Max',
'gpt-5.1-codex': 'GPT-5.1 Codex',
'gpt-5.1-codex-mini': 'GPT-5.1 Codex Mini',
'gpt-5.1': 'GPT-5.1',
// Cursor models (common ones)
'cursor-auto': 'Cursor Auto',
'cursor-composer-1': 'Composer 1',
};
return displayNames[model] || model;
}