11import React , { useCallback , useEffect , useState } from "react" ;
22import { useAPI } from "@/browser/contexts/API" ;
3- import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext " ;
4- import { Trash2 , Play , Loader2 , CheckCircle , XCircle } from "lucide-react" ;
3+ import { useProjectContext } from "@/browser/contexts/ProjectContext " ;
4+ import { Trash2 , Play , Loader2 , CheckCircle , XCircle , Plus , ChevronDown } from "lucide-react" ;
55
66type TestResult = { success : true ; tools : string [ ] } | { success : false ; error : string } ;
77
88export const ProjectSettingsSection : React . FC = ( ) => {
99 const { api } = useAPI ( ) ;
10- const { selectedWorkspace } = useWorkspaceContext ( ) ;
11- const projectPath = selectedWorkspace ?. projectPath ;
10+ const { projects } = useProjectContext ( ) ;
11+ const projectList = Array . from ( projects . keys ( ) ) ;
1212
13+ const [ selectedProject , setSelectedProject ] = useState < string > ( "" ) ;
1314 const [ servers , setServers ] = useState < Record < string , string > > ( { } ) ;
1415 const [ loading , setLoading ] = useState ( false ) ;
1516 const [ error , setError ] = useState < string | null > ( null ) ;
1617 const [ testingServer , setTestingServer ] = useState < string | null > ( null ) ;
1718 const [ testResults , setTestResults ] = useState < Map < string , TestResult > > ( new Map ( ) ) ;
1819
20+ // Add server form state
21+ const [ newServerName , setNewServerName ] = useState ( "" ) ;
22+ const [ newServerCommand , setNewServerCommand ] = useState ( "" ) ;
23+ const [ addingServer , setAddingServer ] = useState ( false ) ;
24+
25+ // Set default project when projects load
26+ useEffect ( ( ) => {
27+ if ( projectList . length > 0 && ! selectedProject ) {
28+ setSelectedProject ( projectList [ 0 ] ) ;
29+ }
30+ } , [ projectList , selectedProject ] ) ;
31+
1932 const refresh = useCallback ( async ( ) => {
20- if ( ! api || ! projectPath ) return ;
33+ if ( ! api || ! selectedProject ) return ;
2134 setLoading ( true ) ;
2235 try {
23- const result = await api . projects . mcp . list ( { projectPath } ) ;
36+ const result = await api . projects . mcp . list ( { projectPath : selectedProject } ) ;
2437 setServers ( result ?? { } ) ;
2538 setError ( null ) ;
2639 } catch ( err ) {
2740 setError ( err instanceof Error ? err . message : "Failed to load MCP servers" ) ;
2841 } finally {
2942 setLoading ( false ) ;
3043 }
31- } , [ api , projectPath ] ) ;
44+ } , [ api , selectedProject ] ) ;
3245
3346 useEffect ( ( ) => {
3447 void refresh ( ) ;
48+ // Clear test results when project changes
49+ setTestResults ( new Map ( ) ) ;
3550 } , [ refresh ] ) ;
3651
3752 const handleRemove = useCallback (
3853 async ( name : string ) => {
39- if ( ! api || ! projectPath ) return ;
54+ if ( ! api || ! selectedProject ) return ;
4055 setLoading ( true ) ;
4156 try {
42- const result = await api . projects . mcp . remove ( { projectPath, name } ) ;
57+ const result = await api . projects . mcp . remove ( { projectPath : selectedProject , name } ) ;
4358 if ( ! result . success ) {
4459 setError ( result . error ?? "Failed to remove MCP server" ) ;
4560 } else {
@@ -51,12 +66,12 @@ export const ProjectSettingsSection: React.FC = () => {
5166 setLoading ( false ) ;
5267 }
5368 } ,
54- [ api , projectPath , refresh ]
69+ [ api , selectedProject , refresh ]
5570 ) ;
5671
5772 const handleTest = useCallback (
5873 async ( name : string ) => {
59- if ( ! api || ! projectPath ) return ;
74+ if ( ! api || ! selectedProject ) return ;
6075 setTestingServer ( name ) ;
6176 // Clear previous result for this server
6277 setTestResults ( ( prev ) => {
@@ -65,7 +80,7 @@ export const ProjectSettingsSection: React.FC = () => {
6580 return next ;
6681 } ) ;
6782 try {
68- const result = await api . projects . mcp . test ( { projectPath, name } ) ;
83+ const result = await api . projects . mcp . test ( { projectPath : selectedProject , name } ) ;
6984 setTestResults ( ( prev ) => new Map ( prev ) . set ( name , result ) ) ;
7085 } catch ( err ) {
7186 setTestResults ( ( prev ) =>
@@ -78,24 +93,73 @@ export const ProjectSettingsSection: React.FC = () => {
7893 setTestingServer ( null ) ;
7994 }
8095 } ,
81- [ api , projectPath ]
96+ [ api , selectedProject ]
8297 ) ;
8398
84- if ( ! projectPath ) {
99+ const handleAddServer = useCallback ( async ( ) => {
100+ if ( ! api || ! selectedProject || ! newServerName . trim ( ) || ! newServerCommand . trim ( ) ) return ;
101+ setAddingServer ( true ) ;
102+ setError ( null ) ;
103+ try {
104+ const result = await api . projects . mcp . add ( {
105+ projectPath : selectedProject ,
106+ name : newServerName . trim ( ) ,
107+ command : newServerCommand . trim ( ) ,
108+ } ) ;
109+ if ( ! result . success ) {
110+ setError ( result . error ?? "Failed to add MCP server" ) ;
111+ } else {
112+ setNewServerName ( "" ) ;
113+ setNewServerCommand ( "" ) ;
114+ await refresh ( ) ;
115+ }
116+ } catch ( err ) {
117+ setError ( err instanceof Error ? err . message : "Failed to add MCP server" ) ;
118+ } finally {
119+ setAddingServer ( false ) ;
120+ }
121+ } , [ api , selectedProject , newServerName , newServerCommand , refresh ] ) ;
122+
123+ if ( projectList . length === 0 ) {
85124 return (
86125 < p className = "text-muted-foreground text-sm" >
87- Select a workspace to manage project settings.
126+ No projects configured. Add a project first to manage its settings.
88127 </ p >
89128 ) ;
90129 }
91130
131+ const projectName = ( path : string ) => path . split ( / [ \\ / ] / ) . pop ( ) ?? path ;
132+
92133 return (
93- < div className = "space-y-4" >
134+ < div className = "space-y-6" >
135+ { /* Project selector */ }
136+ < div >
137+ < label htmlFor = "project-select" className = "mb-1.5 block text-sm font-medium" >
138+ Project
139+ </ label >
140+ < div className = "relative" >
141+ < select
142+ id = "project-select"
143+ value = { selectedProject }
144+ onChange = { ( e ) => setSelectedProject ( e . target . value ) }
145+ className = "border-border-medium bg-secondary/30 text-foreground focus:ring-accent w-full appearance-none rounded-md border py-2 pr-8 pl-3 text-sm focus:ring-1 focus:outline-none"
146+ >
147+ { projectList . map ( ( path ) => (
148+ < option key = { path } value = { path } >
149+ { projectName ( path ) }
150+ </ option >
151+ ) ) }
152+ </ select >
153+ < ChevronDown className = "text-muted-foreground pointer-events-none absolute top-1/2 right-2 h-4 w-4 -translate-y-1/2" />
154+ </ div >
155+ < p className = "text-muted-foreground mt-1 text-xs" > { selectedProject } </ p >
156+ </ div >
157+
158+ { /* MCP Servers section */ }
94159 < div >
95- < h3 className = "text-lg font-semibold" > MCP Servers</ h3 >
160+ < h3 className = "text-base font-semibold" > MCP Servers</ h3 >
96161 < p className = "text-muted-foreground text-sm" >
97- Servers are stored in < code > .mux/mcp.jsonc</ code > in this project. Use{ " " }
98- < code > /mcp add</ code > to add new entries.
162+ Servers are stored in < code className = "bg-secondary/50 rounded px-1" > .mux/mcp.jsonc</ code >
99163 </ p >
100164 </ div >
101165
@@ -172,6 +236,45 @@ export const ProjectSettingsSection: React.FC = () => {
172236 ) ;
173237 } ) }
174238 </ ul >
239+
240+ { /* Add server form */ }
241+ < div className = "border-border-medium/60 space-y-3 rounded-md border p-3" >
242+ < h4 className = "text-sm font-medium" > Add MCP Server</ h4 >
243+ < div className = "space-y-2" >
244+ < input
245+ type = "text"
246+ placeholder = "Server name (e.g., memory)"
247+ value = { newServerName }
248+ onChange = { ( e ) => setNewServerName ( e . target . value ) }
249+ className = "border-border-medium bg-secondary/30 text-foreground placeholder:text-muted-foreground focus:ring-accent w-full rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:outline-none"
250+ />
251+ < input
252+ type = "text"
253+ placeholder = "Command (e.g., npx -y @modelcontextprotocol/server-memory)"
254+ value = { newServerCommand }
255+ onChange = { ( e ) => setNewServerCommand ( e . target . value ) }
256+ className = "border-border-medium bg-secondary/30 text-foreground placeholder:text-muted-foreground focus:ring-accent w-full rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:outline-none"
257+ onKeyDown = { ( e ) => {
258+ if ( e . key === "Enter" && newServerName . trim ( ) && newServerCommand . trim ( ) ) {
259+ void handleAddServer ( ) ;
260+ }
261+ } }
262+ />
263+ </ div >
264+ < button
265+ type = "button"
266+ onClick = { ( ) => void handleAddServer ( ) }
267+ disabled = { addingServer || ! newServerName . trim ( ) || ! newServerCommand . trim ( ) }
268+ className = "bg-accent hover:bg-accent/90 flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-white disabled:opacity-50"
269+ >
270+ { addingServer ? (
271+ < Loader2 className = "h-4 w-4 animate-spin" />
272+ ) : (
273+ < Plus className = "h-4 w-4" />
274+ ) }
275+ { addingServer ? "Adding…" : "Add Server" }
276+ </ button >
277+ </ div >
175278 </ div >
176279 ) ;
177280} ;
0 commit comments