workspaces to dashboards, imap email sync, ffmpeg tool, tts fix, file browser refresh, automation sidebar reorder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ 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 { appRegistryMetas as dashboardMetas } from '../apps/Dashboards';
|
||||
import { appRegistryMetas as projectMetas } from '../apps/Projects';
|
||||
import { appRegistryMetas as chatHistoryMetas } from '../apps/ChatHistory';
|
||||
import { appRegistryMetas as previewMetas } from '../apps/Preview';
|
||||
@@ -15,7 +15,7 @@ import { createUserAppPanel } from '../apps/UserApp/UserAppPanel';
|
||||
import { createUserAppHeader } from '../apps/UserApp/UserAppHeader';
|
||||
import { resolveIcon } from '../utils/resolve-icon';
|
||||
|
||||
const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...workspaceMetas, ...projectMetas, ...chatHistoryMetas, ...previewMetas, ...widgetMetas];
|
||||
const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...dashboardMetas, ...projectMetas, ...chatHistoryMetas, ...previewMetas, ...widgetMetas];
|
||||
|
||||
export const AppRegistry = () => {
|
||||
const { registerApp } = useAppRegistry(apps);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getProviderDisplayName } from 'state/useModels';
|
||||
type ChatSessionSelection = {
|
||||
sessionId: string | null;
|
||||
model?: string | null;
|
||||
workspaceId?: string;
|
||||
dashboardId?: string;
|
||||
};
|
||||
|
||||
function formatModel(model: string): string {
|
||||
@@ -22,13 +22,13 @@ function formatModel(model: string): string {
|
||||
}
|
||||
|
||||
export const ChatHeader = () => {
|
||||
const { workspaceId } = useWorkspace();
|
||||
const contextFilter = workspaceId === 'email' || workspaceId === 'screens/email'
|
||||
const { dashboardId } = useWorkspace();
|
||||
const contextFilter = dashboardId === 'email' || dashboardId === 'screens/email'
|
||||
? { context: 'email' as const }
|
||||
: workspaceId?.startsWith('proj-layout-')
|
||||
? { context: 'project' as const, contextId: workspaceId.replace('proj-layout-', '') }
|
||||
: workspaceId && !workspaceId.startsWith('screens/')
|
||||
? { context: 'workspace' as const, contextId: workspaceId }
|
||||
: dashboardId?.startsWith('proj-layout-')
|
||||
? { context: 'project' as const, contextId: dashboardId.replace('proj-layout-', '') }
|
||||
: dashboardId && !dashboardId.startsWith('screens/')
|
||||
? { context: 'dashboard' as const, contextId: dashboardId }
|
||||
: undefined;
|
||||
const { sessions, clearSessions } = useChatSessions(contextFilter);
|
||||
const [selection, setSelection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
|
||||
@@ -38,25 +38,25 @@ export const ChatHeader = () => {
|
||||
|
||||
// Clear stale selection from a different workspace
|
||||
useEffect(() => {
|
||||
if (selection && selection.workspaceId !== workspaceId) {
|
||||
if (selection && selection.dashboardId !== dashboardId) {
|
||||
setSelection(null);
|
||||
autoResumedRef.current = false;
|
||||
}
|
||||
}, [workspaceId, selection]);
|
||||
}, [dashboardId, selection]);
|
||||
|
||||
// Auto-resume the latest session for this workspace
|
||||
useEffect(() => {
|
||||
if (autoResumedRef.current) return;
|
||||
if (selection && selection.workspaceId === workspaceId) return;
|
||||
if (selection && selection.dashboardId === dashboardId) return;
|
||||
if (sessions.length > 0) {
|
||||
const latest = sessions[0]!;
|
||||
setSelection({ sessionId: latest.id, model: latest.model ?? null, workspaceId });
|
||||
setSelection({ sessionId: latest.id, model: latest.model ?? null, dashboardId });
|
||||
autoResumedRef.current = true;
|
||||
}
|
||||
}, [sessions, selection, workspaceId]);
|
||||
}, [sessions, selection, dashboardId]);
|
||||
|
||||
const selectSession = (sessionId: string | null, model?: string | null) => {
|
||||
setSelection({ sessionId, model, workspaceId });
|
||||
setSelection({ sessionId, model, dashboardId });
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { EmbeddableChat } from './EmbeddableChat';
|
||||
type ChatSessionSelection = {
|
||||
sessionId: string | null;
|
||||
model?: string | null;
|
||||
workspaceId?: string;
|
||||
dashboardId?: string;
|
||||
};
|
||||
|
||||
type ChatPanelInnerProps = {
|
||||
@@ -45,30 +45,34 @@ const ChatPanelInner = ({ sessionId, model, scoped, sandboxed, cwdParam, promptP
|
||||
};
|
||||
|
||||
export const ChatPanelWrapper = () => {
|
||||
const { workspaceId, cwd, root, promptPrefix } = useWorkspace();
|
||||
const { dashboardId, cwd, root, promptPrefix } = useWorkspace();
|
||||
const scoped = cwd !== '~';
|
||||
const hostRoot = root === '~' || root === 'officer.dev';
|
||||
const sandboxed = !hostRoot;
|
||||
|
||||
const chatContext = workspaceId === 'email' || workspaceId === 'screens/email'
|
||||
const chatContext = dashboardId === 'email' || dashboardId === 'screens/email'
|
||||
? { context: 'email' as const }
|
||||
: workspaceId?.startsWith('proj-layout-')
|
||||
? { context: 'project' as const, contextId: workspaceId.replace('proj-layout-', '') }
|
||||
: workspaceId && !workspaceId.startsWith('screens/')
|
||||
? { context: 'workspace' as const, contextId: workspaceId }
|
||||
: dashboardId?.startsWith('proj-layout-')
|
||||
? { context: 'project' as const, contextId: dashboardId.replace('proj-layout-', '') }
|
||||
: dashboardId && !dashboardId.startsWith('screens/')
|
||||
? { context: 'dashboard' as const, contextId: dashboardId }
|
||||
: {};
|
||||
|
||||
const [selection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
|
||||
const [, setActiveSession] = usePanelChannel<string | null>('chat:active-session', null);
|
||||
const [, setPreviewRefresh] = usePanelChannel<number>('preview:refresh', 0);
|
||||
const [, setFilesRefresh] = usePanelChannel<number>('files:refresh-signal', 0);
|
||||
|
||||
const onTurnComplete = useCallback((hadToolCalls: boolean) => {
|
||||
if (hadToolCalls) setPreviewRefresh(Date.now());
|
||||
}, [setPreviewRefresh]);
|
||||
if (hadToolCalls) {
|
||||
setPreviewRefresh(Date.now());
|
||||
setFilesRefresh(Date.now());
|
||||
}
|
||||
}, [setPreviewRefresh, setFilesRefresh]);
|
||||
|
||||
const cwdParam = scoped ? { root, path: cwd } : undefined;
|
||||
|
||||
const isCurrentWorkspace = !selection?.workspaceId || selection.workspaceId === workspaceId;
|
||||
const isCurrentWorkspace = !selection?.dashboardId || selection.dashboardId === dashboardId;
|
||||
const sessionId = isCurrentWorkspace ? selection?.sessionId ?? undefined : undefined;
|
||||
const model = isCurrentWorkspace ? selection?.model ?? undefined : undefined;
|
||||
|
||||
|
||||
@@ -22,10 +22,11 @@ type EmbeddableChatProps = {
|
||||
chat?: UsePiChatType;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
onMessageComplete?: () => void;
|
||||
};
|
||||
|
||||
export const EmbeddableChat = ({ className, ...params }: EmbeddableChatProps) => {
|
||||
const manager = useEmbeddableChat(params);
|
||||
export const EmbeddableChat = ({ className, onMessageComplete, ...params }: EmbeddableChatProps) => {
|
||||
const manager = useEmbeddableChat(params, onMessageComplete);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col ${className ?? ''}`}>
|
||||
|
||||
@@ -25,7 +25,7 @@ type UseEmbeddableChatParams = {
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export function useEmbeddableChat(params: UseEmbeddableChatParams) {
|
||||
export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComplete?: () => void) {
|
||||
const { initialMessage, defaultInput = '', promptPrefix, cwd, sandboxed, autoSend = false, chat: externalChat } = params;
|
||||
|
||||
const internalChat = usePiChat(params.sessionId, params.initialModel, { replaceUrl: params.replaceUrl ?? false, context: params.context, contextId: params.contextId });
|
||||
@@ -51,6 +51,17 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams) {
|
||||
const attachmentManager = useAttachments({ sessionId });
|
||||
const slashCommands = useSlashCommands({ sessionId });
|
||||
|
||||
// Detect when agent finishes a message (isGenerating: true → false)
|
||||
const wasGeneratingRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (isGenerating) {
|
||||
wasGeneratingRef.current = true;
|
||||
} else if (wasGeneratingRef.current) {
|
||||
wasGeneratingRef.current = false;
|
||||
onMessageComplete?.();
|
||||
}
|
||||
}, [isGenerating]);
|
||||
|
||||
const [input, setInput] = useState(defaultInput);
|
||||
const [showJumpToBottom, setShowJumpToBottom] = useState(false);
|
||||
const [commandFeedback, setCommandFeedback] = useState<string | null>(null);
|
||||
|
||||
@@ -13,12 +13,12 @@ import { SessionContextMenu } from './SessionContextMenu';
|
||||
|
||||
export const SessionList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { workspaceId } = useWorkspace();
|
||||
const { dashboardId } = useWorkspace();
|
||||
// Only scope sessions for actual project/workspace contexts, not screen layout IDs like 'screens/chat'
|
||||
const contextFilter = workspaceId?.startsWith('proj-layout-')
|
||||
? { context: 'project' as const, contextId: workspaceId.replace('proj-layout-', '') }
|
||||
: workspaceId && !workspaceId.startsWith('screens/')
|
||||
? { context: 'workspace' as const, contextId: workspaceId }
|
||||
const contextFilter = dashboardId?.startsWith('proj-layout-')
|
||||
? { context: 'project' as const, contextId: dashboardId.replace('proj-layout-', '') }
|
||||
: dashboardId && !dashboardId.startsWith('screens/')
|
||||
? { context: 'dashboard' as const, contextId: dashboardId }
|
||||
: undefined;
|
||||
const { sessions, deleteSession } = useChatSessions(contextFilter);
|
||||
const { groups } = useChatGroups();
|
||||
|
||||
+38
-38
@@ -6,8 +6,8 @@ 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 type { WorkspaceDefinition } from '../../components/Workspace';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import type { DashboardDefinition } from '../../components/Workspace';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -19,39 +19,39 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
SELECTED_WORKSPACE_KEY,
|
||||
CREATING_WORKSPACE_KEY,
|
||||
EDITING_WORKSPACE_KEY,
|
||||
NEW_WS_NAME_KEY,
|
||||
NEW_WS_DESC_KEY,
|
||||
NEW_WS_TEMPLATE_KEY,
|
||||
SELECTED_DASHBOARD_KEY,
|
||||
CREATING_DASHBOARD_KEY,
|
||||
EDITING_DASHBOARD_KEY,
|
||||
NEW_DASH_NAME_KEY,
|
||||
NEW_DASH_DESC_KEY,
|
||||
NEW_DASH_TEMPLATE_KEY,
|
||||
} from './constants';
|
||||
|
||||
export const WorkspaceListApp = () => {
|
||||
export const DashboardListApp = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { value: workspaces, setValue: setWorkspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const [selected, setSelected] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
|
||||
const [, setCreating] = useGlobal<boolean>(CREATING_WORKSPACE_KEY, false);
|
||||
const [, setEditing] = useGlobal<string | null>(EDITING_WORKSPACE_KEY, null);
|
||||
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 { value: dashboards, setValue: setDashboards } = useDashboardState<DashboardDefinition[]>('workspaces', []);
|
||||
const [selected, setSelected] = useGlobal<string | null>(SELECTED_DASHBOARD_KEY, null);
|
||||
const [, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
|
||||
const [, setEditing] = useGlobal<string | null>(EDITING_DASHBOARD_KEY, null);
|
||||
const [, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
|
||||
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
||||
const [, setTemplateIdx] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
|
||||
const [, setFilePath] = useUserState<string>('files/currentPath', '/');
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [deleting, setDeleting] = useState<WorkspaceDefinition | null>(null);
|
||||
const isWorkspacesPage = location.pathname === '/workspaces';
|
||||
const [deleting, setDeleting] = useState<DashboardDefinition | null>(null);
|
||||
const isDashboardsPage = location.pathname === '/dashboards';
|
||||
const filtered = search
|
||||
? workspaces.filter((ws) => {
|
||||
? dashboards.filter((ws) => {
|
||||
const q = search.toLowerCase();
|
||||
return [ws.name, ws.id, ws.description ?? '', ws.cwd ?? ''].some((field) => field.toLowerCase().includes(q));
|
||||
})
|
||||
: workspaces;
|
||||
: dashboards;
|
||||
|
||||
const handleEdit = (ev: React.MouseEvent, ws: WorkspaceDefinition) => {
|
||||
const handleEdit = (ev: React.MouseEvent, ws: DashboardDefinition) => {
|
||||
ev.stopPropagation();
|
||||
setSelected(null);
|
||||
setCreating(false);
|
||||
@@ -63,31 +63,31 @@ export const WorkspaceListApp = () => {
|
||||
setFilePath(cwdPath);
|
||||
};
|
||||
|
||||
const handleDelete = (ev: React.MouseEvent, ws: WorkspaceDefinition) => {
|
||||
const handleDelete = (ev: React.MouseEvent, ws: DashboardDefinition) => {
|
||||
ev.stopPropagation();
|
||||
setDeleting(ws);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (!deleting) return;
|
||||
setWorkspaces((prev) => prev.filter((w) => w.id !== deleting.id));
|
||||
setDashboards((prev) => prev.filter((w) => w.id !== deleting.id));
|
||||
if (selected === deleting.id) setSelected(null);
|
||||
|
||||
const layoutKey = `ws-layout-${deleting.id}`;
|
||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['DASHBOARD_STATE']) ?? {};
|
||||
const { [layoutKey]: _, ...rest } = currentState;
|
||||
queryClient.setQueryData(['WORKSPACES_STATE'], rest);
|
||||
client.patch('/workspaces', { [layoutKey]: null }).catch(() => {});
|
||||
queryClient.setQueryData(['DASHBOARD_STATE'], rest);
|
||||
client.patch('/dashboards', { [layoutKey]: null }).catch(() => {});
|
||||
setDeleting(null);
|
||||
};
|
||||
|
||||
const handleClick = (ws: WorkspaceDefinition) => {
|
||||
if (isWorkspacesPage) {
|
||||
const handleClick = (ws: DashboardDefinition) => {
|
||||
if (isDashboardsPage) {
|
||||
setSelected(ws.id);
|
||||
setCreating(false);
|
||||
setEditing(null);
|
||||
} else {
|
||||
navigate(`/workspaces/${ws.id}`);
|
||||
navigate(`/dashboards/${ws.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,11 +95,11 @@ export const WorkspaceListApp = () => {
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
<div className="p-3 pb-0 flex flex-col gap-2">
|
||||
<Link
|
||||
to="/workspaces"
|
||||
to="/dashboards"
|
||||
className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal"
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
Workspaces
|
||||
Dashboards
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
@@ -111,12 +111,12 @@ export const WorkspaceListApp = () => {
|
||||
setTemplateIdx(0);
|
||||
setFilePath('/');
|
||||
setCreating(true);
|
||||
if (!isWorkspacesPage) navigate('/workspaces');
|
||||
if (!isDashboardsPage) navigate('/dashboards');
|
||||
}}
|
||||
className="flex items-center justify-center gap-1 py-2 px-3 rounded-lg text-sm font-medium bg-duck-teal hover:bg-duck-teal/90 text-white transition-all cursor-pointer"
|
||||
>
|
||||
<Plus className="h-4 w-4 shrink-0" />
|
||||
New Workspace
|
||||
New Dashboard
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative px-3 pt-2">
|
||||
@@ -125,7 +125,7 @@ export const WorkspaceListApp = () => {
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
placeholder="Search workspaces"
|
||||
placeholder="Search dashboards"
|
||||
className="w-full rounded-lg border border-duck-dark/15 bg-transparent py-1.5 pl-8 pr-3 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-duck-teal/30 focus:border-duck-teal/40"
|
||||
/>
|
||||
</div>
|
||||
@@ -134,7 +134,7 @@ export const WorkspaceListApp = () => {
|
||||
<div
|
||||
key={ws.id}
|
||||
className={`flex items-center gap-2.5 py-2 px-3 rounded-lg text-sm font-medium cursor-pointer group ${
|
||||
isWorkspacesPage && selected === ws.id
|
||||
isDashboardsPage && selected === ws.id
|
||||
? 'bg-duck-teal/10 text-duck-teal'
|
||||
: 'text-white/80 hover:bg-duck-dark/5'
|
||||
}`}
|
||||
@@ -142,7 +142,7 @@ export const WorkspaceListApp = () => {
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4 shrink-0" />
|
||||
<span className="flex-1 text-left truncate">{ws.name}</span>
|
||||
{isWorkspacesPage && (
|
||||
{isDashboardsPage && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
@@ -164,7 +164,7 @@ export const WorkspaceListApp = () => {
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<p className="text-xs text-gray-500 px-3 py-4 text-center">
|
||||
{search ? 'No matches' : 'No workspaces yet'}
|
||||
{search ? 'No matches' : 'No dashboards yet'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -172,7 +172,7 @@ export const WorkspaceListApp = () => {
|
||||
<AlertDialog open={deleting !== null} onOpenChange={(open) => { if (!open) setDeleting(null); }}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete workspace</AlertDialogTitle>
|
||||
<AlertDialogTitle>Delete dashboard</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete <strong>{deleting?.name}</strong>? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
+62
-62
@@ -5,20 +5,20 @@ 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 { useDashboardState } from 'state/useDashboardState';
|
||||
import { WorkspaceLayout, WorkspaceView, createDefaultLayout } from '../../components/Workspace';
|
||||
import type { LayoutNode, WorkspaceDefinition, HomeRoot } from '../../components/Workspace';
|
||||
import type { LayoutNode, DashboardDefinition, HomeRoot } from '../../components/Workspace';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { generateSlug, slugify } from 'helpers/slug';
|
||||
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,
|
||||
SELECTED_DASHBOARD_KEY,
|
||||
CREATING_DASHBOARD_KEY,
|
||||
EDITING_DASHBOARD_KEY,
|
||||
NEW_DASH_NAME_KEY,
|
||||
NEW_DASH_DESC_KEY,
|
||||
NEW_DASH_TEMPLATE_KEY,
|
||||
} from './constants';
|
||||
|
||||
// --- Layout Templates ---
|
||||
@@ -162,7 +162,7 @@ const ThumbnailNode = ({ node }: { node: LayoutNode }) => {
|
||||
|
||||
const TemplateCell = ({ index }: { index: number }) => {
|
||||
const tpl = templates[index]!;
|
||||
const [selected, setSelected] = useGlobal<number>(NEW_WS_TEMPLATE_KEY, 0);
|
||||
const [selected, setSelected] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
|
||||
const node = tpl.layout();
|
||||
const isSelected = selected === index;
|
||||
|
||||
@@ -184,7 +184,7 @@ const TemplateCell = ({ index }: { index: number }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// --- Panel: Template Picker (3x2 workspace) ---
|
||||
// --- Panel: Template Picker (3x2 layout) ---
|
||||
|
||||
const tplPanelLayout: LayoutNode = {
|
||||
type: 'group',
|
||||
@@ -231,8 +231,8 @@ const TemplatePanel = () => (
|
||||
// --- Panel: Name ---
|
||||
|
||||
const NamePanel = () => {
|
||||
const [name, setName] = useGlobal<string>(NEW_WS_NAME_KEY, '');
|
||||
const [description, setDescription] = useGlobal<string>(NEW_WS_DESC_KEY, '');
|
||||
const [name, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
|
||||
const [description, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
@@ -246,7 +246,7 @@ const NamePanel = () => {
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(ev) => setName(ev.target.value)}
|
||||
placeholder="My Workspace"
|
||||
placeholder="My Dashboard"
|
||||
autoFocus
|
||||
className="rounded-lg border border-duck-dark/20 bg-background px-3 py-2 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
|
||||
/>
|
||||
@@ -259,7 +259,7 @@ const NamePanel = () => {
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(ev) => setDescription(ev.target.value)}
|
||||
placeholder="What is this workspace for?"
|
||||
placeholder="What is this dashboard for?"
|
||||
className="flex-1 min-h-0 resize-none rounded-lg border border-duck-dark/20 bg-background px-3 py-2 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
|
||||
/>
|
||||
</div>
|
||||
@@ -274,18 +274,18 @@ const CreatePanel = () => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const client = useClient();
|
||||
const [name, setName] = useGlobal<string>(NEW_WS_NAME_KEY, '');
|
||||
const [name, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
|
||||
const [filePath] = useUserState<string>('files/currentPath', '/');
|
||||
const [homeRoot] = useUserState<HomeRoot>('files/homeRoot', 'home');
|
||||
const { value: workspaces, setValue: setWorkspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const { value: dashboards, setValue: setDashboards } = useDashboardState<DashboardDefinition[]>('workspaces', []);
|
||||
|
||||
const cwd = filePath === '/' ? '~' : `~/${filePath.replace(/^\//, '')}`;
|
||||
|
||||
const [description, setDescription] = useGlobal<string>(NEW_WS_DESC_KEY, '');
|
||||
const [templateIdx, setTemplateIdx] = useGlobal<number>(NEW_WS_TEMPLATE_KEY, 0);
|
||||
const [editingId, setEditingId] = useGlobal<string | null>(EDITING_WORKSPACE_KEY, null);
|
||||
const [, setCreating] = useGlobal<boolean>(CREATING_WORKSPACE_KEY, false);
|
||||
const [, setSelected] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
|
||||
const [description, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
||||
const [templateIdx, setTemplateIdx] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
|
||||
const [editingId, setEditingId] = useGlobal<string | null>(EDITING_DASHBOARD_KEY, null);
|
||||
const [, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
|
||||
const [, setSelected] = useGlobal<string | null>(SELECTED_DASHBOARD_KEY, null);
|
||||
|
||||
const isEditing = !!editingId;
|
||||
|
||||
@@ -295,13 +295,13 @@ const CreatePanel = () => {
|
||||
const desc = description.trim();
|
||||
|
||||
if (isEditing) {
|
||||
const existingIds = new Set(workspaces.filter((w) => w.id !== editingId).map((w) => w.id));
|
||||
const existingIds = new Set(dashboards.filter((w) => w.id !== editingId).map((w) => w.id));
|
||||
let newId = slugify(trimmed) || generateSlug();
|
||||
while (existingIds.has(newId)) newId = `${newId}-${generateSlug(1)}`;
|
||||
|
||||
const idChanged = newId !== editingId;
|
||||
|
||||
setWorkspaces((prev) =>
|
||||
setDashboards((prev) =>
|
||||
prev.map((ws) =>
|
||||
ws.id === editingId
|
||||
? { ...ws, id: newId, name: trimmed, description: desc || undefined, templateIdx }
|
||||
@@ -310,31 +310,31 @@ const CreatePanel = () => {
|
||||
);
|
||||
|
||||
const wsLayout = templates[templateIdx]?.layout() ?? createDefaultLayout();
|
||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['DASHBOARD_STATE']) ?? {};
|
||||
const newLayoutKey = `ws-layout-${newId}`;
|
||||
|
||||
if (idChanged) {
|
||||
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 });
|
||||
queryClient.setQueryData(['DASHBOARD_STATE'], { ...cleaned, [newLayoutKey]: wsLayout });
|
||||
|
||||
const patch: Record<string, unknown> = { [newLayoutKey]: wsLayout };
|
||||
for (const k of oldKeys) patch[k] = null;
|
||||
client.patch('/workspaces', patch).catch(() => {});
|
||||
client.patch('/dashboards', patch).catch(() => {});
|
||||
} else {
|
||||
queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [newLayoutKey]: wsLayout });
|
||||
client.patch('/workspaces', { [newLayoutKey]: wsLayout }).catch(() => {});
|
||||
queryClient.setQueryData(['DASHBOARD_STATE'], { ...currentState, [newLayoutKey]: wsLayout });
|
||||
client.patch('/dashboards', { [newLayoutKey]: wsLayout }).catch(() => {});
|
||||
}
|
||||
|
||||
setEditingId(null);
|
||||
setSelected(newId);
|
||||
} else {
|
||||
const existingIds = new Set(workspaces.map((w) => w.id));
|
||||
const existingIds = new Set(dashboards.map((w) => w.id));
|
||||
let id = slugify(trimmed) || generateSlug();
|
||||
while (existingIds.has(id)) id = `${id}-${generateSlug(1)}`;
|
||||
|
||||
const ws: WorkspaceDefinition = {
|
||||
const ws: DashboardDefinition = {
|
||||
id,
|
||||
name: trimmed,
|
||||
cwd,
|
||||
@@ -345,15 +345,15 @@ const CreatePanel = () => {
|
||||
const wsLayout = templates[templateIdx]?.layout() ?? createDefaultLayout();
|
||||
const layoutKey = `ws-layout-${ws.id}`;
|
||||
|
||||
setWorkspaces((prev) => [...prev, ws]);
|
||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
||||
queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [layoutKey]: wsLayout });
|
||||
client.patch('/workspaces', { [layoutKey]: wsLayout }).catch(() => {});
|
||||
setDashboards((prev) => [...prev, ws]);
|
||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['DASHBOARD_STATE']) ?? {};
|
||||
queryClient.setQueryData(['DASHBOARD_STATE'], { ...currentState, [layoutKey]: wsLayout });
|
||||
client.patch('/dashboards', { [layoutKey]: wsLayout }).catch(() => {});
|
||||
|
||||
setName('');
|
||||
setDescription('');
|
||||
setTemplateIdx(0);
|
||||
navigate(`/workspaces/${ws.id}`);
|
||||
navigate(`/dashboards/${ws.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -371,16 +371,16 @@ const CreatePanel = () => {
|
||||
className="bg-duck-teal hover:bg-duck-teal/90 cursor-pointer disabled:opacity-40"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{isEditing ? 'Update Workspace' : 'Create Workspace'}
|
||||
{isEditing ? 'Update Dashboard' : 'Create Dashboard'}
|
||||
</Button>
|
||||
{isEditing && (
|
||||
<Button
|
||||
onClick={() => navigate(`/workspaces/${editingId}`)}
|
||||
onClick={() => navigate(`/dashboards/${editingId}`)}
|
||||
variant="outline"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4 mr-1" />
|
||||
Open Workspace
|
||||
Open Dashboard
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -396,9 +396,9 @@ const CwdFileBrowser = () => {
|
||||
return <FileBrowserApp basePath={basePath} />;
|
||||
};
|
||||
|
||||
// --- New Workspace Layout ---
|
||||
// --- New Dashboard Layout ---
|
||||
|
||||
const newWsLayout: LayoutNode = {
|
||||
const newDashLayout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'new-ws-root',
|
||||
direction: 'vertical',
|
||||
@@ -430,9 +430,9 @@ const newWsLayout: LayoutNode = {
|
||||
],
|
||||
};
|
||||
|
||||
const NewWorkspaceForm = () => {
|
||||
const NewDashboardForm = () => {
|
||||
const { registry } = useAppRegistry();
|
||||
const newWsRegistry = {
|
||||
const newDashRegistry = {
|
||||
...registry,
|
||||
'file-browser-cwd': { name: 'File Browser', icon: FolderOpen, component: CwdFileBrowser },
|
||||
'new-ws-name': { name: 'Name', icon: Type, component: NamePanel },
|
||||
@@ -440,20 +440,20 @@ const NewWorkspaceForm = () => {
|
||||
'new-ws-create': { name: 'Create', icon: Rocket, component: CreatePanel },
|
||||
};
|
||||
|
||||
return <WorkspaceLayout layout={newWsLayout} onLayoutChange={() => {}} registry={newWsRegistry} noHeader />;
|
||||
return <WorkspaceLayout layout={newDashLayout} onLayoutChange={() => {}} registry={newDashRegistry} noHeader />;
|
||||
};
|
||||
|
||||
// --- Empty State ---
|
||||
|
||||
const WorkspacePreviewEmpty = () => {
|
||||
const [creating, setCreating] = useGlobal<boolean>(CREATING_WORKSPACE_KEY, false);
|
||||
const [editingId] = useGlobal<string | null>(EDITING_WORKSPACE_KEY, null);
|
||||
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 DashboardPreviewEmpty = () => {
|
||||
const [creating, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
|
||||
const [editingId] = useGlobal<string | null>(EDITING_DASHBOARD_KEY, null);
|
||||
const [, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
|
||||
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
||||
const [, setTemplateIdx] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
|
||||
const [, setFilePath] = useUserState<string>('files/currentPath', '/');
|
||||
|
||||
if (creating || editingId) return <NewWorkspaceForm />;
|
||||
if (creating || editingId) return <NewDashboardForm />;
|
||||
|
||||
const handleCreate = () => {
|
||||
setName(generateSlug());
|
||||
@@ -467,10 +467,10 @@ const WorkspacePreviewEmpty = () => {
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4">
|
||||
<LayoutGrid className="h-10 w-10 text-white/80" />
|
||||
<p className="text-sm text-white/80">Select a workspace or create a new one</p>
|
||||
<p className="text-sm text-white/80">Select a dashboard or create a new one</p>
|
||||
<Button onClick={handleCreate} className="bg-duck-teal hover:bg-duck-teal/90 cursor-pointer">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
New Workspace
|
||||
New Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -479,26 +479,26 @@ const WorkspacePreviewEmpty = () => {
|
||||
|
||||
// --- Preview Inner ---
|
||||
|
||||
const WorkspacePreviewInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
|
||||
const ws = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
||||
const DashboardPreviewInner = ({ dashboard }: { dashboard: DashboardDefinition }) => {
|
||||
const ws = useDashboardState<LayoutNode>(`ws-layout-${dashboard.id}`, createDefaultLayout());
|
||||
|
||||
return <WorkspaceView workspace={ws} cwd={workspace.cwd} root={workspace.root} />;
|
||||
return <WorkspaceView workspace={ws} cwd={dashboard.cwd} root={dashboard.root} />;
|
||||
};
|
||||
|
||||
// --- 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;
|
||||
export const DashboardPreview = () => {
|
||||
const [selectedId] = useGlobal<string | null>(SELECTED_DASHBOARD_KEY, null);
|
||||
const { value: dashboards } = useDashboardState<DashboardDefinition[]>('workspaces', []);
|
||||
const dashboard = selectedId ? dashboards.find((ws) => ws.id === selectedId) : null;
|
||||
|
||||
if (!workspace) return <WorkspacePreviewEmpty />;
|
||||
if (!dashboard) return <DashboardPreviewEmpty />;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<WorkspacePreviewInner key={workspace.id} workspace={workspace} />
|
||||
<DashboardPreviewInner key={dashboard.id} dashboard={dashboard} />
|
||||
<Link
|
||||
to={`/workspaces/${workspace.id}`}
|
||||
to={`/dashboards/${dashboard.id}`}
|
||||
className="absolute inset-0 z-10 flex items-center justify-center bg-transparent hover:bg-duck-dark/10 transition-colors group"
|
||||
>
|
||||
<ArrowRight className="h-16 w-16 text-duck-teal/0 group-hover:text-duck-teal/60 transition-colors" />
|
||||
@@ -0,0 +1,6 @@
|
||||
export const SELECTED_DASHBOARD_KEY = 'SELECTED_DASHBOARD';
|
||||
export const CREATING_DASHBOARD_KEY = 'CREATING_DASHBOARD';
|
||||
export const EDITING_DASHBOARD_KEY = 'EDITING_DASHBOARD';
|
||||
export const NEW_DASH_NAME_KEY = 'NEW_DASH_NAME';
|
||||
export const NEW_DASH_DESC_KEY = 'NEW_DASH_DESC';
|
||||
export const NEW_DASH_TEMPLATE_KEY = 'NEW_DASH_TEMPLATE';
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { AppRegistryMeta } from '../../AppRegistry';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { DashboardListApp } from './DashboardListApp';
|
||||
import { DashboardPreview } from './DashboardPreview';
|
||||
|
||||
export { DashboardListApp };
|
||||
export { DashboardPreview };
|
||||
export { SELECTED_DASHBOARD_KEY, CREATING_DASHBOARD_KEY, EDITING_DASHBOARD_KEY, NEW_DASH_NAME_KEY, NEW_DASH_DESC_KEY, NEW_DASH_TEMPLATE_KEY } from './constants';
|
||||
|
||||
export const appRegistryMetas: AppRegistryMeta[] = [
|
||||
{
|
||||
key: 'dashboard-list',
|
||||
name: 'Dashboards',
|
||||
icon: LayoutGrid,
|
||||
component: DashboardListApp
|
||||
},
|
||||
{
|
||||
key: 'dashboard-preview',
|
||||
name: 'Dashboard Preview',
|
||||
icon: LayoutGrid,
|
||||
component: DashboardPreview,
|
||||
availableOnPanel: false
|
||||
},
|
||||
];
|
||||
+2
-2
@@ -68,7 +68,7 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
||||
handlePlay,
|
||||
getMatchingTasks,
|
||||
handleRunTask,
|
||||
handleCreateWorkspace,
|
||||
handleCreateDashboard,
|
||||
fileScrollRef: scrollRef,
|
||||
} = fileBrowserManager;
|
||||
const { defaultSort } = fileBrowserManager;
|
||||
@@ -187,7 +187,7 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
||||
onPlay={handlePlay}
|
||||
matchingTasks={getMatchingTasks(entry.name, entry.type)}
|
||||
onRunTask={handleRunTask}
|
||||
onCreateWorkspace={handleCreateWorkspace}
|
||||
onCreateDashboard={handleCreateDashboard}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
+18
-14
@@ -52,7 +52,7 @@ export type FileItemProps = {
|
||||
onPlay: (entry: DirEntry) => void;
|
||||
matchingTasks: TaskSummary[];
|
||||
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
|
||||
onCreateWorkspace: (entry: DirEntry) => void;
|
||||
onCreateDashboard: (entry: DirEntry) => void;
|
||||
};
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
@@ -87,7 +87,7 @@ type MenuItemsProps = {
|
||||
onCopy: () => void;
|
||||
matchingTasks: TaskSummary[];
|
||||
onRunTask: (task: TaskSummary, entry: DirEntry) => void;
|
||||
onCreateWorkspace: (e: DirEntry) => void;
|
||||
onCreateDashboard: (e: DirEntry) => void;
|
||||
};
|
||||
|
||||
const DropdownMenuItems = ({
|
||||
@@ -108,7 +108,7 @@ const DropdownMenuItems = ({
|
||||
onCopy,
|
||||
matchingTasks,
|
||||
onRunTask,
|
||||
onCreateWorkspace,
|
||||
onCreateDashboard,
|
||||
}: MenuItemsProps) => {
|
||||
const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
|
||||
const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
|
||||
@@ -186,9 +186,9 @@ const DropdownMenuItems = ({
|
||||
{entry.type === 'directory' && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onCreateWorkspace(entry)} className="cursor-pointer">
|
||||
<DropdownMenuItem onClick={() => onCreateDashboard(entry)} className="cursor-pointer">
|
||||
<LayoutGrid className="mr-2 h-4 w-4" />
|
||||
Create Workspace here
|
||||
Create Dashboard here
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
@@ -234,7 +234,7 @@ const ContextMenuItems = ({
|
||||
onCopy,
|
||||
matchingTasks,
|
||||
onRunTask,
|
||||
onCreateWorkspace,
|
||||
onCreateDashboard,
|
||||
}: MenuItemsProps) => {
|
||||
const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
|
||||
const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
|
||||
@@ -312,9 +312,9 @@ const ContextMenuItems = ({
|
||||
{entry.type === 'directory' && (
|
||||
<>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={() => onCreateWorkspace(entry)} className="cursor-pointer">
|
||||
<ContextMenuItem onClick={() => onCreateDashboard(entry)} className="cursor-pointer">
|
||||
<LayoutGrid className="mr-2 h-4 w-4" />
|
||||
Create Workspace here
|
||||
Create Dashboard here
|
||||
</ContextMenuItem>
|
||||
</>
|
||||
)}
|
||||
@@ -367,9 +367,13 @@ const InlineRenameInput = ({
|
||||
onCancel: () => void;
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialName);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const focused = useRef(false);
|
||||
|
||||
const mountRef = (node: HTMLInputElement | null) => {
|
||||
if (!node) return;
|
||||
useEffect(() => {
|
||||
const node = inputRef.current;
|
||||
if (!node || focused.current) return;
|
||||
focused.current = true;
|
||||
requestAnimationFrame(() => {
|
||||
node.focus();
|
||||
const dotIndex = initialName.lastIndexOf('.');
|
||||
@@ -379,7 +383,7 @@ const InlineRenameInput = ({
|
||||
node.select();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const commit = () => {
|
||||
const trimmed = value.trim();
|
||||
@@ -392,7 +396,7 @@ const InlineRenameInput = ({
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={mountRef}
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
onChange={(ev) => setValue(ev.target.value)}
|
||||
onBlur={commit}
|
||||
@@ -459,7 +463,7 @@ export const FileItem = ({
|
||||
onPlay,
|
||||
matchingTasks,
|
||||
onRunTask,
|
||||
onCreateWorkspace,
|
||||
onCreateDashboard,
|
||||
}: FileItemProps) => {
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const clickTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -543,7 +547,7 @@ export const FileItem = ({
|
||||
onCopy,
|
||||
matchingTasks,
|
||||
onRunTask,
|
||||
onCreateWorkspace,
|
||||
onCreateDashboard,
|
||||
};
|
||||
|
||||
const cutOpacity = isCut ? 'opacity-50' : '';
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import { Loader2, Folder, ClipboardPaste, FolderPlus, LayoutGrid, Download, Upload, ClipboardCopy, MessageSquare } from 'lucide-react';
|
||||
import { Loader2, Folder, ClipboardPaste, FolderPlus, LayoutGrid, Upload, ClipboardCopy, MessageSquare } from 'lucide-react';
|
||||
import { getIcon } from 'material-file-icons';
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
||||
import type { UseFileBrowserAppType } from '../useFileBrowserApp';
|
||||
@@ -26,7 +26,7 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
|
||||
handleCopyCurrentPath,
|
||||
handleChatHere,
|
||||
handleCreateDir,
|
||||
handleCreateWorkspaceHere,
|
||||
handleCreateDashboardHere,
|
||||
setShowVideoDownload,
|
||||
} = fileBrowserManager;
|
||||
|
||||
@@ -120,14 +120,14 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
New folder
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={handleCreateWorkspaceHere} className="cursor-pointer">
|
||||
<ContextMenuItem onClick={handleCreateDashboardHere} className="cursor-pointer">
|
||||
<LayoutGrid className="mr-2 h-4 w-4" />
|
||||
Create Workspace here
|
||||
Create Dashboard here
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => setShowVideoDownload(true)} className="cursor-pointer">
|
||||
{/* <ContextMenuItem onClick={() => setShowVideoDownload(true)} className="cursor-pointer">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download video
|
||||
</ContextMenuItem>
|
||||
</ContextMenuItem> */}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)}
|
||||
|
||||
@@ -378,9 +378,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, initi
|
||||
setRunningTask({ task, entry });
|
||||
};
|
||||
|
||||
const handleCreateWorkspace = (entry: DirEntry) => {
|
||||
const handleCreateDashboard = (entry: DirEntry) => {
|
||||
const folderPath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
|
||||
navigate(`/workspaces/new?name=${encodeURIComponent(entry.name)}&cwd=${encodeURIComponent(folderPath)}`);
|
||||
navigate(`/dashboards/new?name=${encodeURIComponent(entry.name)}&cwd=${encodeURIComponent(folderPath)}`);
|
||||
};
|
||||
|
||||
const handleCopyPath = (entry: DirEntry) => {
|
||||
@@ -402,11 +402,11 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, initi
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateWorkspaceHere = () => {
|
||||
const handleCreateDashboardHere = () => {
|
||||
const dirName = currentPath === '/' ? '' : currentPath.split('/').pop()!;
|
||||
const params = new URLSearchParams({ cwd: currentPath });
|
||||
if (dirName) params.set('name', dirName);
|
||||
navigate(`/workspaces/new?${params}`);
|
||||
navigate(`/dashboards/new?${params}`);
|
||||
};
|
||||
|
||||
const handleReadAloud = async (entry: DirEntry) => {
|
||||
@@ -703,8 +703,8 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, initi
|
||||
handleDownload,
|
||||
handleDownloadSelected,
|
||||
handleRunTask,
|
||||
handleCreateWorkspace,
|
||||
handleCreateWorkspaceHere,
|
||||
handleCreateDashboard,
|
||||
handleCreateDashboardHere,
|
||||
handleReadAloud,
|
||||
handleOcr,
|
||||
handleTranscribe,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAuth } from 'hooks/useAuth';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { useWorkspace } from '../../components/Workspace';
|
||||
import type { ProjectDefinition } from '../../components/Workspace';
|
||||
import { useWorkspacesState } from 'state/useWorkspacesState';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { PreviewContext } from './PreviewContext';
|
||||
|
||||
type DevServerResponse = { url: string; port: number };
|
||||
@@ -17,7 +17,7 @@ export const PreviewProvider = ({ panelId, children }: PreviewProviderProps) =>
|
||||
const { cwd } = useWorkspace();
|
||||
const client = useClient();
|
||||
const { user } = useAuth();
|
||||
const { value: projects } = useWorkspacesState<ProjectDefinition[]>('projects', []);
|
||||
const { value: projects } = useDashboardState<ProjectDefinition[]>('projects', []);
|
||||
const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
const [port, setPort] = useState<number | null>(null);
|
||||
|
||||
@@ -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 { useWorkspacesState } from 'state/useWorkspacesState';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import type { ProjectDefinition, ProjectType } from '../../components/Workspace';
|
||||
import { PublishDialog } from './PublishDialog';
|
||||
import {
|
||||
@@ -41,7 +41,7 @@ export const ProjectListApp = () => {
|
||||
const navigate = useNavigate();
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { value: projects } = useWorkspacesState<ProjectDefinition[]>('projects', []);
|
||||
const { value: projects } = useDashboardState<ProjectDefinition[]>('projects', []);
|
||||
const [selected, setSelected] = useGlobal<string | null>(SELECTED_PROJECT, null);
|
||||
const [, setCreating] = useGlobal<boolean>(CREATING_PROJECT, false);
|
||||
const [, setEditing] = useGlobal<string | null>(EDITING_PROJECT, null);
|
||||
@@ -91,19 +91,19 @@ export const ProjectListApp = () => {
|
||||
const confirmDelete = () => {
|
||||
if (!deleting) return;
|
||||
// Optimistic: remove from cache immediately
|
||||
const current = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
||||
const current = queryClient.getQueryData<Record<string, unknown>>(['DASHBOARD_STATE']) ?? {};
|
||||
const optimistic = { ...current };
|
||||
optimistic['projects'] = (optimistic['projects'] as ProjectDefinition[]).filter((p) => p.id !== deleting.id);
|
||||
delete optimistic[`proj-layout-${deleting.id}`];
|
||||
delete optimistic[`proj-terminals-${deleting.id}`];
|
||||
delete optimistic[`proj-host-terminals-${deleting.id}`];
|
||||
queryClient.setQueryData(['WORKSPACES_STATE'], optimistic);
|
||||
queryClient.setQueryData(['DASHBOARD_STATE'], optimistic);
|
||||
|
||||
if (selected === deleting.id) setSelected(null);
|
||||
setDeleting(null);
|
||||
client
|
||||
.patch('/workspaces', { [`proj-meta-${deleting.id}`]: null })
|
||||
.then((res) => queryClient.setQueryData(['WORKSPACES_STATE'], res))
|
||||
.patch('/dashboards', { [`proj-meta-${deleting.id}`]: null })
|
||||
.then((res) => queryClient.setQueryData(['DASHBOARD_STATE'], res))
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FolderKanban, Plus, ArrowRight, Type, Rocket, FileText, Layout, Loader2
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useWorkspacesState } from 'state/useWorkspacesState';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { WorkspaceLayout, WorkspaceView, createDefaultLayout } from '../../components/Workspace';
|
||||
import type { LayoutNode, ProjectDefinition, ProjectType } from '../../components/Workspace';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -373,7 +373,7 @@ const CreatePanel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const client = useClient();
|
||||
const [name, setName] = useGlobal<string>(NEW_PROJ_NAME, '');
|
||||
const { value: projects } = useWorkspacesState<ProjectDefinition[]>('projects', []);
|
||||
const { value: projects } = useDashboardState<ProjectDefinition[]>('projects', []);
|
||||
|
||||
const [description, setDescription] = useGlobal<string>(NEW_PROJ_DESC, '');
|
||||
const [templateIdx, setTemplateIdx] = useGlobal<number>(NEW_PROJ_TEMPLATE, 0);
|
||||
@@ -405,8 +405,8 @@ const CreatePanel = () => {
|
||||
|
||||
if (isEditing) {
|
||||
client
|
||||
.patch('/workspaces', { [`proj-meta-${editingId}`]: meta, [`proj-layout-${editingId}`]: previewLayout })
|
||||
.then((res) => queryClient.setQueryData(['WORKSPACES_STATE'], res))
|
||||
.patch('/dashboards', { [`proj-meta-${editingId}`]: meta, [`proj-layout-${editingId}`]: previewLayout })
|
||||
.then((res) => queryClient.setQueryData(['DASHBOARD_STATE'], res))
|
||||
.catch(() => {});
|
||||
|
||||
setEditingId(null);
|
||||
@@ -422,8 +422,8 @@ const CreatePanel = () => {
|
||||
: previewLayout;
|
||||
|
||||
try {
|
||||
const res = await client.patch('/workspaces', { [`proj-meta-${id}`]: meta, [`proj-layout-${id}`]: finalLayout });
|
||||
queryClient.setQueryData(['WORKSPACES_STATE'], res);
|
||||
const res = await client.patch('/dashboards', { [`proj-meta-${id}`]: meta, [`proj-layout-${id}`]: finalLayout });
|
||||
queryClient.setQueryData(['DASHBOARD_STATE'], res);
|
||||
setName('');
|
||||
setDescription('');
|
||||
setTemplateIdx(0);
|
||||
@@ -576,13 +576,13 @@ const ProjectPreviewEmpty = () => {
|
||||
};
|
||||
|
||||
const ProjectPreviewInner = ({ project }: { project: ProjectDefinition }) => {
|
||||
const ws = useWorkspacesState<LayoutNode>(`proj-layout-${project.id}`, createDefaultLayout());
|
||||
const ws = useDashboardState<LayoutNode>(`proj-layout-${project.id}`, createDefaultLayout());
|
||||
return <WorkspaceView workspace={ws} cwd={project.cwd} />;
|
||||
};
|
||||
|
||||
export const ProjectPreview = () => {
|
||||
const [selectedId] = useGlobal<string | null>(SELECTED_PROJECT, null);
|
||||
const { value: projects } = useWorkspacesState<ProjectDefinition[]>('projects', []);
|
||||
const { value: projects } = useDashboardState<ProjectDefinition[]>('projects', []);
|
||||
const project = selectedId ? projects.find((p) => p.id === selectedId) : null;
|
||||
|
||||
if (!project) return <ProjectPreviewEmpty />;
|
||||
|
||||
@@ -24,7 +24,6 @@ export const appRegistryMetas: AppRegistryMeta[] = [
|
||||
name: 'Projects',
|
||||
icon: FolderKanban,
|
||||
component: ProjectListApp,
|
||||
availableOnPanel: false,
|
||||
},
|
||||
{
|
||||
key: 'project-preview',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useWorkspace } from '../../components/Workspace';
|
||||
import { useWorkspacesState } from 'state/useWorkspacesState';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { TerminalView } from './Terminal';
|
||||
|
||||
const EMPTY_TERMINALS: Record<string, string> = {};
|
||||
@@ -12,9 +12,9 @@ type CommandTerminalWrapperProps = {
|
||||
};
|
||||
|
||||
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 { dashboardId, cwd } = useWorkspace();
|
||||
const stateKey = dashboardId ? `ws-${statePrefix}-${dashboardId}` : `ws-${statePrefix}-default`;
|
||||
const { value: terminals, setValue: setTerminals } = useDashboardState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const setTerminalsRef = useRef(setTerminals);
|
||||
setTerminalsRef.current = setTerminals;
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useWorkspace } from '../../components/Workspace';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useWorkspacesState } from 'state/useWorkspacesState';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
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 { dashboardId, cwd } = useWorkspace();
|
||||
const stateKey = dashboardId ? `ws-host-terminals-${dashboardId}` : 'ws-host-terminals-default';
|
||||
const { value: terminals, setValue: setTerminals } = useDashboardState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const setTerminalsRef = useRef(setTerminals);
|
||||
setTerminalsRef.current = setTerminals;
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useWorkspace } from '../../components/Workspace';
|
||||
import { useWorkspacesState } from 'state/useWorkspacesState';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { TerminalView } from './Terminal';
|
||||
import { useTerminalMode } from './useTerminalMode';
|
||||
|
||||
const EMPTY_TERMINALS: Record<string, string> = {};
|
||||
|
||||
export const TerminalWrapper = ({ panelId }: { panelId: string }) => {
|
||||
const { workspaceId, cwd, root } = useWorkspace();
|
||||
const { dashboardId, cwd, root } = useWorkspace();
|
||||
const { mode } = useTerminalMode(panelId);
|
||||
const hostRoot = root === '~' || root === 'officer.dev';
|
||||
const sandboxed = !hostRoot && (cwd !== '~' || 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 stateKey = dashboardId ? `ws-terminals-${mode}-${dashboardId}` : `ws-terminals-${mode}-default`;
|
||||
const { value: terminals, setValue: setTerminals } = useDashboardState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const setTerminalsRef = useRef(setTerminals);
|
||||
setTerminalsRef.current = setTerminals;
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export const SELECTED_WORKSPACE_KEY = 'SELECTED_WORKSPACE';
|
||||
export const CREATING_WORKSPACE_KEY = 'CREATING_WORKSPACE';
|
||||
export const EDITING_WORKSPACE_KEY = 'EDITING_WORKSPACE';
|
||||
export const NEW_WS_NAME_KEY = 'NEW_WS_NAME';
|
||||
export const NEW_WS_DESC_KEY = 'NEW_WS_DESC';
|
||||
export const NEW_WS_TEMPLATE_KEY = 'NEW_WS_TEMPLATE';
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
},
|
||||
];
|
||||
@@ -8,7 +8,7 @@ export type DefaultFileSort = {
|
||||
};
|
||||
|
||||
type WorkspaceContextValue = {
|
||||
workspaceId: string | null;
|
||||
dashboardId: string | null;
|
||||
cwd: string;
|
||||
root?: string;
|
||||
initialFilePath?: string;
|
||||
@@ -30,7 +30,7 @@ type WorkspaceContextValue = {
|
||||
const noop = () => {};
|
||||
|
||||
const WorkspaceContext = createContext<WorkspaceContextValue>({
|
||||
workspaceId: null,
|
||||
dashboardId: null,
|
||||
cwd: '~',
|
||||
swapSourceId: null,
|
||||
setSwapSourceId: noop,
|
||||
|
||||
@@ -10,7 +10,7 @@ type WorkspaceLayoutProps = {
|
||||
onLayoutChange: (layout: LayoutNode) => void;
|
||||
registry?: AppRegistry;
|
||||
components?: PanelComponents;
|
||||
workspaceId?: string;
|
||||
dashboardId?: string;
|
||||
cwd?: string;
|
||||
promptPrefix?: string;
|
||||
noHeader?: boolean;
|
||||
@@ -21,7 +21,7 @@ type WorkspaceLayoutProps = {
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const WorkspaceLayout = ({ layout, onLayoutChange, registry: registryProp, components, workspaceId, cwd, promptPrefix, noHeader, isMobile, mobilePanelId, onMobileBack }: WorkspaceLayoutProps) => {
|
||||
export const WorkspaceLayout = ({ layout, onLayoutChange, registry: registryProp, components, dashboardId, cwd, promptPrefix, noHeader, isMobile, mobilePanelId, onMobileBack }: WorkspaceLayoutProps) => {
|
||||
const { registry: globalRegistry } = useAppRegistry();
|
||||
const registry = registryProp ?? globalRegistry;
|
||||
|
||||
@@ -33,7 +33,7 @@ export const WorkspaceLayout = ({ layout, onLayoutChange, registry: registryProp
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceProvider value={{ workspaceId: workspaceId ?? null, cwd: cwd ?? '~', promptPrefix, swapSourceId: null, setSwapSourceId: noop, onSwap: noop, dragSourceId: null, setDragSourceId: noop, onMove: noop, maximizedPanelId: null, setMaximizedPanelId: noop, transitioningPanelId: null, isMobile: isMobile ?? false, onMobileBack: onMobileBack ?? null }}>
|
||||
<WorkspaceProvider value={{ dashboardId: dashboardId ?? null, cwd: cwd ?? '~', promptPrefix, swapSourceId: null, setSwapSourceId: noop, onSwap: noop, dragSourceId: null, setDragSourceId: noop, onMove: noop, maximizedPanelId: null, setMaximizedPanelId: noop, transitioningPanelId: null, isMobile: isMobile ?? false, onMobileBack: onMobileBack ?? null }}>
|
||||
<WorkspaceRenderer
|
||||
layout={layout}
|
||||
registry={registry}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef, useMemo, type ComponentRef }
|
||||
import { flushSync } from 'react-dom';
|
||||
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '@/components/ui/resizable';
|
||||
import { useIsMobile } from 'hooks/useIsMobile';
|
||||
import type { LayoutNode, WorkspaceState, EphemeralPanels, PanelComponents } from './types';
|
||||
import type { LayoutNode, DashboardState, EphemeralPanels, PanelComponents } from './types';
|
||||
import type { DefaultFileSort } from './WorkspaceContext';
|
||||
import type { DropPosition } from './layout-utils';
|
||||
import { splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, countPanels } from './layout-utils';
|
||||
@@ -11,7 +11,7 @@ import { WorkspaceRenderer } from './WorkspaceRenderer';
|
||||
import { useAppRegistry } from '../../AppRegistry/useAppRegistry';
|
||||
|
||||
type WorkspaceViewProps = {
|
||||
workspace: WorkspaceState;
|
||||
workspace: DashboardState;
|
||||
locked?: boolean;
|
||||
cwd?: string;
|
||||
root?: string;
|
||||
@@ -143,7 +143,7 @@ export const WorkspaceView = ({ workspace, locked, cwd = '~', root, initialFileP
|
||||
return (
|
||||
<WorkspaceProvider
|
||||
value={{
|
||||
workspaceId: workspace.key,
|
||||
dashboardId: workspace.key,
|
||||
cwd,
|
||||
root,
|
||||
initialFilePath,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, WorkspaceState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry, EphemeralPanels, HomeRoot } from './types';
|
||||
export type { LayoutNode, LayoutGroup, LayoutPanel, DashboardDefinition, DashboardState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry, EphemeralPanels, HomeRoot } from './types';
|
||||
export type { DropPosition } from './layout-utils';
|
||||
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './layout-utils';
|
||||
export type { DefaultFileSort } from './WorkspaceContext';
|
||||
|
||||
@@ -19,7 +19,7 @@ export type LayoutNode = LayoutGroup | LayoutPanel;
|
||||
|
||||
export type HomeRoot = 'home' | '~' | 'officer.dev';
|
||||
|
||||
export type WorkspaceDefinition = {
|
||||
export type DashboardDefinition = {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
@@ -28,7 +28,7 @@ export type WorkspaceDefinition = {
|
||||
templateIdx?: number;
|
||||
};
|
||||
|
||||
export type WorkspaceState = {
|
||||
export type DashboardState = {
|
||||
key: string;
|
||||
value: LayoutNode;
|
||||
setValue: (value: LayoutNode | ((prev: LayoutNode) => LayoutNode)) => void;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { useUserState } from 'state/useUserState';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { FileViewerProvider } from '../../apps/FileViewer';
|
||||
import { EmbeddableChat } from '../../apps/Chat/EmbeddableChat';
|
||||
|
||||
@@ -66,6 +67,7 @@ export function Ephemeral2Provider({ children }: { children: ReactNode }) {
|
||||
export const ChatEphemeralBody = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [homeRoot] = useUserState<string>('files/homeRoot', 'home');
|
||||
const [, setRefreshSignal] = usePanelChannel<number>('files:refresh-signal', 0);
|
||||
const chatContext = searchParams.get('chatContext') ?? '';
|
||||
const chatType = searchParams.get('chatType') as 'file' | 'folder' | null;
|
||||
const hostRoot = homeRoot === '~' || homeRoot === 'officer.dev';
|
||||
@@ -81,12 +83,17 @@ export const ChatEphemeralBody = () => {
|
||||
? `[${tag}: ${path}] Let's talk about this file`
|
||||
: `[${tag}: ${path || '/'}] consider, for this session, this directory as your current working directory`;
|
||||
|
||||
const handleMessageComplete = useCallback(() => {
|
||||
setRefreshSignal((n) => n + 1);
|
||||
}, [setRefreshSignal]);
|
||||
|
||||
return (
|
||||
<EmbeddableChat
|
||||
className="h-full"
|
||||
cwd={{ root: homeRoot, path: cwdPath.replace(/^\//, '') || '/' }}
|
||||
sandboxed={sandboxed}
|
||||
defaultInput={message}
|
||||
onMessageComplete={handleMessageComplete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export { FileViewerView, FileViewerProvider, FileViewerHeader, FileViewerBody, F
|
||||
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';
|
||||
export { DashboardListApp, DashboardPreview, SELECTED_DASHBOARD_KEY, CREATING_DASHBOARD_KEY, EDITING_DASHBOARD_KEY, NEW_DASH_NAME_KEY, NEW_DASH_DESC_KEY, NEW_DASH_TEMPLATE_KEY } from './apps/Dashboards';
|
||||
export { ProjectListApp, ProjectPreview, SELECTED_PROJECT, CREATING_PROJECT, EDITING_PROJECT, NEW_PROJ_NAME, NEW_PROJ_DESC, NEW_PROJ_TEMPLATE, NEW_PROJ_TYPE, NEW_PROJ_HAS_BACKEND, NEW_PROJ_HAS_AUTH, NEW_PROJ_PREVIEW_LAYOUT } from './apps/Projects';
|
||||
export { createUserAppPanel, createUserAppHeader } from './apps/UserApp';
|
||||
export { resolveIcon, availableIconNames } from './utils/resolve-icon';
|
||||
@@ -23,4 +23,4 @@ export { resolveIcon, availableIconNames } from './utils/resolve-icon';
|
||||
// Workspace
|
||||
export { WorkspaceView, WorkspaceLayout, WorkspaceProvider, useWorkspace } from './components/Workspace';
|
||||
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './components/Workspace';
|
||||
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, WorkspaceState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry, EphemeralPanels, DropPosition, HomeRoot, DefaultFileSort } from './components/Workspace';
|
||||
export type { LayoutNode, LayoutGroup, LayoutPanel, DashboardDefinition, DashboardState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry, EphemeralPanels, DropPosition, HomeRoot, DefaultFileSort } from './components/Workspace';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { useSettings, DEFAULT_SETTINGS } from './useSettings';
|
||||
export type { UseSettingsType, UserSettings, UserState } from './useSettings';
|
||||
export { useUserState } from './useUserState';
|
||||
export { useWorkspacesState } from './useWorkspacesState';
|
||||
export { useDashboardState } from './useDashboardState';
|
||||
export { usePiModels, useVisiblePiModels, useEnabledPiModels, modelKey } from './useModels';
|
||||
export type { ModelOption } from './useModels';
|
||||
export { useAccessPolicy } from './useAccessPolicy';
|
||||
|
||||
+5
-5
@@ -4,9 +4,9 @@ import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import type { UserState } from './useSettings';
|
||||
|
||||
const QUERY_KEY = ['WORKSPACES_STATE'];
|
||||
const QUERY_KEY = ['DASHBOARD_STATE'];
|
||||
|
||||
export function useWorkspacesState<T>(key: string, defaultValue: T) {
|
||||
export function useDashboardState<T>(key: string, defaultValue: T) {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -16,7 +16,7 @@ export function useWorkspacesState<T>(key: string, defaultValue: T) {
|
||||
const { data: state = {}, isSuccess } = useQuery<UserState>({
|
||||
queryKey: QUERY_KEY,
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.get<UserState>('/workspaces'),
|
||||
queryFn: () => client.get<UserState>('/dashboards'),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useWorkspacesState<T>(key: string, defaultValue: T) {
|
||||
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(() => { });
|
||||
clientRef.current.patch('/dashboards', { [key]: defaultValue }).catch(() => { });
|
||||
}
|
||||
}, [isSuccess, key, defaultValue, queryClient]);
|
||||
|
||||
@@ -43,7 +43,7 @@ export function useWorkspacesState<T>(key: string, defaultValue: T) {
|
||||
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue });
|
||||
|
||||
clientRef.current.patch('/workspaces', { [key]: newValue }).catch(() => { });
|
||||
clientRef.current.patch('/dashboards', { [key]: newValue }).catch(() => { });
|
||||
},
|
||||
[key, defaultValue, queryClient],
|
||||
);
|
||||
Reference in New Issue
Block a user