Workspaces and Terminals

This commit is contained in:
2026-02-19 18:01:08 +00:00
parent 9870fa7ae8
commit 4733a972dd
14 changed files with 402 additions and 57 deletions
@@ -23,6 +23,7 @@ type FileGridProps = {
onRenamingChange: (name: string | null) => void;
getMatchingTasks: (fileName: string, entryType: 'file' | 'directory') => TaskSummary[];
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
onCreateWorkspace: (entry: DirEntry) => void;
scrollRef: React.RefObject<HTMLDivElement | null>;
};
@@ -72,6 +73,7 @@ export const FileGrid = ({
onRenamingChange,
getMatchingTasks,
onRunTask,
onCreateWorkspace,
scrollRef,
}: FileGridProps) => {
const lastClickedIdx = useRef<number>(-1);
@@ -153,6 +155,7 @@ export const FileGrid = ({
onRenamingChange={onRenamingChange}
matchingTasks={getMatchingTasks(entry.name, entry.type)}
onRunTask={onRunTask}
onCreateWorkspace={onCreateWorkspace}
/>
);
@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { Folder, Trash2, Pencil, MoreVertical, MessageSquare, Scissors, Copy, Check, Play, Download } from 'lucide-react';
import { Folder, Trash2, Pencil, MoreVertical, MessageSquare, Scissors, Copy, Check, Play, Download, LayoutGrid } from 'lucide-react';
import { getIcon } from 'material-file-icons';
import {
DropdownMenu,
@@ -43,6 +43,7 @@ export type FileItemProps = {
onRenamingChange: (name: string | null) => void;
matchingTasks: TaskSummary[];
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
onCreateWorkspace: (entry: DirEntry) => void;
};
function formatSize(bytes: number): string {
@@ -70,6 +71,7 @@ type MenuItemsProps = {
onCopy: () => void;
matchingTasks: TaskSummary[];
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
onCreateWorkspace: (e: DirEntry) => void;
};
const DropdownMenuItems = ({
@@ -83,6 +85,7 @@ const DropdownMenuItems = ({
onCopy,
matchingTasks,
onRunTask,
onCreateWorkspace,
}: MenuItemsProps) => (
<>
<DropdownMenuItem onClick={() => onChat(entry)} className="cursor-pointer">
@@ -108,6 +111,15 @@ const DropdownMenuItems = ({
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
{entry.type === 'directory' && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onCreateWorkspace(entry)} className="cursor-pointer">
<LayoutGrid className="mr-2 h-4 w-4" />
Create Workspace here
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onCut} className="cursor-pointer">
<Scissors className="mr-2 h-4 w-4" />
@@ -142,6 +154,7 @@ const ContextMenuItems = ({
onCopy,
matchingTasks,
onRunTask,
onCreateWorkspace,
}: MenuItemsProps) => (
<>
<ContextMenuItem onClick={() => onChat(entry)} className="cursor-pointer">
@@ -167,6 +180,15 @@ const ContextMenuItems = ({
</ContextMenuSubContent>
</ContextMenuSub>
)}
{entry.type === 'directory' && (
<>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => onCreateWorkspace(entry)} className="cursor-pointer">
<LayoutGrid className="mr-2 h-4 w-4" />
Create Workspace here
</ContextMenuItem>
</>
)}
<ContextMenuSeparator />
<ContextMenuItem onClick={onCut} className="cursor-pointer">
<Scissors className="mr-2 h-4 w-4" />
@@ -300,6 +322,7 @@ export const FileItem = ({
onRenamingChange,
matchingTasks,
onRunTask,
onCreateWorkspace,
}: FileItemProps) => {
const [renaming, setRenaming] = useState(false);
const clickTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -376,6 +399,7 @@ export const FileItem = ({
onCopy,
matchingTasks,
onRunTask,
onCreateWorkspace,
};
const cutOpacity = isCut ? 'opacity-50' : '';
@@ -33,7 +33,11 @@ type ClipboardState = { paths: string[]; mode: 'copy' | 'cut' } | null;
type HomeRoot = 'home' | '~' | 'officer.dev';
export const Files = () => {
type FilesProps = {
basePath?: string;
};
export const Files = ({ basePath = '/' }: FilesProps) => {
const { user } = useAuth();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
@@ -86,6 +90,10 @@ export const Files = () => {
}, []);
useEffect(() => {
if (basePath !== '/' && !currentPath.startsWith(basePath)) {
setCurrentPath(basePath);
return;
}
refresh();
}, [currentPath, homeRoot]);
@@ -137,6 +145,10 @@ export const Files = () => {
const selectedPaths = () => Array.from(selected).map(entryPath);
const handleNavigate = (path: string) => {
if (basePath !== '/' && !path.startsWith(basePath)) {
setCurrentPath(basePath);
return;
}
setCurrentPath(path);
};
@@ -262,6 +274,18 @@ export const Files = () => {
setRunningTask({ task, entry });
};
const handleCreateWorkspace = (entry: DirEntry) => {
const folderPath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
navigate(`/workspaces/new?name=${encodeURIComponent(entry.name)}&cwd=${encodeURIComponent(folderPath)}`);
};
const handleCreateWorkspaceHere = () => {
const dirName = currentPath === '/' ? '' : currentPath.split('/').pop()!;
const params = new URLSearchParams({ cwd: currentPath });
if (dirName) params.set('name', dirName);
navigate(`/workspaces/new?${params}`);
};
const handleGitClone = async () => {
const url = cloneUrl.trim();
if (!url) return;
@@ -509,7 +533,7 @@ export const Files = () => {
</div>
{/* Home dir selector (Super Admin only) */}
{user?.role === 'Super Admin' && (
{basePath === '/' && user?.role === 'Super Admin' && (
<div className="shrink-0 border-b border-duck-dark/10 px-4 py-2">
<RadioGroup
value={homeRoot}
@@ -539,7 +563,7 @@ export const Files = () => {
{/* Breadcrumb */}
<div className="shrink-0 border-b border-duck-dark/10 px-4 py-2">
<Breadcrumb path={currentPath} onNavigate={handleNavigate} />
<Breadcrumb path={currentPath} onNavigate={handleNavigate} basePath={basePath} />
</div>
{/* Upload progress */}
@@ -625,6 +649,7 @@ export const Files = () => {
onRenamingChange={setRenamingName}
getMatchingTasks={getMatchingTasks}
onRunTask={handleRunTask}
onCreateWorkspace={handleCreateWorkspace}
scrollRef={fileScrollRef}
/>
)}
@@ -645,6 +670,10 @@ export const Files = () => {
<FolderPlus className="mr-2 h-4 w-4" />
New folder
</ContextMenuItem>
<ContextMenuItem onClick={handleCreateWorkspaceHere} className="cursor-pointer">
<LayoutGrid className="mr-2 h-4 w-4" />
Create Workspace here
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)}