feat(web): add workspace delete and profile discovery to profile-switcher
This commit is contained in:
parent
af0d34b5db
commit
9542c60f66
@ -19,6 +19,7 @@ export type ProfileSwitcherTriggerProps = {
|
||||
|
||||
type ProfileSwitcherProps = {
|
||||
onProfileSwitch?: () => void;
|
||||
onWorkspaceDelete?: (profileName: string) => void;
|
||||
onCreateWorkspace?: () => void;
|
||||
/** Parent-tracked active profile -- triggers a re-fetch when it changes (e.g. after workspace creation). */
|
||||
activeProfileHint?: string | null;
|
||||
@ -26,11 +27,26 @@ type ProfileSwitcherProps = {
|
||||
trigger?: (props: ProfileSwitcherTriggerProps) => React.ReactNode;
|
||||
};
|
||||
|
||||
export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace, activeProfileHint, trigger }: ProfileSwitcherProps) {
|
||||
function shortenPath(p: string): string {
|
||||
return p
|
||||
.replace(/^\/Users\/[^/]+/, "~")
|
||||
.replace(/^\/home\/[^/]+/, "~")
|
||||
.replace(/^[A-Za-z]:[/\\]Users[/\\][^/\\]+/, "~");
|
||||
}
|
||||
|
||||
export function ProfileSwitcher({
|
||||
onProfileSwitch,
|
||||
onWorkspaceDelete,
|
||||
onCreateWorkspace,
|
||||
activeProfileHint,
|
||||
trigger,
|
||||
}: ProfileSwitcherProps) {
|
||||
const [profiles, setProfiles] = useState<ProfileInfo[]>([]);
|
||||
const [activeProfile, setActiveProfile] = useState("default");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [switching, setSwitching] = useState(false);
|
||||
const [deletingProfile, setDeletingProfile] = useState<string | null>(null);
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchProfiles = useCallback(async () => {
|
||||
@ -66,6 +82,7 @@ export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace, activeProf
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
setActionError(null);
|
||||
setSwitching(true);
|
||||
try {
|
||||
const res = await fetch("/api/profiles/switch", {
|
||||
@ -78,15 +95,55 @@ export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace, activeProf
|
||||
setActiveProfile(data.activeProfile ?? "default");
|
||||
onProfileSwitch?.();
|
||||
void fetchProfiles();
|
||||
} else {
|
||||
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
setActionError(data.error ?? "Failed to switch profile.");
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
setActionError("Failed to switch profile.");
|
||||
} finally {
|
||||
setSwitching(false);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorkspace = async (profileName: string) => {
|
||||
const target = profiles.find((p) => p.name === profileName);
|
||||
if (!target?.workspaceDir) {
|
||||
return;
|
||||
}
|
||||
const confirmed = window.confirm(
|
||||
`Delete workspace for profile "${profileName}"?\n\nThis runs openclaw --profile ${profileName} workspace delete.`,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionError(null);
|
||||
setDeletingProfile(profileName);
|
||||
try {
|
||||
const res = await fetch("/api/workspace/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile: profileName }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
setActionError(data.error ?? `Failed to delete workspace for profile '${profileName}'.`);
|
||||
return;
|
||||
}
|
||||
if (profileName === activeProfile) {
|
||||
onProfileSwitch?.();
|
||||
}
|
||||
onWorkspaceDelete?.(profileName);
|
||||
await fetchProfiles();
|
||||
} catch {
|
||||
setActionError(`Failed to delete workspace for profile '${profileName}'.`);
|
||||
} finally {
|
||||
setDeletingProfile(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Don't show the switcher if there's only one profile and no way to create more
|
||||
const showSwitcher = profiles.length > 0;
|
||||
const handleToggle = () => {
|
||||
@ -158,53 +215,98 @@ export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace, activeProf
|
||||
{profiles.map((p) => {
|
||||
const isCurrent = p.name === activeProfile;
|
||||
return (
|
||||
<button
|
||||
key={p.name}
|
||||
onClick={() => handleSwitch(p.name)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-[var(--color-surface-hover)]"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
>
|
||||
{/* Active indicator */}
|
||||
<span
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
background: isCurrent ? "var(--color-success)" : "transparent",
|
||||
border: isCurrent ? "none" : "1px solid var(--color-border-strong)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate font-medium">
|
||||
{p.name === "default" ? "Default" : p.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="text-xs truncate mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{p.workspaceDir
|
||||
? p.workspaceDir.replace(/^\/Users\/[^/]+/, "~")
|
||||
: "No workspace yet"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCurrent && (
|
||||
<div key={p.name} className="flex items-center gap-1 px-1.5 py-0.5">
|
||||
<button
|
||||
onClick={() => void handleSwitch(p.name)}
|
||||
disabled={switching || !!deletingProfile}
|
||||
className="flex-1 min-w-0 flex items-center gap-2 px-1.5 py-1.5 rounded text-left text-sm transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
>
|
||||
{/* Active indicator */}
|
||||
<span
|
||||
className="text-xs px-1.5 py-0.5 rounded"
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
background: "var(--color-accent-light)",
|
||||
color: "var(--color-accent)",
|
||||
background: isCurrent ? "var(--color-success)" : "transparent",
|
||||
border: isCurrent ? "none" : "1px solid var(--color-border-strong)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate font-medium">
|
||||
{p.name === "default" ? "Default" : p.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="text-xs truncate mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{p.workspaceDir
|
||||
? shortenPath(p.workspaceDir)
|
||||
: "No workspace yet"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCurrent && (
|
||||
<span
|
||||
className="text-xs px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
background: "var(--color-accent-light)",
|
||||
color: "var(--color-accent)",
|
||||
}}
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{p.workspaceDir && (
|
||||
<button
|
||||
onClick={() => void handleDeleteWorkspace(p.name)}
|
||||
disabled={switching || !!deletingProfile}
|
||||
title={`Delete workspace for ${p.name}`}
|
||||
className="p-1.5 rounded transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
|
||||
style={{
|
||||
color: deletingProfile === p.name
|
||||
? "var(--color-text-muted)"
|
||||
: "var(--color-error)",
|
||||
}}
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M8 6V4h8v2" />
|
||||
<path d="M19 6l-1 14H6L5 6" />
|
||||
<path d="M10 11v6" />
|
||||
<path d="M14 11v6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{actionError && (
|
||||
<p
|
||||
className="mx-3 mb-2 mt-1 rounded px-2 py-1 text-xs"
|
||||
style={{
|
||||
background: "rgba(220, 38, 38, 0.08)",
|
||||
color: "var(--color-error)",
|
||||
}}
|
||||
>
|
||||
{actionError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Create new */}
|
||||
<div style={{ borderTop: "1px solid var(--color-border)" }}>
|
||||
<button
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user