Workspaces

This commit is contained in:
2026-02-22 00:05:29 +00:00
parent 9e9acd9631
commit 23b369c7e1
101 changed files with 701 additions and 512 deletions
+12
View File
@@ -101,6 +101,7 @@
"shiki": "^3.22.0",
"sonner": "^2.0.7",
"sounds": "workspace:*",
"state": "workspace:*",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"three": "^0.182.0",
@@ -245,6 +246,7 @@
"version": "0.0.1",
"dependencies": {
"hooks": "workspace:*",
"state": "workspace:*",
"widgets": "workspace:*",
},
},
@@ -252,6 +254,14 @@
"name": "sounds",
"version": "1.0.0",
},
"src/workspaces/state": {
"name": "state",
"version": "0.0.1",
"dependencies": {
"hooks": "workspace:*",
"officerdev": "workspace:*",
},
},
"src/workspaces/types": {
"name": "types",
"version": "0.0.1",
@@ -1989,6 +1999,8 @@
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"state": ["state@workspace:src/workspaces/state"],
"state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="],
"stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="],
+1
View File
@@ -119,6 +119,7 @@
"shiki": "^3.22.0",
"sonner": "^2.0.7",
"sounds": "workspace:*",
"state": "workspace:*",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"three": "^0.182.0",
+2 -4
View File
@@ -2,7 +2,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router';
import * as Authentication from './Screens/Authentication';
import * as Dashboard from './Screens/Dashboard';
import { useAuth } from 'hooks/useAuth';
import { useServerSettings } from '@/state/useServerSettings';
import { useServerSettings } from 'state/useServerSettings';
import { useInitialData } from '@/state/useInitialData';
export function App() {
@@ -51,9 +51,7 @@ 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/new" element={<Dashboard.NewWorkspaceRedirect />} />
<Route path="/workspaces/:id" element={<Dashboard.WorkspaceScreen />} />
<Route path="/workspaces" element={<Dashboard.WorkspaceScreen />} />
<Route path="/projects" element={<Dashboard.ProjectListScreen />} />
<Route path="/projects/new" element={<Dashboard.NewProjectRedirect />} />
<Route path="/projects/:id" element={<Dashboard.ProjectScreen />} />
@@ -1,5 +1,5 @@
import { OfficerSvgLogo } from '@/components/Logos';
import { useLandingPage } from '@/state/useLandingPage';
import { useLandingPage } from 'state/useLandingPage';
import { Bootstrap } from './Bootstrap';
import { Login } from './Login';
@@ -5,8 +5,8 @@ import { useAuth } from 'hooks/useAuth';
import { usePanelChannel } from 'hooks/usePanelChannel';
import type { LayoutNode, PanelComponents } from '@/components/Workspace';
import { WorkspaceLayout } from '@/components/Workspace';
import { useUserState } from '@/state/useUserState';
import { appRegistry } from '../Workspaces/app-registry';
import { useUserState } from 'state/useUserState';
import { AutomationRightPanel } from './AutomationRightPanel';
import type { AutomationSelection } from './AutomationRightPanel';
import { AutomationEditChat } from './AutomationEditChat';
@@ -98,7 +98,7 @@ export const Automation = () => {
return (
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={handleLayoutChange} registry={appRegistry} components={panelComponents} />
<WorkspaceLayout layout={layout} onLayoutChange={handleLayoutChange} components={panelComponents} />
</div>
);
};
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { useLocation } from 'react-router';
import { Trash2 } from 'lucide-react';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useChatSessions } from '@/state/useChatSessions';
import { useChatSessions } from 'state/useChatSessions';
import { usePiChat, EmbeddableChat } from 'officerdev';
export type SelectedSession = {
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { X } from 'lucide-react';
import { useChatGroups } from '@/state/useChatGroups';
import { useChatGroups } from 'state/useChatGroups';
type CreateGroupDialogProps = {
onClose: () => void;
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { MoreVertical, Edit2, Trash2 } from 'lucide-react';
import { useChatGroups } from '@/state/useChatGroups';
import { useChatGroups } from 'state/useChatGroups';
import type { GroupEntry } from 'officerdev';
type GroupContextMenuProps = {
@@ -1,8 +1,8 @@
import { useEffect, useRef, useCallback, useState } from 'react';
import { Plus, MessageSquare, Folder, ChevronRight, FolderPlus } from 'lucide-react';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useChatSessions } from '@/state/useChatSessions';
import { useChatGroups } from '@/state/useChatGroups';
import { useChatSessions } from 'state/useChatSessions';
import { useChatGroups } from 'state/useChatGroups';
import type { SelectedSession } from './ChatDetailPanel';
import { CreateGroupDialog } from './CreateGroupDialog';
import { GroupContextMenu } from './GroupContextMenu';
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { MoreVertical, FolderInput, Edit2, Trash2 } from 'lucide-react';
import { useChatGroups } from '@/state/useChatGroups';
import { useChatSessions } from '@/state/useChatSessions';
import { useChatGroups } from 'state/useChatGroups';
import { useChatSessions } from 'state/useChatSessions';
import type { SessionEntry } from 'officerdev';
type SessionContextMenuProps = {
@@ -1,7 +1,7 @@
import { Link } from 'react-router';
import { MessageSquare, Trash2 } from 'lucide-react';
import { Widget } from 'widgets/Widget';
import { useChatSessions } from '@/state/useChatSessions';
import { useChatSessions } from 'state/useChatSessions';
export const ChatHistory = () => {
const { sessions, deleteSession } = useChatSessions();
@@ -3,8 +3,8 @@ import { useParams } from 'react-router';
import type { LayoutNode, PanelComponents } from '@/components/Workspace';
import { WorkspaceLayout } from '@/components/Workspace';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useChatSessions } from '@/state/useChatSessions';
import { appRegistry } from '../Workspaces/app-registry';
import { useChatSessions } from 'state/useChatSessions';
import { SessionList } from './Screen';
import { ChatDetailPanel, type SelectedSession } from './ChatDetailPanel';
@@ -50,7 +50,7 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => {
return (
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} registry={appRegistry} components={panelComponents} />
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
</div>
);
};
@@ -1,6 +1,6 @@
import { useRef, useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router';
import { useChatSessions } from '@/state/useChatSessions';
import { useChatSessions } from 'state/useChatSessions';
import { SessionBar, EmbeddableChat, type UsePiChatType, type Attachment } from 'officerdev';
import { Card } from '@/components/Card';
@@ -1,8 +1,7 @@
import type { LayoutNode } from '@/components/Workspace';
import { WorkspaceView } from '@/components/Workspace';
import { useWorkspacesState } from '@/state/useWorkspacesState';
import { useWorkspacesState } from 'state/useWorkspacesState';
import { useFileViewerPanels } from 'officerdev';
import { appRegistry } from '../Workspaces/app-registry';
import { defaultLayout } from './defaultLayout';
export const FilesScreen = () => {
@@ -11,7 +10,7 @@ export const FilesScreen = () => {
return (
<div className="h-full w-full">
<WorkspaceView workspace={workspace} registry={appRegistry} ephemeral={ephemeral} />
<WorkspaceView workspace={workspace} ephemeral={ephemeral} />
</div>
);
};
@@ -3,5 +3,5 @@ import type { LayoutNode } from '@/components/Workspace';
export const defaultLayout: LayoutNode = {
type: 'panel',
id: 'files-main',
appType: 'file-browser',
appType: 'officerdev/file-browser',
};
@@ -1,6 +1,6 @@
import type { LayoutNode } from '@/components/Workspace';
import { WorkspaceView } from '@/components/Workspace';
import { useWorkspacesState } from '@/state/useWorkspacesState';
import { appRegistry } from '../Workspaces/app-registry';
import { useWorkspacesState } from 'state/useWorkspacesState';
import { defaultLayout } from './defaultLayout';
export const HomeScreen = () => {
@@ -8,8 +8,7 @@ export const HomeScreen = () => {
return (
<div className="h-full w-full">
<WorkspaceView workspace={workspace} registry={appRegistry} />
<WorkspaceView workspace={workspace} />
</div>
);
};
@@ -120,7 +120,7 @@ export const dockItems: DockItem[] = [
{ label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
{ label: 'Automation', to: '/automation', icon: Bot, color: '#2dd4bf' },
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316', role: 'Super Admin' },
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
{ label: 'Projects', to: '/projects', icon: FolderKanban, color: '#10b981' },
{ label: 'Workspaces', to: '/workspaces', icon: LayoutGrid, color: '#8b5cf6' },
];
@@ -5,7 +5,7 @@ import { User, LogOut, Settings, Package, Sun, Moon } from 'lucide-react';
import { useAuth } from 'hooks/useAuth';
import { useTranslation } from '@/lib/i18n';
import { useColorMode } from '@/components/ui/ThemeProvider';
import { useSettings } from '@/state/useSettings';
import { useSettings } from 'state/useSettings';
const { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } = Dropdown;
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useServerSettings } from '@/state/useServerSettings';
import { useServerSettings } from 'state/useServerSettings';
import { ServerTypeCard } from './ServerTypeCard';
import { AIHarnessesCard } from './AIHarnessesCard';
@@ -5,7 +5,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { useGlobal } from 'hooks/useGlobal';
import { useClient } from 'hooks/useClient';
import { generateSlug } from 'helpers/slug';
import { useProjectsState } from '@/state/useProjectsState';
import { useProjectsState } from 'state/useProjectsState';
import type { ProjectDefinition, ProjectType } from '@/components/Workspace';
import {
AlertDialog,
@@ -4,12 +4,12 @@ import { FolderKanban, Plus, ArrowRight, Type, Rocket, FileText, Layout } from '
import { useQueryClient } from '@tanstack/react-query';
import { useGlobal } from 'hooks/useGlobal';
import { useClient } from 'hooks/useClient';
import { useProjectsState } from '@/state/useProjectsState';
import { useProjectsState } from 'state/useProjectsState';
import { WorkspaceLayout, WorkspaceView, createDefaultLayout } from '@/components/Workspace';
import type { LayoutNode, ProjectDefinition, ProjectType } from '@/components/Workspace';
import { Button } from '@/components/ui/button';
import { generateSlug, slugify } from 'helpers/slug';
import { appRegistry } from '../Workspaces/app-registry';
import { useAppRegistry } from 'officerdev';
import {
SELECTED_PROJECT,
CREATING_PROJECT,
@@ -482,7 +482,7 @@ const TemplatePreviewInner = ({ templateIdx }: { templateIdx: number }) => {
queryClient.setQueryData(PREVIEW_LAYOUT_KEY, next);
};
return <WorkspaceView workspace={null} layout={layout} onLayoutChange={handleLayoutChange} registry={appRegistry} />;
return <WorkspaceView workspace={null} layout={layout} onLayoutChange={handleLayoutChange} />;
};
const TemplatePreviewPanel = () => {
@@ -524,17 +524,18 @@ const newProjLayout: LayoutNode = {
],
};
const newProjRegistry = {
...appRegistry,
'new-proj-details': { name: 'Details', icon: Type, component: DetailsPanel },
'new-proj-placeholder': { name: 'Preview', icon: Layout, component: TemplatePreviewPanel },
'new-proj-template': { name: 'Template', icon: Layout, component: TemplatePanel },
'new-proj-create': { name: 'Create', icon: Rocket, component: CreatePanel },
};
const NewProjectForm = () => {
const { registry } = useAppRegistry();
const newProjRegistry = {
...registry,
'new-proj-details': { name: 'Details', icon: Type, component: DetailsPanel },
'new-proj-placeholder': { name: 'Preview', icon: Layout, component: TemplatePreviewPanel },
'new-proj-template': { name: 'Template', icon: Layout, component: TemplatePanel },
'new-proj-create': { name: 'Create', icon: Rocket, component: CreatePanel },
};
const NewProjectForm = () => (
<WorkspaceLayout layout={newProjLayout} onLayoutChange={() => {}} registry={newProjRegistry} />
);
return <WorkspaceLayout layout={newProjLayout} onLayoutChange={() => {}} registry={newProjRegistry} />;
};
const ProjectPreviewEmpty = () => {
const [creating, setCreating] = useGlobal<boolean>(CREATING_PROJECT, false);
@@ -576,7 +577,7 @@ const ProjectPreviewInner = ({ project }: { project: ProjectDefinition }) => {
const [layout, setLayout] = useProjectsState<LayoutNode>(`proj-layout-${project.id}`, createDefaultLayout());
const workspace = { id: project.id, name: project.name, cwd: project.cwd };
return <WorkspaceView workspace={workspace} layout={layout} onLayoutChange={setLayout} registry={appRegistry} />;
return <WorkspaceView workspace={workspace} layout={layout} onLayoutChange={setLayout} />;
};
const ProjectPreview = () => {
@@ -599,14 +600,14 @@ const ProjectPreview = () => {
);
};
const projHomeRegistry = {
...appRegistry,
'project-list': { name: 'Projects', icon: FolderKanban, component: () => <ProjectListApp /> },
'project-preview': { name: 'Project Preview', icon: FolderKanban, component: ProjectPreview },
};
export const ProjectListScreen = () => {
const [layout, setLayout, isLoaded] = useProjectsState<LayoutNode>('proj-layout-proj-homepage', defaultLayout);
const { registry } = useAppRegistry();
const projHomeRegistry = {
...registry,
'project-list': { name: 'Projects', icon: FolderKanban, component: () => <ProjectListApp /> },
'project-preview': { name: 'Project Preview', icon: FolderKanban, component: ProjectPreview },
};
if (!isLoaded) return null;
@@ -1,8 +1,8 @@
import { useParams, Navigate } from 'react-router';
import { useProjectsState } from '@/state/useProjectsState';
import { useProjectsState } from 'state/useProjectsState';
import { WorkspaceView, createDefaultLayout } from '@/components/Workspace';
import type { LayoutNode, ProjectDefinition } from '@/components/Workspace';
import { appRegistry } from '../Workspaces/app-registry';
export const ProjectScreen = () => {
const { id } = useParams<{ id: string }>();
@@ -25,7 +25,7 @@ const ProjectScreenInner = ({ project }: { project: ProjectDefinition }) => {
workspace={workspace}
layout={layout}
onLayoutChange={setLayout}
registry={appRegistry}
/>
</div>
);
@@ -1,4 +1,4 @@
import { useSettings } from '@/state/useSettings';
import { useSettings } from 'state/useSettings';
import { themes } from 'themes';
export const Appearance = () => {
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { X } from 'lucide-react';
import { Label } from '@/components/ui/label';
import { useSettings } from '@/state/useSettings';
import { useSettings } from 'state/useSettings';
import { useTranslation } from '@/lib/i18n';
const LANGUAGES = [
@@ -11,8 +11,8 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useSettings } from '@/state/useSettings';
import { useVisiblePiModels, type ModelOption } from '@/state/useModels';
import { useSettings } from 'state/useSettings';
import { useVisiblePiModels, type ModelOption } from 'state/useModels';
export const TaskDefaults = () => {
const { settings, saveSettings } = useSettings();
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { User, Lock, Globe, ListChecks } 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 { UserData } from './UserData';
import { ChangePassword } from './ChangePassword';
@@ -46,7 +46,7 @@ export const ProfileSettings = () => {
return (
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} registry={appRegistry} components={panelComponents} />
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
</div>
);
};
@@ -3,7 +3,7 @@ import { Circle, Server, Wrench } from 'lucide-react';
import { useGlobal } from 'hooks/useGlobal';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { useResources, getResourceCategory, type Resource } from '@/state/useResources';
import { useResources, getResourceCategory, type Resource } from 'state/useResources';
type ResourceItemProps = {
resource: Resource;
@@ -15,7 +15,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { useResources, getResourceCategory, type Resource, type PingResult } from '@/state/useResources';
import { useResources, getResourceCategory, type Resource, type PingResult } from 'state/useResources';
import { RUN_COMMAND_CHANNEL, type RunCommandState } from './run-command-channel';
type ConnectionSectionProps = {
@@ -7,7 +7,7 @@ import { WorkspaceLayout } from '@/components/Workspace';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useClient } from 'hooks/useClient';
import { TerminalView, FileViewerView } from 'officerdev';
import { appRegistry } from '../../Workspaces/app-registry';
import { Resources } from './Resources';
import { ResourceSidebar } from './ResourceSidebar';
import {
@@ -174,7 +174,7 @@ export const ResourceSettings = () => {
return (
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} registry={appRegistry} components={panelComponents} />
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
</div>
);
};
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { Switch } from '@/components/ui/switch';
import { useClient } from 'hooks/useClient';
import { useServerSettings } from '@/state/useServerSettings';
import { useServerSettings } from 'state/useServerSettings';
type PluginInfo = {
id: string;
@@ -12,12 +12,12 @@ import type { LayoutNode, PanelComponents } from '@/components/Workspace';
import { WorkspaceLayout } from '@/components/Workspace';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { TerminalView } from 'officerdev';
import { appRegistry } from '../Workspaces/app-registry';
import { createSettingsPanelComponents, type SettingsSectionGroup } from './SettingsPanel';
import { useSettings } from '@/state/useSettings';
import { useUserState } from '@/state/useUserState';
import { usePiModels, useVisiblePiModels, type ModelOption } from '@/state/useModels';
import type { UserSettings } from '@/state/types/user-settings';
import { useSettings } from 'state/useSettings';
import { useUserState } from 'state/useUserState';
import { usePiModels, useVisiblePiModels, type ModelOption } from 'state/useModels';
import type { UserSettings } from 'state/useSettings';
import { AIHarnessesSection } from './ServerSettings/AIHarnessesSection';
import { RUN_COMMAND_CHANNEL, type RunCommandState } from './ServerSettings/run-command-channel';
@@ -158,7 +158,7 @@ export const SystemSettings = () => {
return (
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} registry={appRegistry} components={panelComponents} />
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
</div>
);
};
@@ -1,20 +1,14 @@
import { Navigate } from 'react-router';
import { useAuth } from 'hooks/useAuth';
import type { LayoutNode } from '@/components/Workspace';
import { WorkspaceView } from '@/components/Workspace';
import { useWorkspacesState } from '@/state/useWorkspacesState';
import { appRegistry } from '../Workspaces/app-registry';
import { useWorkspacesState } from 'state/useWorkspacesState';
import { defaultLayout } from './defaultLayout';
export const TerminalScreen = () => {
const { user } = useAuth();
const workspace = useWorkspacesState<LayoutNode>('screens/terminal', defaultLayout);
if (user?.role !== 'Super Admin') return <Navigate to="/" replace />;
return (
<div className="h-full w-full">
<WorkspaceView workspace={workspace} registry={appRegistry} />
<WorkspaceView workspace={workspace} />
</div>
);
};
@@ -3,5 +3,5 @@ import type { LayoutNode } from '@/components/Workspace';
export const defaultLayout: LayoutNode = {
type: 'panel',
id: 'terminal-screen',
appType: 'terminal-host',
appType: 'officerdev/terminal',
};
@@ -1,26 +1,14 @@
import { useParams, Navigate } from 'react-router';
import { useWorkspacesState } from '@/state/useWorkspacesState';
import { WorkspaceView, createDefaultLayout } from '@/components/Workspace';
import type { LayoutNode, WorkspaceDefinition } from '@/components/Workspace';
import { appRegistry } from './app-registry';
import type { LayoutNode } from '@/components/Workspace';
import { WorkspaceView } from '@/components/Workspace';
import { useWorkspacesState } from 'state/useWorkspacesState';
import { defaultLayout } from './defaultLayout';
export const WorkspaceScreen = () => {
const { id } = useParams<{ id: string }>();
const { value: workspaces, isLoaded } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
const workspace = workspaces.find((ws) => ws.id === id);
if (!isLoaded) return null;
if (!workspace) return <Navigate to="/workspaces" replace />;
return <WorkspaceScreenInner workspace={workspace} />;
};
const WorkspaceScreenInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
const ws = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
const workspace = useWorkspacesState<LayoutNode>('screens/workspaces', defaultLayout);
return (
<div className="h-full w-full">
<WorkspaceView workspace={ws} registry={appRegistry} cwd={workspace.cwd} />
<WorkspaceView workspace={workspace} />
</div>
);
};
@@ -1,250 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import type { ReactNode } from '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 type { AppRegistry } from '@/components/Workspace';
import { useWorkspace } from '@/components/Workspace';
import { CodeEditorView, TerminalView, FileViewerProvider, FileViewerHeader, FileViewerBody, EmbeddableChat, ChatLauncher, usePiChat, FileBrowserApp } from 'officerdev';
import { useWorkspacesState } from '@/state/useWorkspacesState';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useVisiblePiModels } from '@/state/useModels';
import { ChatHistoryApp as ChatHistory } from '../ChatHistory';
import { Catalog } from 'sounds';
import { WorkspaceListApp } from './WorkspaceListApp';
import { ProjectListApp } from '../Projects/ProjectListApp';
import { widgetRegistry } from 'widgets/widget-registry';
import { WidgetPanel } from 'widgets/WidgetPanel';
const ChatWidget = () => <EmbeddableChat className="h-full" />;
const ChatLauncherWidget = () => {
const pi = usePiChat();
const models = useVisiblePiModels();
console.log(models)
return (
<ChatLauncher
availableModels={models}
selectedModel={pi.selectedModel}
onModelChange={pi.setSelectedModel}
onSubmit={({ prompt, model, attachmentIds, images }) => pi.sendPrompt(prompt, attachmentIds, images)}
/>
);
};
const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1));
const FileBrowserWrapper = () => {
const { cwd } = useWorkspace();
const basePath = cwdToPath(cwd);
return <FileBrowserApp basePath={basePath} />;
};
const CodeEditorWrapper = () => <CodeEditorView className="h-full w-full" />;
const TerminalHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<TerminalSquare className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Terminal</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
const HostTerminalHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<Monitor className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Terminal</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
const CodeEditorHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<Code className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Code Editor</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
const EMPTY_TERMINALS: Record<string, string> = {};
const TerminalWrapper = ({ panelId }: { panelId: string }) => {
const { workspaceId, cwd } = useWorkspace();
const stateKey = workspaceId ? `ws-terminals-${workspaceId}` : 'ws-terminals-default';
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
const setTerminalsRef = useRef(setTerminals);
setTerminalsRef.current = setTerminals;
const sessionId = terminals[panelId];
useEffect(() => {
if (!sessionId) {
setTerminals((prev) => ({ ...prev, [panelId]: crypto.randomUUID() }));
}
}, [panelId, sessionId, setTerminals]);
useEffect(() => {
return () => {
setTerminalsRef.current((prev) => {
const { [panelId]: _, ...rest } = prev;
return rest;
});
};
}, [panelId]);
if (!sessionId) return null;
return <TerminalView className="h-full w-full p-2" sessionId={sessionId} cwd={cwd} />;
};
const HostTerminalWrapper = ({ panelId }: { panelId: string }) => {
const { user } = useAuth();
const { workspaceId, cwd } = useWorkspace();
const stateKey = workspaceId ? `ws-host-terminals-${workspaceId}` : 'ws-host-terminals-default';
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
const setTerminalsRef = useRef(setTerminals);
setTerminalsRef.current = setTerminals;
const sessionId = terminals[panelId];
useEffect(() => {
if (!sessionId) {
setTerminals((prev) => ({ ...prev, [panelId]: crypto.randomUUID() }));
}
}, [panelId, sessionId, setTerminals]);
useEffect(() => {
return () => {
setTerminalsRef.current((prev) => {
const { [panelId]: _, ...rest } = prev;
return rest;
});
};
}, [panelId]);
if (user?.role !== 'Super Admin') {
return (
<div className="flex h-full w-full items-center justify-center text-sm text-muted-foreground">
Host Terminal requires Super Admin permissions.
</div>
);
}
if (!sessionId) return null;
return <TerminalView className="h-full w-full p-2" sessionId={sessionId} sandboxed={false} cwd={cwd} />;
};
const CommandTerminalWrapper = ({ panelId, command, statePrefix }: { panelId: string; command: string; statePrefix: string }) => {
const { workspaceId, cwd } = useWorkspace();
const stateKey = workspaceId ? `ws-${statePrefix}-${workspaceId}` : `ws-${statePrefix}-default`;
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
const setTerminalsRef = useRef(setTerminals);
setTerminalsRef.current = setTerminals;
const sessionId = terminals[panelId];
useEffect(() => {
if (!sessionId) {
setTerminals((prev) => ({ ...prev, [panelId]: crypto.randomUUID() }));
}
}, [panelId, sessionId, setTerminals]);
useEffect(() => {
return () => {
setTerminalsRef.current((prev) => {
const { [panelId]: _, ...rest } = prev;
return rest;
});
};
}, [panelId]);
if (!sessionId) return null;
const fullCommand = cwd && cwd !== '~' ? `cd ${cwd} && ${command}` : command;
return <TerminalView className="h-full w-full p-2" sessionId={sessionId} cwd={cwd} initialInput={fullCommand} />;
};
const TmuxWrapper = ({ panelId }: { panelId: string }) => (
<CommandTerminalWrapper panelId={panelId} command="tmux" statePrefix="tmux" />
);
const NvimWrapper = ({ panelId }: { panelId: string }) => (
<CommandTerminalWrapper panelId={panelId} command="nvim" statePrefix="nvim" />
);
const TmuxHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<Columns2 className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Tmux</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
const NvimHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<PenLine className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Neovim</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
type FileViewerChannelState = {
filePath: string;
fileName: string;
root?: string;
} | null;
const FILE_VIEWER_CHANNEL = 'file-viewer';
const FileViewerWorkspaceProvider = ({ panelId, children }: { panelId: string; children: ReactNode }) => {
const [state] = usePanelChannel<FileViewerChannelState>(`${FILE_VIEWER_CHANNEL}:${panelId}`, null);
if (!state) {
return (
<div className="flex h-full w-full items-center justify-center text-duck-dark/30 text-sm">
No file selected
</div>
);
}
return (
<FileViewerProvider filePath={state.filePath} fileName={state.fileName} root={state.root}>
{children}
</FileViewerProvider>
);
};
export const appRegistry: AppRegistry = {
'chat': { name: 'Chat', icon: MessageSquare, component: ChatWidget },
'file-browser': { name: 'File Browser', icon: FolderOpen, component: FileBrowserWrapper },
'chat-history': { name: 'Chat History', icon: History, component: () => <ChatHistory />, availableOnPanel: false },
'sound-library': { name: 'Sound Library', icon: Music, component: () => <Catalog />, availableOnPanel: false },
'code-editor': { name: 'Code Editor', icon: Code, component: CodeEditorWrapper, header: CodeEditorHeader },
'terminal': { name: 'Terminal', icon: TerminalSquare, component: TerminalWrapper, header: TerminalHeader },
'tmux': { name: 'Tmux', icon: Columns2, component: TmuxWrapper, header: TmuxHeader },
'nvim': { name: 'Neovim', icon: PenLine, component: NvimWrapper, header: NvimHeader },
'terminal-host': { name: 'Host Terminal', icon: Monitor, component: HostTerminalWrapper, header: HostTerminalHeader, availableOnPanel: false },
'workspace-list': { name: 'Workspaces', icon: LayoutGrid, component: () => <WorkspaceListApp />, availableOnPanel: false },
'project-list': { name: 'Projects', icon: FolderKanban, component: () => <ProjectListApp />, availableOnPanel: false },
'file-viewer': { name: 'File Viewer', icon: Eye, component: FileViewerBody, header: FileViewerHeader, provider: FileViewerWorkspaceProvider, availableOnPanel: false },
'chat-launcher': { name: 'Chat Launcher', icon: Sparkles, component: ChatLauncherWidget, fixedHeight: 180, availableOnPanel: false },
'widget-panel': { name: 'Widget Panel', icon: LayoutDashboard, component: WidgetPanel, transparent: true },
...widgetRegistry,
};
@@ -0,0 +1,11 @@
import type { LayoutNode } from '@/components/Workspace';
export const defaultLayout: LayoutNode = {
type: 'group',
id: 'ws-home-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'ws-home-list', appType: 'workspace-list' }, size: 50 },
{ node: { type: 'panel', id: 'ws-home-right', appType: 'workspace-preview' }, size: 50 },
],
};
@@ -1,2 +1 @@
export { WorkspaceListScreen, NewWorkspaceRedirect } from './WorkspaceListScreen';
export { WorkspaceScreen } from './WorkspaceScreen';
+2
View File
@@ -1,6 +1,7 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import { AppRegistry } from 'officerdev';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ColorModeProvider } from '@/components/ui/ThemeProvider';
import { I18nBridge } from '@/lib/I18nBridge';
@@ -39,6 +40,7 @@ const app = (
<TooltipProvider>
<Toaster />
<Sonner position="top-center" toastOptions={{ style: { padding: '16px 20px', fontSize: '16px' } }} />
<AppRegistry />
<App />
</TooltipProvider>
</I18nBridge>
+1 -1
View File
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
import type { TranslationMap } from 'i18n';
import { I18nProvider } from 'i18n';
import { useSettings } from '@/state/useSettings';
import { useSettings } from 'state/useSettings';
import en from '../locales/en.json';
import pt from '../locales/pt.json';
+3 -3
View File
@@ -1,6 +1,6 @@
import { useChatSessions } from '@/state/useChatSessions';
import { usePlans } from './usePlans';
import { useSettings } from './useSettings';
import { useChatSessions } from 'state/useChatSessions';
import { usePlans } from 'state/usePlans';
import { useSettings } from 'state/useSettings';
import { useColorModeSync } from './useThemeSync';
export const useInitialData = () => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useColorMode } from '@/components/ui/ThemeProvider';
import { useSettings } from './useSettings';
import { useSettings } from 'state/useSettings';
export const useColorModeSync = () => {
const { settings } = useSettings();
@@ -6,7 +6,7 @@ type AppPickerProps = {
};
export const AppPicker = ({ registry, onSelect }: AppPickerProps) => {
const entries = Object.entries(registry).filter(([, entry]) => !entry.widget && entry.availableOnPanel !== false);
const entries = Object.entries(registry).filter(([, entry]) => entry.availableOnPanel !== false);
return (
<div className="flex flex-wrap gap-1.5 max-w-md">
@@ -3,11 +3,12 @@ import type { LayoutNode, AppRegistry, PanelComponents } from './types';
import { updateSizes } from './layout-utils';
import { WorkspaceProvider } from './WorkspaceContext';
import { WorkspaceRenderer } from './WorkspaceRenderer';
import { useAppRegistry } from 'officerdev';
type WorkspaceLayoutProps = {
layout: LayoutNode;
onLayoutChange: (layout: LayoutNode) => void;
registry: AppRegistry;
registry?: AppRegistry;
components?: PanelComponents;
workspaceId?: string;
cwd?: string;
@@ -15,7 +16,10 @@ type WorkspaceLayoutProps = {
const noop = () => {};
export const WorkspaceLayout = ({ layout, onLayoutChange, registry, components, workspaceId, cwd }: WorkspaceLayoutProps) => {
export const WorkspaceLayout = ({ layout, onLayoutChange, registry: registryProp, components, workspaceId, cwd }: WorkspaceLayoutProps) => {
const { registry: globalRegistry } = useAppRegistry();
const registry = registryProp ?? globalRegistry;
const handleResized = useCallback(
(groupId: string, sizes: number[]) => {
onLayoutChange(updateSizes(layout, groupId, sizes));
@@ -1,25 +1,26 @@
import { useState, useCallback, useEffect } from 'react';
import { flushSync } from 'react-dom';
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '../ui/resizable';
import type { LayoutNode, AppRegistry, WorkspaceState, EphemeralPanels } from './types';
import type { LayoutNode, WorkspaceState, EphemeralPanels } from './types';
import type { DropPosition } from './layout-utils';
import { splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, countPanels } from './layout-utils';
import { WorkspaceProvider } from './WorkspaceContext';
import { WorkspaceRenderer } from './WorkspaceRenderer';
import { useAppRegistry } from 'officerdev';
type WorkspaceViewProps = {
workspace: WorkspaceState;
registry: AppRegistry;
cwd?: string;
ephemeral?: EphemeralPanels | null;
};
const noop = () => {};
export const WorkspaceView = ({ workspace, registry, cwd = '~', ephemeral }: WorkspaceViewProps) => {
if (!workspace.isLoaded) return null;
export const WorkspaceView = ({ workspace, cwd = '~', ephemeral }: WorkspaceViewProps) => {
const { registry } = useAppRegistry();
const { value: layout, setValue: onLayoutChange } = workspace;
const layout = workspace.value;
const onLayoutChange = workspace.setValue;
const [swapSourceId, setSwapSourceId] = useState<string | null>(null);
const [dragSourceId, setDragSourceId] = useState<string | null>(null);
const [maximizedPanelId, setMaximizedPanelId] = useState<string | null>(null);
@@ -105,6 +106,8 @@ export const WorkspaceView = ({ workspace, registry, cwd = '~', ephemeral }: Wor
return () => window.removeEventListener('keydown', onKeyDown);
}, [swapSourceId, dragSourceId, maximizedPanelId, setMaximizedAnimated]);
if (!workspace.isLoaded) return null;
const baseRenderer = (
<WorkspaceRenderer
layout={layout}
@@ -53,7 +53,6 @@ export type AppRegistryEntry = {
provider?: ComponentType<{ panelId: string; children: ReactNode }>;
transparent?: boolean;
fixedHeight?: number;
widget?: boolean;
availableOnPanel?: boolean;
};
+1
View File
@@ -9,6 +9,7 @@
},
"dependencies": {
"hooks": "workspace:*",
"state": "workspace:*",
"widgets": "workspace:*"
}
}
@@ -0,0 +1,14 @@
import { appRegistryMetas as fileBrowserMetas } from '../apps/FileBrowser';
import { appRegistryMetas as terminalMetas } from '../apps/Terminal';
import { appRegistryMetas as codeEditorMetas } from '../apps/CodeEditor';
import { appRegistryMetas as chatMetas } from '../apps/Chat';
import { appRegistryMetas as fileViewerMetas } from '../apps/FileViewer';
import { appRegistryMetas as workspaceMetas } from '../apps/Workspaces';
import { useAppRegistry } from './useAppRegistry';
const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...workspaceMetas];
export const AppRegistry = () => {
useAppRegistry(apps);
return null;
};
@@ -0,0 +1,2 @@
export * from './AppRegistry';
export * from './useAppRegistry';
@@ -0,0 +1 @@
export { useAppRegistry, type UseAppRegistryType, type AppRegistryMeta } from './useAppRegistry';
@@ -0,0 +1,19 @@
import type { AppRegistry, AppRegistryEntry } from '@/components/Workspace';
import { useGlobal } from 'hooks/useGlobal';
export type AppRegistryMeta = { key: string } & AppRegistryEntry;
export function useAppRegistry(initialApps: AppRegistryMeta[] = []) {
const [registry, setRegistry] = useGlobal<AppRegistry>('APP_REGISTRY', () => metasToRegistry(initialApps));
const registerApp = (key: string, entry: AppRegistryEntry) => {
setRegistry((prev) => ({ ...prev, [key]: entry }));
};
return { registry, registerApp };
}
export type UseAppRegistryType = ReturnType<typeof useAppRegistry>;
const metasToRegistry = (metas: AppRegistryMeta[]): AppRegistry =>
Object.fromEntries(metas.map(({ key, ...entry }) => [key, entry]));
@@ -1,5 +1,5 @@
import { MessageSquare } from 'lucide-react';
import { useChatSessions } from '@/state/useChatSessions';
import { useChatSessions } from 'state/useChatSessions';
export const ChatList = () => {
const { sessions } = useChatSessions();
@@ -0,0 +1,3 @@
import { EmbeddableChat } from './EmbeddableChat';
export const ChatPanelWrapper = () => <EmbeddableChat className="h-full" />;
@@ -1,6 +1,6 @@
import type { KeyboardEvent } from 'react';
import { useState, useRef, useEffect } from 'react';
import { useVisiblePiModels } from '@/state/useModels';
import { useVisiblePiModels } from 'state/useModels';
import { usePiChat, type UsePiChatType } from '../../../hooks/usePiChat';
import { useAttachments } from '../useAttachments';
import { useSlashCommands } from '../useSlashCommands';
@@ -1,5 +1,5 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { ModelOption } from '@/state/useModels';
import type { ModelOption } from 'state/useModels';
import type { ChatMessage } from '../types';
const PROVIDER_DISPLAY: Record<string, string> = {
@@ -1,3 +1,7 @@
import type { AppRegistryMeta } from '../../AppRegistry';
import { MessageSquare } from 'lucide-react';
import { ChatPanelWrapper } from './ChatPanelWrapper';
export { MessageList } from './components/MessageList';
export { MessageBubble, StreamingBubble } from './components/MessageBubble';
export { ToolActivity } from './components/ToolActivity';
@@ -18,3 +22,12 @@ export { useAttachments, type UseAttachmentsType } from './useAttachments';
export { useAudioRecording, type UseAudioRecordingType } from './useAudioRecording';
export * from './types';
export const appRegistryMetas: AppRegistryMeta[] = [
{
key: 'officerdev/chat',
name: 'Chat',
icon: MessageSquare,
component: ChatPanelWrapper,
},
];
@@ -0,0 +1,13 @@
import { Code } from 'lucide-react';
import { useWorkspace } from '@/components/Workspace';
export const CodeEditorHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<Code className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Code Editor</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
@@ -0,0 +1,3 @@
import { CodeEditorView } from './CodeEditor';
export const CodeEditorPanelWrapper = () => <CodeEditorView className="h-full w-full" />;
@@ -1 +1,16 @@
import type { AppRegistryMeta } from '../../AppRegistry';
import { Code } from 'lucide-react';
import { CodeEditorPanelWrapper } from './CodeEditorPanelWrapper';
import { CodeEditorHeader } from './CodeEditorHeader';
export { CodeEditorView } from './CodeEditor';
export const appRegistryMetas: AppRegistryMeta[] = [
{
key: 'officerdev/code-editor',
name: 'Code Editor',
icon: Code,
component: CodeEditorPanelWrapper,
header: CodeEditorHeader,
},
];
@@ -13,24 +13,20 @@ type FileBrowserAppProps = {
export const FileBrowserApp = ({ basePath = '/' }: FileBrowserAppProps) => {
const fileBrowserManager = useFileBrowserApp(basePath);
const { handleNavigate } = fileBrowserManager;
return (
<div className="flex flex-col h-full overflow-hidden">
<Toolbar fileBrowserManager={fileBrowserManager} />
<HomeDirSelector fileBrowserManager={fileBrowserManager} basePath={basePath} />
{/* Breadcrumb */}
<div className="shrink-0 border-b border-duck-dark/10 px-4 py-2">
<Breadcrumb path={fileBrowserManager.currentPath} onNavigate={fileBrowserManager.handleNavigate} basePath={basePath} />
</div>
<UploadProgress fileBrowserManager={fileBrowserManager} />
<FileViewContainer fileBrowserManager={fileBrowserManager} />
<TaskRunnerDialog fileBrowserManager={fileBrowserManager} />
<VideoDownloadDialog fileBrowserManager={fileBrowserManager} />
</div>
<div className="flex flex-col h-full overflow-hidden">
<Toolbar fileBrowserManager={fileBrowserManager} />
<HomeDirSelector fileBrowserManager={fileBrowserManager} basePath={basePath} />
<Breadcrumb
path={fileBrowserManager.currentPath}
onNavigate={handleNavigate} basePath={basePath} />
<UploadProgress fileBrowserManager={fileBrowserManager} />
<FileViewContainer fileBrowserManager={fileBrowserManager} />
<TaskRunnerDialog fileBrowserManager={fileBrowserManager} />
<VideoDownloadDialog fileBrowserManager={fileBrowserManager} />
</div>
);
};
@@ -0,0 +1,9 @@
import { useWorkspace } from '@/components/Workspace';
import { FileBrowserApp } from './FileBrowserApp';
const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1));
export const FileBrowserPanelWrapper = () => {
const { cwd } = useWorkspace();
return <FileBrowserApp basePath={cwdToPath(cwd)} />;
};
@@ -11,36 +11,38 @@ export const Breadcrumb = ({ path, onNavigate, basePath = '/' }: BreadcrumbProps
const segments = relativePath.split('/').filter(Boolean);
return (
<nav className="flex items-center gap-1 text-sm flex-wrap">
<button
onClick={() => onNavigate(basePath)}
className="flex items-center gap-1 text-duck-forest hover:text-duck-teal transition-colors cursor-pointer font-medium"
>
<Home className="h-4 w-4" />
<span>{basePath === '/' ? 'home' : basePath.split('/').pop()}</span>
</button>
<div className="shrink-0 border-b border-duck-dark/10 px-4 py-2">
<nav className="flex items-center gap-1 text-sm flex-wrap">
<button
onClick={() => onNavigate(basePath)}
className="flex items-center gap-1 text-duck-forest hover:text-duck-teal transition-colors cursor-pointer font-medium"
>
<Home className="h-4 w-4" />
<span>{basePath === '/' ? 'home' : basePath.split('/').pop()}</span>
</button>
{segments.map((segment, i) => {
const relative = '/' + segments.slice(0, i + 1).join('/');
const segmentPath = basePath === '/' ? relative : basePath + relative;
const isLast = i === segments.length - 1;
{segments.map((segment, i) => {
const relative = '/' + segments.slice(0, i + 1).join('/');
const segmentPath = basePath === '/' ? relative : basePath + relative;
const isLast = i === segments.length - 1;
return (
<span key={segmentPath} className="flex items-center gap-1">
<ChevronRight className="h-4 w-4 text-duck-dark/40" />
{isLast ? (
<span className="text-duck-dark font-semibold">{segment}</span>
) : (
<button
onClick={() => onNavigate(segmentPath)}
className="text-duck-forest hover:text-duck-teal transition-colors cursor-pointer font-medium"
>
{segment}
</button>
)}
</span>
);
})}
</nav>
return (
<span key={segmentPath} className="flex items-center gap-1">
<ChevronRight className="h-4 w-4 text-duck-dark/40" />
{isLast ? (
<span className="text-duck-dark font-semibold">{segment}</span>
) : (
<button
onClick={() => onNavigate(segmentPath)}
className="text-duck-forest hover:text-duck-teal transition-colors cursor-pointer font-medium"
>
{segment}
</button>
)}
</span>
);
})}
</nav>
</div>
);
};
@@ -5,7 +5,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cardStyle } from '@/components/Card';
import type { TaskInfo } from '../../../Chat';
import { usePiChat, EmbeddableChat } from '../../../Chat';
import { useSettings } from '@/state/useSettings';
import { useSettings } from 'state/useSettings';
import type { TaskSummary } from '../../useTasks';
const playDing = () => {
@@ -1 +1,2 @@
export * from './FileBrowserApp';
export * from './FileBrowserPanelWrapper';
@@ -3,7 +3,7 @@ import { useSearchParams, useNavigate } from 'react-router';
import { toast } from 'sonner';
import { useFilesAPI, type DirEntry } from '../../../hooks/useFilesAPI';
import { useTasks, type TaskSummary } from '../useTasks';
import { useUserState } from '@/state/useUserState';
import { useUserState } from 'state/useUserState';
import { useAuth } from 'hooks/useAuth';
export const useFileBrowserApp = (basePath: string) => {
@@ -1,7 +1,28 @@
import type { AppRegistryMeta } from '../../AppRegistry';
import { FolderOpen } from 'lucide-react';
import { FileBrowserPanelWrapper } from './FileBrowserApp';
import { FileBrowserWidget } from './FileBrowserWidget';
export { useFilesAPI, type DirEntry } from '../../hooks/useFilesAPI';
export { useTasks, type TaskSummary } from './useTasks';
export { useRecentFiles } from './useRecentFiles';
export { usePinnedFiles } from './usePinnedFiles';
export { FileBrowserApp } from './FileBrowserApp';
export { FileBrowserApp, FileBrowserPanelWrapper } from './FileBrowserApp';
export { FileBrowserWidget } from './FileBrowserWidget';
export { TaskRunnerModal } from './FileBrowserApp/components/TaskRunnerModal';
export const appRegistryMetas: AppRegistryMeta[] = [
{
key: 'officerdev/file-browser',
name: 'File Browser',
icon: FolderOpen,
component: FileBrowserPanelWrapper,
},
{
key: 'officerdev/file-browser-widget',
name: 'File Browser',
icon: FolderOpen,
component: FileBrowserWidget,
availableOnPanel: false,
},
];
@@ -1,4 +1,4 @@
import { useUserState } from '@/state/useUserState';
import { useUserState } from 'state/useUserState';
type PinnedFile = { path: string; name: string; pinnedAt: number };
@@ -1,4 +1,4 @@
import { useUserState } from '@/state/useUserState';
import { useUserState } from 'state/useUserState';
type RecentFile = { path: string; name: string; openedAt: number };
@@ -0,0 +1,29 @@
import type { ReactNode } from 'react';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { FileViewerProvider } from './FileViewerProvider';
type FileViewerChannelState = {
filePath: string;
fileName: string;
root?: string;
} | null;
const FILE_VIEWER_CHANNEL = 'file-viewer';
export const FileViewerPanelProvider = ({ panelId, children }: { panelId: string; children: ReactNode }) => {
const [state] = usePanelChannel<FileViewerChannelState>(`${FILE_VIEWER_CHANNEL}:${panelId}`, null);
if (!state) {
return (
<div className="flex h-full w-full items-center justify-center text-duck-dark/30 text-sm">
No file selected
</div>
);
}
return (
<FileViewerProvider filePath={state.filePath} fileName={state.fileName} root={state.root}>
{children}
</FileViewerProvider>
);
};
@@ -1,6 +1,25 @@
import type { AppRegistryMeta } from '../../AppRegistry';
import { Eye } from 'lucide-react';
import { FileViewerBody } from './FileViewerBody';
import { FileViewerHeader } from './FileViewerHeader';
import { FileViewerPanelProvider } from './FileViewerPanelWrapper';
export { FileViewerView } from './FileViewerView';
export { FileViewerProvider } from './FileViewerProvider';
export { FileViewerHeader } from './FileViewerHeader';
export { FileViewerBody } from './FileViewerBody';
export { FileViewerPanelProvider } from './FileViewerPanelWrapper';
export { useFileViewer } from './FileViewerContext';
export { getFileType, getLang, getExt, getArchiveBaseName, ARCHIVE_EXTS, type FileType } from './file-types';
export const appRegistryMetas: AppRegistryMeta[] = [
{
key: 'officerdev/file-viewer',
name: 'File Viewer',
icon: Eye,
component: FileViewerBody,
header: FileViewerHeader,
provider: FileViewerPanelProvider,
availableOnPanel: false,
},
];
@@ -0,0 +1,43 @@
import { useEffect, useRef } from 'react';
import { useWorkspace } from '@/components/Workspace';
import { useWorkspacesState } from 'state/useWorkspacesState';
import { TerminalView } from './Terminal';
const EMPTY_TERMINALS: Record<string, string> = {};
type CommandTerminalWrapperProps = {
panelId: string;
command: string;
statePrefix: string;
};
export const CommandTerminalWrapper = ({ panelId, command, statePrefix }: CommandTerminalWrapperProps) => {
const { workspaceId, cwd } = useWorkspace();
const stateKey = workspaceId ? `ws-${statePrefix}-${workspaceId}` : `ws-${statePrefix}-default`;
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
const setTerminalsRef = useRef(setTerminals);
setTerminalsRef.current = setTerminals;
const sessionId = terminals[panelId];
useEffect(() => {
if (!sessionId) {
setTerminals((prev) => ({ ...prev, [panelId]: crypto.randomUUID() }));
}
}, [panelId, sessionId, setTerminals]);
useEffect(() => {
return () => {
setTerminalsRef.current((prev) => {
const { [panelId]: _, ...rest } = prev;
return rest;
});
};
}, [panelId]);
if (!sessionId) return null;
const fullCommand = cwd && cwd !== '~' ? `cd ${cwd} && ${command}` : command;
return <TerminalView className="h-full w-full p-2" sessionId={sessionId} cwd={cwd} initialInput={fullCommand} />;
};
@@ -0,0 +1,62 @@
import { TerminalSquare, Monitor, Columns2, PenLine } from 'lucide-react';
import { useWorkspace } from '@/components/Workspace';
import { useAuth } from 'hooks/useAuth';
import { useTerminalMode } from './useTerminalMode';
export const TerminalHeader = ({ panelId }: { panelId: string }) => {
const { cwd } = useWorkspace();
const { user } = useAuth();
const { mode, toggle } = useTerminalMode(panelId);
const isHost = mode === 'host';
const Icon = isHost ? Monitor : TerminalSquare;
return (
<>
<Icon className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Terminal</span>
{user?.role === 'Super Admin' && (
<button
type="button"
onClick={toggle}
className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-white/10 hover:bg-white/20 transition-colors cursor-pointer shrink-0"
>
{isHost ? 'Host' : 'Home'}
</button>
)}
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
export const HostTerminalHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<Monitor className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Terminal</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
export const TmuxHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<Columns2 className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Tmux</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
export const NvimHeader = () => {
const { cwd } = useWorkspace();
return (
<>
<PenLine className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Neovim</span>
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
};
@@ -0,0 +1,45 @@
import { useEffect, useRef } from 'react';
import { useWorkspace } from '@/components/Workspace';
import { useAuth } from 'hooks/useAuth';
import { useWorkspacesState } from 'state/useWorkspacesState';
import { TerminalView } from './Terminal';
const EMPTY_TERMINALS: Record<string, string> = {};
export const HostTerminalWrapper = ({ panelId }: { panelId: string }) => {
const { user } = useAuth();
const { workspaceId, cwd } = useWorkspace();
const stateKey = workspaceId ? `ws-host-terminals-${workspaceId}` : 'ws-host-terminals-default';
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
const setTerminalsRef = useRef(setTerminals);
setTerminalsRef.current = setTerminals;
const sessionId = terminals[panelId];
useEffect(() => {
if (!sessionId) {
setTerminals((prev) => ({ ...prev, [panelId]: crypto.randomUUID() }));
}
}, [panelId, sessionId, setTerminals]);
useEffect(() => {
return () => {
setTerminalsRef.current((prev) => {
const { [panelId]: _, ...rest } = prev;
return rest;
});
};
}, [panelId]);
if (user?.role !== 'Super Admin') {
return (
<div className="flex h-full w-full items-center justify-center text-sm text-muted-foreground">
Host Terminal requires Super Admin permissions.
</div>
);
}
if (!sessionId) return null;
return <TerminalView className="h-full w-full p-2" sessionId={sessionId} sandboxed={false} cwd={cwd} />;
};
@@ -0,0 +1,38 @@
import { useEffect, useRef } from 'react';
import { useWorkspace } from '@/components/Workspace';
import { useWorkspacesState } from 'state/useWorkspacesState';
import { TerminalView } from './Terminal';
import { useTerminalMode } from './useTerminalMode';
const EMPTY_TERMINALS: Record<string, string> = {};
export const TerminalWrapper = ({ panelId }: { panelId: string }) => {
const { workspaceId, cwd } = useWorkspace();
const { mode } = useTerminalMode(panelId);
const sandboxed = mode === 'sandboxed';
const stateKey = workspaceId ? `ws-terminals-${mode}-${workspaceId}` : `ws-terminals-${mode}-default`;
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
const setTerminalsRef = useRef(setTerminals);
setTerminalsRef.current = setTerminals;
const sessionId = terminals[panelId];
useEffect(() => {
if (!sessionId) {
setTerminals((prev) => ({ ...prev, [panelId]: crypto.randomUUID() }));
}
}, [panelId, sessionId, setTerminals]);
useEffect(() => {
return () => {
setTerminalsRef.current((prev) => {
const { [panelId]: _, ...rest } = prev;
return rest;
});
};
}, [panelId]);
if (!sessionId) return null;
return <TerminalView className="h-full w-full p-2" sessionId={sessionId} sandboxed={sandboxed} cwd={cwd} />;
};
@@ -1 +0,0 @@
export { TerminalView, type TerminalViewProps } from './Terminal';
@@ -0,0 +1,48 @@
import type { AppRegistryMeta } from '../../AppRegistry';
import { TerminalSquare, Monitor, Columns2, PenLine } from 'lucide-react';
import { TerminalWrapper } from './TerminalWrapper';
import { HostTerminalWrapper } from './HostTerminalWrapper';
import { CommandTerminalWrapper } from './CommandTerminalWrapper';
import { TerminalHeader, HostTerminalHeader, TmuxHeader, NvimHeader } from './Headers';
export { TerminalView, type TerminalViewProps } from './Terminal';
const TmuxWrapper = ({ panelId }: { panelId: string }) => (
<CommandTerminalWrapper panelId={panelId} command="tmux" statePrefix="tmux" />
);
const NvimWrapper = ({ panelId }: { panelId: string }) => (
<CommandTerminalWrapper panelId={panelId} command="nvim" statePrefix="nvim" />
);
export const appRegistryMetas: AppRegistryMeta[] = [
{
key: 'officerdev/terminal',
name: 'Terminal',
icon: TerminalSquare,
component: TerminalWrapper,
header: TerminalHeader,
},
{
key: 'officerdev/terminal-host',
name: 'Host Terminal',
icon: Monitor,
component: HostTerminalWrapper,
header: HostTerminalHeader,
availableOnPanel: false,
},
{
key: 'officerdev/tmux',
name: 'Tmux',
icon: Columns2,
component: TmuxWrapper,
header: TmuxHeader,
},
{
key: 'officerdev/nvim',
name: 'Neovim',
icon: PenLine,
component: NvimWrapper,
header: NvimHeader,
},
];
@@ -0,0 +1,11 @@
import { useGlobal } from 'hooks/useGlobal';
type TerminalMode = 'sandboxed' | 'host';
export const useTerminalMode = (panelId: string) => {
const [mode, setMode] = useGlobal<TerminalMode>(`terminal-mode-${panelId}`, 'sandboxed');
const toggle = () => setMode((prev) => (prev === 'sandboxed' ? 'host' : 'sandboxed'));
return { mode, setMode, toggle };
};
@@ -5,8 +5,8 @@ import { useQueryClient } from '@tanstack/react-query';
import { useGlobal } from 'hooks/useGlobal';
import { useClient } from 'hooks/useClient';
import { generateSlug } from 'helpers/slug';
import { useUserState } from '@/state/useUserState';
import { useWorkspacesState } from '@/state/useWorkspacesState';
import { useUserState } from 'state/useUserState';
import { useWorkspacesState } from 'state/useWorkspacesState';
import type { WorkspaceDefinition } from '@/components/Workspace';
import {
AlertDialog,
@@ -1,28 +1,25 @@
import { useEffect, useState } from 'react';
import { Link, Navigate, useNavigate, useSearchParams } from 'react-router';
import { useState } from 'react';
import { Link, useNavigate } from 'react-router';
import { FolderOpen, 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 { 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 { FileBrowserApp } from 'officerdev';
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';
const defaultLayout: LayoutNode = {
type: 'group',
id: 'ws-home-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'ws-home-list', appType: 'workspace-list' }, size: 50 },
{ node: { type: 'panel', id: 'ws-home-right', appType: 'workspace-preview' }, size: 50 },
],
};
import { FileBrowserApp } from '../FileBrowser';
import { useAppRegistry } from '../../AppRegistry';
import {
SELECTED_WORKSPACE_KEY,
CREATING_WORKSPACE_KEY,
EDITING_WORKSPACE_KEY,
NEW_WS_NAME_KEY,
NEW_WS_DESC_KEY,
NEW_WS_TEMPLATE_KEY,
} from './constants';
// --- Layout Templates ---
@@ -316,13 +313,11 @@ const CreatePanel = () => {
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<string, unknown> = { [newLayoutKey]: wsLayout };
for (const k of oldKeys) patch[k] = null;
client.patch('/workspaces', patch).catch(() => {});
@@ -391,7 +386,7 @@ const CreatePanel = () => {
);
};
// --- CwdFileBrowser (basePath-constrained file browser for workspace form) ---
// --- CwdFileBrowser ---
const CwdFileBrowser = () => {
const [currentPath] = useUserState<string>('files/currentPath', '/');
@@ -433,17 +428,20 @@ const newWsLayout: LayoutNode = {
],
};
const newWsRegistry = {
...appRegistry,
'file-browser-cwd': { name: 'File Browser', icon: FolderOpen, component: CwdFileBrowser },
'new-ws-name': { name: 'Name', icon: Type, component: NamePanel },
'new-ws-template': { name: 'Template', icon: Layout, component: TemplatePanel },
'new-ws-create': { name: 'Create', icon: Rocket, component: CreatePanel },
const NewWorkspaceForm = () => {
const { registry } = useAppRegistry();
const newWsRegistry = {
...registry,
'file-browser-cwd': { name: 'File Browser', icon: FolderOpen, component: CwdFileBrowser },
'new-ws-name': { name: 'Name', icon: Type, component: NamePanel },
'new-ws-template': { name: 'Template', icon: Layout, component: TemplatePanel },
'new-ws-create': { name: 'Create', icon: Rocket, component: CreatePanel },
};
return <WorkspaceLayout layout={newWsLayout} onLayoutChange={() => {}} registry={newWsRegistry} />;
};
const NewWorkspaceForm = () => (
<WorkspaceLayout layout={newWsLayout} onLayoutChange={() => {}} registry={newWsRegistry} />
);
// --- Empty State ---
const WorkspacePreviewEmpty = () => {
const [creating, setCreating] = useGlobal<boolean>(CREATING_WORKSPACE_KEY, false);
@@ -477,13 +475,17 @@ const WorkspacePreviewEmpty = () => {
);
};
// --- Preview Inner ---
const WorkspacePreviewInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
const ws = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
return <WorkspaceView workspace={ws} registry={appRegistry} cwd={workspace.cwd} />;
return <WorkspaceView workspace={ws} cwd={workspace.cwd} />;
};
const WorkspacePreview = () => {
// --- Main Export ---
export const WorkspacePreview = () => {
const [selectedId] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
const { value: workspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
const workspace = selectedId ? workspaces.find((ws) => ws.id === selectedId) : null;
@@ -502,40 +504,3 @@ const WorkspacePreview = () => {
</div>
);
};
const registry = { ...appRegistry, 'workspace-preview': { name: 'Workspace Preview', icon: LayoutGrid, component: WorkspacePreview } };
export const WorkspaceListScreen = () => {
const workspace = useWorkspacesState<LayoutNode>('screens/homepage', defaultLayout);
return (
<div className="h-full w-full">
<WorkspaceLayout layout={workspace.value} onLayoutChange={workspace.setValue} registry={registry} />
</div>
);
};
export const NewWorkspaceRedirect = () => {
const navigate = useNavigate();
const [params] = useSearchParams();
const [, setName] = useGlobal<string>(NEW_WS_NAME_KEY, '');
const [, setDescription] = useGlobal<string>(NEW_WS_DESC_KEY, '');
const [, setTemplateIdx] = useGlobal<number>(NEW_WS_TEMPLATE_KEY, 0);
const [, setCreating] = useGlobal<boolean>(CREATING_WORKSPACE_KEY, false);
const [, setEditing] = useGlobal<string | null>(EDITING_WORKSPACE_KEY, null);
const [, setSelected] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
const [, setFilePath] = useUserState<string>('files/currentPath', '/');
useEffect(() => {
setSelected(null);
setEditing(null);
setName(params.get('name') || generateSlug());
setDescription('');
setTemplateIdx(0);
setFilePath(params.get('cwd') || '/');
setCreating(true);
navigate('/workspaces', { replace: true });
}, []);
return null;
};
@@ -0,0 +1,25 @@
import type { AppRegistryMeta } from '../../AppRegistry';
import { LayoutGrid } from 'lucide-react';
import { WorkspaceListApp } from './WorkspaceListApp';
import { WorkspacePreview } from './WorkspacePreview';
export { WorkspaceListApp };
export { WorkspacePreview };
export { SELECTED_WORKSPACE_KEY, CREATING_WORKSPACE_KEY, EDITING_WORKSPACE_KEY, NEW_WS_NAME_KEY, NEW_WS_DESC_KEY, NEW_WS_TEMPLATE_KEY } from './constants';
export const appRegistryMetas: AppRegistryMeta[] = [
{
key: 'workspace-list',
name: 'Workspaces',
icon: LayoutGrid,
component: WorkspaceListApp,
availableOnPanel: false
},
{
key: 'workspace-preview',
name: 'Workspace Preview',
icon: LayoutGrid,
component: WorkspacePreview,
availableOnPanel: false
},
];
@@ -1,5 +0,0 @@
import type { AppRegistry } from '@/components/Workspace';
export function useAppRegistry(): AppRegistry {
return {};
}
@@ -1 +0,0 @@
export { useAppRegistry } from './appRegistry';
@@ -1,4 +1,3 @@
export * from './appRegistry';
export * from './useFilesAPI';
export * from './useFileViewerPanels';
export * from './usePiChat';
@@ -1,6 +1,6 @@
import type { ReactNode } from 'react';
import { useSearchParams } from 'react-router';
import { useUserState } from '@/state/useUserState';
import { useUserState } from 'state/useUserState';
import { FileViewerProvider } from '../../apps/FileViewer';
export function ViewerProvider({ children }: { children: ReactNode }) {
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { useChatWebSocket } from 'hooks/useChatWebSocket';
import { useChatSessions } from '@/state/useChatSessions';
import { useChatSessions } from 'state/useChatSessions';
import type { ChatMessage, ServerMessage, TaskInfo, Message } from '../apps/Chat/types';
const SAVE_DEBOUNCE_MS = 1000;
+15 -6
View File
@@ -1,7 +1,16 @@
export * from './hooks';
export * from './apps/Chat';
export * from './apps/ChatHistory';
export * from './apps/CodeEditor';
export * from './apps/FileBrowser';
export * from './apps/FileViewer';
export * from './apps/Terminal';
export * from './AppRegistry';
// Re-export app modules (excluding appRegistryMetas to avoid name collisions)
export { MessageList, MessageBubble, StreamingBubble, ToolActivity, QuestionActivity, ModelSelector, InputArea, ChatLauncher, AttachmentList, AttachButton, WebpageDialog, EmbeddableChat, usePiChat, ChatList, useSlashCommands, useChatSessions, useChatSession, useAttachments, useAudioRecording } from './apps/Chat';
export type { UseEmbeddableChatType, UsePiChatType, UseChatSessionsType, UseChatSessionType, UseAttachmentsType, UseAudioRecordingType } from './apps/Chat';
export * from './apps/Chat/types';
export { SessionBar } from './apps/ChatHistory';
export { CodeEditorView } from './apps/CodeEditor';
export { useFilesAPI, useTasks, useRecentFiles, usePinnedFiles, FileBrowserApp, FileBrowserPanelWrapper, FileBrowserWidget, TaskRunnerModal } from './apps/FileBrowser';
export type { DirEntry, TaskSummary } from './apps/FileBrowser';
export { FileViewerView, FileViewerProvider, FileViewerHeader, FileViewerBody, FileViewerPanelProvider, useFileViewer, getFileType, getLang, getExt, getArchiveBaseName, ARCHIVE_EXTS } from './apps/FileViewer';
export type { FileType } from './apps/FileViewer';
export { TerminalView } from './apps/Terminal';
export type { TerminalViewProps } from './apps/Terminal';
export { WorkspaceListApp, WorkspacePreview, SELECTED_WORKSPACE_KEY, CREATING_WORKSPACE_KEY, EDITING_WORKSPACE_KEY, NEW_WS_NAME_KEY, NEW_WS_DESC_KEY, NEW_WS_TEMPLATE_KEY } from './apps/Workspaces';
+14
View File
@@ -0,0 +1,14 @@
{
"name": "state",
"version": "0.0.1",
"license": "MIT",
"type": "module",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
},
"dependencies": {
"hooks": "workspace:*",
"officerdev": "workspace:*"
}
}
+16
View File
@@ -0,0 +1,16 @@
export { useSettings, DEFAULT_SETTINGS } from './useSettings';
export type { UseSettingsType, UserSettings, UserState } from './useSettings';
export { useUserState } from './useUserState';
export { useWorkspacesState } from './useWorkspacesState';
export { useProjectsState } from './useProjectsState';
export { usePiModels, useVisiblePiModels, modelKey } from './useModels';
export type { ModelOption } from './useModels';
export { useRecentModels } from './useRecentModels';
export { usePlans } from './usePlans';
export { useLandingPage } from './useLandingPage';
export { useServerSettings } from './useServerSettings';
export { useResources, getResourceCategory } from './useResources';
export type { Resource, ResourceCredentials, ResourceConnectionConfig, PingResult, ResourceCategory } from './useResources';
export { useChatSessions } from './useChatSessions';
export type { UseChatSessionsType } from './useChatSessions';
export { useChatGroups } from './useChatGroups';
@@ -2,7 +2,7 @@ import { useCallback, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
import { useAuth } from 'hooks/useAuth';
import type { UserState } from './types/user-settings';
import type { UserState } from './useSettings';
const QUERY_KEY = ['PROJECTS_STATE'];
@@ -2,7 +2,7 @@ import { useCallback, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
import { useAuth } from 'hooks/useAuth';
import type { UserState } from './types/user-settings';
import type { UserState } from './useSettings';
const QUERY_KEY = ['USER_STATE'];
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
import { useAuth } from 'hooks/useAuth';
import type { UserState } from './types/user-settings';
import type { UserState } from './useSettings';
const QUERY_KEY = ['WORKSPACES_STATE'];

Some files were not shown because too many files have changed in this diff Show More