[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:
haikdc
2026-04-18 06:02:13 -07:00
parent e720f97444
commit 09ea76b12d
23 changed files with 51 additions and 488 deletions
+6 -1
View File
@@ -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 || {});
});
+1
View File
@@ -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'),
-2
View File
@@ -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 &amp; 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 || ''}
/>
</>
);
+2 -3
View File
@@ -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>
+10 -3
View File
@@ -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,
+1 -8
View File
@@ -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,
+7 -2
View File
@@ -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;