From 3d9dfb21715708af7741f2dab7b040d2ac2f5701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 10 Jan 2026 22:41:51 +0000 Subject: [PATCH] feat: settings page --- apps/web/app/settings/agent/[id]/page.tsx | 258 ++++++++++++ apps/web/app/settings/agents/page.tsx | 238 +++++++++++ .../app/settings/environment/[id]/page.tsx | 366 +++++++++++++++++ apps/web/app/settings/environments/page.tsx | 205 ++++++++++ apps/web/app/settings/general/page.tsx | 222 ++++++++++ apps/web/app/settings/integrations/page.tsx | 151 +++++++ apps/web/app/settings/layout.tsx | 41 ++ apps/web/app/settings/page.tsx | 5 + apps/web/app/settings/plugins/page.tsx | 144 +++++++ apps/web/app/settings/workspace/[id]/page.tsx | 378 ++++++++++++++++++ apps/web/app/settings/workspace/page.tsx | 116 ++++++ apps/web/components/kanban-board.tsx | 10 +- apps/web/components/settings/agent-card.tsx | 55 +++ apps/web/components/settings/context-card.tsx | 214 ++++++++++ .../components/settings/environment-card.tsx | 49 +++ .../components/settings/key-value-input.tsx | 73 ++++ .../components/settings/repository-card.tsx | 249 ++++++++++++ .../settings/settings-app-sidebar.tsx | 212 ++++++++++ .../settings/settings-mobile-nav.tsx | 24 ++ .../components/settings/settings-section.tsx | 29 ++ .../components/settings/settings-sidebar.tsx | 66 +++ apps/web/components/theme-provider.tsx | 4 +- apps/web/components/ui/breadcrumb.tsx | 123 ++++++ apps/web/components/ui/slider.tsx | 59 +++ apps/web/components/ui/switch.tsx | 33 ++ apps/web/lib/settings/dummy-data.ts | 134 +++++++ apps/web/lib/settings/types.ts | 87 ++++ 27 files changed, 3541 insertions(+), 4 deletions(-) create mode 100644 apps/web/app/settings/agent/[id]/page.tsx create mode 100644 apps/web/app/settings/agents/page.tsx create mode 100644 apps/web/app/settings/environment/[id]/page.tsx create mode 100644 apps/web/app/settings/environments/page.tsx create mode 100644 apps/web/app/settings/general/page.tsx create mode 100644 apps/web/app/settings/integrations/page.tsx create mode 100644 apps/web/app/settings/layout.tsx create mode 100644 apps/web/app/settings/page.tsx create mode 100644 apps/web/app/settings/plugins/page.tsx create mode 100644 apps/web/app/settings/workspace/[id]/page.tsx create mode 100644 apps/web/app/settings/workspace/page.tsx create mode 100644 apps/web/components/settings/agent-card.tsx create mode 100644 apps/web/components/settings/context-card.tsx create mode 100644 apps/web/components/settings/environment-card.tsx create mode 100644 apps/web/components/settings/key-value-input.tsx create mode 100644 apps/web/components/settings/repository-card.tsx create mode 100644 apps/web/components/settings/settings-app-sidebar.tsx create mode 100644 apps/web/components/settings/settings-mobile-nav.tsx create mode 100644 apps/web/components/settings/settings-section.tsx create mode 100644 apps/web/components/settings/settings-sidebar.tsx create mode 100644 apps/web/components/ui/breadcrumb.tsx create mode 100644 apps/web/components/ui/slider.tsx create mode 100644 apps/web/components/ui/switch.tsx create mode 100644 apps/web/lib/settings/dummy-data.ts create mode 100644 apps/web/lib/settings/types.ts diff --git a/apps/web/app/settings/agent/[id]/page.tsx b/apps/web/app/settings/agent/[id]/page.tsx new file mode 100644 index 0000000000..f1fc90abd4 --- /dev/null +++ b/apps/web/app/settings/agent/[id]/page.tsx @@ -0,0 +1,258 @@ +'use client'; + +import { use, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { IconTrash } from '@tabler/icons-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { Switch } from '@/components/ui/switch'; +import { Slider } from '@/components/ui/slider'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { SETTINGS_DATA } from '@/lib/settings/dummy-data'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import type { AgentProfile, AgentType } from '@/lib/settings/types'; + +const AGENT_LABELS: Record = { + 'claude-code': 'Claude Code', + 'codex': 'Codex', + 'auggie': 'Auggie', +}; + +const MODEL_OPTIONS: Record = { + 'claude-code': [ + { value: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5' }, + { value: 'claude-opus-4', label: 'Claude Opus 4' }, + { value: 'claude-haiku-4', label: 'Claude Haiku 4' }, + ], + 'codex': [ + { value: 'codex-v2', label: 'Codex v2' }, + { value: 'codex-v1', label: 'Codex v1' }, + ], + 'auggie': [ + { value: 'auggie-v1', label: 'Auggie v1' }, + { value: 'auggie-lite', label: 'Auggie Lite' }, + ], +}; + +export default function AgentEditPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params); + const router = useRouter(); + const [agent, setAgent] = useState( + SETTINGS_DATA.agents.find((a) => a.id === id) + ); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteConfirmText, setDeleteConfirmText] = useState(''); + + if (!agent) { + return ( +
+ + +

Agent not found

+ +
+
+
+ ); + } + + const handleDeleteAgent = () => { + if (deleteConfirmText === 'delete') { + router.push('/settings/agents'); + setDeleteDialogOpen(false); + } + }; + + return ( +
+
+

{agent.name}

+

+ Configure agent behavior and capabilities +

+
+ + + + + + Agent Name + + + setAgent({ ...agent, name: e.target.value })} + placeholder="Code Assistant" + /> + + + + + + Agent Configuration + + +
+
+ + +
+
+ + +
+
+
+
+ + + + Advanced Settings + + +
+
+
+ +

