Skip to content

Commit 0c4ace6

Browse files
committed
feat: improve Projects settings page UX
- Rename sidebar tab from 'Project' to 'Projects' - Add project dropdown selector at top of page - Show full project path below dropdown - Add inline form to add MCP servers (name + command) - Enter key submits the add form - Remove reference to /mcp add command (now done in UI)
1 parent 03fa43a commit 0c4ace6

File tree

2 files changed

+124
-21
lines changed

2 files changed

+124
-21
lines changed

src/browser/components/Settings/SettingsModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ const SECTIONS: SettingsSection[] = [
2323
component: ProvidersSection,
2424
},
2525
{
26-
id: "project",
27-
label: "Project",
26+
id: "projects",
27+
label: "Projects",
2828
icon: <Briefcase className="h-4 w-4" />,
2929
component: ProjectSettingsSection,
3030
},

src/browser/components/Settings/sections/ProjectSettingsSection.tsx

Lines changed: 122 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,60 @@
11
import React, { useCallback, useEffect, useState } from "react";
22
import { 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

66
type TestResult = { success: true; tools: string[] } | { success: false; error: string };
77

88
export 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

Comments
 (0)