diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx
index f627eba188e..e2e94c2f4b9 100644
--- a/apps/web/app/workspace/page.tsx
+++ b/apps/web/app/workspace/page.tsx
@@ -8,9 +8,10 @@ import { useWorkspaceWatcher } from "../hooks/use-workspace-watcher";
import { ObjectTable } from "../components/workspace/object-table";
import { ObjectKanban } from "../components/workspace/object-kanban";
import { DocumentView } from "../components/workspace/document-view";
-import { FileViewer } from "../components/workspace/file-viewer";
+import { FileViewer, isSpreadsheetFile } from "../components/workspace/file-viewer";
import { CodeViewer } from "../components/workspace/code-viewer";
import { MediaViewer, detectMediaType, type MediaType } from "../components/workspace/media-viewer";
+import { HtmlViewer } from "../components/workspace/html-viewer";
import { DatabaseViewer, DuckDBMissing } from "../components/workspace/database-viewer";
import { Breadcrumbs } from "../components/workspace/breadcrumbs";
import { ChatSessionsSidebar } from "../components/workspace/chat-sessions-sidebar";
@@ -95,6 +96,8 @@ type ContentState =
| { kind: "document"; data: FileData; title: string }
| { kind: "file"; data: FileData; filename: string }
| { kind: "code"; data: FileData; filename: string }
+ | { kind: "html"; filename: string; rawUrl: string; contentUrl: string }
+ | { kind: "spreadsheet"; url: string; filename: string }
| { kind: "media"; url: string; mediaType: MediaType; filename: string; filePath: string }
| { kind: "database"; dbPath: string; filename: string }
| { kind: "report"; reportPath: string; filename: string }
@@ -236,6 +239,7 @@ function WorkspacePageInner() {
reconnect: reconnectWorkspace,
browseDir, setBrowseDir, parentDir: browseParentDir, workspaceRoot, openclawDir,
activeProfile,
+ showHidden, setShowHidden,
} = useWorkspaceWatcher();
// handleProfileSwitch is defined below fetchSessions/fetchCronJobs (avoids TDZ)
@@ -464,13 +468,33 @@ function WorkspacePageInner() {
return;
}
+ // HTML files: load iframe immediately, lazy-fetch source for code view
+ const ext = node.name.split(".").pop()?.toLowerCase() ?? "";
+ if (ext === "html" || ext === "htm") {
+ setContent({
+ kind: "html",
+ filename: node.name,
+ rawUrl: rawFileUrl(node.path),
+ contentUrl: fileApiUrl(node.path),
+ });
+ return;
+ }
+
+ if (isSpreadsheetFile(node.name)) {
+ setContent({
+ kind: "spreadsheet",
+ url: rawFileUrl(node.path),
+ filename: node.name,
+ });
+ return;
+ }
+
const res = await fetch(fileApiUrl(node.path));
if (!res.ok) {
setContent({ kind: "none" });
return;
}
const data: FileData = await res.json();
- // Route code files to the syntax-highlighted CodeViewer
if (isCodeFile(node.name)) {
setContent({ kind: "code", data, filename: node.name });
} else {
@@ -672,7 +696,9 @@ function WorkspacePageInner() {
// Sync URL bar with active content / chat state.
// Uses window.location instead of searchParams in the comparison to
// avoid a circular dependency (searchParams updates → effect fires →
- // router.replace → searchParams updates → …).
+ // router.push → searchParams updates → …).
+ // push (not replace) so the browser back button walks through previous
+ // workspace views instead of jumping straight out of /workspace.
useEffect(() => {
const current = new URLSearchParams(window.location.search);
@@ -683,12 +709,12 @@ function WorkspacePageInner() {
params.set("path", activePath);
const entry = current.get("entry");
if (entry) {params.set("entry", entry);}
- router.replace(`/workspace?${params.toString()}`, { scroll: false });
+ router.push(`/workspace?${params.toString()}`, { scroll: false });
}
} else if (activeSessionId) {
// Chat mode — no file selected.
if (current.get("chat") !== activeSessionId || current.has("path")) {
- router.replace(`/workspace?chat=${encodeURIComponent(activeSessionId)}`, { scroll: false });
+ router.push(`/workspace?chat=${encodeURIComponent(activeSessionId)}`, { scroll: false });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally excludes searchParams to avoid infinite loop
@@ -700,7 +726,7 @@ function WorkspacePageInner() {
setEntryModal({ objectName, entryId });
const params = new URLSearchParams(searchParams.toString());
params.set("entry", `${objectName}:${entryId}`);
- router.replace(`/workspace?${params.toString()}`, { scroll: false });
+ router.push(`/workspace?${params.toString()}`, { scroll: false });
},
[searchParams, router],
);
@@ -940,6 +966,8 @@ function WorkspacePageInner() {
onExternalDrop={handleSidebarExternalDrop}
activeProfile={activeProfile}
onProfileSwitch={handleProfileSwitch}
+ showHidden={showHidden}
+ onToggleHidden={() => setShowHidden((v) => !v)}
mobile
onClose={() => setSidebarOpen(false)}
/>
@@ -962,6 +990,8 @@ function WorkspacePageInner() {
onExternalDrop={handleSidebarExternalDrop}
activeProfile={activeProfile}
onProfileSwitch={handleProfileSwitch}
+ showHidden={showHidden}
+ onToggleHidden={() => setShowHidden((v) => !v)}
/>
)}
@@ -1305,6 +1335,15 @@ function ContentRenderer({
/>
);
+ case "spreadsheet":
+ return (
+
+ );
+
case "code":
return (
);
+ case "html":
+ return (
+
+ );
+
case "media":
return (