Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions packages/happy-app/sources/app/(app)/new/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const useProfileMap = (profiles: AIBackendProfile[]) => {

// Environment variable transformation helper
// Returns ALL profile environment variables - daemon will use them as-is
const transformProfileToEnvironmentVars = (profile: AIBackendProfile, agentType: 'claude' | 'codex' | 'gemini' = 'claude') => {
const transformProfileToEnvironmentVars = (profile: AIBackendProfile, agentType: 'claude' | 'codex' | 'gemini' | 'kimi' = 'claude') => {
// getProfileEnvironmentVariables already returns ALL env vars from profile
// including custom environmentVariables array and provider-specific configs
return getProfileEnvironmentVariables(profile);
Expand Down Expand Up @@ -316,7 +316,7 @@ function NewSessionWizard() {
}
return 'anthropic'; // Default to Anthropic
});
const [agentType, setAgentType] = React.useState<'claude' | 'codex' | 'gemini'>(() => {
const [agentType, setAgentType] = React.useState<'claude' | 'codex' | 'gemini' | 'kimi'>(() => {
// Check if agent type was provided in temp data
if (tempSessionData?.agentType) {
// Only allow gemini if experiments are enabled
Expand Down Expand Up @@ -464,7 +464,7 @@ function NewSessionWizard() {

if (agentAvailable === false) {
// Current agent not available - find first available
const availableAgent: 'claude' | 'codex' | 'gemini' =
const availableAgent: 'claude' | 'codex' | 'gemini' | 'kimi' =
cliAvailability.claude === true ? 'claude' :
cliAvailability.codex === true ? 'codex' :
(cliAvailability.gemini === true && experimentsEnabled) ? 'gemini' :
Expand All @@ -489,10 +489,10 @@ function NewSessionWizard() {
const { variables: daemonEnv } = useEnvironmentVariables(selectedMachineId, envVarRefs);

// Temporary banner dismissal (X button) - resets when component unmounts or machine changes
const [hiddenBanners, setHiddenBanners] = React.useState<{ claude: boolean; codex: boolean; gemini: boolean }>({ claude: false, codex: false, gemini: false });
const [hiddenBanners, setHiddenBanners] = React.useState<{ claude: boolean; codex: boolean; gemini: boolean; kimi: boolean }>({ claude: false, codex: false, gemini: false, kimi: false });

// Helper to check if CLI warning has been dismissed (checks both global and per-machine)
const isWarningDismissed = React.useCallback((cli: 'claude' | 'codex' | 'gemini'): boolean => {
const isWarningDismissed = React.useCallback((cli: 'claude' | 'codex' | 'gemini' | 'kimi'): boolean => {
// Check global dismissal first
if (dismissedCLIWarnings.global?.[cli] === true) return true;
// Check per-machine dismissal
Expand All @@ -501,7 +501,7 @@ function NewSessionWizard() {
}, [selectedMachineId, dismissedCLIWarnings]);

// Unified dismiss handler for all three button types (easy to use correctly, hard to use incorrectly)
const handleCLIBannerDismiss = React.useCallback((cli: 'claude' | 'codex' | 'gemini', type: 'temporary' | 'machine' | 'global') => {
const handleCLIBannerDismiss = React.useCallback((cli: 'claude' | 'codex' | 'gemini' | 'kimi', type: 'temporary' | 'machine' | 'global') => {
if (type === 'temporary') {
// X button: Hide for current session only (not persisted)
setHiddenBanners(prev => ({ ...prev, [cli]: true }));
Expand Down Expand Up @@ -553,7 +553,7 @@ function NewSessionWizard() {
const supportedCLIs = (Object.entries(profile.compatibility) as [string, boolean][])
.filter(([, supported]) => supported)
.map(([agent]) => agent);
const requiredCLI = supportedCLIs.length === 1 ? supportedCLIs[0] as 'claude' | 'codex' | 'gemini' : null;
const requiredCLI = supportedCLIs.length === 1 ? supportedCLIs[0] as 'claude' | 'codex' | 'gemini' | 'kimi' : null;

if (requiredCLI && cliAvailability[requiredCLI] === false) {
return {
Expand Down Expand Up @@ -678,7 +678,7 @@ function NewSessionWizard() {
.map(([agent]) => agent);

if (supportedCLIs.length === 1) {
const requiredAgent = supportedCLIs[0] as 'claude' | 'codex' | 'gemini';
const requiredAgent = supportedCLIs[0] as 'claude' | 'codex' | 'gemini' | 'kimi';
// Check if this agent is available and allowed
const isAvailable = cliAvailability[requiredAgent] !== false;
const isAllowed = requiredAgent !== 'gemini' || experimentsEnabled;
Expand Down Expand Up @@ -777,7 +777,7 @@ function NewSessionWizard() {
name: '',
anthropicConfig: {},
environmentVariables: [],
compatibility: { claude: true, codex: true, gemini: true },
compatibility: { claude: true, codex: true, gemini: true, kimi: true },
isBuiltIn: false,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-app/sources/app/(app)/settings/profiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function ProfileManager({ onProfileSelect, selectedProfileId }: ProfileManagerPr
name: '',
anthropicConfig: {},
environmentVariables: [],
compatibility: { claude: true, codex: true, gemini: true },
compatibility: { claude: true, codex: true, gemini: true, kimi: true },
isBuiltIn: false,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/happy-app/sources/components/AgentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ interface AgentInputProps {
};
alwaysShowContextSize?: boolean;
onFileViewerPress?: () => void;
agentType?: 'claude' | 'codex' | 'gemini';
agentType?: 'claude' | 'codex' | 'gemini' | 'kimi';
onAgentClick?: () => void;
machineName?: string | null;
onMachineClick?: () => void;
Expand Down
1 change: 1 addition & 0 deletions packages/happy-app/sources/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const flavorIcons = {
claude: require('@/assets/images/icon-claude.png'),
codex: require('@/assets/images/icon-gpt.png'),
gemini: require('@/assets/images/icon-gemini.png'),
kimi: require('@/assets/images/icon-kimi.png'),
};

const styles = StyleSheet.create((theme) => ({
Expand Down
16 changes: 8 additions & 8 deletions packages/happy-app/sources/components/NewSessionWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N
description: 'Default Claude configuration',
anthropicConfig: {},
environmentVariables: [],
compatibility: { claude: true, codex: false, gemini: false },
compatibility: { claude: true, codex: false, gemini: false, kimi: false },
isBuiltIn: true,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand All @@ -581,7 +581,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N
{ name: 'ANTHROPIC_SMALL_FAST_MODEL', value: 'deepseek-chat' },
{ name: 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', value: '1' },
],
compatibility: { claude: true, codex: false, gemini: false },
compatibility: { claude: true, codex: false, gemini: false, kimi: false },
isBuiltIn: true,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand All @@ -596,7 +596,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N
model: 'gpt-4-turbo',
},
environmentVariables: [],
compatibility: { claude: false, codex: true, gemini: false },
compatibility: { claude: false, codex: true, gemini: false, kimi: false },
isBuiltIn: true,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand All @@ -612,7 +612,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N
deploymentName: 'gpt-4-turbo',
},
environmentVariables: [],
compatibility: { claude: false, codex: true, gemini: false },
compatibility: { claude: false, codex: true, gemini: false, kimi: false },
isBuiltIn: true,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand All @@ -628,7 +628,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N
environmentVariables: [
{ name: 'AZURE_OPENAI_API_VERSION', value: '2024-02-15-preview' },
],
compatibility: { claude: false, codex: true, gemini: false },
compatibility: { claude: false, codex: true, gemini: false, kimi: false },
isBuiltIn: true,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand All @@ -643,7 +643,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N
model: 'glm-4.6',
},
environmentVariables: [],
compatibility: { claude: true, codex: false, gemini: false },
compatibility: { claude: true, codex: false, gemini: false, kimi: false },
isBuiltIn: true,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand All @@ -658,7 +658,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N
model: 'gpt-4-turbo',
},
environmentVariables: [],
compatibility: { claude: false, codex: true, gemini: false },
compatibility: { claude: false, codex: true, gemini: false, kimi: false },
isBuiltIn: true,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand Down Expand Up @@ -937,7 +937,7 @@ export function NewSessionWizard({ onComplete, onCancel, initialPrompt = '' }: N
description: 'Custom AI profile',
anthropicConfig: {},
environmentVariables: [],
compatibility: { claude: true, codex: true, gemini: true },
compatibility: { claude: true, codex: true, gemini: true, kimi: true },
isBuiltIn: false,
createdAt: Date.now(),
updatedAt: Date.now(),
Expand Down
28 changes: 27 additions & 1 deletion packages/happy-app/sources/components/modelModeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,33 @@ export function getGeminiModelModes(): ModelMode[] {
return GEMINI_MODEL_FALLBACKS;
}

export function getKimiPermissionModes(translate: Translate): PermissionMode[] {
return [
{ key: 'default', name: translate('agentInput.kimiPermissionMode.default'), description: null },
{ key: 'read-only', name: translate('agentInput.kimiPermissionMode.readOnly'), description: null },
{ key: 'safe-yolo', name: translate('agentInput.kimiPermissionMode.safeYolo'), description: null },
{ key: 'yolo', name: translate('agentInput.kimiPermissionMode.yolo'), description: null },
];
}

export function getKimiModelModes(): ModelMode[] {
return [
{ key: 'kimi-k2-0711-preview', name: 'Kimi K2', description: 'Most capable' },
{ key: 'kimi-k1.6-preview', name: 'Kimi K1.6', description: 'Fast & capable' },
{ key: 'kimi-k1.5-preview', name: 'Kimi K1.5', description: 'Fast & efficient' },
];
}

export function getHardcodedPermissionModes(flavor: AgentFlavor, translate: Translate): PermissionMode[] {
if (flavor === 'codex') {
return getCodexPermissionModes(translate);
}
if (flavor === 'gemini') {
return getGeminiPermissionModes(translate);
}
if (flavor === 'kimi') {
return getKimiPermissionModes(translate);
}
return getClaudePermissionModes(translate);
}

Expand All @@ -110,6 +130,9 @@ export function getHardcodedModelModes(flavor: AgentFlavor, translate: Translate
if (flavor === 'gemini') {
return getGeminiModelModes();
}
if (flavor === 'kimi') {
return getKimiModelModes();
}
return getClaudeModelModes();
}

Expand All @@ -130,7 +153,7 @@ export function getAvailablePermissionModes(
metadata: Metadata | null | undefined,
translate: Translate,
): PermissionMode[] {
if (flavor === 'claude' || flavor === 'codex') {
if (flavor === 'claude' || flavor === 'codex' || flavor === 'kimi') {
return hackModes(getHardcodedPermissionModes(flavor, translate));
}

Expand Down Expand Up @@ -169,6 +192,9 @@ export function getDefaultModelKey(flavor: AgentFlavor): string {
if (flavor === 'gemini') {
return 'gemini-2.5-pro';
}
if (flavor === 'kimi') {
return 'kimi-k2-0711-preview';
}
return 'default';
}

Expand Down
11 changes: 8 additions & 3 deletions packages/happy-app/sources/hooks/useCLIDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface CLIAvailability {
claude: boolean | null; // null = unknown/loading, true = installed, false = not installed
codex: boolean | null;
gemini: boolean | null;
kimi: boolean | null;
isDetecting: boolean; // Explicit loading state
timestamp: number; // When detection completed
error?: string; // Detection error message (for debugging)
Expand Down Expand Up @@ -37,13 +38,14 @@ export function useCLIDetection(machineId: string | null): CLIAvailability {
claude: null,
codex: null,
gemini: null,
kimi: null,
isDetecting: false,
timestamp: 0,
});

useEffect(() => {
if (!machineId) {
setAvailability({ claude: null, codex: null, gemini: null, isDetecting: false, timestamp: 0 });
setAvailability({ claude: null, codex: null, gemini: null, kimi: null, isDetecting: false, timestamp: 0 });
return;
}

Expand Down Expand Up @@ -71,12 +73,12 @@ export function useCLIDetection(machineId: string | null): CLIAvailability {
if (result.success && result.exitCode === 0) {
// Parse output: "claude:true\ncodex:false\ngemini:false"
const lines = result.stdout.trim().split('\n');
const cliStatus: { claude?: boolean; codex?: boolean; gemini?: boolean } = {};
const cliStatus: { claude?: boolean; codex?: boolean; gemini?: boolean; kimi?: boolean } = {};

lines.forEach(line => {
const [cli, status] = line.split(':');
if (cli && status) {
cliStatus[cli.trim() as 'claude' | 'codex' | 'gemini'] = status.trim() === 'true';
cliStatus[cli.trim() as 'claude' | 'codex' | 'gemini' | 'kimi'] = status.trim() === 'true';
}
});

Expand All @@ -85,6 +87,7 @@ export function useCLIDetection(machineId: string | null): CLIAvailability {
claude: cliStatus.claude ?? null,
codex: cliStatus.codex ?? null,
gemini: cliStatus.gemini ?? null,
kimi: cliStatus.kimi ?? null,
isDetecting: false,
timestamp: Date.now(),
});
Expand All @@ -95,6 +98,7 @@ export function useCLIDetection(machineId: string | null): CLIAvailability {
claude: null,
codex: null,
gemini: null,
kimi: null,
isDetecting: false,
timestamp: 0,
error: `Detection failed: ${result.stderr || 'Unknown error'}`,
Expand All @@ -109,6 +113,7 @@ export function useCLIDetection(machineId: string | null): CLIAvailability {
claude: null,
codex: null,
gemini: null,
kimi: null,
isDetecting: false,
timestamp: 0,
error: error instanceof Error ? error.message : 'Detection error',
Expand Down
26 changes: 19 additions & 7 deletions packages/happy-app/sources/hooks/useConnectTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,39 @@ export function useConnectTerminal(options?: UseConnectTerminalOptions) {
Modal.alert(t('common.error'), t('modals.invalidAuthUrl'), [{ text: t('common.ok') }]);
return false;
}


if (!auth.credentials?.secret || !auth.credentials?.token) {
console.error('[ConnectTerminal] Missing auth credentials');
Modal.alert(t('common.error'), t('modals.failedToConnectTerminal'), [{ text: t('common.ok') }]);
return false;
}

if (!sync.encryption?.contentDataKey) {
console.error('[ConnectTerminal] Encryption not initialized');
Modal.alert(t('common.error'), t('modals.failedToConnectTerminal'), [{ text: t('common.ok') }]);
return false;
}

setIsLoading(true);
try {
const tail = url.slice('happy://terminal?'.length);
const publicKey = decodeBase64(tail, 'base64url');
const responseV1 = encryptBox(decodeBase64(auth.credentials!.secret, 'base64url'), publicKey);
const responseV1 = encryptBox(decodeBase64(auth.credentials.secret, 'base64url'), publicKey);
let responseV2Bundle = new Uint8Array(sync.encryption.contentDataKey.length + 1);
responseV2Bundle[0] = 0;
responseV2Bundle.set(sync.encryption.contentDataKey, 1);
const responseV2 = encryptBox(responseV2Bundle, publicKey);
await authApprove(auth.credentials!.token, publicKey, responseV1, responseV2);
await authApprove(auth.credentials.token, publicKey, responseV1, responseV2);

Modal.alert(t('common.success'), t('modals.terminalConnectedSuccessfully'), [
{
text: t('common.ok'),
{
text: t('common.ok'),
onPress: () => options?.onSuccess?.()
}
]);
return true;
} catch (e) {
console.error(e);
console.error('[ConnectTerminal] Error:', e);
Modal.alert(t('common.error'), t('modals.failedToConnectTerminal'), [{ text: t('common.ok') }]);
options?.onError?.(e);
return false;
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-app/sources/sync/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export interface SpawnSessionOptions {
directory: string;
approvedNewDirectoryCreation?: boolean;
token?: string;
agent?: 'codex' | 'claude' | 'gemini';
agent?: 'codex' | 'claude' | 'gemini' | 'kimi';
// Environment variables from AI backend profile
// Accepts any environment variables - daemon will pass them to the agent process
// Common variables include:
Expand Down Expand Up @@ -167,7 +167,7 @@ export async function machineSpawnNewSession(options: SpawnSessionOptions): Prom
directory: string
approvedNewDirectoryCreation?: boolean,
token?: string,
agent?: 'codex' | 'claude' | 'gemini',
agent?: 'codex' | 'claude' | 'gemini' | 'kimi',
environmentVariables?: Record<string, string>;
}>(
machineId,
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-app/sources/sync/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { PermissionModeKey } from '@/components/PermissionModeSelector';
const mmkv = new MMKV();
const NEW_SESSION_DRAFT_KEY = 'new-session-draft-v1';

export type NewSessionAgentType = 'claude' | 'codex' | 'gemini';
export type NewSessionAgentType = 'claude' | 'codex' | 'gemini' | 'kimi';
export type NewSessionSessionType = 'simple' | 'worktree';

export interface NewSessionDraft {
Expand Down
Loading