Workspaces

This commit is contained in:
2026-02-18 02:36:15 +00:00
parent c2660cd125
commit 826904d74f
22 changed files with 851 additions and 437 deletions
+2
View File
@@ -53,6 +53,8 @@ export function App() {
<Route path="/tasks" element={<Dashboard.Tasks />} />
<Route path="/processes" element={<Dashboard.Processes />} />
<Route path="/task-logs" element={<Dashboard.TaskLogs />} />
<Route path="/workspaces" element={<Dashboard.WorkspaceListScreen />} />
<Route path="/workspaces/:id" element={<Dashboard.WorkspaceScreen />} />
<Route path="/auth/signout" element={<Authentication.SignoutScreen />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
@@ -1,26 +1,52 @@
import { Workspace, createWorkspaceDefaults } from '@/components/Workspace';
import { useUserState } from '@/state/useUserState';
import { ChatLauncher } from './ChatLauncher';
import { FileBrowserWidget as FileBrowser } from '@/Screens/Dashboard/Files';
import { ChatHistoryWidget as ChatHistory } from '@/Screens/Dashboard/ChatHistory';
import { Catalog } from 'sounds';
import { WorkspaceView } from '@/components/Workspace';
import type { LayoutNode } from '@/components/Workspace';
import { widgetRegistry } from '@/Screens/Dashboard/Workspaces/widget-registry';
const WIDGET_IDS = ['chat-launcher', 'file-browser', 'chat-history', 'sound-library'] as const;
const DEFAULTS = createWorkspaceDefaults([...WIDGET_IDS]);
const DEFAULT_HOME_LAYOUT: LayoutNode = {
type: 'group',
id: 'home-root',
direction: 'horizontal',
children: [
{
size: 50,
node: {
type: 'group',
id: 'home-left',
direction: 'vertical',
children: [
{ size: 50, node: { type: 'panel', id: 'home-tl', widgetType: 'chat-launcher' } },
{ size: 50, node: { type: 'panel', id: 'home-bl', widgetType: 'file-browser' } },
],
},
},
{
size: 50,
node: {
type: 'group',
id: 'home-right',
direction: 'vertical',
children: [
{ size: 50, node: { type: 'panel', id: 'home-tr', widgetType: 'chat-history' } },
{ size: 50, node: { type: 'panel', id: 'home-br', widgetType: 'sound-library' } },
],
},
},
],
};
export const HomeScreen = () => {
const [state, setState] = useUserState('home-layout', DEFAULTS);
const [layout, setLayout] = useUserState<LayoutNode>('home-workspace-layout', DEFAULT_HOME_LAYOUT);
return (
<Workspace
widgets={{
'chat-launcher': <ChatLauncher />,
'file-browser': <FileBrowser />,
'chat-history': <ChatHistory />,
'sound-library': <Catalog />,
}}
state={state}
onChange={setState}
/>
<div className="h-full w-full">
<WorkspaceView
workspace={null}
name="Home"
layout={layout}
onLayoutChange={setLayout}
registry={widgetRegistry}
/>
</div>
);
};
@@ -91,7 +91,7 @@ export const Dock = ({ items, className }: DockProps) => {
};
import { Terminal, TerminalSquare, FileText, FolderOpen, Code } from 'lucide-react';
import { Terminal, TerminalSquare, FileText, FolderOpen, Code, LayoutGrid } from 'lucide-react';
import { Sparkles, ClipboardList, ScrollText, Workflow } from 'lucide-react';
export const dockItems: DockItem[] = [
@@ -104,4 +104,5 @@ export const dockItems: DockItem[] = [
{ label: 'Tasks', to: '/tasks', icon: ClipboardList, color: '#fb923c' },
{ label: 'Processes', to: '/processes', icon: Workflow, color: '#2dd4bf' },
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
{ label: 'Workspaces', to: '/workspaces', icon: LayoutGrid, color: '#8b5cf6' },
];
@@ -0,0 +1,94 @@
import { useState } from 'react';
import { Link } from 'react-router';
import { Plus, Trash2, LayoutGrid, ArrowRight } from 'lucide-react';
import { useUserState } from '@/state/useUserState';
import type { WorkspaceDefinition } from '@/components/Workspace';
import { Widget } from '@/components/Widget';
import { Button } from '@/components/ui/button';
export const WorkspaceListScreen = () => {
const [workspaces, setWorkspaces] = useUserState<WorkspaceDefinition[]>('workspaces', []);
const [newName, setNewName] = useState('');
const createWorkspace = () => {
const name = newName.trim();
if (!name) return;
const ws: WorkspaceDefinition = {
id: `ws-${Date.now()}`,
name,
cwd: '~',
};
setWorkspaces((prev) => [...prev, ws]);
setNewName('');
};
const deleteWorkspace = (id: string) => {
setWorkspaces((prev) => prev.filter((ws) => ws.id !== id));
};
return (
<div className="flex h-full w-full items-start justify-center p-8">
<Widget title="Workspaces" className="w-full max-w-lg">
<div className="flex flex-col gap-4 px-4 pb-4">
<div className="flex gap-2">
<input
type="text"
value={newName}
onChange={(ev) => setNewName(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
createWorkspace();
}
}}
placeholder="New workspace name..."
className="flex-1 rounded-lg border border-duck-dark/20 bg-white px-3 py-2 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
/>
<Button
onClick={createWorkspace}
disabled={!newName.trim()}
className="bg-duck-teal hover:bg-duck-teal/90 cursor-pointer disabled:opacity-40"
>
<Plus className="h-4 w-4 mr-1" />
Create
</Button>
</div>
{workspaces.length === 0 ? (
<div className="flex flex-col items-center gap-2 py-8 text-duck-dark/40">
<LayoutGrid className="h-8 w-8" />
<p className="text-sm">No workspaces yet</p>
</div>
) : (
<ul className="space-y-1">
{workspaces.map((ws) => (
<li
key={ws.id}
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-duck-dark/5 group"
>
<LayoutGrid className="h-4 w-4 shrink-0 text-duck-teal/60" />
<Link to={`/workspaces/${ws.id}`} className="flex-1 min-w-0">
<span className="text-sm font-medium text-duck-dark truncate block">{ws.name}</span>
</Link>
<button
type="button"
onClick={() => deleteWorkspace(ws.id)}
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-red-500 transition-opacity cursor-pointer"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
<Link
to={`/workspaces/${ws.id}`}
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-duck-teal transition-opacity"
>
<ArrowRight className="h-3.5 w-3.5" />
</Link>
</li>
))}
</ul>
)}
</div>
</Widget>
</div>
);
};
@@ -0,0 +1,39 @@
import { Link } from 'react-router';
import { LayoutGrid, ArrowRight } from 'lucide-react';
import { useUserState } from '@/state/useUserState';
import type { WorkspaceDefinition } from '@/components/Workspace';
export const WorkspaceListWidget = () => {
const [workspaces] = useUserState<WorkspaceDefinition[]>('workspaces', []);
return (
<div className="px-4 pb-3 max-h-72 overflow-y-auto">
{workspaces.length === 0 ? (
<div className="flex flex-col items-center gap-2 py-6 text-duck-dark/40">
<LayoutGrid className="h-6 w-6" />
<p className="text-xs">No workspaces</p>
<Link to="/workspaces" className="text-xs text-duck-teal hover:underline">
Create one
</Link>
</div>
) : (
<ul className="space-y-0.5">
{workspaces.map((ws) => (
<li key={ws.id} className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-duck-dark/5 group">
<Link to={`/workspaces/${ws.id}`} className="flex items-center gap-2 flex-1 min-w-0">
<LayoutGrid className="h-4 w-4 shrink-0 text-duck-teal/60" />
<span className="text-sm text-duck-dark truncate">{ws.name}</span>
</Link>
<Link
to={`/workspaces/${ws.id}`}
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-duck-teal transition-opacity"
>
<ArrowRight className="h-3.5 w-3.5" />
</Link>
</li>
))}
</ul>
)}
</div>
);
};
@@ -0,0 +1,30 @@
import { useParams, Navigate } from 'react-router';
import { useUserState } from '@/state/useUserState';
import { WorkspaceView, createDefaultLayout } from '@/components/Workspace';
import type { LayoutNode, WorkspaceDefinition } from '@/components/Workspace';
import { widgetRegistry } from './widget-registry';
export const WorkspaceScreen = () => {
const { id } = useParams<{ id: string }>();
const [workspaces] = useUserState<WorkspaceDefinition[]>('workspaces', []);
const workspace = workspaces.find((ws) => ws.id === id);
if (!workspace) return <Navigate to="/workspaces" replace />;
return <WorkspaceScreenInner workspace={workspace} />;
};
const WorkspaceScreenInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
const [layout, setLayout] = useUserState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
return (
<div className="h-full w-full">
<WorkspaceView
workspace={workspace}
layout={layout}
onLayoutChange={setLayout}
registry={widgetRegistry}
/>
</div>
);
};
@@ -0,0 +1,2 @@
export { WorkspaceListScreen } from './WorkspaceListScreen';
export { WorkspaceScreen } from './WorkspaceScreen';
@@ -0,0 +1,23 @@
import { MessageSquare, FolderOpen, History, Music, Code, TerminalSquare, LayoutGrid } from 'lucide-react';
import type { WidgetRegistry } from '@/components/Workspace';
import { CodeEditorView } from 'widgets/CodeEditor';
import { TerminalView } from 'widgets/Terminal';
import { ChatLauncher } from '../Home/ChatLauncher';
import { ChatHistoryWidget as ChatHistory } from '../ChatHistory';
import { FileBrowserWidget as FileBrowser } from '../Files';
import { Catalog } from 'sounds';
import { WorkspaceListWidget } from './WorkspaceListWidget';
const CodeEditorWrapper = () => <CodeEditorView className="h-full w-full" />;
const TerminalWrapper = () => <TerminalView className="h-full w-full p-2" />;
export const widgetRegistry: WidgetRegistry = {
'chat-launcher': { name: 'Chat', icon: MessageSquare, component: () => <ChatLauncher /> },
'file-browser': { name: 'File Browser', icon: FolderOpen, component: () => <FileBrowser /> },
'chat-history': { name: 'Chat History', icon: History, component: () => <ChatHistory /> },
'sound-library': { name: 'Sound Library', icon: Music, component: () => <Catalog /> },
'code-editor': { name: 'Code Editor', icon: Code, component: CodeEditorWrapper },
'terminal': { name: 'Terminal', icon: TerminalSquare, component: TerminalWrapper },
'workspace-list': { name: 'Workspaces', icon: LayoutGrid, component: () => <WorkspaceListWidget /> },
};
@@ -14,3 +14,4 @@ export * from './Terminal';
export * from './Files';
export * from './ChatHistory';
export * from './CodeEditor';
export * from './Workspaces';