diff --git a/src/main/EryxAppService.ts b/src/main/EryxAppService.ts index eff3a07..452b3e1 100644 --- a/src/main/EryxAppService.ts +++ b/src/main/EryxAppService.ts @@ -1,7 +1,8 @@ import { EventEmitter } from 'node:events'; -import { basename } from 'node:path'; +import { rm } from 'node:fs/promises'; +import { basename, dirname } from 'node:path'; -import { dialog } from 'electron'; +import { dialog, shell } from 'electron'; import type { AgentActivityEvent, @@ -159,6 +160,28 @@ export class EryxAppService extends EventEmitter { void this.secretStore; } + async openAppDataFolder(): Promise { + const appDataPath = dirname(this.workspaceRepository.filePath); + await shell.openPath(appDataPath); + } + + async resetLocalWorkspace(): Promise { + await this.sidecar.dispose(); + + try { + await rm(this.workspaceRepository.filePath, { force: true }); + await rm(this.workspaceRepository.scratchpadPath, { recursive: true, force: true }); + } catch (error) { + console.error('[aryx reset]', error); + } + + this.workspace = undefined; + this.sidecarCapabilities = undefined; + this.didScheduleInitialProjectGitRefresh = false; + + return this.loadWorkspace(); + } + async addProject(): Promise { const workspace = await this.loadWorkspace(); const result = await dialog.showOpenDialog({ diff --git a/src/main/ipc/registerIpcHandlers.ts b/src/main/ipc/registerIpcHandlers.ts index 38775da..c80769e 100644 --- a/src/main/ipc/registerIpcHandlers.ts +++ b/src/main/ipc/registerIpcHandlers.ts @@ -96,6 +96,8 @@ export function registerIpcHandlers(window: BrowserWindow, service: EryxAppServi ipcMain.handle(ipcChannels.selectProject, (_event, projectId?: string) => service.selectProject(projectId)); ipcMain.handle(ipcChannels.selectPattern, (_event, patternId?: string) => service.selectPattern(patternId)); ipcMain.handle(ipcChannels.selectSession, (_event, sessionId?: string) => service.selectSession(sessionId)); + ipcMain.handle(ipcChannels.openAppDataFolder, () => service.openAppDataFolder()); + ipcMain.handle(ipcChannels.resetLocalWorkspace, () => service.resetLocalWorkspace()); service.on('workspace-updated', (workspace) => { window.webContents.send(ipcChannels.workspaceUpdated, workspace); diff --git a/src/preload/index.ts b/src/preload/index.ts index 4abda24..415a40c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -34,7 +34,9 @@ const api: ElectronApi = { selectProject: (projectId) => ipcRenderer.invoke(ipcChannels.selectProject, projectId), selectPattern: (patternId) => ipcRenderer.invoke(ipcChannels.selectPattern, patternId), selectSession: (sessionId) => ipcRenderer.invoke(ipcChannels.selectSession, sessionId), - onWorkspaceUpdated: (listener) => { + openAppDataFolder: () => ipcRenderer.invoke(ipcChannels.openAppDataFolder), + resetLocalWorkspace: () => ipcRenderer.invoke(ipcChannels.resetLocalWorkspace), + onWorkspaceUpdated:(listener) => { const handler = (_event: Electron.IpcRendererEvent, workspace: Awaited>) => listener(workspace); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b044cac..56873d2 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -306,6 +306,10 @@ export default function App() { await api.savePattern({ pattern }); }} onSetTheme={(theme) => void api.setTheme(theme)} + onOpenAppDataFolder={() => void api.openAppDataFolder()} + onResetLocalWorkspace={async () => { + await api.resetLocalWorkspace(); + }} patterns={workspace.patterns} sidecarCapabilities={sidecarCapabilities} theme={workspace.settings.theme} diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx index dee7f38..5af7163 100644 --- a/src/renderer/components/SettingsPanel.tsx +++ b/src/renderer/components/SettingsPanel.tsx @@ -1,5 +1,5 @@ import { useState, type ReactNode } from 'react'; -import { ChevronLeft, ChevronRight, Code, Cpu, Palette, Plus, Server, Workflow } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Code, Cpu, FolderOpen, Palette, Plus, Server, TriangleAlert, Workflow, Wrench } from 'lucide-react'; import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard'; import { PatternEditor } from '@renderer/components/PatternEditor'; @@ -36,9 +36,11 @@ interface SettingsPanelProps { onDeleteLspProfile: (profileId: string) => Promise; onNewLspProfile: () => LspProfileDefinition; onSetTheme: (theme: AppearanceTheme) => void; + onOpenAppDataFolder: () => void; + onResetLocalWorkspace: () => Promise; } -type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles'; +type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting'; interface NavItem { id: SettingsSection; @@ -77,6 +79,12 @@ const navGroups: NavGroup[] = [ { id: 'lsp-profiles', label: 'LSP Profiles', icon: }, ], }, + { + label: 'Support', + items: [ + { id: 'troubleshooting', label: 'Troubleshooting', icon: }, + ], + }, ]; function modeBadgeClasses(pattern: PatternDefinition) { @@ -103,6 +111,8 @@ export function SettingsPanel({ onDeleteLspProfile, onNewLspProfile, onSetTheme, + onOpenAppDataFolder, + onResetLocalWorkspace, }: SettingsPanelProps) { const [activeSection, setActiveSection] = useState('appearance'); const [editingPattern, setEditingPattern] = useState(null); @@ -268,6 +278,12 @@ export function SettingsPanel({ profiles={toolingSettings.lspProfiles} /> )} + {activeSection === 'troubleshooting' && ( + + )} @@ -562,3 +578,112 @@ function EmptyState({ children }: { children: ReactNode }) { ); } + +function TroubleshootingSection({ + onOpenAppDataFolder, + onResetLocalWorkspace, +}: { + onOpenAppDataFolder: () => void; + onResetLocalWorkspace: () => Promise; +}) { + const [isResetting, setIsResetting] = useState(false); + const [confirmingReset, setConfirmingReset] = useState(false); + + async function handleReset() { + setIsResetting(true); + try { + await onResetLocalWorkspace(); + } finally { + setIsResetting(false); + setConfirmingReset(false); + } + } + + return ( +
+ + +
+ } + label="Open App Data Folder" + onClick={onOpenAppDataFolder} + /> +
+ +
+
+ +
+

Reset Local Workspace

+

+ Restore Aryx to its initial state. This permanently removes all sessions, custom patterns, + MCP server definitions, LSP profiles, and scratchpad contents. Your GitHub Copilot sign-in + is not affected. +

+ + {!confirmingReset ? ( + + ) : ( +
+ + +
+ )} +
+
+
+
+ ); +} + +function TroubleshootingAction({ + icon, + label, + description, + onClick, +}: { + icon: ReactNode; + label: string; + description: string; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/src/shared/contracts/channels.ts b/src/shared/contracts/channels.ts index 72af03f..33279b6 100644 --- a/src/shared/contracts/channels.ts +++ b/src/shared/contracts/channels.ts @@ -27,6 +27,8 @@ export const ipcChannels = { selectProject: 'selection:project', selectPattern: 'selection:pattern', selectSession: 'selection:session', + openAppDataFolder: 'troubleshooting:open-app-data-folder', + resetLocalWorkspace: 'troubleshooting:reset-local-workspace', workspaceUpdated: 'workspace:updated', sessionEvent: 'sessions:event', } as const; diff --git a/src/shared/contracts/ipc.ts b/src/shared/contracts/ipc.ts index c48db84..3e6f9d9 100644 --- a/src/shared/contracts/ipc.ts +++ b/src/shared/contracts/ipc.ts @@ -108,6 +108,8 @@ export interface ElectronApi { selectSession(sessionId?: string): Promise; setPatternFavorite(input: SetPatternFavoriteInput): Promise; setTheme(theme: AppearanceTheme): Promise; + openAppDataFolder(): Promise; + resetLocalWorkspace(): Promise; onWorkspaceUpdated(listener: (workspace: WorkspaceState) => void): () => void; onSessionEvent(listener: (event: SessionEventRecord) => void): () => void; } diff --git a/tests/main/appServiceTooling.test.ts b/tests/main/appServiceTooling.test.ts index f024698..0e22cb0 100644 --- a/tests/main/appServiceTooling.test.ts +++ b/tests/main/appServiceTooling.test.ts @@ -18,6 +18,9 @@ mock.module('electron', () => ({ dialog: { showOpenDialog: async () => ({ canceled: true, filePaths: [] }), }, + shell: { + openPath: async () => '', + }, })); mock.module('keytar', () => ({