refactor(web): move profile management to workspace-sidebar
This commit is contained in:
parent
e7bdd45be1
commit
3ed8872f16
@ -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 ? (
|
||||
|
||||
@ -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?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user