refactor(web): move profile management to workspace-sidebar

This commit is contained in:
kumarabhirup 2026-03-02 18:35:34 -08:00
parent e7bdd45be1
commit 3ed8872f16
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
2 changed files with 111 additions and 87 deletions

View File

@ -2,8 +2,6 @@
import { useEffect, useState, useCallback } from "react";
import { FileManagerTree } from "./workspace/file-manager-tree";
import { ProfileSwitcher } from "./workspace/profile-switcher";
import { CreateWorkspaceDialog } from "./workspace/create-workspace-dialog";
// --- Types ---
@ -353,9 +351,8 @@ export function Sidebar({
const [mainMemory, setMainMemory] = useState<string | null>(null);
const [dailyLogs, setDailyLogs] = useState<MemoryFile[]>([]);
const [workspaceTree, setWorkspaceTree] = useState<TreeNode[]>([]);
const [activeProfile, setActiveProfile] = useState("default");
const [loading, setLoading] = useState(true);
const [showCreateWorkspace, setShowCreateWorkspace] = useState(false);
const [sidebarRefreshKey, setSidebarRefreshKey] = useState(0);
const toggleSection = (section: SidebarSection) => {
setOpenSections((prev) => {
@ -366,27 +363,24 @@ export function Sidebar({
});
};
// Full sidebar re-fetch after profile switch or workspace creation
const handleProfileSwitch = useCallback(() => {
setSidebarRefreshKey((k) => k + 1);
}, []);
// Fetch sidebar data (re-runs when refreshKey or sidebarRefreshKey changes)
// Fetch sidebar data
useEffect(() => {
async function load() {
setLoading(true);
try {
const [webSessionsRes, skillsRes, memoriesRes, workspaceRes] = await Promise.all([
const [webSessionsRes, skillsRes, memoriesRes, workspaceRes, profilesRes] = await Promise.all([
fetch("/api/web-sessions").then((r) => r.json()),
fetch("/api/skills").then((r) => r.json()),
fetch("/api/memories").then((r) => r.json()),
fetch("/api/workspace/tree").then((r) => r.json()).catch(() => ({ tree: [] })),
fetch("/api/profiles").then((r) => r.json()).catch(() => ({ activeProfile: "default" })),
]);
setWebSessions(webSessionsRes.sessions ?? []);
setSkills(skillsRes.skills ?? []);
setMainMemory(memoriesRes.mainMemory ?? null);
setDailyLogs(memoriesRes.dailyLogs ?? []);
setWorkspaceTree(workspaceRes.tree ?? []);
setActiveProfile(String(profilesRes.activeProfile || "default"));
} catch (err) {
console.error("Failed to load sidebar data:", err);
} finally {
@ -394,7 +388,7 @@ export function Sidebar({
}
}
void load();
}, [refreshKey, sidebarRefreshKey]);
}, [refreshKey]);
const refreshWorkspace = useCallback(async () => {
try {
@ -434,20 +428,9 @@ export function Sidebar({
</svg>
</button>
</div>
<ProfileSwitcher
onProfileSwitch={handleProfileSwitch}
onCreateWorkspace={() => setShowCreateWorkspace(true)}
activeProfileHint={String(sidebarRefreshKey)}
/>
<p className="text-xs text-[var(--color-text-muted)]">Profile: {activeProfile}</p>
</div>
{/* Create workspace dialog */}
<CreateWorkspaceDialog
isOpen={showCreateWorkspace}
onClose={() => setShowCreateWorkspace(false)}
onCreated={handleProfileSwitch}
/>
{/* Content */}
<div className="flex-1 overflow-y-auto py-2 space-y-1">
{loading ? (

View File

@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useRef, useCallback } from "react";
import { useEffect, useState, useRef, useCallback, useMemo } from "react";
import { FileManagerTree, type TreeNode } from "./file-manager-tree";
import { ProfileSwitcher } from "./profile-switcher";
import { CreateWorkspaceDialog } from "./create-workspace-dialog";
@ -40,18 +40,18 @@ type WorkspaceSidebarProps = {
mobile?: boolean;
/** Close the mobile drawer. */
onClose?: () => void;
/** Active workspace profile name (null = default). */
activeProfile?: string | null;
/** Fixed width in px when not mobile (overrides default 260). */
width?: number;
/** Called after the user switches to a different profile. */
onProfileSwitch?: () => void;
/** Whether hidden (dot) files/folders are currently shown. */
showHidden?: boolean;
/** Toggle hidden files visibility. */
onToggleHidden?: () => void;
/** Called when the user clicks the collapse/hide sidebar button. */
onCollapse?: () => void;
/** Active profile hint used by the profile switcher. */
activeProfile?: string | null;
/** Called after profile switches or workspace creation so parent can refresh state. */
onProfileChanged?: () => void;
};
function HomeIcon() {
@ -389,6 +389,27 @@ function dirDisplayName(dir: string): string {
return dir.split("/").pop() || dir;
}
function filterSidebarTree(nodes: TreeNode[]): TreeNode[] {
const filtered: TreeNode[] = [];
for (const node of nodes) {
// Root identity file is system-managed and hidden in Ironclaw UI.
if (node.path === "IDENTITY.md") {
continue;
}
// Dench is an always-on managed skill; hide it from the sidebar list.
if (node.path === "~skills/dench/SKILL.md") {
continue;
}
const children = node.children ? filterSidebarTree(node.children) : undefined;
if (node.path === "~skills" && (!children || children.length === 0)) {
continue;
}
filtered.push(children ? { ...node, children } : node);
}
return filtered;
}
export function WorkspaceSidebar({
tree,
activePath,
@ -406,16 +427,17 @@ export function WorkspaceSidebar({
onExternalDrop,
mobile,
onClose,
activeProfile,
onProfileSwitch,
showHidden,
onToggleHidden,
width: widthProp,
onCollapse,
activeProfile,
onProfileChanged,
}: WorkspaceSidebarProps) {
const isBrowsing = browseDir != null;
const [showCreateWorkspace, setShowCreateWorkspace] = useState(false);
const width = mobile ? "280px" : (widthProp ?? 260);
const visibleTree = useMemo(() => filterSidebarTree(tree), [tree]);
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const sidebar = (
<aside
@ -491,48 +513,52 @@ export function WorkspaceSidebar({
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
</button>
<ProfileSwitcher
onProfileSwitch={onProfileSwitch}
onCreateWorkspace={() => setShowCreateWorkspace(true)}
activeProfileHint={activeProfile}
trigger={({ isOpen, onClick, activeProfile: profileName, switching }) => (
<button
type="button"
onClick={onClick}
disabled={switching}
className="flex-1 min-w-0 w-full flex items-center justify-between gap-1.5 text-left rounded-lg py-1 px-1.5 transition-colors hover:bg-stone-100 dark:hover:bg-stone-800 disabled:opacity-50"
title="Switch workspace profile"
>
<div className="min-w-0 truncate">
<div
className="text-[13px] font-semibold truncate text-stone-700 dark:text-stone-200"
<div className="flex-1 min-w-0 px-1.5">
<div className="text-[13px] font-semibold truncate text-stone-700 dark:text-stone-200">
{orgName || "Workspace"}
</div>
<div className="mt-1">
<ProfileSwitcher
activeProfileHint={activeProfile ?? null}
onProfileSwitch={() => {
onProfileChanged?.();
}}
onWorkspaceDelete={() => {
onProfileChanged?.();
}}
onCreateWorkspace={() => {
setCreateWorkspaceOpen(true);
}}
trigger={({ onClick, activeProfile: profileName, switching }) => (
<button
type="button"
onClick={onClick}
disabled={switching}
className="text-[11px] flex items-center gap-1 truncate w-full transition-colors"
style={{ color: "var(--color-text-muted)" }}
title="Switch profile"
>
{orgName || "Workspace"}
</div>
<div
className="text-[11px] flex items-center gap-1 truncate text-stone-400 dark:text-stone-500"
>
<span>Ironclaw</span>
{profileName && profileName !== "default" && (
<span
className="px-1 py-0.5 rounded text-[10px] shrink-0 bg-stone-200 text-stone-500 dark:bg-stone-700 dark:text-stone-400"
>
{profileName}
</span>
)}
</div>
</div>
<svg
className={`w-3 h-3 shrink-0 transition-transform text-stone-400 ${isOpen ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
)}
/>
<span>Profile</span>
<span className="px-1 py-0.5 rounded text-[10px] shrink-0 bg-stone-200 text-stone-600 dark:bg-stone-700 dark:text-stone-300">
{profileName || "default"}
</span>
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
)}
/>
</div>
</div>
</>
)}
{onCollapse && (
@ -551,13 +577,6 @@ export function WorkspaceSidebar({
)}
</div>
{/* Create workspace dialog */}
<CreateWorkspaceDialog
isOpen={showCreateWorkspace}
onClose={() => setShowCreateWorkspace(false)}
onCreated={onProfileSwitch}
/>
{/* File search */}
{onFileSearchSelect && (
<FileSearch onSelect={onFileSearchSelect} />
@ -575,7 +594,7 @@ export function WorkspaceSidebar({
</div>
) : (
<FileManagerTree
tree={tree}
tree={visibleTree}
activePath={activePath}
onSelect={onSelect}
onRefresh={onRefresh}
@ -643,14 +662,36 @@ export function WorkspaceSidebar({
</aside>
);
if (!mobile) { return sidebar; }
if (!mobile) {
return (
<>
{sidebar}
<CreateWorkspaceDialog
isOpen={createWorkspaceOpen}
onClose={() => setCreateWorkspaceOpen(false)}
onCreated={() => {
onProfileChanged?.();
}}
/>
</>
);
}
return (
<div className="drawer-backdrop" onClick={() => void onClose?.()}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div onClick={(e) => e.stopPropagation()} className="fixed inset-y-0 left-0 z-50">
{sidebar}
<>
<div className="drawer-backdrop" onClick={() => void onClose?.()}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div onClick={(e) => e.stopPropagation()} className="fixed inset-y-0 left-0 z-50">
{sidebar}
</div>
</div>
</div>
<CreateWorkspaceDialog
isOpen={createWorkspaceOpen}
onClose={() => setCreateWorkspaceOpen(false)}
onCreated={() => {
onProfileChanged?.();
}}
/>
</>
);
}