Workspaces
This commit is contained in:
@@ -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=="],
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+9
@@ -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)} />;
|
||||
};
|
||||
+31
-29
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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 };
|
||||
};
|
||||
+2
-2
@@ -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,
|
||||
+33
-68
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
+1
-1
@@ -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'];
|
||||
|
||||
+1
-1
@@ -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'];
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user