diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx
index 198c98c09b3..17f0a232e01 100644
--- a/apps/web/app/workspace/page.tsx
+++ b/apps/web/app/workspace/page.tsx
@@ -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}
/>
)}
@@ -1508,6 +1505,8 @@ function WorkspacePageInner() {
task={activeSubagent.task}
label={activeSubagent.label}
onBack={handleBackFromSubagent}
+ onSubagentClick={handleSubagentClickFromChat}
+ onFilePathClick={handleFilePathClickFromChat}
/>
) : (
@@ -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 ;
+ return ;
}
return ;
}
@@ -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]);