refactor(web): update workspace page layout and profile integration

This commit is contained in:
kumarabhirup 2026-03-02 18:35:48 -08:00
parent a7a89a990e
commit a61aedd51d
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167

View File

@ -30,6 +30,7 @@ import { useIsMobile } from "../hooks/use-mobile";
import { ObjectFilterBar } from "../components/workspace/object-filter-bar";
import { type FilterGroup, type SortRule, type SavedView, emptyFilterGroup, serializeFilters } from "@/lib/object-filters";
import { UnicodeSpinner } from "../components/unicode-spinner";
import { resolveActiveViewSyncDecision } from "./object-view-active-view";
// --- Types ---
@ -355,14 +356,12 @@ function WorkspacePageInner() {
// Live-reactive tree via SSE watcher (with browse-mode support)
const {
tree, loading: treeLoading, exists: workspaceExists, refresh: refreshTree,
reconnect: reconnectWorkspace,
reconnect: reconnectWorkspaceWatcher,
browseDir, setBrowseDir, parentDir: browseParentDir, workspaceRoot, openclawDir,
activeProfile,
activeProfile: workspaceProfile,
showHidden, setShowHidden,
} = useWorkspaceWatcher();
// handleProfileSwitch is defined below fetchSessions/fetchCronJobs (avoids TDZ)
// Search index for @ mention fuzzy search (files + entries)
const { search: searchIndex } = useSearchIndex();
@ -498,21 +497,20 @@ function WorkspacePageInner() {
});
}, []);
const refreshContext = useCallback(async () => {
try {
const res = await fetch("/api/workspace/context");
const data = await res.json();
setContext(data);
} catch {
// ignore
}
}, []);
// Fetch workspace context on mount
useEffect(() => {
let cancelled = false;
async function loadContext() {
try {
const res = await fetch("/api/workspace/context");
const data = await res.json();
if (!cancelled) {setContext(data);}
} catch {
// ignore
}
}
void loadContext();
return () => { cancelled = true; };
}, []);
void refreshContext();
}, [refreshContext]);
// Fetch chat sessions
const fetchSessions = useCallback(async () => {
@ -536,6 +534,17 @@ function WorkspacePageInner() {
setSidebarRefreshKey((k) => k + 1);
}, []);
const handleProfileChanged = useCallback(() => {
setBrowseDir(null);
setActivePath(null);
setContent({ kind: "none" });
setChatSidebarPreview(null);
setShowChatSidebar(true);
reconnectWorkspaceWatcher();
refreshSessions();
void refreshContext();
}, [reconnectWorkspaceWatcher, refreshContext, refreshSessions, setBrowseDir]);
const handleDeleteSession = useCallback(
async (sessionId: string) => {
const res = await fetch(`/api/web-sessions/${sessionId}`, { method: "DELETE" });
@ -609,18 +618,6 @@ function WorkspacePageInner() {
return () => clearInterval(id);
}, [fetchCronJobs]);
// After profile switch or workspace creation, reconnect SSE + refresh all data
const handleProfileSwitch = useCallback(() => {
reconnectWorkspace();
void fetchSessions();
void fetchCronJobs();
setActivePath(null);
setContent({ kind: "none" });
setActiveSessionId(null);
setSubagents([]);
setActiveSubagentKey(null);
}, [reconnectWorkspace, fetchSessions, fetchCronJobs]);
// Load content when path changes
const loadContent = useCallback(
async (node: TreeNode) => {
@ -1324,10 +1321,10 @@ function WorkspacePageInner() {
workspaceRoot={workspaceRoot}
onGoToChat={() => { handleGoToChat(); setSidebarOpen(false); }}
onExternalDrop={handleSidebarExternalDrop}
activeProfile={activeProfile}
onProfileSwitch={handleProfileSwitch}
showHidden={showHidden}
onToggleHidden={() => setShowHidden((v) => !v)}
activeProfile={workspaceProfile}
onProfileChanged={handleProfileChanged}
mobile
onClose={() => setSidebarOpen(false)}
/>
@ -1361,12 +1358,12 @@ function WorkspacePageInner() {
workspaceRoot={workspaceRoot}
onGoToChat={handleGoToChat}
onExternalDrop={handleSidebarExternalDrop}
activeProfile={activeProfile}
onProfileSwitch={handleProfileSwitch}
showHidden={showHidden}
onToggleHidden={() => setShowHidden((v) => !v)}
width={leftSidebarWidth}
onCollapse={() => setLeftSidebarCollapsed(true)}
activeProfile={workspaceProfile}
onProfileChanged={handleProfileChanged}
/>
</div>
)}
@ -1508,6 +1505,8 @@ function WorkspacePageInner() {
task={activeSubagent.task}
label={activeSubagent.label}
onBack={handleBackFromSubagent}
onSubagentClick={handleSubagentClickFromChat}
onFilePathClick={handleFilePathClickFromChat}
/>
) : (
<ChatPanel
@ -1632,6 +1631,7 @@ function WorkspacePageInner() {
<ContentRenderer
content={content}
workspaceExists={workspaceExists}
expectedPath={workspaceRoot}
tree={tree}
activePath={activePath}
browseDir={browseDir}
@ -1646,7 +1646,6 @@ function WorkspacePageInner() {
searchFn={searchIndex}
onSelectCronJob={handleSelectCronJob}
onBackToCronDashboard={handleBackToCronDashboard}
onWorkspaceCreated={handleProfileSwitch}
/>
</div>
@ -2010,6 +2009,7 @@ function ChatSidebarPreview({
function ContentRenderer({
content,
workspaceExists,
expectedPath,
tree,
activePath,
browseDir,
@ -2024,10 +2024,10 @@ function ContentRenderer({
searchFn,
onSelectCronJob,
onBackToCronDashboard,
onWorkspaceCreated,
}: {
content: ContentState;
workspaceExists: boolean;
expectedPath?: string | null;
tree: TreeNode[];
activePath: string | null;
/** Current browse directory (absolute path), or null in workspace mode. */
@ -2044,8 +2044,6 @@ function ContentRenderer({
searchFn: (query: string, limit?: number) => import("@/lib/search-index").SearchIndexItem[];
onSelectCronJob: (jobId: string) => void;
onBackToCronDashboard: () => void;
/** Called after a new workspace is created from the empty state. */
onWorkspaceCreated?: () => void;
}) {
switch (content.kind) {
case "loading":
@ -2183,7 +2181,7 @@ function ContentRenderer({
case "none":
default:
if (tree.length === 0) {
return <EmptyState workspaceExists={workspaceExists} onWorkspaceCreated={onWorkspaceCreated} />;
return <EmptyState workspaceExists={workspaceExists} expectedPath={expectedPath} />;
}
return <WelcomeView tree={tree} onNodeSelect={onNodeSelect} />;
}
@ -2284,15 +2282,20 @@ function ObjectView({
// Sync saved views when data changes (e.g. SSE refresh from AI editing .object.yaml)
useEffect(() => {
setSavedViews(data.savedViews ?? []);
if (data.activeView && data.activeView !== activeViewName) {
const view = (data.savedViews ?? []).find((v) => v.name === data.activeView);
if (view) {
setFilters(view.filters ?? emptyFilterGroup());
setViewColumns(view.columns);
setActiveViewName(view.name);
// Re-fetch with new filters from the view
void fetchEntries({ page: 1, filters: view.filters ?? emptyFilterGroup() });
}
const decision = resolveActiveViewSyncDecision({
savedViews: data.savedViews,
activeView: data.activeView,
currentActiveViewName: activeViewName,
currentFilters: filters,
currentViewColumns: viewColumns,
});
if (decision?.shouldApply) {
setFilters(decision.nextFilters);
setViewColumns(decision.nextColumns);
setActiveViewName(decision.nextActiveViewName);
// Re-fetch with filters from the synchronized active view.
void fetchEntries({ page: 1, filters: decision.nextFilters });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.savedViews, data.activeView]);