+ Controls randomness in responses (0 = deterministic, 1 = creative) +

+
+ {agent.temperature.toFixed(1)} +
+ setAgent({ ...agent, temperature: value })} + min={0} + max={1} + step={0.1} + className="w-full" + /> +
+ + + +
+
+ +

+ Automatically execute tool calls without confirmation +

+
+ setAgent({ ...agent, autoApprove: checked })} + /> +
+
+
+ + + + {/* Danger Zone */} + + + Danger Zone + + Irreversible actions that will permanently delete this agent + + + +
+
+

Delete this agent

+

+ Once deleted, all configuration will be permanently removed +

+
+ +
+
+
+ + {/* Delete Confirmation Dialog */} + + + + Delete Agent + + This action cannot be undone. This will permanently delete the agent " + {agent.name}" and all its data. + + +
+

+ Please type delete to confirm: +

+ setDeleteConfirmText(e.target.value)} + placeholder="delete" + /> +
+ + + + +
+
+
+ ); +} diff --git a/apps/web/app/settings/agents/page.tsx b/apps/web/app/settings/agents/page.tsx new file mode 100644 index 0000000000..62c8421280 --- /dev/null +++ b/apps/web/app/settings/agents/page.tsx @@ -0,0 +1,238 @@ +'use client'; + +import { useState } from 'react'; +import { IconPlus } from '@tabler/icons-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent } from '@/components/ui/card'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Separator } from '@/components/ui/separator'; +import { AgentCard } from '@/components/settings/agent-card'; +import { SETTINGS_DATA } from '@/lib/settings/dummy-data'; +import type { AgentProfile, AgentType } from '@/lib/settings/types'; + +const AGENT_OPTIONS: { value: AgentType; label: string }[] = [ + { value: 'claude-code', label: 'Claude Code' }, + { value: 'codex', label: 'Codex' }, + { value: 'auggie', label: 'Auggie' }, +]; + +const MODEL_OPTIONS: Record = { + 'claude-code': [ + { value: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5' }, + { value: 'claude-opus-4', label: 'Claude Opus 4' }, + { value: 'claude-haiku-4', label: 'Claude Haiku 4' }, + ], + 'codex': [ + { value: 'codex-v2', label: 'Codex v2' }, + { value: 'codex-v1', label: 'Codex v1' }, + ], + 'auggie': [ + { value: 'auggie-v1', label: 'Auggie v1' }, + { value: 'auggie-lite', label: 'Auggie Lite' }, + ], +}; + +export default function AgentsSettingsPage() { + const [agents, setAgents] = useState(SETTINGS_DATA.agents); + const [isAdding, setIsAdding] = useState(false); + const [newAgent, setNewAgent] = useState<{ + agent: AgentType; + name: string; + model: string; + autoApprove: boolean; + temperature: number; + }>({ + agent: 'claude-code', + name: '', + model: 'claude-sonnet-4.5', + autoApprove: false, + temperature: 0.7, + }); + + const handleUpdate = (agent: AgentProfile) => { + setAgents(agents.map((a) => (a.id === agent.id ? agent : a))); + }; + + const handleDelete = (id: string) => { + setAgents(agents.filter((a) => a.id !== id)); + }; + + const handleAdd = (e: React.FormEvent) => { + e.preventDefault(); + const profile: AgentProfile = { + id: crypto.randomUUID(), + ...newAgent, + }; + setAgents([...agents, profile]); + setIsAdding(false); + setNewAgent({ + agent: 'claude-code', + name: '', + model: 'claude-sonnet-4.5', + autoApprove: false, + temperature: 0.7, + }); + }; + + const handleAgentTypeChange = (type: AgentType) => { + const defaultModel = MODEL_OPTIONS[type][0].value; + setNewAgent({ ...newAgent, agent: type, model: defaultModel }); + }; + + return ( +
+
+
+

Agents

+

+ Configure agent profiles and their behavior +

+
+ +
+ + + +
+
+ {isAdding && ( + + +
+
+ + setNewAgent({ ...newAgent, name: e.target.value })} + placeholder="My Profile" + required + /> +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + + setNewAgent({ ...newAgent, temperature: parseFloat(e.target.value) }) + } + className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer" + /> +
+ Precise + Creative +
+
+ +
+
+ +

+ Automatically approve agent actions without prompting +

+
+ +
+ +
+ + +
+
+
+
+ )} + + {agents.map((agent) => ( + + ))} + + {agents.length === 0 && !isAdding && ( + + +

+ No agent profiles configured. Add your first profile to get started. +

+
+
+ )} +
+
+
+ ); +} diff --git a/apps/web/app/settings/environment/[id]/page.tsx b/apps/web/app/settings/environment/[id]/page.tsx new file mode 100644 index 0000000000..cea4613a88 --- /dev/null +++ b/apps/web/app/settings/environment/[id]/page.tsx @@ -0,0 +1,366 @@ +'use client'; + +import { use, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { IconPlus, IconTrash, IconX } from '@tabler/icons-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { SETTINGS_DATA } from '@/lib/settings/dummy-data'; +import type { Environment, EnvironmentType, BaseDocker } from '@/lib/settings/types'; + +export default function EnvironmentEditPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params); + const router = useRouter(); + const [environment, setEnvironment] = useState( + SETTINGS_DATA.environments.find((e) => e.id === id) + ); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteConfirmText, setDeleteConfirmText] = useState(''); + const [newEnvKey, setNewEnvKey] = useState(''); + const [newEnvValue, setNewEnvValue] = useState(''); + const [newSecretKey, setNewSecretKey] = useState(''); + const [newSecretValue, setNewSecretValue] = useState(''); + + if (!environment) { + return ( +
+ + +

Environment not found

+ +
+
+
+ ); + } + + const handleAddEnvVar = () => { + if (newEnvKey.trim()) { + setEnvironment({ + ...environment, + envVariables: [ + ...environment.envVariables, + { id: crypto.randomUUID(), key: newEnvKey, value: newEnvValue }, + ], + }); + setNewEnvKey(''); + setNewEnvValue(''); + } + }; + + const handleRemoveEnvVar = (id: string) => { + setEnvironment({ + ...environment, + envVariables: environment.envVariables.filter((v) => v.id !== id), + }); + }; + + const handleAddSecret = () => { + if (newSecretKey.trim()) { + setEnvironment({ + ...environment, + secrets: [ + ...environment.secrets, + { id: crypto.randomUUID(), key: newSecretKey, value: newSecretValue }, + ], + }); + setNewSecretKey(''); + setNewSecretValue(''); + } + }; + + const handleRemoveSecret = (id: string) => { + setEnvironment({ + ...environment, + secrets: environment.secrets.filter((s) => s.id !== id), + }); + }; + + const handleToggleAgent = (agentId: string) => { + const installed = environment.installedAgents || []; + if (installed.includes(agentId)) { + setEnvironment({ + ...environment, + installedAgents: installed.filter((id) => id !== agentId), + }); + } else { + setEnvironment({ + ...environment, + installedAgents: [...installed, agentId], + }); + } + }; + + const handleDeleteEnvironment = () => { + if (deleteConfirmText === 'delete') { + router.push('/settings/environments'); + setDeleteDialogOpen(false); + } + }; + + return ( +
+
+

{environment.name}

+

+ Configure Docker environment and runtime settings +

+
+ + + + + + Environment Name + + + setEnvironment({ ...environment, name: e.target.value })} + /> + + + + + + Docker Configuration + + +
+
+ + +
+
+ + +
+
+
+ +