mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[hAIk]: Replace custom DirectoryBrowser with native Electron file dialog; move ContextPath type to agentsTypes, remove BrowseResult, update all imports. Also removed unused Analytics page
This commit is contained in:
+6
-1
@@ -1,4 +1,4 @@
|
||||
const { app, components, BrowserWindow, ipcMain, shell, session } = require('electron');
|
||||
const { app, components, BrowserWindow, ipcMain, shell, session, dialog } = require('electron');
|
||||
let autoUpdater;
|
||||
try { autoUpdater = require('electron-updater').autoUpdater; } catch (_) {}
|
||||
const path = require('path');
|
||||
@@ -558,3 +558,8 @@ ipcMain.handle('open-external', (_event, url) => {
|
||||
shell.openExternal(url);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('show-open-dialog', async (_event, options) => {
|
||||
const win = BrowserWindow.getFocusedWindow();
|
||||
return dialog.showOpenDialog(win || BrowserWindow.getAllWindows()[0], options || {});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ const { contextBridge, ipcRenderer } = require('electron');
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
||||
capturePage: (rect) => ipcRenderer.invoke('capture-page', rect),
|
||||
showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
|
||||
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||
|
||||
@@ -22,7 +22,6 @@ import Tools from './pages/Tools/Tools';
|
||||
import Modes from './pages/Modes/Modes';
|
||||
import Views from './pages/Views/Views';
|
||||
import Customization from './pages/Customization/Customization';
|
||||
import Analytics from './pages/Analytics/Analytics';
|
||||
import OnboardingModal from './components/OnboardingModal';
|
||||
import { useKeyboardShortcuts } from '@/shared/hooks/useKeyboardShortcuts';
|
||||
import KeyboardShortcutsHelp from './components/KeyboardShortcutsHelp';
|
||||
@@ -109,7 +108,6 @@ const ThemedApp: React.FC = () => {
|
||||
<Route path="/modes" element={<Modes />} />
|
||||
<Route path="/apps" element={<Views />} />
|
||||
<Route path="/apps/:id" element={<Views />} />
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<OnboardingModal />
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { BrowseResult } from '@/shared/state/settingsSlice';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import DirectoryFileList from './DirectoryFileList';
|
||||
|
||||
const SETTINGS_API = `${API_BASE}/settings`;
|
||||
|
||||
export interface ContextPath {
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
}
|
||||
|
||||
interface DirectoryBrowserProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (item: ContextPath) => void;
|
||||
initialPath?: string;
|
||||
}
|
||||
|
||||
const DirectoryBrowser: React.FC<DirectoryBrowserProps> = ({ open, onClose, onSelect, initialPath }) => {
|
||||
const c = useClaudeTokens();
|
||||
const [browseData, setBrowseData] = useState<BrowseResult | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [manualPath, setManualPath] = useState('');
|
||||
const [selected, setSelected] = useState<{ name: string; type: 'file' | 'directory' } | null>(null);
|
||||
|
||||
const browse = useCallback(async (path: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSelected(null);
|
||||
try {
|
||||
const res = await fetch(`${SETTINGS_API}/browse-directories?path=${encodeURIComponent(path)}`);
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.detail || 'Failed to browse');
|
||||
}
|
||||
const data: BrowseResult = await res.json();
|
||||
setBrowseData(data);
|
||||
setManualPath(data.current);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSelected(null);
|
||||
browse(initialPath || '');
|
||||
}
|
||||
}, [open, initialPath, browse]);
|
||||
|
||||
const handleNavigate = (dir: string) => {
|
||||
if (browseData) browse(`${browseData.current}/${dir}`);
|
||||
};
|
||||
|
||||
const handleGoUp = () => {
|
||||
if (browseData?.parent) browse(browseData.parent);
|
||||
};
|
||||
|
||||
const handleManualGo = () => {
|
||||
if (manualPath.trim()) browse(manualPath.trim());
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') handleManualGo();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!browseData) return;
|
||||
if (selected) {
|
||||
const fullPath = `${browseData.current}/${selected.name}`;
|
||||
onSelect({ path: fullPath, type: selected.type });
|
||||
} else {
|
||||
onSelect({ path: browseData.current, type: 'directory' });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: c.bg.surface,
|
||||
backgroundImage: 'none',
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
height: 520,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600, pb: 1 }}>
|
||||
Browse Files & Folders
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1.5,
|
||||
overflow: 'hidden',
|
||||
'&::-webkit-scrollbar': { width: 5 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: c.border.medium,
|
||||
borderRadius: 3,
|
||||
'&:hover': { background: c.border.strong },
|
||||
},
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${c.border.medium} transparent`,
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
value={manualPath}
|
||||
onChange={(e) => setManualPath(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Type a path..."
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
bgcolor: c.bg.page,
|
||||
fontSize: '0.85rem',
|
||||
fontFamily: c.font.mono,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleManualGo}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
borderColor: c.border.medium,
|
||||
textTransform: 'none',
|
||||
minWidth: 'auto',
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
Go
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<DirectoryFileList
|
||||
browseData={browseData}
|
||||
selected={selected}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onBrowse={browse}
|
||||
onNavigate={handleNavigate}
|
||||
onGoUp={handleGoUp}
|
||||
onSelect={setSelected}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'space-between' }}>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.72rem', pl: 1 }}>
|
||||
{selected
|
||||
? `Selected: ${selected.name}`
|
||||
: 'Click to select, double-click folders to open'}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={onClose} sx={{ color: c.text.tertiary, textTransform: 'none' }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleConfirm}
|
||||
disabled={!browseData}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{selected ? `Attach ${selected.type === 'file' ? 'File' : 'Folder'}` : 'Attach This Folder'}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectoryBrowser;
|
||||
@@ -1,177 +0,0 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
||||
import Link from '@mui/material/Link';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { BrowseResult } from '@/shared/state/settingsSlice';
|
||||
|
||||
interface DirectoryFileListProps {
|
||||
browseData: BrowseResult | null;
|
||||
selected: { name: string; type: 'file' | 'directory' } | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
onBrowse: (path: string) => void;
|
||||
onNavigate: (dir: string) => void;
|
||||
onGoUp: () => void;
|
||||
onSelect: (item: { name: string; type: 'file' | 'directory' } | null) => void;
|
||||
}
|
||||
|
||||
const DirectoryFileList: React.FC<DirectoryFileListProps> = ({
|
||||
browseData, selected, loading, error, onBrowse, onNavigate, onGoUp, onSelect,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
const pathSegments = browseData?.current.split('/').filter(Boolean) ?? [];
|
||||
const hasEntries = (browseData?.directories.length ?? 0) + (browseData?.files.length ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{browseData && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onGoUp}
|
||||
disabled={!browseData.parent}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.accent.primary } }}
|
||||
>
|
||||
<ArrowUpwardIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
<Breadcrumbs
|
||||
separator="/"
|
||||
sx={{
|
||||
'& .MuiBreadcrumbs-separator': { color: c.text.ghost, mx: 0.25 },
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
component="button"
|
||||
underline="hover"
|
||||
onClick={() => onBrowse('/')}
|
||||
sx={{ color: c.text.tertiary, fontSize: '0.78rem' }}
|
||||
>
|
||||
/
|
||||
</Link>
|
||||
{pathSegments.map((seg, i) => {
|
||||
const fullPath = '/' + pathSegments.slice(0, i + 1).join('/');
|
||||
const isLast = i === pathSegments.length - 1;
|
||||
return isLast ? (
|
||||
<Typography key={fullPath} sx={{ color: c.text.primary, fontSize: '0.78rem', fontWeight: 500 }}>
|
||||
{seg}
|
||||
</Typography>
|
||||
) : (
|
||||
<Link
|
||||
key={fullPath}
|
||||
component="button"
|
||||
underline="hover"
|
||||
onClick={() => onBrowse(fullPath)}
|
||||
sx={{ color: c.text.tertiary, fontSize: '0.78rem' }}
|
||||
>
|
||||
{seg}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Typography sx={{ color: c.status.error, fontSize: '0.82rem', px: 1 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
borderRadius: 2,
|
||||
bgcolor: c.bg.page,
|
||||
'&::-webkit-scrollbar': { width: 5 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: c.border.medium,
|
||||
borderRadius: 3,
|
||||
'&:hover': { background: c.border.strong },
|
||||
},
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${c.border.medium} transparent`,
|
||||
}}>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress size={24} sx={{ color: c.accent.primary }} />
|
||||
</Box>
|
||||
) : !hasEntries ? (
|
||||
<Box sx={{ py: 4, textAlign: 'center' }}>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.85rem' }}>
|
||||
Empty directory
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<List dense disablePadding>
|
||||
{browseData?.directories.map((dir) => (
|
||||
<ListItemButton
|
||||
key={`d-${dir}`}
|
||||
selected={selected?.name === dir && selected.type === 'directory'}
|
||||
onDoubleClick={() => onNavigate(dir)}
|
||||
onClick={() =>
|
||||
onSelect(
|
||||
selected?.name === dir && selected.type === 'directory' ? null : { name: dir, type: 'directory' },
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
py: 0.75,
|
||||
'&.Mui-selected': { bgcolor: `${c.accent.primary}0c` },
|
||||
'&:hover': { bgcolor: `${c.accent.primary}08` },
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 32, color: c.accent.primary }}>
|
||||
<FolderIcon sx={{ fontSize: 18 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={dir}
|
||||
primaryTypographyProps={{ sx: { fontSize: '0.84rem', color: c.text.primary } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
{browseData?.files.map((file) => (
|
||||
<ListItemButton
|
||||
key={`f-${file}`}
|
||||
selected={selected?.name === file && selected.type === 'file'}
|
||||
onClick={() =>
|
||||
onSelect(
|
||||
selected?.name === file && selected.type === 'file' ? null : { name: file, type: 'file' },
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
py: 0.75,
|
||||
'&.Mui-selected': { bgcolor: `${c.accent.primary}0c` },
|
||||
'&:hover': { bgcolor: `${c.accent.primary}08` },
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 32, color: c.text.muted }}>
|
||||
<InsertDriveFileOutlinedIcon sx={{ fontSize: 17 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={file}
|
||||
primaryTypographyProps={{ sx: { fontSize: '0.84rem', color: c.text.secondary } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectoryFileList;
|
||||
@@ -10,7 +10,7 @@ import OpenSwarmComposer from './composer/OpenSwarmComposer';
|
||||
import { useAgentChat } from './hooks/useAgentChat';
|
||||
import { useOpenSwarmRuntime, type ComposerExtras, type DispatchableMessage } from './runtime/useOpenSwarmRuntime';
|
||||
import { toolkit } from './toolkit';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
interface AgentChatProps {
|
||||
|
||||
@@ -7,7 +7,7 @@ import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutl
|
||||
import AdsClickIcon from '@mui/icons-material/AdsClick';
|
||||
import { getToolGroupIcon } from '@/app/components/CommandPicker';
|
||||
import type { SelectedElement } from '@/app/components/ElementSelectionContext';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
|
||||
export interface ForcedToolGroup {
|
||||
label: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import CircularProgress from '@mui/material/CircularProgress';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import CommandPicker from '@/app/components/CommandPicker';
|
||||
import { useElementSelection } from '@/app/components/ElementSelectionContext';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import { type AttachedSkill, type TriggerState, EMPTY_TRIGGER, serializeEditorContent } from '@/app/components/richEditorUtils';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LexicalComposerInput } from '@assistant-ui/react-lexical';
|
||||
import type { Unstable_MentionItem } from '@assistant-ui/core';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
import type { ComposerExtras } from '../runtime/useOpenSwarmRuntime';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import { useOpenSwarmMentionAdapter, type MentionItemMetadata } from './OpenSwarmMentionAdapter';
|
||||
import { useComposerAttachments } from './useComposerAttachments';
|
||||
import { MentionSelectOverride, MentionPopover, ComposerAttachmentChips } from './ComposerParts';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
|
||||
interface AttachedImage {
|
||||
data: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useElementSelection, type SelectedElement } from '@/app/components/Elem
|
||||
import { getClipboardCards, clearClipboard } from '@/shared/dashboardClipboard';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import {
|
||||
SKILL_PILL_ATTR, type AttachedSkill, createSkillPillElement,
|
||||
serializeEditorContent, type TriggerState, detectEditorTrigger,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const Analytics: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', overflow: 'auto', p: 3 }}>
|
||||
<Box sx={{ maxWidth: 800, mx: 'auto' }}>
|
||||
<Typography variant="h5" sx={{ color: c.text.primary, fontWeight: 600, mb: 3 }}>
|
||||
Analytics
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{
|
||||
p: 4,
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke={c.accent.primary} strokeWidth="1.5">
|
||||
<path d="M3 3v18h18" />
|
||||
<path d="M7 16l4-4 4 4 5-5" />
|
||||
<circle cx="20" cy="7" r="1.5" fill={c.accent.primary} />
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography sx={{ color: c.text.primary, fontSize: '1.1rem', fontWeight: 600, mb: 1 }}>
|
||||
Analytics powered by PostHog
|
||||
</Typography>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem', lineHeight: 1.6, mb: 3, maxWidth: 500, mx: 'auto' }}>
|
||||
Usage data is automatically collected — sessions, costs, tool usage, model distribution, and task categories.
|
||||
All data is anonymous and can be disabled in Settings.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 2, mt: 3, textAlign: 'left' }}>
|
||||
{[
|
||||
{ label: 'Sessions & Usage', desc: 'How often agents are launched, session duration, completion rates' },
|
||||
{ label: 'Cost Tracking', desc: 'Spend by model, provider, and time period' },
|
||||
{ label: 'Task Categories', desc: 'What users do — coding, email, research, social, browsing' },
|
||||
{ label: 'Model Distribution', desc: 'Which models and providers are most popular' },
|
||||
{ label: 'Tool Usage', desc: 'Most used MCP tools, execution times, approval rates' },
|
||||
{ label: 'Retention & Funnels', desc: 'User engagement, feature adoption, onboarding flow' },
|
||||
].map((item) => (
|
||||
<Box key={item.label} sx={{ p: 2, borderRadius: `${c.radius.md}px`, bgcolor: c.bg.elevated }}>
|
||||
<Typography sx={{ color: c.text.primary, fontSize: '0.82rem', fontWeight: 600, mb: 0.5 }}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.72rem', lineHeight: 1.4 }}>
|
||||
{item.desc}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Analytics;
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
BrowserCardPosition
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { GENERATE_DASHBOARD_NAME } from '@/shared/backend-bridge/apps/dashboards';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import type { CanvasActions } from '../useCanvasControls';
|
||||
|
||||
interface ToolbarDeps {
|
||||
|
||||
@@ -3,7 +3,7 @@ import Tooltip, { tooltipClasses } from '@mui/material/Tooltip';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
|
||||
export interface Props {
|
||||
inputOpen: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import { useElementSelection } from '@/app/components/ElementSelectionContext';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
|
||||
@@ -16,7 +16,6 @@ import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import RestoreIcon from '@mui/icons-material/Restore';
|
||||
import { Mode } from '@/shared/state/modesSlice';
|
||||
import RichPromptEditor from '@/app/components/RichPromptEditor';
|
||||
import DirectoryBrowser from '@/app/components/DirectoryBrowser';
|
||||
import { ModeForm, ICON_MAP, ICON_OPTIONS, COLOR_OPTIONS } from './modesConstants';
|
||||
import ToolsSelector from './ToolsSelector';
|
||||
|
||||
@@ -32,15 +31,14 @@ interface ModeFormDialogProps {
|
||||
onReset: () => void;
|
||||
otherModes: Mode[];
|
||||
mcpToolNames: string[];
|
||||
browseOpen: boolean;
|
||||
setBrowseOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
browseFolder: () => void;
|
||||
c: any;
|
||||
}
|
||||
|
||||
const ModeFormDialog: React.FC<ModeFormDialogProps> = ({
|
||||
open, onClose, editingId, editingIsBuiltin, hasDiverged,
|
||||
form, setForm, onSave, onReset, otherModes, mcpToolNames,
|
||||
browseOpen, setBrowseOpen, c,
|
||||
browseFolder, c,
|
||||
}) => (
|
||||
<>
|
||||
<Dialog
|
||||
@@ -122,7 +120,7 @@ const ModeFormDialog: React.FC<ModeFormDialogProps> = ({
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setBrowseOpen(true)}
|
||||
onClick={browseFolder}
|
||||
startIcon={<FolderOpenIcon />}
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
@@ -220,13 +218,6 @@ const ModeFormDialog: React.FC<ModeFormDialogProps> = ({
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<DirectoryBrowser
|
||||
open={browseOpen}
|
||||
onClose={() => setBrowseOpen(false)}
|
||||
onSelect={(item) => setForm({ ...form, default_folder: item.path })}
|
||||
initialPath={form.default_folder || ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const Modes: React.FC = () => {
|
||||
const {
|
||||
modes, items, loading,
|
||||
dialogOpen, setDialogOpen, editingId,
|
||||
form, setForm, browseOpen, setBrowseOpen,
|
||||
form, setForm, browseFolder,
|
||||
openCreate, openEdit, handleSave, handleDelete,
|
||||
editingIsBuiltin, hasDiverged, handleReset,
|
||||
otherModes, mcpToolNames,
|
||||
@@ -99,8 +99,7 @@ const Modes: React.FC = () => {
|
||||
onReset={handleReset}
|
||||
otherModes={otherModes}
|
||||
mcpToolNames={mcpToolNames}
|
||||
browseOpen={browseOpen}
|
||||
setBrowseOpen={setBrowseOpen}
|
||||
browseFolder={browseFolder}
|
||||
c={c}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -27,7 +27,15 @@ export function useModes() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [form, setForm] = useState<ModeForm>(emptyForm);
|
||||
const [browseOpen, setBrowseOpen] = useState(false);
|
||||
const browseFolder = async () => {
|
||||
const result = await (window as any).openswarm?.showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
defaultPath: form.default_folder || undefined,
|
||||
});
|
||||
if (result && !result.canceled && result.filePaths?.length > 0) {
|
||||
setForm((prev) => ({ ...prev, default_folder: result.filePaths[0] }));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(LIST_MODES());
|
||||
@@ -132,8 +140,7 @@ export function useModes() {
|
||||
editingId,
|
||||
form,
|
||||
setForm,
|
||||
browseOpen,
|
||||
setBrowseOpen,
|
||||
browseFolder,
|
||||
openCreate,
|
||||
openEdit,
|
||||
handleSave,
|
||||
|
||||
@@ -17,7 +17,7 @@ import AboutSection from './AboutSection';
|
||||
import type { UseSettingsReturn } from './hooks/useSettings';
|
||||
|
||||
const GeneralTab: React.FC<{ s: UseSettingsReturn }> = ({ s }) => {
|
||||
const { form, setForm, c, dispatch, modesList, setBrowseOpen,
|
||||
const { form, setForm, c, dispatch, modesList, browseFolder,
|
||||
fieldSx, sectionSx, rowSx, rowLastSx, inlineRowSx, inlineRowLastSx, labelSx, descSx } = s;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', pt: 2.5, pb: 1, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
@@ -88,7 +88,7 @@ const GeneralTab: React.FC<{ s: UseSettingsReturn }> = ({ s }) => {
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setBrowseOpen(true)}
|
||||
onClick={browseFolder}
|
||||
startIcon={<FolderOpenIcon sx={{ fontSize: 16 }} />}
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
|
||||
@@ -13,7 +13,6 @@ import Snackbar from '@mui/material/Snackbar';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DirectoryBrowser from '@/app/components/DirectoryBrowser';
|
||||
import { CommandsContent } from '@/app/pages/Commands/Commands';
|
||||
import { useSettings } from './hooks/useSettings';
|
||||
import GeneralTab from './GeneralTab';
|
||||
@@ -24,7 +23,7 @@ const Settings: React.FC = () => {
|
||||
const s = useSettings();
|
||||
const { c, open, activeTab, setActiveTab, hasChanges, handleSave, handleRequestClose,
|
||||
confirmDiscard, setConfirmDiscard, handleConfirmDiscard, handleSaveAndClose,
|
||||
saved, setSaved, browseOpen, setBrowseOpen, form, setForm } = s;
|
||||
saved, setSaved, form, setForm } = s;
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
@@ -131,12 +130,6 @@ const Settings: React.FC = () => {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
)}
|
||||
<DirectoryBrowser
|
||||
open={browseOpen}
|
||||
onClose={() => setBrowseOpen(false)}
|
||||
onSelect={(item) => setForm({ ...form, default_folder: item.path })}
|
||||
initialPath={form.default_folder ?? ''}
|
||||
/>
|
||||
<Snackbar
|
||||
open={saved}
|
||||
autoHideDuration={3000}
|
||||
|
||||
@@ -24,7 +24,6 @@ export function useSettings() {
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'models' | 'usage' | 'commands'>('general');
|
||||
const [form, setForm] = useState<AppSettings>({ ...settings });
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [browseOpen, setBrowseOpen] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [recordingShortcut, setRecordingShortcut] = useState(false);
|
||||
const [confirmDiscard, setConfirmDiscard] = useState(false);
|
||||
@@ -68,6 +67,15 @@ export function useSettings() {
|
||||
try { await (window as any).openswarm?.downloadUpdate(); } catch {}
|
||||
};
|
||||
const handleInstallUpdate = () => { (window as any).openswarm?.installUpdate(); };
|
||||
const browseFolder = async () => {
|
||||
const result = await (window as any).openswarm?.showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
defaultPath: form.default_folder || undefined,
|
||||
});
|
||||
if (result && !result.canceled && result.filePaths?.length > 0) {
|
||||
setForm({ ...form, default_folder: result.filePaths[0] });
|
||||
}
|
||||
};
|
||||
const fieldSx = { '& .MuiOutlinedInput-root': { fontSize: '0.85rem' } };
|
||||
const sectionSx = { fontSize: '0.7rem', fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase' as const, color: c.text.tertiary, mb: 0.5, mt: 0.5 };
|
||||
const rowSx = { py: 2, borderBottom: `1px solid ${c.border.subtle}` };
|
||||
@@ -79,7 +87,7 @@ export function useSettings() {
|
||||
return {
|
||||
c, dispatch, open, settings, modesList,
|
||||
activeTab, setActiveTab, form, setForm,
|
||||
showApiKey, setShowApiKey, browseOpen, setBrowseOpen,
|
||||
showApiKey, setShowApiKey, browseFolder,
|
||||
saved, setSaved, recordingShortcut, setRecordingShortcut,
|
||||
confirmDiscard, setConfirmDiscard, showApiHelp, setShowApiHelp,
|
||||
hasChanges, handleSave, handleRequestClose, handleConfirmDiscard,
|
||||
|
||||
@@ -81,6 +81,11 @@ export interface AgentConfig {
|
||||
dashboard_id?: string;
|
||||
}
|
||||
|
||||
export interface ContextPath {
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
}
|
||||
|
||||
export interface SendMessagePayload {
|
||||
sessionId: string;
|
||||
prompt: string;
|
||||
@@ -88,7 +93,7 @@ export interface SendMessagePayload {
|
||||
model?: string;
|
||||
provider?: string;
|
||||
images?: Array<{ data: string; media_type: string }>;
|
||||
contextPaths?: Array<{ path: string; type: 'file' | 'directory' }>;
|
||||
contextPaths?: Array<ContextPath>;
|
||||
forcedTools?: string[];
|
||||
attachedSkills?: Array<{ id: string; name: string; content: string }>;
|
||||
hidden?: boolean;
|
||||
@@ -103,7 +108,7 @@ export interface LaunchAndSendPayload {
|
||||
model: string;
|
||||
provider?: string;
|
||||
images?: Array<{ data: string; media_type: string }>;
|
||||
contextPaths?: Array<{ path: string; type: 'file' | 'directory' }>;
|
||||
contextPaths?: Array<ContextPath>;
|
||||
forcedTools?: string[];
|
||||
attachedSkills?: Array<{ id: string; name: string; content: string }>;
|
||||
expand?: boolean;
|
||||
|
||||
@@ -23,13 +23,6 @@ export const DEFAULT_SYSTEM_PROMPT =
|
||||
`make reasonable assumptions and act. If you need to ask, use the AskUserQuestion tool.\n` +
|
||||
`Do not over-explain what you are about to do. Just do it and show the results.`;
|
||||
|
||||
export interface BrowseResult {
|
||||
current: string;
|
||||
parent: string | null;
|
||||
directories: string[];
|
||||
files: string[];
|
||||
}
|
||||
|
||||
interface SettingsState {
|
||||
data: AppSettings;
|
||||
loading: boolean;
|
||||
|
||||
Reference in New Issue
Block a user