ready for app-registry refactor
This commit is contained in:
@@ -1,44 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
import { Widget } from 'widgets/Widget';
|
|
||||||
import { useSettings } from '@/state/useSettings';
|
|
||||||
import { useVisiblePiModels } from '@/state/useModels';
|
|
||||||
import { ChatLauncher as ChatLauncherComponent } from 'apps/Chat';
|
|
||||||
|
|
||||||
export const ChatLauncher = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { settings } = useSettings();
|
|
||||||
const piModels = useVisiblePiModels();
|
|
||||||
const [model, setModel] = useState<string | null>(settings.chat.defaultModel);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setModel(settings.chat.defaultModel);
|
|
||||||
}, [settings.chat.defaultModel]);
|
|
||||||
|
|
||||||
const handleSubmit = (data: {
|
|
||||||
prompt: string;
|
|
||||||
model: string | null;
|
|
||||||
attachmentIds?: string[];
|
|
||||||
images?: { filename: string; dataUrl: string }[];
|
|
||||||
}) => {
|
|
||||||
navigate('/chat/new', {
|
|
||||||
state: {
|
|
||||||
initialMessage: data.prompt,
|
|
||||||
model: data.model,
|
|
||||||
attachmentIds: data.attachmentIds,
|
|
||||||
images: data.images,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Widget title="Start Chat">
|
|
||||||
<ChatLauncherComponent
|
|
||||||
availableModels={piModels}
|
|
||||||
selectedModel={model}
|
|
||||||
onModelChange={setModel}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
/>
|
|
||||||
</Widget>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { WorkspaceView } from '@/components/Workspace';
|
||||||
|
import { useWorkspacesState } from '@/state/useWorkspacesState';
|
||||||
|
import { appRegistry } from '../Workspaces/app-registry';
|
||||||
|
import { defaultLayout } from './defaultLayout';
|
||||||
|
|
||||||
|
export const HomeScreen = () => {
|
||||||
|
const workspace = useWorkspacesState<LayoutNode>('screens/home', defaultLayout);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<WorkspaceView workspace={workspace} registry={appRegistry} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { LayoutNode } from '@/components/Workspace';
|
||||||
|
|
||||||
|
export const defaultLayout: LayoutNode = {
|
||||||
|
type: 'group',
|
||||||
|
id: 'home-root',
|
||||||
|
direction: 'horizontal',
|
||||||
|
children: [
|
||||||
|
{ node: { type: 'panel', id: 'home-left', appType: 'workspace-list' }, size: 20 },
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
type: 'group',
|
||||||
|
id: 'home-center',
|
||||||
|
direction: 'vertical',
|
||||||
|
children: [
|
||||||
|
{ node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 },
|
||||||
|
{ node: { type: 'panel', id: 'home-chat', appType: 'chat-launcher' }, size: 40 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
size: 60,
|
||||||
|
},
|
||||||
|
{ node: { type: 'panel', id: 'home-right', appType: 'project-list' }, size: 20 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
@@ -1,36 +1 @@
|
|||||||
import { useState } from 'react';
|
export * from './HomeScreen';
|
||||||
import type { LayoutNode } from '@/components/Workspace';
|
|
||||||
import { WorkspaceView } from '@/components/Workspace';
|
|
||||||
import { appRegistry } from '../Workspaces/app-registry';
|
|
||||||
|
|
||||||
const initialLayout: LayoutNode = {
|
|
||||||
type: 'group',
|
|
||||||
id: 'home-root',
|
|
||||||
direction: 'horizontal',
|
|
||||||
children: [
|
|
||||||
{ node: { type: 'panel', id: 'home-left', appType: 'workspace-list' }, size: 20 },
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
type: 'group',
|
|
||||||
id: 'home-center',
|
|
||||||
direction: 'vertical',
|
|
||||||
children: [
|
|
||||||
{ node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 },
|
|
||||||
{ node: { type: 'panel', id: 'home-chat', appType: 'chat-launcher' }, size: 40 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
size: 60,
|
|
||||||
},
|
|
||||||
{ node: { type: 'panel', id: 'home-right', appType: 'project-list' }, size: 20 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HomeScreen = () => {
|
|
||||||
const [layout, setLayout] = useState<LayoutNode>(initialLayout);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full">
|
|
||||||
<WorkspaceView layout={layout} onLayoutChange={setLayout} registry={appRegistry} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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 { 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} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { LayoutNode } from '@/components/Workspace';
|
||||||
|
|
||||||
|
export const defaultLayout: LayoutNode = {
|
||||||
|
type: 'panel',
|
||||||
|
id: 'terminal-screen',
|
||||||
|
appType: 'terminal-host',
|
||||||
|
};
|
||||||
@@ -1,30 +1 @@
|
|||||||
import { useState } from 'react';
|
export * from './TerminalScreen';
|
||||||
import { Navigate } from 'react-router';
|
|
||||||
import { useAuth } from 'hooks/useAuth';
|
|
||||||
import { WorkspaceView, createDefaultLayout } from '@/components/Workspace';
|
|
||||||
import type { LayoutNode } from '@/components/Workspace';
|
|
||||||
import { appRegistry } from '../Workspaces/app-registry';
|
|
||||||
|
|
||||||
|
|
||||||
export const TerminalScreen = () => {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const [layout, setLayout] = useState<LayoutNode>(defaultLayout);
|
|
||||||
|
|
||||||
if (user?.role !== 'Super Admin') return <Navigate to="/" replace />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full">
|
|
||||||
<WorkspaceView
|
|
||||||
workspace={null}
|
|
||||||
layout={layout}
|
|
||||||
onLayoutChange={setLayout}
|
|
||||||
registry={appRegistry} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultLayout: LayoutNode = {
|
|
||||||
type: 'panel',
|
|
||||||
id: 'terminal-screen',
|
|
||||||
appType: 'terminal-host'
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const WorkspaceListApp = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [workspaces, setWorkspaces] = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
const { value: workspaces, setValue: setWorkspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||||
const [selected, setSelected] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
|
const [selected, setSelected] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
|
||||||
const [, setCreating] = useGlobal<boolean>(CREATING_WORKSPACE_KEY, false);
|
const [, setCreating] = useGlobal<boolean>(CREATING_WORKSPACE_KEY, false);
|
||||||
const [, setEditing] = useGlobal<string | null>(EDITING_WORKSPACE_KEY, null);
|
const [, setEditing] = useGlobal<string | null>(EDITING_WORKSPACE_KEY, null);
|
||||||
@@ -77,7 +77,7 @@ export const WorkspaceListApp = () => {
|
|||||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
||||||
const { [layoutKey]: _, ...rest } = currentState;
|
const { [layoutKey]: _, ...rest } = currentState;
|
||||||
queryClient.setQueryData(['WORKSPACES_STATE'], rest);
|
queryClient.setQueryData(['WORKSPACES_STATE'], rest);
|
||||||
client.patch('/user/workspaces-state', { [layoutKey]: null }).catch(() => {});
|
client.patch('/workspaces', { [layoutKey]: null }).catch(() => {});
|
||||||
setDeleting(null);
|
setDeleting(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ const CreatePanel = () => {
|
|||||||
const client = useClient();
|
const client = useClient();
|
||||||
const [name, setName] = useGlobal<string>(NEW_WS_NAME_KEY, '');
|
const [name, setName] = useGlobal<string>(NEW_WS_NAME_KEY, '');
|
||||||
const [filePath] = useUserState<string>('files/currentPath', '/');
|
const [filePath] = useUserState<string>('files/currentPath', '/');
|
||||||
const [workspaces, setWorkspaces] = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
const { value: workspaces, setValue: setWorkspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||||
|
|
||||||
const cwd = filePath === '/' ? '~' : `~/${filePath.replace(/^\//, '')}`;
|
const cwd = filePath === '/' ? '~' : `~/${filePath.replace(/^\//, '')}`;
|
||||||
|
|
||||||
@@ -325,10 +325,10 @@ const CreatePanel = () => {
|
|||||||
// Delete old files, write new layout
|
// Delete old files, write new layout
|
||||||
const patch: Record<string, unknown> = { [newLayoutKey]: wsLayout };
|
const patch: Record<string, unknown> = { [newLayoutKey]: wsLayout };
|
||||||
for (const k of oldKeys) patch[k] = null;
|
for (const k of oldKeys) patch[k] = null;
|
||||||
client.patch('/user/workspaces-state', patch).catch(() => {});
|
client.patch('/workspaces', patch).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [newLayoutKey]: wsLayout });
|
queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [newLayoutKey]: wsLayout });
|
||||||
client.patch('/user/workspaces-state', { [newLayoutKey]: wsLayout }).catch(() => {});
|
client.patch('/workspaces', { [newLayoutKey]: wsLayout }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
@@ -351,7 +351,7 @@ const CreatePanel = () => {
|
|||||||
setWorkspaces((prev) => [...prev, ws]);
|
setWorkspaces((prev) => [...prev, ws]);
|
||||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
||||||
queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [layoutKey]: wsLayout });
|
queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [layoutKey]: wsLayout });
|
||||||
client.patch('/user/workspaces-state', { [layoutKey]: wsLayout }).catch(() => {});
|
client.patch('/workspaces', { [layoutKey]: wsLayout }).catch(() => {});
|
||||||
|
|
||||||
setName('');
|
setName('');
|
||||||
setDescription('');
|
setDescription('');
|
||||||
@@ -478,14 +478,14 @@ const WorkspacePreviewEmpty = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WorkspacePreviewInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
|
const WorkspacePreviewInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
|
||||||
const [layout, setLayout] = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
const ws = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
||||||
|
|
||||||
return <WorkspaceView workspace={workspace} layout={layout} onLayoutChange={setLayout} registry={appRegistry} />;
|
return <WorkspaceView workspace={ws} registry={appRegistry} cwd={workspace.cwd} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WorkspacePreview = () => {
|
const WorkspacePreview = () => {
|
||||||
const [selectedId] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
|
const [selectedId] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
|
||||||
const [workspaces] = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
const { value: workspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||||
const workspace = selectedId ? workspaces.find((ws) => ws.id === selectedId) : null;
|
const workspace = selectedId ? workspaces.find((ws) => ws.id === selectedId) : null;
|
||||||
|
|
||||||
if (!workspace) return <WorkspacePreviewEmpty />;
|
if (!workspace) return <WorkspacePreviewEmpty />;
|
||||||
@@ -506,13 +506,11 @@ const WorkspacePreview = () => {
|
|||||||
const registry = { ...appRegistry, 'workspace-preview': { name: 'Workspace Preview', icon: LayoutGrid, component: WorkspacePreview } };
|
const registry = { ...appRegistry, 'workspace-preview': { name: 'Workspace Preview', icon: LayoutGrid, component: WorkspacePreview } };
|
||||||
|
|
||||||
export const WorkspaceListScreen = () => {
|
export const WorkspaceListScreen = () => {
|
||||||
const [layout, setLayout, isLoaded] = useWorkspacesState<LayoutNode>('ws-layout-ws-homepage', defaultLayout);
|
const workspace = useWorkspacesState<LayoutNode>('screens/homepage', defaultLayout);
|
||||||
|
|
||||||
if (!isLoaded) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<WorkspaceLayout layout={layout} onLayoutChange={setLayout} registry={registry} />
|
<WorkspaceLayout layout={workspace.value} onLayoutChange={workspace.setValue} registry={registry} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { appRegistry } from './app-registry';
|
|||||||
|
|
||||||
export const WorkspaceScreen = () => {
|
export const WorkspaceScreen = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const [workspaces, , isLoaded] = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
const { value: workspaces, isLoaded } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||||
const workspace = workspaces.find((ws) => ws.id === id);
|
const workspace = workspaces.find((ws) => ws.id === id);
|
||||||
|
|
||||||
if (!isLoaded) return null;
|
if (!isLoaded) return null;
|
||||||
@@ -16,16 +16,11 @@ export const WorkspaceScreen = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WorkspaceScreenInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
|
const WorkspaceScreenInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
|
||||||
const [layout, setLayout] = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
const ws = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<WorkspaceView
|
<WorkspaceView workspace={ws} registry={appRegistry} cwd={workspace.cwd} />
|
||||||
workspace={workspace}
|
|
||||||
layout={layout}
|
|
||||||
onLayoutChange={setLayout}
|
|
||||||
registry={appRegistry}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,22 +9,30 @@ import { TerminalView } from 'apps/Terminal';
|
|||||||
import { useWorkspacesState } from '@/state/useWorkspacesState';
|
import { useWorkspacesState } from '@/state/useWorkspacesState';
|
||||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||||
import { FileViewerProvider, FileViewerHeader, FileViewerBody } from 'apps/FileViewer';
|
import { FileViewerProvider, FileViewerHeader, FileViewerBody } from 'apps/FileViewer';
|
||||||
import { usePi } from 'apps/Chat';
|
import { EmbeddableChat, ChatLauncher, usePi } from 'apps/Chat';
|
||||||
import { ChatPanel } from 'apps/Chat';
|
|
||||||
import { useVisiblePiModels } from '@/state/useModels';
|
import { useVisiblePiModels } from '@/state/useModels';
|
||||||
import { ChatHistoryApp as ChatHistory } from '../ChatHistory';
|
import { ChatHistoryApp as ChatHistory } from '../ChatHistory';
|
||||||
import { Files } from '../Files';
|
import { Files } from '../Files';
|
||||||
import { Catalog } from 'sounds';
|
import { Catalog } from 'sounds';
|
||||||
import { WorkspaceListApp } from './WorkspaceListApp';
|
import { WorkspaceListApp } from './WorkspaceListApp';
|
||||||
import { ProjectListApp } from '../Projects/ProjectListApp';
|
import { ProjectListApp } from '../Projects/ProjectListApp';
|
||||||
import { ChatLauncher } from '../Home/ChatLauncher';
|
|
||||||
import { widgetRegistry } from 'widgets/widget-registry';
|
import { widgetRegistry } from 'widgets/widget-registry';
|
||||||
import { WidgetPanel } from 'widgets/WidgetPanel';
|
import { WidgetPanel } from 'widgets/WidgetPanel';
|
||||||
|
|
||||||
const ChatWidget = () => {
|
const ChatWidget = () => <EmbeddableChat className="h-full" />;
|
||||||
|
|
||||||
|
const ChatLauncherWidget = () => {
|
||||||
const pi = usePi();
|
const pi = usePi();
|
||||||
const models = useVisiblePiModels();
|
const models = useVisiblePiModels();
|
||||||
return <ChatPanel chat={pi} availableModels={models} />;
|
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 cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1));
|
||||||
@@ -75,7 +83,7 @@ const EMPTY_TERMINALS: Record<string, string> = {};
|
|||||||
const TerminalWrapper = ({ panelId }: { panelId: string }) => {
|
const TerminalWrapper = ({ panelId }: { panelId: string }) => {
|
||||||
const { workspaceId, cwd } = useWorkspace();
|
const { workspaceId, cwd } = useWorkspace();
|
||||||
const stateKey = workspaceId ? `ws-terminals-${workspaceId}` : 'ws-terminals-default';
|
const stateKey = workspaceId ? `ws-terminals-${workspaceId}` : 'ws-terminals-default';
|
||||||
const [terminals, setTerminals] = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||||
const setTerminalsRef = useRef(setTerminals);
|
const setTerminalsRef = useRef(setTerminals);
|
||||||
setTerminalsRef.current = setTerminals;
|
setTerminalsRef.current = setTerminals;
|
||||||
|
|
||||||
@@ -105,7 +113,7 @@ const HostTerminalWrapper = ({ panelId }: { panelId: string }) => {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { workspaceId, cwd } = useWorkspace();
|
const { workspaceId, cwd } = useWorkspace();
|
||||||
const stateKey = workspaceId ? `ws-host-terminals-${workspaceId}` : 'ws-host-terminals-default';
|
const stateKey = workspaceId ? `ws-host-terminals-${workspaceId}` : 'ws-host-terminals-default';
|
||||||
const [terminals, setTerminals] = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||||
const setTerminalsRef = useRef(setTerminals);
|
const setTerminalsRef = useRef(setTerminals);
|
||||||
setTerminalsRef.current = setTerminals;
|
setTerminalsRef.current = setTerminals;
|
||||||
|
|
||||||
@@ -142,7 +150,7 @@ const HostTerminalWrapper = ({ panelId }: { panelId: string }) => {
|
|||||||
const CommandTerminalWrapper = ({ panelId, command, statePrefix }: { panelId: string; command: string; statePrefix: string }) => {
|
const CommandTerminalWrapper = ({ panelId, command, statePrefix }: { panelId: string; command: string; statePrefix: string }) => {
|
||||||
const { workspaceId, cwd } = useWorkspace();
|
const { workspaceId, cwd } = useWorkspace();
|
||||||
const stateKey = workspaceId ? `ws-${statePrefix}-${workspaceId}` : `ws-${statePrefix}-default`;
|
const stateKey = workspaceId ? `ws-${statePrefix}-${workspaceId}` : `ws-${statePrefix}-default`;
|
||||||
const [terminals, setTerminals] = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||||
const setTerminalsRef = useRef(setTerminals);
|
const setTerminalsRef = useRef(setTerminals);
|
||||||
setTerminalsRef.current = setTerminals;
|
setTerminalsRef.current = setTerminals;
|
||||||
|
|
||||||
@@ -239,7 +247,7 @@ export const appRegistry: AppRegistry = {
|
|||||||
'workspace-list': { name: 'Workspaces', icon: LayoutGrid, component: () => <WorkspaceListApp />, availableOnPanel: false },
|
'workspace-list': { name: 'Workspaces', icon: LayoutGrid, component: () => <WorkspaceListApp />, availableOnPanel: false },
|
||||||
'project-list': { name: 'Projects', icon: FolderKanban, component: () => <ProjectListApp />, 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 },
|
'file-viewer': { name: 'File Viewer', icon: Eye, component: FileViewerBody, header: FileViewerHeader, provider: FileViewerWorkspaceProvider, availableOnPanel: false },
|
||||||
'chat-launcher': { name: 'Chat Launcher', icon: Sparkles, component: () => <ChatLauncher />, fixedHeight: 180, 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 },
|
'widget-panel': { name: 'Widget Panel', icon: LayoutDashboard, component: WidgetPanel, transparent: true },
|
||||||
...widgetRegistry,
|
...widgetRegistry,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
import { useAuth } from 'hooks/useAuth';
|
||||||
@@ -6,7 +6,7 @@ import type { UserState } from './types/user-settings';
|
|||||||
|
|
||||||
const QUERY_KEY = ['WORKSPACES_STATE'];
|
const QUERY_KEY = ['WORKSPACES_STATE'];
|
||||||
|
|
||||||
export function useWorkspacesState<T>(key: string, defaultValue: T): [T, (value: T | ((prev: T) => T)) => void, boolean] {
|
export function useWorkspacesState<T>(key: string, defaultValue: T) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -16,10 +16,23 @@ export function useWorkspacesState<T>(key: string, defaultValue: T): [T, (value:
|
|||||||
const { data: state = {}, isSuccess } = useQuery<UserState>({
|
const { data: state = {}, isSuccess } = useQuery<UserState>({
|
||||||
queryKey: QUERY_KEY,
|
queryKey: QUERY_KEY,
|
||||||
enabled: isAuthenticated,
|
enabled: isAuthenticated,
|
||||||
queryFn: () => client.get<UserState>('/user/workspaces-state'),
|
queryFn: () => client.get<UserState>('/workspaces'),
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Seed default to backend when key is missing after initial fetch
|
||||||
|
const seededKeyRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSuccess || seededKeyRef.current === key) return;
|
||||||
|
seededKeyRef.current = key;
|
||||||
|
|
||||||
|
const currentState = queryClient.getQueryData<UserState>(QUERY_KEY) ?? {};
|
||||||
|
if (!(key in currentState)) {
|
||||||
|
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: defaultValue });
|
||||||
|
clientRef.current.patch('/workspaces', { [key]: defaultValue }).catch(() => { });
|
||||||
|
}
|
||||||
|
}, [isSuccess, key, defaultValue, queryClient]);
|
||||||
|
|
||||||
const value = key in state ? (state[key] as T) : defaultValue;
|
const value = key in state ? (state[key] as T) : defaultValue;
|
||||||
|
|
||||||
const setValue = useCallback(
|
const setValue = useCallback(
|
||||||
@@ -30,10 +43,10 @@ export function useWorkspacesState<T>(key: string, defaultValue: T): [T, (value:
|
|||||||
|
|
||||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue });
|
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue });
|
||||||
|
|
||||||
clientRef.current.patch('/user/workspaces-state', { [key]: newValue }).catch(() => {});
|
clientRef.current.patch('/workspaces', { [key]: newValue }).catch(() => { });
|
||||||
},
|
},
|
||||||
[key, defaultValue, queryClient],
|
[key, defaultValue, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [value, setValue, isSuccess];
|
return { key, value, setValue, isLoaded: isSuccess };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './workspaces';
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type KeyMapping = { file: string; dir?: string };
|
||||||
|
|
||||||
|
export type ResolveDirs = { wsDir: string; screensDir: string };
|
||||||
@@ -1,58 +1,58 @@
|
|||||||
import { mkdir, readdir, rename, rm } from 'node:fs/promises';
|
import { mkdir, readdir, rename, rm } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { createRouter } from '../../create-router';
|
|
||||||
import { getUserWorkspacesDir, getUserHomepageWorkspaceDir, getUserStateFile } from '@@/data-path';
|
import { getUserWorkspacesDir, getUserHomepageWorkspaceDir, getUserStateFile } from '@@/data-path';
|
||||||
|
import type { KeyMapping, ResolveDirs } from './types';
|
||||||
|
|
||||||
export const workspacesRouter = createRouter();
|
const RESERVED_DIRS = new Set(['screens']);
|
||||||
|
|
||||||
type KeyMapping = { file: string; dir?: string };
|
export function resolveKey(dirs: ResolveDirs, key: string): KeyMapping | null {
|
||||||
|
|
||||||
type ResolveDirs = { wsDir: string; homepageDir: string };
|
|
||||||
|
|
||||||
function workspaceDir(dirs: ResolveDirs, id: string) {
|
|
||||||
return id === 'ws-homepage' ? dirs.homepageDir : join(dirs.wsDir, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveKey(dirs: ResolveDirs, key: string): KeyMapping | null {
|
|
||||||
if (key === 'workspaces') return { file: join(dirs.wsDir, 'index.json') };
|
if (key === 'workspaces') return { file: join(dirs.wsDir, 'index.json') };
|
||||||
if (key === 'ws-terminals-default') return { file: join(dirs.wsDir, 'default-terminals.json') };
|
if (key === 'ws-terminals-default') return { file: join(dirs.wsDir, 'default-terminals.json') };
|
||||||
if (key === 'ws-host-terminals-default') return { file: join(dirs.wsDir, 'default-host-terminals.json') };
|
if (key === 'ws-host-terminals-default') return { file: join(dirs.wsDir, 'default-host-terminals.json') };
|
||||||
|
|
||||||
|
// screens/{name} → screens/{name}/layout.json
|
||||||
|
const screensMatch = key.match(/^screens\/(.+)$/);
|
||||||
|
if (screensMatch) {
|
||||||
|
const name = screensMatch[1]!;
|
||||||
|
const dir = join(dirs.screensDir, name);
|
||||||
|
return { file: join(dir, 'layout.json'), dir };
|
||||||
|
}
|
||||||
|
|
||||||
const layoutMatch = key.match(/^ws-layout-(.+)$/);
|
const layoutMatch = key.match(/^ws-layout-(.+)$/);
|
||||||
if (layoutMatch) {
|
if (layoutMatch) {
|
||||||
const id = layoutMatch[1]!;
|
const id = layoutMatch[1]!;
|
||||||
const dir = workspaceDir(dirs, id);
|
const dir = join(dirs.wsDir, id);
|
||||||
return { file: join(dir, 'layout.json'), dir };
|
return { file: join(dir, 'layout.json'), dir };
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminalsMatch = key.match(/^ws-terminals-(.+)$/);
|
const terminalsMatch = key.match(/^ws-terminals-(.+)$/);
|
||||||
if (terminalsMatch) {
|
if (terminalsMatch) {
|
||||||
const id = terminalsMatch[1]!;
|
const id = terminalsMatch[1]!;
|
||||||
const dir = workspaceDir(dirs, id);
|
const dir = join(dirs.wsDir, id);
|
||||||
return { file: join(dir, 'terminals.json'), dir };
|
return { file: join(dir, 'terminals.json'), dir };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostTerminalsMatch = key.match(/^ws-host-terminals-(.+)$/);
|
const hostTerminalsMatch = key.match(/^ws-host-terminals-(.+)$/);
|
||||||
if (hostTerminalsMatch) {
|
if (hostTerminalsMatch) {
|
||||||
const id = hostTerminalsMatch[1]!;
|
const id = hostTerminalsMatch[1]!;
|
||||||
const dir = workspaceDir(dirs, id);
|
const dir = join(dirs.wsDir, id);
|
||||||
return { file: join(dir, 'host-terminals.json'), dir };
|
return { file: join(dir, 'host-terminals.json'), dir };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readJsonFile(path: string): Promise<unknown | null> {
|
export async function readJsonFile(path: string): Promise<unknown | null> {
|
||||||
const file = Bun.file(path);
|
const file = Bun.file(path);
|
||||||
if (await file.exists()) return file.json();
|
if (await file.exists()) return file.json();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeJsonFile(path: string, data: unknown) {
|
export async function writeJsonFile(path: string, data: unknown) {
|
||||||
await Bun.write(path, JSON.stringify(data, null, 2));
|
await Bun.write(path, JSON.stringify(data, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateFromState(email: string, dirs: ResolveDirs) {
|
export async function migrateFromState(email: string, dirs: ResolveDirs) {
|
||||||
const stateFile = getUserStateFile(email);
|
const stateFile = getUserStateFile(email);
|
||||||
const file = Bun.file(stateFile);
|
const file = Bun.file(stateFile);
|
||||||
if (!(await file.exists())) return;
|
if (!(await file.exists())) return;
|
||||||
@@ -66,7 +66,7 @@ async function migrateFromState(email: string, dirs: ResolveDirs) {
|
|||||||
await mkdir(dirs.wsDir, { recursive: true });
|
await mkdir(dirs.wsDir, { recursive: true });
|
||||||
|
|
||||||
for (const key of wsKeys) {
|
for (const key of wsKeys) {
|
||||||
const migratedKey = key === 'ws-layout-workspaces' ? 'ws-layout-ws-homepage' : key;
|
const migratedKey = key === 'ws-layout-workspaces' ? 'screens/homepage' : key;
|
||||||
const mapping = resolveKey(dirs, migratedKey);
|
const mapping = resolveKey(dirs, migratedKey);
|
||||||
if (!mapping) continue;
|
if (!mapping) continue;
|
||||||
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
|
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
|
||||||
@@ -78,26 +78,40 @@ async function migrateFromState(email: string, dirs: ResolveDirs) {
|
|||||||
await Bun.write(stateFile, JSON.stringify(cleaned, null, 2));
|
await Bun.write(stateFile, JSON.stringify(cleaned, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateListLayout(dirs: ResolveDirs) {
|
export async function migrateHomepageToScreens(dirs: ResolveDirs, email: string) {
|
||||||
const oldFile = join(dirs.wsDir, 'list-layout.json');
|
// Migrate from old ws-homepage/ dir to workspaces/screens/homepage/
|
||||||
if (!(await Bun.file(oldFile).exists())) return;
|
const oldHomepageDir = getUserHomepageWorkspaceDir(email);
|
||||||
await mkdir(dirs.homepageDir, { recursive: true });
|
const layoutFile = join(oldHomepageDir, 'layout.json');
|
||||||
await rename(oldFile, join(dirs.homepageDir, 'layout.json'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateHomepageFromWorkspaces(dirs: ResolveDirs) {
|
|
||||||
const oldDir = join(dirs.wsDir, 'ws-homepage');
|
|
||||||
const layoutFile = join(oldDir, 'layout.json');
|
|
||||||
if (!(await Bun.file(layoutFile).exists())) return;
|
if (!(await Bun.file(layoutFile).exists())) return;
|
||||||
await mkdir(dirs.homepageDir, { recursive: true });
|
|
||||||
|
const targetDir = join(dirs.screensDir, 'homepage');
|
||||||
|
await mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
for (const name of ['layout.json', 'terminals.json', 'host-terminals.json']) {
|
for (const name of ['layout.json', 'terminals.json', 'host-terminals.json']) {
|
||||||
const src = join(oldDir, name);
|
const src = join(oldHomepageDir, name);
|
||||||
if (await Bun.file(src).exists()) {
|
if (await Bun.file(src).exists()) {
|
||||||
await rename(src, join(dirs.homepageDir, name));
|
await rename(src, join(targetDir, name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const remaining = await readdir(oldDir);
|
|
||||||
if (remaining.length === 0) await rm(oldDir, { recursive: true, force: true });
|
const remaining = await readdir(oldHomepageDir);
|
||||||
|
if (remaining.length === 0) await rm(oldHomepageDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
// Also migrate from workspaces/ws-homepage/ if it exists
|
||||||
|
const oldWsHomepageDir = join(dirs.wsDir, 'ws-homepage');
|
||||||
|
const oldWsLayout = join(oldWsHomepageDir, 'layout.json');
|
||||||
|
if (!(await Bun.file(oldWsLayout).exists())) return;
|
||||||
|
|
||||||
|
for (const name of ['layout.json', 'terminals.json', 'host-terminals.json']) {
|
||||||
|
const src = join(oldWsHomepageDir, name);
|
||||||
|
const target = join(targetDir, name);
|
||||||
|
if (await Bun.file(src).exists() && !(await Bun.file(target).exists())) {
|
||||||
|
await rename(src, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsRemaining = await readdir(oldWsHomepageDir);
|
||||||
|
if (wsRemaining.length === 0) await rm(oldWsHomepageDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readWorkspaceDir(dirPath: string, id: string, result: Record<string, unknown>) {
|
async function readWorkspaceDir(dirPath: string, id: string, result: Record<string, unknown>) {
|
||||||
@@ -111,7 +125,22 @@ async function readWorkspaceDir(dirPath: string, id: string, result: Record<stri
|
|||||||
if (hostTerminals !== null) result[`ws-host-terminals-${id}`] = hostTerminals;
|
if (hostTerminals !== null) result[`ws-host-terminals-${id}`] = hostTerminals;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string, unknown>> {
|
async function readScreensDir(screensDir: string, result: Record<string, unknown>) {
|
||||||
|
let entries: import('node:fs').Dirent[] = [];
|
||||||
|
try {
|
||||||
|
entries = await readdir(screensDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const layout = await readJsonFile(join(screensDir, entry.name, 'layout.json'));
|
||||||
|
if (layout !== null) result[`screens/${entry.name}`] = layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string, unknown>> {
|
||||||
const result: Record<string, unknown> = {};
|
const result: Record<string, unknown> = {};
|
||||||
|
|
||||||
const indexData = await readJsonFile(join(dirs.wsDir, 'index.json'));
|
const indexData = await readJsonFile(join(dirs.wsDir, 'index.json'));
|
||||||
@@ -123,8 +152,8 @@ async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string,
|
|||||||
const defaultHostTerminals = await readJsonFile(join(dirs.wsDir, 'default-host-terminals.json'));
|
const defaultHostTerminals = await readJsonFile(join(dirs.wsDir, 'default-host-terminals.json'));
|
||||||
if (defaultHostTerminals !== null) result['ws-host-terminals-default'] = defaultHostTerminals;
|
if (defaultHostTerminals !== null) result['ws-host-terminals-default'] = defaultHostTerminals;
|
||||||
|
|
||||||
// Read ws-homepage from its own dir
|
// Read screens
|
||||||
await readWorkspaceDir(dirs.homepageDir, 'ws-homepage', result);
|
await readScreensDir(dirs.screensDir, result);
|
||||||
|
|
||||||
// Read per-workspace subdirs
|
// Read per-workspace subdirs
|
||||||
let entries: import('node:fs').Dirent[] = [];
|
let entries: import('node:fs').Dirent[] = [];
|
||||||
@@ -135,63 +164,16 @@ async function readAllWorkspacesState(dirs: ResolveDirs): Promise<Record<string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isDirectory()) continue;
|
if (!entry.isDirectory() || RESERVED_DIRS.has(entry.name)) continue;
|
||||||
await readWorkspaceDir(join(dirs.wsDir, entry.name), entry.name, result);
|
await readWorkspaceDir(join(dirs.wsDir, entry.name), entry.name, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDirs(email: string): ResolveDirs {
|
export function getDirs(email: string): ResolveDirs {
|
||||||
return { wsDir: getUserWorkspacesDir(email), homepageDir: getUserHomepageWorkspaceDir(email) };
|
return {
|
||||||
|
wsDir: getUserWorkspacesDir(email),
|
||||||
|
screensDir: join(getUserWorkspacesDir(email), 'screens'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /workspaces-state
|
|
||||||
workspacesRouter.get('/workspaces-state', async (ctx) => {
|
|
||||||
const email = ctx.get('user').email;
|
|
||||||
const dirs = getDirs(email);
|
|
||||||
|
|
||||||
const dirFile = Bun.file(join(dirs.wsDir, 'index.json'));
|
|
||||||
if (!(await dirFile.exists())) {
|
|
||||||
await migrateFromState(email, dirs);
|
|
||||||
}
|
|
||||||
|
|
||||||
await migrateListLayout(dirs);
|
|
||||||
await migrateHomepageFromWorkspaces(dirs);
|
|
||||||
|
|
||||||
const state = await readAllWorkspacesState(dirs);
|
|
||||||
return ctx.json(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
// PATCH /workspaces-state
|
|
||||||
workspacesRouter.patch('/workspaces-state', async (ctx) => {
|
|
||||||
const email = ctx.get('user').email;
|
|
||||||
const body = ctx.get('body') as Record<string, unknown>;
|
|
||||||
const dirs = getDirs(email);
|
|
||||||
|
|
||||||
await mkdir(dirs.wsDir, { recursive: true });
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(body)) {
|
|
||||||
const mapping = resolveKey(dirs, key);
|
|
||||||
if (!mapping) continue;
|
|
||||||
|
|
||||||
if (value === null) {
|
|
||||||
try {
|
|
||||||
await rm(mapping.file, { force: true });
|
|
||||||
if (mapping.dir) {
|
|
||||||
const remaining = await readdir(mapping.dir);
|
|
||||||
if (remaining.length === 0) await rm(mapping.dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
|
|
||||||
await writeJsonFile(mapping.file, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = await readAllWorkspacesState(dirs);
|
|
||||||
return ctx.json(state);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { mkdir, readdir, rm } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { createRouter } from '@@/create-router';
|
||||||
|
import {
|
||||||
|
getDirs,
|
||||||
|
migrateFromState,
|
||||||
|
migrateHomepageToScreens,
|
||||||
|
readAllWorkspacesState,
|
||||||
|
resolveKey,
|
||||||
|
writeJsonFile,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
export const workspacesRouter = createRouter();
|
||||||
|
|
||||||
|
// GET /workspaces
|
||||||
|
workspacesRouter.get('/', async (ctx) => {
|
||||||
|
const email = ctx.get('user').email;
|
||||||
|
const dirs = getDirs(email);
|
||||||
|
|
||||||
|
const dirFile = Bun.file(join(dirs.wsDir, 'index.json'));
|
||||||
|
if (!(await dirFile.exists())) {
|
||||||
|
await migrateFromState(email, dirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
await migrateHomepageToScreens(dirs, email);
|
||||||
|
|
||||||
|
const state = await readAllWorkspacesState(dirs);
|
||||||
|
return ctx.json(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// PATCH /workspaces
|
||||||
|
workspacesRouter.patch('/', async (ctx) => {
|
||||||
|
const email = ctx.get('user').email;
|
||||||
|
const body = ctx.get('body') as Record<string, unknown>;
|
||||||
|
const dirs = getDirs(email);
|
||||||
|
|
||||||
|
await mkdir(dirs.wsDir, { recursive: true });
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(body)) {
|
||||||
|
const mapping = resolveKey(dirs, key);
|
||||||
|
if (!mapping) continue;
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
try {
|
||||||
|
await rm(mapping.file, { force: true });
|
||||||
|
if (mapping.dir) {
|
||||||
|
const remaining = await readdir(mapping.dir);
|
||||||
|
if (remaining.length === 0) await rm(mapping.dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.dir) await mkdir(mapping.dir, { recursive: true });
|
||||||
|
await writeJsonFile(mapping.file, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await readAllWorkspacesState(dirs);
|
||||||
|
return ctx.json(state);
|
||||||
|
});
|
||||||
@@ -26,9 +26,13 @@ export const getArchivedSessionDir = (email: string, sessionId: string) =>
|
|||||||
|
|
||||||
export const getHomeDir = (email: string) => join(DATA_PATH, email, 'home');
|
export const getHomeDir = (email: string) => join(DATA_PATH, email, 'home');
|
||||||
|
|
||||||
export const getUserSettingsFile = (email: string) => join(DATA_PATH, email, 'settings.json');
|
export const getUserSettingsDir = (email: string) => join(DATA_PATH, email, 'settings');
|
||||||
|
|
||||||
export const getUserStateFile = (email: string) => join(DATA_PATH, email, 'state.json');
|
export const getUserSettingsFile = (email: string) => join(DATA_PATH, email, 'settings', 'settings.json');
|
||||||
|
|
||||||
|
export const getUserStateDir = (email: string) => join(DATA_PATH, email, 'state');
|
||||||
|
|
||||||
|
export const getUserStateFile = (email: string) => join(DATA_PATH, email, 'state', 'state.json');
|
||||||
|
|
||||||
export const getUserWorkspacesDir = (email: string) => join(DATA_PATH, email, 'workspaces');
|
export const getUserWorkspacesDir = (email: string) => join(DATA_PATH, email, 'workspaces');
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ import { sessionsRouter } from './api/sessions/sessions';
|
|||||||
import { scrapeRouter } from './api/scrape/scrape';
|
import { scrapeRouter } from './api/scrape/scrape';
|
||||||
import { uploadRouter } from './api/upload/upload';
|
import { uploadRouter } from './api/upload/upload';
|
||||||
import { settingsRouter } from './api/settings/settings';
|
import { settingsRouter } from './api/settings/settings';
|
||||||
import { workspacesRouter } from './api/settings/workspaces';
|
import { workspacesRouter } from './api/workspaces';
|
||||||
import { projectsRouter } from './api/settings/projects';
|
import { projectsRouter } from './api/settings/projects';
|
||||||
import { taskLogsRouter } from './api/task-logs/task-logs';
|
import { taskLogsRouter } from './api/task-logs/task-logs';
|
||||||
import { router as fileBrowserRouter } from './api/file-browser/router';
|
import { router as fileBrowserRouter } from './api/file-browser/router';
|
||||||
@@ -54,7 +54,7 @@ protectedRouter.route('/', sessionsRouter);
|
|||||||
protectedRouter.route('/scrape', scrapeRouter);
|
protectedRouter.route('/scrape', scrapeRouter);
|
||||||
protectedRouter.route('/upload', uploadRouter);
|
protectedRouter.route('/upload', uploadRouter);
|
||||||
protectedRouter.route('/user', settingsRouter);
|
protectedRouter.route('/user', settingsRouter);
|
||||||
protectedRouter.route('/user', workspacesRouter);
|
protectedRouter.route('/workspaces', workspacesRouter);
|
||||||
protectedRouter.route('/user', projectsRouter);
|
protectedRouter.route('/user', projectsRouter);
|
||||||
protectedRouter.route('/task-logs', taskLogsRouter);
|
protectedRouter.route('/task-logs', taskLogsRouter);
|
||||||
protectedRouter.route('/file-browser', fileBrowserRouter);
|
protectedRouter.route('/file-browser', fileBrowserRouter);
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
import type { LayoutNode, WorkspaceDefinition, AppRegistry } from './types';
|
import type { LayoutNode, AppRegistry, WorkspaceState } from './types';
|
||||||
import type { DropPosition } from './layout-utils';
|
import type { DropPosition } from './layout-utils';
|
||||||
import { splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, countPanels } from './layout-utils';
|
import { splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, countPanels } from './layout-utils';
|
||||||
import { WorkspaceProvider } from './WorkspaceContext';
|
import { WorkspaceProvider } from './WorkspaceContext';
|
||||||
import { WorkspaceRenderer } from './WorkspaceRenderer';
|
import { WorkspaceRenderer } from './WorkspaceRenderer';
|
||||||
|
|
||||||
type WorkspaceViewProps = {
|
type WorkspaceViewProps = {
|
||||||
workspace: WorkspaceDefinition | null;
|
workspace: WorkspaceState;
|
||||||
layout: LayoutNode;
|
|
||||||
onLayoutChange: (layout: LayoutNode) => void;
|
|
||||||
registry: AppRegistry;
|
registry: AppRegistry;
|
||||||
|
cwd?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: WorkspaceViewProps) => {
|
export const WorkspaceView = ({ workspace, registry, cwd = '~' }: WorkspaceViewProps) => {
|
||||||
|
if (!workspace.isLoaded) return null;
|
||||||
|
|
||||||
|
const { value: layout, setValue: onLayoutChange } = workspace;
|
||||||
const [swapSourceId, setSwapSourceId] = useState<string | null>(null);
|
const [swapSourceId, setSwapSourceId] = useState<string | null>(null);
|
||||||
const [dragSourceId, setDragSourceId] = useState<string | null>(null);
|
const [dragSourceId, setDragSourceId] = useState<string | null>(null);
|
||||||
const [maximizedPanelId, setMaximizedPanelId] = useState<string | null>(null);
|
const [maximizedPanelId, setMaximizedPanelId] = useState<string | null>(null);
|
||||||
@@ -102,8 +104,8 @@ export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: W
|
|||||||
return (
|
return (
|
||||||
<WorkspaceProvider
|
<WorkspaceProvider
|
||||||
value={{
|
value={{
|
||||||
workspaceId: workspace?.id ?? null,
|
workspaceId: workspace.key,
|
||||||
cwd: workspace?.cwd ?? '~',
|
cwd,
|
||||||
swapSourceId,
|
swapSourceId,
|
||||||
setSwapSourceId,
|
setSwapSourceId,
|
||||||
onSwap: handleSwap,
|
onSwap: handleSwap,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry } from './types';
|
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, WorkspaceState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry } from './types';
|
||||||
export type { DropPosition } from './layout-utils';
|
export type { DropPosition } from './layout-utils';
|
||||||
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './layout-utils';
|
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './layout-utils';
|
||||||
export { WorkspaceProvider, useWorkspace } from './WorkspaceContext';
|
export { WorkspaceProvider, useWorkspace } from './WorkspaceContext';
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ export type WorkspaceDefinition = {
|
|||||||
templateIdx?: number;
|
templateIdx?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkspaceState = {
|
||||||
|
key: string;
|
||||||
|
value: LayoutNode;
|
||||||
|
setValue: (value: LayoutNode | ((prev: LayoutNode) => LayoutNode)) => void;
|
||||||
|
isLoaded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProjectType = 'landing-page' | 'website' | 'app';
|
export type ProjectType = 'landing-page' | 'website' | 'app';
|
||||||
|
|
||||||
export type ProjectDefinition = {
|
export type ProjectDefinition = {
|
||||||
|
|||||||
Reference in New Issue
Block a user