extracted all File components to apps/FileBrowser

This commit is contained in:
2026-02-21 18:18:39 +00:00
parent fe7874dd65
commit e275c0fea6
23 changed files with 80 additions and 237 deletions
+7
View File
@@ -82,6 +82,7 @@
"node-pty": "^1.1.0", "node-pty": "^1.1.0",
"nodemailer": "^7.0.12", "nodemailer": "^7.0.12",
"officerdb": "workspace:*", "officerdb": "workspace:*",
"officerdev": "workspace:*",
"pg": "^8.16.3", "pg": "^8.16.3",
"postgres": "^3.4.5", "postgres": "^3.4.5",
"react": "^19", "react": "^19",
@@ -243,6 +244,10 @@
"name": "injector", "name": "injector",
"version": "1.0.0", "version": "1.0.0",
}, },
"src/workspaces/officerdev": {
"name": "officerdev",
"version": "0.0.1",
},
"src/workspaces/sounds": { "src/workspaces/sounds": {
"name": "sounds", "name": "sounds",
"version": "1.0.0", "version": "1.0.0",
@@ -1696,6 +1701,8 @@
"officerdb": ["officerdb@workspace:src/databases/officer_db"], "officerdb": ["officerdb@workspace:src/databases/officer_db"],
"officerdev": ["officerdev@workspace:src/workspaces/officerdev"],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
+1
View File
@@ -125,6 +125,7 @@
"types": "workspace:*", "types": "workspace:*",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"apps": "workspace:*", "apps": "workspace:*",
"officerdev": "workspace:*",
"widgets": "workspace:*", "widgets": "workspace:*",
"ws": "^8.18.1", "ws": "^8.18.1",
"zod": "^4.2.1" "zod": "^4.2.1"
+1 -1
View File
@@ -44,7 +44,7 @@ export function App() {
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} /> <Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
<Route path="/chat/:sessionId" element={<Dashboard.SessionListPage />} /> <Route path="/chat/:sessionId" element={<Dashboard.SessionListPage />} />
<Route path="/plans" element={<Dashboard.Plans />} /> <Route path="/plans" element={<Dashboard.Plans />} />
<Route path="/files" element={<Dashboard.FilesPage />} /> <Route path="/files" element={<Dashboard.FilesScreen />} />
<Route path="/code-editor" element={<Dashboard.CodeEditor />} /> <Route path="/code-editor" element={<Dashboard.CodeEditor />} />
<Route path="/skills" element={<Dashboard.Skills />} /> <Route path="/skills" element={<Dashboard.Skills />} />
@@ -13,7 +13,7 @@ import { Card } from '@/components/Card';
import { FrontmatterBlock } from '../CapabilityPage'; import { FrontmatterBlock } from '../CapabilityPage';
import type { CapabilityDetail } from '../CapabilityPage'; import type { CapabilityDetail } from '../CapabilityPage';
import type { AutomationSelection } from './AutomationRightPanel'; import type { AutomationSelection } from './AutomationRightPanel';
import { TaskRunnerModal } from '../Files/Screen/TaskRunnerModal'; import { TaskRunnerModal } from 'apps/FileBrowser';
type SelectOption = { value: string; label: string }; type SelectOption = { value: string; label: string };
@@ -0,0 +1,15 @@
import type { LayoutNode } from '@/components/Workspace';
import { WorkspaceView } from '@/components/Workspace';
import { useWorkspacesState } from '@/state/useWorkspacesState';
import { appRegistry } from '../Workspaces/app-registry';
import { defaultLayout } from './defaultLayout';
export const FilesScreen = () => {
const workspace = useWorkspacesState<LayoutNode>('screens/files', defaultLayout);
return (
<div className="h-full w-full">
<WorkspaceView workspace={workspace} registry={appRegistry} />
</div>
);
};
@@ -0,0 +1,7 @@
import type { LayoutNode } from '@/components/Workspace';
export const defaultLayout: LayoutNode = {
type: 'panel',
id: 'files-main',
appType: 'file-browser',
};
@@ -1,225 +1 @@
import { useCallback, useMemo } from 'react'; export * from './FilesScreen';
import type { ReactNode } from 'react';
import { useSearchParams } from 'react-router';
import type { LayoutNode, PanelComponents } from '@/components/Workspace';
import { WorkspaceLayout } from '@/components/Workspace';
import { FileViewerProvider, FileViewerHeader, FileViewerBody } from 'apps/FileViewer';
import { useUserState } from '@/state/useUserState';
import { appRegistry } from '../Workspaces/app-registry';
import { Files as FilesInner } from './Screen';
export { FilesInner as Files };
const baseLayout: LayoutNode = {
type: 'panel',
id: 'files-main',
appType: null,
};
const viewerLayout: LayoutNode = {
type: 'group',
id: 'files-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'files-main', appType: null }, size: 40 },
{ node: { type: 'panel', id: 'files-viewer', appType: null }, size: 60 },
],
};
const viewerWithEphemeralLayout: LayoutNode = {
type: 'group',
id: 'files-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'files-main', appType: null }, size: 40 },
{
node: {
type: 'group',
id: 'files-viewer-group',
direction: 'vertical',
children: [
{ node: { type: 'panel', id: 'files-viewer', appType: null }, size: 50 },
{ node: { type: 'panel', id: 'files-ephemeral', appType: null }, size: 50 },
],
},
size: 60,
},
],
};
const viewerWithEphemeralSplitLayout: LayoutNode = {
type: 'group',
id: 'files-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'files-main', appType: null }, size: 40 },
{
node: {
type: 'group',
id: 'files-viewer-group',
direction: 'vertical',
children: [
{ node: { type: 'panel', id: 'files-viewer', appType: null }, size: 50 },
{
node: {
type: 'group',
id: 'files-ephemeral-group',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'files-ephemeral', appType: null }, size: 50 },
{ node: { type: 'panel', id: 'files-ephemeral2', appType: null }, size: 50 },
],
},
size: 50,
},
],
},
size: 60,
},
],
};
const FilesFileViewerProvider = ({ children }: { children: ReactNode }) => {
const [searchParams, setSearchParams] = useSearchParams();
const [homeRoot] = useUserState<string>('files/homeRoot', 'home');
const viewPath = searchParams.get('view');
const fileName = viewPath ? viewPath.split('/').pop()! : '';
const handleOpenFile = useCallback(
(filePath: string, root: string) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set('ephemeral', filePath);
next.set('ephemeralRoot', root);
return next;
});
},
[setSearchParams],
);
if (!viewPath) return null;
return (
<FileViewerProvider filePath={viewPath} fileName={fileName} root={homeRoot} onOpenFile={handleOpenFile}>
{children}
</FileViewerProvider>
);
};
const EphemeralFileViewerProvider = ({ children }: { children: ReactNode }) => {
const [searchParams, setSearchParams] = useSearchParams();
const ephemeralPath = searchParams.get('ephemeral');
const ephemeralRoot = searchParams.get('ephemeralRoot') ?? 'home';
const fileName = ephemeralPath ? ephemeralPath.split('/').pop()! : '';
const handleOpenFile = useCallback(
(filePath: string, root: string) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set('ephemeral2', filePath);
next.set('ephemeral2Root', root);
next.set('ephemeral2Auto', '1');
return next;
});
},
[setSearchParams],
);
if (!ephemeralPath) return null;
return (
<FileViewerProvider filePath={ephemeralPath} fileName={fileName} root={ephemeralRoot} onOpenFile={handleOpenFile}>
{children}
</FileViewerProvider>
);
};
const Ephemeral2FileViewerProvider = ({ children }: { children: ReactNode }) => {
const [searchParams] = useSearchParams();
const path = searchParams.get('ephemeral2');
const root = searchParams.get('ephemeral2Root') ?? 'home';
const autoPlay = searchParams.get('ephemeral2Auto') === '1';
const fileName = path ? path.split('/').pop()! : '';
if (!path) return null;
return (
<FileViewerProvider filePath={path} fileName={fileName} root={root} autoPlay={autoPlay}>
{children}
</FileViewerProvider>
);
};
export const FilesPage = () => {
const [searchParams, setSearchParams] = useSearchParams();
const viewPath = searchParams.get('view');
const ephemeralPath = searchParams.get('ephemeral');
const ephemeral2Path = searchParams.get('ephemeral2');
const layout = useMemo(
() =>
viewPath && ephemeralPath && ephemeral2Path
? viewerWithEphemeralSplitLayout
: viewPath && ephemeralPath
? viewerWithEphemeralLayout
: viewPath
? viewerLayout
: baseLayout,
[viewPath, ephemeralPath, ephemeral2Path],
);
const closeEphemeral = useCallback(() => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.delete('ephemeral');
next.delete('ephemeralRoot');
next.delete('ephemeral2');
next.delete('ephemeral2Root');
next.delete('ephemeral2Auto');
return next;
});
}, [setSearchParams]);
const closeEphemeral2 = useCallback(() => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.delete('ephemeral2');
next.delete('ephemeral2Root');
next.delete('ephemeral2Auto');
return next;
});
}, [setSearchParams]);
const panelComponents: PanelComponents = useMemo(
() => ({
'files-main': FilesInner,
'files-viewer': {
provider: FilesFileViewerProvider,
header: FileViewerHeader,
component: FileViewerBody,
onClose: () => setSearchParams({}),
},
'files-ephemeral': {
provider: EphemeralFileViewerProvider,
header: FileViewerHeader,
component: FileViewerBody,
onClose: closeEphemeral,
},
'files-ephemeral2': {
provider: Ephemeral2FileViewerProvider,
header: FileViewerHeader,
component: FileViewerBody,
onClose: closeEphemeral2,
},
}),
[setSearchParams, closeEphemeral, closeEphemeral2],
);
return (
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} registry={appRegistry} components={panelComponents} />
</div>
);
};
export { FileBrowser as FileBrowserApp } from './Widget';
@@ -10,7 +10,7 @@ import { WorkspaceLayout, WorkspaceView, createDefaultLayout } from '@/component
import type { LayoutNode, WorkspaceDefinition } from '@/components/Workspace'; import type { LayoutNode, WorkspaceDefinition } from '@/components/Workspace';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { generateSlug, slugify } from 'helpers/slug'; import { generateSlug, slugify } from 'helpers/slug';
import { Files } from '../Files'; import { Files } from 'apps/FileBrowser';
import { appRegistry } from './app-registry'; import { appRegistry } from './app-registry';
import { SELECTED_WORKSPACE_KEY, CREATING_WORKSPACE_KEY, EDITING_WORKSPACE_KEY, NEW_WS_NAME_KEY, NEW_WS_DESC_KEY, NEW_WS_TEMPLATE_KEY } from './constants'; import { SELECTED_WORKSPACE_KEY, CREATING_WORKSPACE_KEY, EDITING_WORKSPACE_KEY, NEW_WS_NAME_KEY, NEW_WS_DESC_KEY, NEW_WS_TEMPLATE_KEY } from './constants';
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { MessageSquare, FolderOpen, History, Music, Code, TerminalSquare, Monitor, LayoutGrid, LayoutDashboard, Sparkles, Eye, FolderKanban, Columns2, PenLine } from 'lucide-react'; import { MessageSquare, FolderOpen, History, Music, Code, TerminalSquare } from 'lucide-react';
import { Monitor, LayoutGrid, LayoutDashboard, Sparkles, Eye, FolderKanban, Columns2, PenLine } from 'lucide-react';
import { useAuth } from 'hooks/useAuth'; import { useAuth } from 'hooks/useAuth';
import type { AppRegistry } from '@/components/Workspace'; import type { AppRegistry } from '@/components/Workspace';
import { useWorkspace } from '@/components/Workspace'; import { useWorkspace } from '@/components/Workspace';
@@ -12,7 +13,7 @@ import { FileViewerProvider, FileViewerHeader, FileViewerBody } from 'apps/FileV
import { EmbeddableChat, ChatLauncher, usePi } from 'apps/Chat'; import { EmbeddableChat, ChatLauncher, usePi } from 'apps/Chat';
import { useVisiblePiModels } from '@/state/useModels'; import { useVisiblePiModels } from '@/state/useModels';
import { ChatHistoryApp as ChatHistory } from '../ChatHistory'; import { ChatHistoryApp as ChatHistory } from '../ChatHistory';
import { Files } from '../Files'; import { Files } from 'apps/FileBrowser';
import { Catalog } from 'sounds'; import { Catalog } from 'sounds';
import { WorkspaceListApp } from './WorkspaceListApp'; import { WorkspaceListApp } from './WorkspaceListApp';
import { ProjectListApp } from '../Projects/ProjectListApp'; import { ProjectListApp } from '../Projects/ProjectListApp';
@@ -1,7 +1,8 @@
import { useRef, useCallback, useMemo, useState, useEffect } from 'react'; import { useRef, useCallback, useMemo, useState, useEffect } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { ChevronUp, ChevronDown } from 'lucide-react'; import { ChevronUp, ChevronDown } from 'lucide-react';
import type { DirEntry, TaskSummary } from 'apps/FileBrowser'; import type { DirEntry } from './useFiles';
import type { TaskSummary } from './useTasks';
import { getFileType } from 'apps/FileViewer'; import { getFileType } from 'apps/FileViewer';
import { FileItem } from './FileItem'; import { FileItem } from './FileItem';
@@ -22,7 +22,8 @@ import {
ContextMenuTrigger, ContextMenuTrigger,
} from '@/components/ui/context-menu'; } from '@/components/ui/context-menu';
import { cardStyle } from '@/components/Card'; import { cardStyle } from '@/components/Card';
import type { DirEntry, TaskSummary } from 'apps/FileBrowser'; import type { DirEntry } from './useFiles';
import type { TaskSummary } from './useTasks';
import { getFileType } from 'apps/FileViewer'; import { getFileType } from 'apps/FileViewer';
export type FileItemProps = { export type FileItemProps = {
@@ -24,7 +24,10 @@ import { getIcon } from 'material-file-icons';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { useFiles, type DirEntry, useTasks, type TaskSummary, Breadcrumb, Toolbar } from 'apps/FileBrowser'; import { useFiles, type DirEntry } from './useFiles';
import { useTasks, type TaskSummary } from './useTasks';
import { Breadcrumb } from './Breadcrumb';
import { Toolbar } from './Toolbar';
import { useClient } from 'hooks/useClient'; import { useClient } from 'hooks/useClient';
import { useUserState } from '@/state/useUserState'; import { useUserState } from '@/state/useUserState';
import { useAuth } from 'hooks/useAuth'; import { useAuth } from 'hooks/useAuth';
@@ -7,7 +7,7 @@ import type { TaskInfo } from 'apps/Chat';
import { usePi, EmbeddableChat } from 'apps/Chat'; import { usePi, EmbeddableChat } from 'apps/Chat';
import { useVisiblePiModels } from '@/state/useModels'; import { useVisiblePiModels } from '@/state/useModels';
import { useSettings } from '@/state/useSettings'; import { useSettings } from '@/state/useSettings';
import type { TaskSummary } from 'apps/FileBrowser'; import type { TaskSummary } from './useTasks';
const playDing = () => { const playDing = () => {
const ctx = new AudioContext(); const ctx = new AudioContext();
@@ -2,9 +2,10 @@ import { useState, useEffect, useRef, useMemo } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { Folder, Pin, PinOff, Search, Clock, FolderOpen, Loader2, X } from 'lucide-react'; import { Folder, Pin, PinOff, Search, Clock, FolderOpen, Loader2, X } from 'lucide-react';
import { getIcon } from 'material-file-icons'; import { getIcon } from 'material-file-icons';
import { useFiles, type DirEntry, Breadcrumb } from 'apps/FileBrowser'; import { useFiles, type DirEntry } from './useFiles';
import { useRecentFiles } from './state/useRecentFiles'; import { Breadcrumb } from './Breadcrumb';
import { usePinnedFiles } from './state/usePinnedFiles'; import { useRecentFiles } from './useRecentFiles';
import { usePinnedFiles } from './usePinnedFiles';
import { Widget } from 'widgets/Widget'; import { Widget } from 'widgets/Widget';
type Tab = 'browse' | 'recent' | 'pinned'; type Tab = 'browse' | 'recent' | 'pinned';
+7
View File
@@ -2,3 +2,10 @@ export { Breadcrumb } from './Breadcrumb';
export { Toolbar } from './Toolbar'; export { Toolbar } from './Toolbar';
export { useFiles, type DirEntry } from './useFiles'; export { useFiles, type DirEntry } from './useFiles';
export { useTasks, type TaskSummary } from './useTasks'; export { useTasks, type TaskSummary } from './useTasks';
export { Files } from './Files';
export { FileBrowser } from './Widget';
export { FileGrid } from './FileGrid';
export { FileItem, type FileItemProps } from './FileItem';
export { TaskRunnerModal } from './TaskRunnerModal';
export { useRecentFiles } from './useRecentFiles';
export { usePinnedFiles } from './usePinnedFiles';
+10
View File
@@ -0,0 +1,10 @@
{
"name": "officerdev",
"version": "0.0.1",
"license": "MIT",
"type": "module",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
}
}
@@ -0,0 +1,5 @@
import type { AppRegistry } from '@/components/Workspace';
export function useAppRegistry(): AppRegistry {
return {};
}
@@ -0,0 +1 @@
export { useAppRegistry } from './appRegistry';
@@ -0,0 +1 @@
export * from './appRegistry';
+1
View File
@@ -0,0 +1 @@
export * from './hooks';
@@ -0,0 +1,5 @@
import type { AppRegistry } from '@/components/Workspace';
export function useAppRegistry(): AppRegistry {
return {};
}