diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx
index 97c474dd..06acdded 100644
--- a/src/apps/officer-web/App.tsx
+++ b/src/apps/officer-web/App.tsx
@@ -48,13 +48,14 @@ export function App() {
} />
} />
} />
- } />
+
} />
} />
} />
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
index 907afa21..3ea9b48c 100644
--- a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
@@ -109,12 +109,12 @@ export const Dock = ({ items, className }: DockProps) => {
};
-import { MessageCircle, TerminalSquare, FileText, FolderOpen, Code, LayoutGrid, Bot, ScrollText } from 'lucide-react';
+import { MessageCircle, FileText, FolderOpen, Code, LayoutGrid, Bot, ScrollText } from 'lucide-react';
export const dockItems: DockItem[] = [
{ label: 'Files', to: '/files', icon: FolderOpen, color: '#fbbf24' },
{ label: 'Chat', to: '/chat', icon: MessageCircle, color: '#60a5fa' },
- { label: 'Terminal', to: '/terminal', icon: TerminalSquare, color: '#34d399' },
+
{ label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
{ label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
{ label: 'Automation', to: '/automation', icon: Bot, color: '#2dd4bf' },
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TerminalSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TerminalSection.tsx
deleted file mode 100644
index 17ad5c9b..00000000
--- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TerminalSection.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Switch } from '@/components/ui/switch';
-import { useServerSettings } from '@/state/useServerSettings';
-
-export const TerminalSection = () => {
- const { terminalSandboxed, saveSettings } = useServerSettings();
-
- const toggleSandbox = (checked: boolean) => {
- saveSettings({ terminalSandboxed: checked });
- };
-
- return (
-
-
-
Sandbox terminal (Docker)
-
Restrict terminal access to the user's home directory.
-
-
-
- );
-};
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/index.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/index.tsx
index a9c65721..f59f348f 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/index.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/index.tsx
@@ -1,17 +1,15 @@
import { useMemo } from 'react';
-import { Terminal, Shield, Server } from 'lucide-react';
+import { Terminal, Server } from 'lucide-react';
import type { LayoutNode, PanelComponents } from '@/components/Workspace';
import { WorkspaceLayout } from '@/components/Workspace';
import { appRegistry } from '../../Workspaces/app-registry';
import { createSettingsPanelComponents, type SettingsSection } from '../SettingsPanel';
import { AIHarnessesSection } from './AIHarnessesSection';
-import { TerminalSection } from './TerminalSection';
const GLOBAL_KEY = 'SERVER_SETTINGS_SELECTED';
const sections: SettingsSection[] = [
{ key: 'ai-harnesses', icon: Terminal, title: 'AI Harnesses', description: 'AI coding tools setup', content: },
- { key: 'terminal', icon: Shield, title: 'Terminal', description: 'Sandbox and access controls', content: },
];
const { Sidebar, Content } = createSettingsPanelComponents({
diff --git a/src/apps/officer-web/Screens/Dashboard/Terminal/index.tsx b/src/apps/officer-web/Screens/Dashboard/Terminal/index.tsx
deleted file mode 100644
index 72ebdc9b..00000000
--- a/src/apps/officer-web/Screens/Dashboard/Terminal/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { TerminalView } from 'apps/Terminal';
-
-export const Terminal = () => {
- return ;
-};
diff --git a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx
index 6c128ea4..6f583ad3 100644
--- a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx
@@ -1,9 +1,11 @@
+import { useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router';
-import { LayoutGrid, Plus, Pencil, Trash2 } from 'lucide-react';
+import { LayoutGrid, Plus, Pencil, Trash2, Search } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { useGlobal } from 'hooks/useGlobal';
import { useClient } from 'hooks/useClient';
-import { useUserState } from '@/state/useUserState';
+import { generateSlug } from 'helpers/slug';
+import { useWorkspacesState } from '@/state/useWorkspacesState';
import type { WorkspaceDefinition } from '@/components/Workspace';
import {
SELECTED_WORKSPACE_KEY,
@@ -19,7 +21,7 @@ export const WorkspaceListApp = () => {
const navigate = useNavigate();
const client = useClient();
const queryClient = useQueryClient();
- const [workspaces, setWorkspaces] = useUserState('workspaces', []);
+ const [workspaces, setWorkspaces] = useWorkspacesState('workspaces', []);
const [selected, setSelected] = useGlobal(SELECTED_WORKSPACE_KEY, null);
const [, setCreating] = useGlobal(CREATING_WORKSPACE_KEY, false);
const [, setEditing] = useGlobal(EDITING_WORKSPACE_KEY, null);
@@ -27,7 +29,14 @@ export const WorkspaceListApp = () => {
const [, setDescription] = useGlobal(NEW_WS_DESC_KEY, '');
const [, setTemplateIdx] = useGlobal(NEW_WS_TEMPLATE_KEY, 0);
+ const [search, setSearch] = useState('');
const isWorkspacesPage = location.pathname === '/workspaces';
+ const filtered = search
+ ? workspaces.filter((ws) => {
+ const q = search.toLowerCase();
+ return [ws.name, ws.id, ws.description ?? '', ws.cwd ?? ''].some((field) => field.toLowerCase().includes(q));
+ })
+ : workspaces;
const handleEdit = (ev: React.MouseEvent, ws: WorkspaceDefinition) => {
ev.stopPropagation();
@@ -45,10 +54,10 @@ export const WorkspaceListApp = () => {
if (selected === ws.id) setSelected(null);
const layoutKey = `ws-layout-${ws.id}`;
- const currentState = queryClient.getQueryData>(['USER_STATE']) ?? {};
+ const currentState = queryClient.getQueryData>(['WORKSPACES_STATE']) ?? {};
const { [layoutKey]: _, ...rest } = currentState;
- queryClient.setQueryData(['USER_STATE'], rest);
- client.patch('/user/state', { [layoutKey]: null }).catch(() => {});
+ queryClient.setQueryData(['WORKSPACES_STATE'], rest);
+ client.patch('/user/workspaces-state', { [layoutKey]: null }).catch(() => {});
};
const handleClick = (ws: WorkspaceDefinition) => {
@@ -63,7 +72,7 @@ export const WorkspaceListApp = () => {
return (
-
+
{
Workspaces
+
-
- {workspaces.map((ws) => (
+
+
+ setSearch(ev.target.value)}
+ placeholder="Search workspaces"
+ className="w-full rounded-lg border border-duck-dark/15 bg-transparent py-1.5 pl-8 pr-3 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-duck-teal/30 focus:border-duck-teal/40"
+ />
+
+
+ {filtered.map((ws) => (
handleClick(ws)}
>
@@ -105,22 +140,10 @@ export const WorkspaceListApp = () => {
)}
))}
- {workspaces.length === 0 && (
-
No workspaces yet
- )}
- {isWorkspacesPage && (
-
+ {filtered.length === 0 && (
+
+ {search ? 'No matches' : 'No workspaces yet'}
+
)}
diff --git a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListScreen.tsx
index cf7530f1..cf154799 100644
--- a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListScreen.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListScreen.tsx
@@ -1,12 +1,14 @@
-import { Link, useNavigate } from 'react-router';
+import { Link, Navigate, useNavigate, useSearchParams } from 'react-router';
import { LayoutGrid, Plus, ArrowRight, Type, Rocket, FileText, Layout } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { useGlobal } from 'hooks/useGlobal';
import { useClient } from 'hooks/useClient';
import { useUserState } from '@/state/useUserState';
+import { useWorkspacesState } from '@/state/useWorkspacesState';
import { WorkspaceLayout, WorkspaceView, createDefaultLayout } from '@/components/Workspace';
import type { LayoutNode, WorkspaceDefinition } from '@/components/Workspace';
import { Button } from '@/components/ui/button';
+import { generateSlug, slugify } from 'helpers/slug';
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';
@@ -275,7 +277,7 @@ const CreatePanel = () => {
const client = useClient();
const [name, setName] = useGlobal
(NEW_WS_NAME_KEY, '');
const [filePath] = useUserState('files/currentPath', '/');
- const [, setWorkspaces] = useUserState('workspaces', []);
+ const [workspaces, setWorkspaces] = useWorkspacesState('workspaces', []);
const cwd = filePath === '/' ? '~' : `~/${filePath.replace(/^\//, '')}`;
@@ -293,22 +295,49 @@ const CreatePanel = () => {
const desc = description.trim();
if (isEditing) {
+ const existingIds = new Set(workspaces.filter((w) => w.id !== editingId).map((w) => w.id));
+ let newId = slugify(trimmed) || generateSlug();
+ while (existingIds.has(newId)) newId = `${newId}-${generateSlug(1)}`;
+
+ const idChanged = newId !== editingId;
+
setWorkspaces((prev) =>
prev.map((ws) =>
ws.id === editingId
- ? { ...ws, name: trimmed, description: desc || undefined, templateIdx }
+ ? { ...ws, id: newId, name: trimmed, description: desc || undefined, templateIdx }
: ws,
),
);
const wsLayout = templates[templateIdx]?.layout() ?? createDefaultLayout();
- const layoutKey = `ws-layout-${editingId}`;
- const currentState = queryClient.getQueryData>(['USER_STATE']) ?? {};
- queryClient.setQueryData(['USER_STATE'], { ...currentState, [layoutKey]: wsLayout });
- client.patch('/user/state', { [layoutKey]: wsLayout }).catch(() => {});
+ const currentState = queryClient.getQueryData>(['WORKSPACES_STATE']) ?? {};
+ const newLayoutKey = `ws-layout-${newId}`;
+
+ if (idChanged) {
+ // Remove old keys from cache
+ const oldKeys = [`ws-layout-${editingId}`, `ws-terminals-${editingId}`, `ws-host-terminals-${editingId}`];
+ const cleaned = { ...currentState };
+ for (const k of oldKeys) delete cleaned[k];
+ queryClient.setQueryData(['WORKSPACES_STATE'], { ...cleaned, [newLayoutKey]: wsLayout });
+
+ // Delete old files, write new layout
+ const patch: Record = { [newLayoutKey]: wsLayout };
+ for (const k of oldKeys) patch[k] = null;
+ client.patch('/user/workspaces-state', patch).catch(() => {});
+ } else {
+ queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [newLayoutKey]: wsLayout });
+ client.patch('/user/workspaces-state', { [newLayoutKey]: wsLayout }).catch(() => {});
+ }
+
+ setEditingId(null);
+ setSelected(newId);
} else {
+ const existingIds = new Set(workspaces.map((w) => w.id));
+ let id = slugify(trimmed) || generateSlug();
+ while (existingIds.has(id)) id = `${id}-${generateSlug(1)}`;
+
const ws: WorkspaceDefinition = {
- id: `ws-${Date.now()}`,
+ id,
name: trimmed,
cwd,
description: desc || undefined,
@@ -318,9 +347,9 @@ const CreatePanel = () => {
const layoutKey = `ws-layout-${ws.id}`;
setWorkspaces((prev) => [...prev, ws]);
- const currentState = queryClient.getQueryData>(['USER_STATE']) ?? {};
- queryClient.setQueryData(['USER_STATE'], { ...currentState, [layoutKey]: wsLayout });
- client.patch('/user/state', { [layoutKey]: wsLayout }).catch(() => {});
+ const currentState = queryClient.getQueryData>(['WORKSPACES_STATE']) ?? {};
+ queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [layoutKey]: wsLayout });
+ client.patch('/user/workspaces-state', { [layoutKey]: wsLayout }).catch(() => {});
setName('');
setDescription('');
@@ -408,15 +437,25 @@ const NewWorkspaceForm = () => (
const WorkspacePreviewEmpty = () => {
const [creating, setCreating] = useGlobal(CREATING_WORKSPACE_KEY, false);
const [editingId] = useGlobal(EDITING_WORKSPACE_KEY, null);
+ const [, setName] = useGlobal(NEW_WS_NAME_KEY, '');
+ const [, setDescription] = useGlobal(NEW_WS_DESC_KEY, '');
+ const [, setTemplateIdx] = useGlobal(NEW_WS_TEMPLATE_KEY, 0);
if (creating || editingId) return ;
+ const handleCreate = () => {
+ setName(generateSlug());
+ setDescription('');
+ setTemplateIdx(0);
+ setCreating(true);
+ };
+
return (
-
-
-
Select a workspace or create a new one
-