Workspaces in Workspaces all around
This commit is contained in:
@@ -15,6 +15,7 @@ type FileGridProps = {
|
||||
onDelete: (entry: DirEntry) => void;
|
||||
onRename: (entry: DirEntry, newName: string) => void;
|
||||
onChat: (entry: DirEntry) => void;
|
||||
onDownload: (entry: DirEntry) => void;
|
||||
onSelect: (names: Set<string>) => void;
|
||||
onCut: () => void;
|
||||
onCopy: () => void;
|
||||
@@ -63,6 +64,7 @@ export const FileGrid = ({
|
||||
onDelete,
|
||||
onRename,
|
||||
onChat,
|
||||
onDownload,
|
||||
onSelect,
|
||||
onCut,
|
||||
onCopy,
|
||||
@@ -143,6 +145,7 @@ export const FileGrid = ({
|
||||
onDelete={onDelete}
|
||||
onRename={onRename}
|
||||
onChat={onChat}
|
||||
onDownload={onDownload}
|
||||
onSelect={handleSelect}
|
||||
onCut={onCut}
|
||||
onCopy={onCopy}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { Folder, Trash2, Pencil, MoreVertical, MessageSquare, Scissors, Copy, Check, Play } from 'lucide-react';
|
||||
import { Folder, Trash2, Pencil, MoreVertical, MessageSquare, Scissors, Copy, Check, Play, Download } from 'lucide-react';
|
||||
import { getIcon } from 'material-file-icons';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -35,6 +35,7 @@ export type FileItemProps = {
|
||||
onDelete: (entry: DirEntry) => void;
|
||||
onRename: (entry: DirEntry, newName: string) => void;
|
||||
onChat: (entry: DirEntry) => void;
|
||||
onDownload: (entry: DirEntry) => void;
|
||||
onSelect: (entry: DirEntry, ev: React.MouseEvent) => void;
|
||||
onCut: () => void;
|
||||
onCopy: () => void;
|
||||
@@ -64,6 +65,7 @@ type MenuItemsProps = {
|
||||
onDelete: (e: DirEntry) => void;
|
||||
onStartRename: () => void;
|
||||
onChat: (e: DirEntry) => void;
|
||||
onDownload: (e: DirEntry) => void;
|
||||
onCut: () => void;
|
||||
onCopy: () => void;
|
||||
matchingTasks: TaskSummary[];
|
||||
@@ -76,6 +78,7 @@ const DropdownMenuItems = ({
|
||||
onDelete,
|
||||
onStartRename,
|
||||
onChat,
|
||||
onDownload,
|
||||
onCut,
|
||||
onCopy,
|
||||
matchingTasks,
|
||||
@@ -86,6 +89,10 @@ const DropdownMenuItems = ({
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Chat...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onDownload(entry)} className="cursor-pointer">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</DropdownMenuItem>
|
||||
{matchingTasks.length > 0 && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="cursor-pointer">
|
||||
@@ -130,6 +137,7 @@ const ContextMenuItems = ({
|
||||
onDelete,
|
||||
onStartRename,
|
||||
onChat,
|
||||
onDownload,
|
||||
onCut,
|
||||
onCopy,
|
||||
matchingTasks,
|
||||
@@ -140,6 +148,10 @@ const ContextMenuItems = ({
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Chat...
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => onDownload(entry)} className="cursor-pointer">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</ContextMenuItem>
|
||||
{matchingTasks.length > 0 && (
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger className="cursor-pointer">
|
||||
@@ -237,7 +249,7 @@ const InlineRenameInput = ({
|
||||
if (ev.key === 'Escape') onCancel();
|
||||
}}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
className="text-sm font-medium text-duck-dark bg-white border border-duck-teal/50 rounded px-1 py-0.5 outline-none w-full text-center"
|
||||
className="text-sm font-medium text-duck-dark bg-background border border-duck-teal/50 rounded px-1 py-0.5 outline-none w-full text-center"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -257,7 +269,7 @@ const Checkbox = ({
|
||||
onClick(ev);
|
||||
}}
|
||||
className={`flex items-center justify-center h-5 w-5 rounded border-2 transition-all cursor-pointer ${
|
||||
checked ? 'bg-duck-teal border-duck-teal text-white' : 'border-duck-dark/30 bg-white/80 hover:border-duck-teal/50'
|
||||
checked ? 'bg-duck-teal border-duck-teal text-white' : 'border-duck-dark/30 bg-background/80 hover:border-duck-teal/50'
|
||||
} ${anySelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
|
||||
>
|
||||
{checked && <Check className="h-3 w-3" />}
|
||||
@@ -280,6 +292,7 @@ export const FileItem = ({
|
||||
onDelete,
|
||||
onRename,
|
||||
onChat,
|
||||
onDownload,
|
||||
onSelect,
|
||||
onCut,
|
||||
onCopy,
|
||||
@@ -358,6 +371,7 @@ export const FileItem = ({
|
||||
onDelete,
|
||||
onStartRename: () => setRenaming(true),
|
||||
onChat,
|
||||
onDownload,
|
||||
onCut,
|
||||
onCopy,
|
||||
matchingTasks,
|
||||
|
||||
@@ -401,7 +401,7 @@ const MarkdownRenderer = ({
|
||||
|
||||
// ── Text renderer ──
|
||||
const TextRenderer = ({ content }: { content: string }) => (
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm text-duck-dark leading-relaxed p-4">{content}</pre>
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm text-foreground leading-relaxed p-4">{content}</pre>
|
||||
);
|
||||
|
||||
// ── Code renderer with syntax highlighting ──
|
||||
@@ -851,7 +851,7 @@ const ImageRenderer = ({ src, fileName }: { src: string; fileName: string }) =>
|
||||
{/* Image area */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`flex-1 min-h-0 flex items-center justify-center overflow-hidden bg-[repeating-conic-gradient(#e5e7eb_0%_25%,transparent_0%_50%)] bg-[length:16px_16px] ${
|
||||
className={`flex-1 min-h-0 flex items-center justify-center overflow-hidden bg-[repeating-conic-gradient(hsl(var(--muted))_0%_25%,transparent_0%_50%)] bg-[length:16px_16px] ${
|
||||
zoom > 1 ? (dragging ? 'cursor-grabbing' : 'cursor-grab') : 'cursor-zoom-in'
|
||||
}`}
|
||||
onWheel={onWheel}
|
||||
@@ -880,7 +880,7 @@ const ImageRenderer = ({ src, fileName }: { src: string; fileName: string }) =>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="shrink-0 flex items-center justify-center gap-1 py-2 border-t border-duck-dark/10 bg-white/80">
|
||||
<div className="shrink-0 flex items-center justify-center gap-1 py-2 border-t border-duck-dark/10 bg-background/80">
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
disabled={zoom <= minZoom}
|
||||
@@ -1050,22 +1050,12 @@ export const FileViewer = ({ open, onOpenChange, filePath, fileName, root }: Fil
|
||||
className={`fixed left-[50%] top-[50%] z-[600] translate-x-[-50%] translate-y-[-50%] flex flex-col overflow-hidden rounded-xl border-2 border-duck-dark/30 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 ${
|
||||
expanded ? 'w-[95vw] h-[95vh]' : 'w-[90vw] max-w-4xl h-[85vh]'
|
||||
}`}
|
||||
style={
|
||||
fileType === 'video'
|
||||
? { backgroundColor: '#000' }
|
||||
: cardStyle({
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.97)',
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, rgba(20, 83, 45, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(20, 83, 45, 0.04) 1px, transparent 1px)
|
||||
`,
|
||||
})
|
||||
}
|
||||
style={fileType === 'video' ? { backgroundColor: '#000' } : cardStyle()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
className={`shrink-0 flex items-center gap-3 px-5 py-3 border-b ${
|
||||
fileType === 'video' ? 'border-white/10 bg-black/80' : 'border-duck-dark/10 bg-white/60'
|
||||
fileType === 'video' ? 'border-white/10 bg-black/80' : 'border-duck-dark/10 bg-background/60'
|
||||
}`}
|
||||
>
|
||||
{headerIcon}
|
||||
|
||||
@@ -104,17 +104,21 @@ type TaskRunnerModalProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
task: TaskSummary;
|
||||
entryName: string;
|
||||
entryType: 'file' | 'directory';
|
||||
cwd: { root?: string; path: string };
|
||||
entryName?: string;
|
||||
entryType?: 'file' | 'directory';
|
||||
cwd?: { root?: string; path: string };
|
||||
promptOverride?: string;
|
||||
};
|
||||
|
||||
export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryType, cwd }: TaskRunnerModalProps) => {
|
||||
export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryType, cwd = { path: '' }, promptOverride }: TaskRunnerModalProps) => {
|
||||
const { settings } = useSettings();
|
||||
const taskSettings = settings.tasks;
|
||||
const [provider, setProvider] = useState<'claude' | 'opencode'>(taskSettings.defaultProvider);
|
||||
const defaultInput = `Read the task instructions at ${task.filePath} and execute them on the ${entryType}: ${entryName}`;
|
||||
const taskInfo: TaskInfo = { taskName: task.name, taskDirName: task.dirName, entryName, entryType };
|
||||
const defaultInput = promptOverride
|
||||
?? (entryName && entryType
|
||||
? `Read the task instructions at ${task.filePath} and execute them on the ${entryType}: ${entryName}`
|
||||
: `Read the task instructions at ${task.filePath} and execute them`);
|
||||
const taskInfo: TaskInfo = { taskName: task.name, taskDirName: task.dirName, entryName: entryName ?? '', entryType: entryType ?? 'file' };
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -123,16 +127,10 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryType
|
||||
<DialogPrimitive.Content
|
||||
onOpenAutoFocus={(ev) => ev.preventDefault()}
|
||||
className="fixed left-[50%] top-[50%] z-[700] translate-x-[-50%] translate-y-[-50%] flex flex-col overflow-hidden rounded-xl border-2 border-duck-dark/30 shadow-2xl w-[90vw] max-w-3xl h-[80vh] duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
|
||||
style={cardStyle({
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.97)',
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, rgba(20, 83, 45, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(20, 83, 45, 0.04) 1px, transparent 1px)
|
||||
`,
|
||||
})}
|
||||
style={cardStyle()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="shrink-0 flex items-center gap-3 px-5 py-3 border-b border-duck-dark/10 bg-white/60">
|
||||
<div className="shrink-0 flex items-center gap-3 px-5 py-3 border-b border-duck-dark/10 bg-background/60">
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-semibold text-duck-dark truncate block">{task.name}</span>
|
||||
<span className="text-xs text-duck-dark/50 truncate block">{entryName}</span>
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
Loader2,
|
||||
LayoutGrid,
|
||||
List,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
|
||||
ClipboardPaste,
|
||||
FolderPlus,
|
||||
Search,
|
||||
@@ -43,7 +42,7 @@ export const Files = () => {
|
||||
const [entries, setEntries] = useState<DirEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [viewMode, setViewMode] = useUserState<'grid' | 'list'>('files/viewMode', 'grid');
|
||||
const [fullscreen, setFullscreen] = useUserState<boolean>('files/fullscreen', false);
|
||||
|
||||
const [showHidden, setShowHidden] = useUserState<boolean>('files/showHidden', false);
|
||||
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
@@ -240,6 +239,25 @@ export const Files = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async (entry: DirEntry) => {
|
||||
const path = entryPath(entry.name);
|
||||
try {
|
||||
await files.download([path]);
|
||||
} catch {
|
||||
toast.error('Failed to download');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadSelected = async () => {
|
||||
const paths = selectedPaths();
|
||||
if (paths.length === 0) return;
|
||||
try {
|
||||
await files.download(paths);
|
||||
} catch {
|
||||
toast.error('Failed to download');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunTask = (task: TaskSummary, entry: DirEntry) => {
|
||||
setRunningTask({ task, entry });
|
||||
};
|
||||
@@ -378,6 +396,7 @@ export const Files = () => {
|
||||
{/* Toolbar */}
|
||||
<div className="shrink-0 flex items-center gap-2 md:gap-3 px-3 md:px-4 h-12 md:h-14 border-b border-duck-dark/10 overflow-hidden">
|
||||
<Toolbar
|
||||
onRefresh={refresh}
|
||||
onCreateDir={handleCreateDir}
|
||||
onUpload={handleUpload}
|
||||
selectionCount={selected.size}
|
||||
@@ -385,6 +404,7 @@ export const Files = () => {
|
||||
onCut={handleCut}
|
||||
onCopy={handleCopy}
|
||||
onPaste={handlePaste}
|
||||
onDownloadSelected={handleDownloadSelected}
|
||||
onDeleteSelected={handleDeleteSelected}
|
||||
onClearSelection={() => setSelected(new Set())}
|
||||
/>
|
||||
@@ -402,7 +422,7 @@ export const Files = () => {
|
||||
value={cloneUrl}
|
||||
onChange={(ev) => setCloneUrl(ev.target.value)}
|
||||
placeholder="https://github.com/user/repo.git"
|
||||
className="h-8 w-40 md:w-64 text-sm rounded-md border border-duck-dark/20 bg-white/60 text-duck-dark placeholder:text-duck-dark/40 outline-none focus:border-duck-teal/50 px-2"
|
||||
className="h-8 w-40 md:w-64 text-sm rounded-md border border-duck-dark/20 bg-background/60 text-duck-dark placeholder:text-duck-dark/40 outline-none focus:border-duck-teal/50 px-2"
|
||||
disabled={cloning}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Escape') {
|
||||
@@ -454,7 +474,7 @@ export const Files = () => {
|
||||
}
|
||||
}}
|
||||
placeholder="Search files..."
|
||||
className="h-8 w-28 focus:w-40 md:w-40 md:focus:w-56 transition-all pl-8 pr-7 text-base md:text-sm rounded-md border border-duck-dark/20 bg-white/60 text-duck-dark placeholder:text-duck-dark/40 outline-none focus:border-duck-teal/50"
|
||||
className="h-8 w-28 focus:w-40 md:w-40 md:focus:w-56 transition-all pl-8 pr-7 text-base md:text-sm rounded-md border border-duck-dark/20 bg-background/60 text-duck-dark placeholder:text-duck-dark/40 outline-none focus:border-duck-teal/50"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
@@ -486,12 +506,6 @@ export const Files = () => {
|
||||
<List className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setFullscreen((f) => !f)}
|
||||
className="hidden md:block p-1.5 rounded-md text-duck-dark/50 hover:bg-duck-dark/5 cursor-pointer transition-colors"
|
||||
>
|
||||
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Home dir selector (Super Admin only) */}
|
||||
@@ -603,6 +617,7 @@ export const Files = () => {
|
||||
onDelete={handleDelete}
|
||||
onRename={handleRename}
|
||||
onChat={handleChat}
|
||||
onDownload={handleDownload}
|
||||
onSelect={setSelected}
|
||||
onCut={handleCut}
|
||||
onCopy={handleCopy}
|
||||
|
||||
@@ -96,7 +96,7 @@ export const FileBrowser = () => {
|
||||
value={searchQuery}
|
||||
onChange={(ev) => setSearchQuery(ev.target.value)}
|
||||
placeholder="Search..."
|
||||
className="w-full rounded-lg border border-duck-dark/20 bg-white pl-8 pr-8 py-1 text-xs text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
|
||||
className="w-full rounded-lg border border-duck-dark/20 bg-background pl-8 pr-8 py-1 text-xs text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import { Card } from '@/components/Card';
|
||||
import { useMemo } from 'react';
|
||||
import type { LayoutNode, PanelComponents } from '@/components/Workspace';
|
||||
import { WorkspaceLayout } from '@/components/Workspace';
|
||||
import { appRegistry } from '../Workspaces/app-registry';
|
||||
import { Files as FilesInner } from './Screen';
|
||||
|
||||
export { FilesInner as Files };
|
||||
|
||||
export const FilesPage = () => (
|
||||
<div className="flex items-center justify-center h-full p-0 md:p-4">
|
||||
<Card className="flex flex-col overflow-hidden w-full h-full md:w-[90vw] md:h-[90%] lg:w-[50vw]">
|
||||
<FilesInner />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
const layout: LayoutNode = {
|
||||
type: 'panel',
|
||||
id: 'files-main',
|
||||
appType: null,
|
||||
};
|
||||
|
||||
export const FilesPage = () => {
|
||||
const panelComponents: PanelComponents = useMemo(
|
||||
() => ({
|
||||
'files-main': FilesInner,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} registry={appRegistry} components={panelComponents} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { FileBrowser as FileBrowserApp } from './Widget';
|
||||
|
||||
Reference in New Issue
Block a user