Skip to content

Commit c0fd9c8

Browse files
feat: custom env & claude login (#15)
* feat: custom env * feat: support claude code login with credentails
1 parent 951c16a commit c0fd9c8

File tree

7 files changed

+370
-10
lines changed

7 files changed

+370
-10
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,5 @@ storybook-static/
177177
coverage/
178178
.DS_Store
179179
*.pem
180-
tasks_backup.json
180+
tasks_backup.json
181+
.credentials.json

async-code-web/app/settings/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"use client";
22

33
import { useState, useEffect } from "react";
4-
import { Github, CheckCircle, ArrowLeft, Settings, Key, Shield, Info } from "lucide-react";
4+
import { Github, CheckCircle, ArrowLeft, Settings, Key, Shield, Info, Code } from "lucide-react";
55
import { Button } from "@/components/ui/button";
66
import { Input } from "@/components/ui/input";
77
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
88
import { Label } from "@/components/ui/label";
99
import { toast } from "sonner";
10+
import { CodeAgentSettings } from "@/components/code-agent-settings";
1011

1112
import Link from "next/link";
1213

@@ -106,7 +107,7 @@ export default function SettingsPage() {
106107
</div>
107108
<div>
108109
<h1 className="text-xl font-semibold text-slate-900">Settings</h1>
109-
<p className="text-sm text-slate-500">Configure your GitHub authentication</p>
110+
<p className="text-sm text-slate-500">Configure GitHub authentication and code agent environments</p>
110111
</div>
111112
</div>
112113
</div>
@@ -236,6 +237,9 @@ export default function SettingsPage() {
236237
</CardContent>
237238
</Card>
238239

240+
{/* Code Agent Settings */}
241+
<CodeAgentSettings />
242+
239243
{/* Token Creation Instructions */}
240244
<Card className="bg-blue-50 border-blue-200">
241245
<CardHeader>
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"use client";
2+
3+
import React, { useState, useEffect } from "react";
4+
import CodeMirror from "@uiw/react-codemirror";
5+
import { javascript } from "@codemirror/lang-javascript";
6+
import { githubLight } from "@uiw/codemirror-theme-github";
7+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8+
import { Button } from "@/components/ui/button";
9+
import { Label } from "@/components/ui/label";
10+
import { Alert, AlertDescription } from "@/components/ui/alert";
11+
import { AlertCircle, Save } from "lucide-react";
12+
import { toast } from "sonner";
13+
import { SupabaseService } from "@/lib/supabase-service";
14+
import { useUserProfile } from "@/hooks/useUserProfile";
15+
16+
interface CodeAgentConfig {
17+
claudeCode?: Record<string, string>;
18+
codexCLI?: Record<string, string>;
19+
}
20+
21+
const DEFAULT_CLAUDE_ENV = {
22+
ANTHROPIC_API_KEY: "",
23+
// Add other Claude-specific env vars here if needed
24+
};
25+
26+
const DEFAULT_CODEX_ENV = {
27+
OPENAI_API_KEY: "",
28+
DISABLE_SANDBOX: "yes",
29+
CONTINUE_ON_BROWSER: "no",
30+
// Add other Codex-specific env vars here if needed
31+
};
32+
33+
export function CodeAgentSettings() {
34+
const { profile, refreshProfile } = useUserProfile();
35+
const [claudeEnv, setClaudeEnv] = useState("");
36+
const [codexEnv, setCodexEnv] = useState("");
37+
const [isLoading, setIsLoading] = useState(false);
38+
const [errors, setErrors] = useState<{ claude?: string; codex?: string }>({});
39+
40+
// Load settings from profile on mount
41+
useEffect(() => {
42+
if (profile?.preferences) {
43+
const prefs = profile.preferences as CodeAgentConfig;
44+
setClaudeEnv(JSON.stringify(prefs.claudeCode || DEFAULT_CLAUDE_ENV, null, 2));
45+
setCodexEnv(JSON.stringify(prefs.codexCLI || DEFAULT_CODEX_ENV, null, 2));
46+
} else {
47+
setClaudeEnv(JSON.stringify(DEFAULT_CLAUDE_ENV, null, 2));
48+
setCodexEnv(JSON.stringify(DEFAULT_CODEX_ENV, null, 2));
49+
}
50+
}, [profile]);
51+
52+
const validateJSON = (value: string, agent: "claude" | "codex") => {
53+
try {
54+
JSON.parse(value);
55+
setErrors(prev => ({ ...prev, [agent]: undefined }));
56+
return true;
57+
} catch (e) {
58+
setErrors(prev => ({ ...prev, [agent]: "Invalid JSON format" }));
59+
return false;
60+
}
61+
};
62+
63+
const handleSave = async () => {
64+
// Validate both JSONs
65+
const isClaudeValid = validateJSON(claudeEnv, "claude");
66+
const isCodexValid = validateJSON(codexEnv, "codex");
67+
68+
if (!isClaudeValid || !isCodexValid) {
69+
toast.error("Please fix JSON errors before saving");
70+
return;
71+
}
72+
73+
setIsLoading(true);
74+
try {
75+
const claudeConfig = JSON.parse(claudeEnv);
76+
const codexConfig = JSON.parse(codexEnv);
77+
78+
const preferences: CodeAgentConfig = {
79+
claudeCode: claudeConfig,
80+
codexCLI: codexConfig,
81+
};
82+
83+
// Merge with existing preferences if any
84+
const existingPrefs = (profile?.preferences || {}) as Record<string, any>;
85+
const mergedPrefs = {
86+
...existingPrefs,
87+
...preferences,
88+
};
89+
90+
await SupabaseService.updateUserProfile({ preferences: mergedPrefs });
91+
await refreshProfile();
92+
toast.success("Code agent settings saved successfully");
93+
} catch (error) {
94+
console.error("Failed to save settings:", error);
95+
toast.error("Failed to save settings");
96+
} finally {
97+
setIsLoading(false);
98+
}
99+
};
100+
101+
return (
102+
<div className="space-y-6">
103+
<Card>
104+
<CardHeader>
105+
<CardTitle>Code Agent Settings</CardTitle>
106+
<CardDescription>
107+
Configure environment variables for each code agent. These settings will be used when creating containers.
108+
</CardDescription>
109+
</CardHeader>
110+
<CardContent className="space-y-6">
111+
<Alert>
112+
<AlertCircle className="h-4 w-4" />
113+
<AlertDescription>
114+
<strong>Important:</strong> Store sensitive API keys here instead of hardcoding them. Personal settings will override default environment variables.
115+
</AlertDescription>
116+
</Alert>
117+
118+
{/* Claude Code Settings */}
119+
<div className="space-y-2">
120+
<Label htmlFor="claude-env">Claude Code Environment Variables</Label>
121+
<div className="border rounded-lg overflow-hidden">
122+
<CodeMirror
123+
id="claude-env"
124+
value={claudeEnv}
125+
height="200px"
126+
extensions={[javascript({ jsx: false })]}
127+
theme={githubLight}
128+
onChange={(value) => {
129+
setClaudeEnv(value);
130+
validateJSON(value, "claude");
131+
}}
132+
placeholder={JSON.stringify(DEFAULT_CLAUDE_ENV, null, 2)}
133+
/>
134+
</div>
135+
{errors.claude && (
136+
<p className="text-sm text-red-500 mt-1">{errors.claude}</p>
137+
)}
138+
<p className="text-sm text-muted-foreground">
139+
Configure environment variables for Claude Code CLI (@anthropic-ai/claude-code)
140+
</p>
141+
</div>
142+
143+
{/* Codex CLI Settings */}
144+
<div className="space-y-2">
145+
<Label htmlFor="codex-env">Codex CLI Environment Variables</Label>
146+
<div className="border rounded-lg overflow-hidden">
147+
<CodeMirror
148+
id="codex-env"
149+
value={codexEnv}
150+
height="200px"
151+
extensions={[javascript({ jsx: false })]}
152+
theme={githubLight}
153+
onChange={(value) => {
154+
setCodexEnv(value);
155+
validateJSON(value, "codex");
156+
}}
157+
placeholder={JSON.stringify(DEFAULT_CODEX_ENV, null, 2)}
158+
/>
159+
</div>
160+
{errors.codex && (
161+
<p className="text-sm text-red-500 mt-1">{errors.codex}</p>
162+
)}
163+
<p className="text-sm text-muted-foreground">
164+
Configure environment variables for Codex CLI (@openai/codex)
165+
</p>
166+
</div>
167+
168+
<Button
169+
onClick={handleSave}
170+
disabled={isLoading || !!errors.claude || !!errors.codex}
171+
className="w-full"
172+
>
173+
<Save className="w-4 h-4 mr-2" />
174+
{isLoading ? "Saving..." : "Save Settings"}
175+
</Button>
176+
</CardContent>
177+
</Card>
178+
</div>
179+
);
180+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as React from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
const alertVariants = cva(
7+
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8+
{
9+
variants: {
10+
variant: {
11+
default: "bg-background text-foreground",
12+
destructive:
13+
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14+
},
15+
},
16+
defaultVariants: {
17+
variant: "default",
18+
},
19+
}
20+
)
21+
22+
const Alert = React.forwardRef<
23+
HTMLDivElement,
24+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25+
>(({ className, variant, ...props }, ref) => (
26+
<div
27+
ref={ref}
28+
role="alert"
29+
className={cn(alertVariants({ variant }), className)}
30+
{...props}
31+
/>
32+
))
33+
Alert.displayName = "Alert"
34+
35+
const AlertTitle = React.forwardRef<
36+
HTMLParagraphElement,
37+
React.HTMLAttributes<HTMLHeadingElement>
38+
>(({ className, ...props }, ref) => (
39+
<h5
40+
ref={ref}
41+
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42+
{...props}
43+
/>
44+
))
45+
AlertTitle.displayName = "AlertTitle"
46+
47+
const AlertDescription = React.forwardRef<
48+
HTMLParagraphElement,
49+
React.HTMLAttributes<HTMLParagraphElement>
50+
>(({ className, ...props }, ref) => (
51+
<div
52+
ref={ref}
53+
className={cn("text-sm [&_p]:leading-relaxed", className)}
54+
{...props}
55+
/>
56+
))
57+
AlertDescription.displayName = "AlertDescription"
58+
59+
export { Alert, AlertTitle, AlertDescription }
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
3+
import { useState, useEffect, useCallback } from "react";
4+
import { SupabaseService } from "@/lib/supabase-service";
5+
import { User } from "@/types";
6+
7+
export function useUserProfile() {
8+
const [profile, setProfile] = useState<User | null>(null);
9+
const [isLoading, setIsLoading] = useState(true);
10+
const [error, setError] = useState<Error | null>(null);
11+
12+
const fetchProfile = useCallback(async () => {
13+
try {
14+
setIsLoading(true);
15+
const data = await SupabaseService.getUserProfile();
16+
setProfile(data);
17+
setError(null);
18+
} catch (err) {
19+
setError(err as Error);
20+
console.error("Failed to fetch user profile:", err);
21+
} finally {
22+
setIsLoading(false);
23+
}
24+
}, []);
25+
26+
useEffect(() => {
27+
fetchProfile();
28+
}, [fetchProfile]);
29+
30+
const refreshProfile = useCallback(async () => {
31+
await fetchProfile();
32+
}, [fetchProfile]);
33+
34+
return {
35+
profile,
36+
isLoading,
37+
error,
38+
refreshProfile,
39+
};
40+
}

server/database.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,14 @@ def migrate_legacy_task(legacy_task: Dict, user_id: str) -> Optional[Dict]:
216216
return result.data[0] if result.data else None
217217
except Exception as e:
218218
logger.error(f"Error migrating legacy task: {e}")
219-
raise
219+
raise
220+
221+
@staticmethod
222+
def get_user_by_id(user_id: str) -> Optional[Dict]:
223+
"""Get user by ID"""
224+
try:
225+
result = supabase.table('users').select('*').eq('id', user_id).single().execute()
226+
return result.data
227+
except Exception as e:
228+
logger.error(f"Error getting user: {e}")
229+
return None

0 commit comments

Comments
 (0)