mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 05:28:46 +02:00
feat: add troubleshooting settings with open app data and reset workspace
Add a Troubleshooting section under a Support nav group in Settings: - Open App Data Folder: reveals the user-data directory in the OS file explorer via shell.openPath. - Reset Local Workspace: destructive action that deletes workspace.json and scratchpad contents, clears the in-memory cache, and reseeds defaults. Auth secrets are preserved. Requires two-step confirmation. New IPC channels, ElectronApi methods, and preload bridge added for openAppDataFolder and resetLocalWorkspace. Existing electron mock in tests updated to include shell. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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<AppServiceEvents> {
|
||||
void this.secretStore;
|
||||
}
|
||||
|
||||
async openAppDataFolder(): Promise<void> {
|
||||
const appDataPath = dirname(this.workspaceRepository.filePath);
|
||||
await shell.openPath(appDataPath);
|
||||
}
|
||||
|
||||
async resetLocalWorkspace(): Promise<WorkspaceState> {
|
||||
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<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const result = await dialog.showOpenDialog({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ReturnType<ElectronApi['loadWorkspace']>>) =>
|
||||
listener(workspace);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<void>;
|
||||
onNewLspProfile: () => LspProfileDefinition;
|
||||
onSetTheme: (theme: AppearanceTheme) => void;
|
||||
onOpenAppDataFolder: () => void;
|
||||
onResetLocalWorkspace: () => Promise<void>;
|
||||
}
|
||||
|
||||
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: <Code className="size-3.5" /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Support',
|
||||
items: [
|
||||
{ id: 'troubleshooting', label: 'Troubleshooting', icon: <Wrench className="size-3.5" /> },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function modeBadgeClasses(pattern: PatternDefinition) {
|
||||
@@ -103,6 +111,8 @@ export function SettingsPanel({
|
||||
onDeleteLspProfile,
|
||||
onNewLspProfile,
|
||||
onSetTheme,
|
||||
onOpenAppDataFolder,
|
||||
onResetLocalWorkspace,
|
||||
}: SettingsPanelProps) {
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
|
||||
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
|
||||
@@ -268,6 +278,12 @@ export function SettingsPanel({
|
||||
profiles={toolingSettings.lspProfiles}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'troubleshooting' && (
|
||||
<TroubleshootingSection
|
||||
onOpenAppDataFolder={onOpenAppDataFolder}
|
||||
onResetLocalWorkspace={onResetLocalWorkspace}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -562,3 +578,112 @@ function EmptyState({ children }: { children: ReactNode }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TroubleshootingSection({
|
||||
onOpenAppDataFolder,
|
||||
onResetLocalWorkspace,
|
||||
}: {
|
||||
onOpenAppDataFolder: () => void;
|
||||
onResetLocalWorkspace: () => Promise<void>;
|
||||
}) {
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const [confirmingReset, setConfirmingReset] = useState(false);
|
||||
|
||||
async function handleReset() {
|
||||
setIsResetting(true);
|
||||
try {
|
||||
await onResetLocalWorkspace();
|
||||
} finally {
|
||||
setIsResetting(false);
|
||||
setConfirmingReset(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader
|
||||
description="Diagnose issues and manage local application data"
|
||||
title="Troubleshooting"
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<TroubleshootingAction
|
||||
description="Reveal the folder where Aryx stores workspace data, scratchpad files, and configuration."
|
||||
icon={<FolderOpen className="size-4" />}
|
||||
label="Open App Data Folder"
|
||||
onClick={onOpenAppDataFolder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 rounded-xl border border-red-500/20 bg-red-500/5 p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<TriangleAlert className="mt-0.5 size-4 shrink-0 text-red-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="text-[13px] font-semibold text-red-300">Reset Local Workspace</h4>
|
||||
<p className="mt-1 text-[12px] leading-relaxed text-zinc-400">
|
||||
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.
|
||||
</p>
|
||||
|
||||
{!confirmingReset ? (
|
||||
<button
|
||||
className="mt-3 rounded-lg border border-red-500/30 bg-red-500/10 px-3.5 py-1.5 text-[13px] font-medium text-red-300 transition hover:border-red-500/50 hover:bg-red-500/20"
|
||||
onClick={() => setConfirmingReset(true)}
|
||||
type="button"
|
||||
>
|
||||
Reset workspace…
|
||||
</button>
|
||||
) : (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<button
|
||||
className="rounded-lg bg-red-600 px-3.5 py-1.5 text-[13px] font-medium text-white transition hover:bg-red-500 disabled:opacity-50"
|
||||
disabled={isResetting}
|
||||
onClick={() => void handleReset()}
|
||||
type="button"
|
||||
>
|
||||
{isResetting ? 'Resetting…' : 'Confirm reset'}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-lg border border-[var(--color-border)] px-3.5 py-1.5 text-[13px] font-medium text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
||||
disabled={isResetting}
|
||||
onClick={() => setConfirmingReset(false)}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TroubleshootingAction({
|
||||
icon,
|
||||
label,
|
||||
description,
|
||||
onClick,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
description: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className="group flex w-full items-center gap-3 rounded-xl border border-transparent px-4 py-3 text-left transition hover:border-zinc-800 hover:bg-zinc-900"
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<span className="text-zinc-500 transition group-hover:text-zinc-300">{icon}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-[13px] font-medium text-zinc-200">{label}</span>
|
||||
<p className="mt-0.5 text-[12px] text-zinc-500">{description}</p>
|
||||
</div>
|
||||
<ChevronRight className="size-4 text-zinc-700 transition group-hover:text-zinc-500" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -108,6 +108,8 @@ export interface ElectronApi {
|
||||
selectSession(sessionId?: string): Promise<WorkspaceState>;
|
||||
setPatternFavorite(input: SetPatternFavoriteInput): Promise<WorkspaceState>;
|
||||
setTheme(theme: AppearanceTheme): Promise<WorkspaceState>;
|
||||
openAppDataFolder(): Promise<void>;
|
||||
resetLocalWorkspace(): Promise<WorkspaceState>;
|
||||
onWorkspaceUpdated(listener: (workspace: WorkspaceState) => void): () => void;
|
||||
onSessionEvent(listener: (event: SessionEventRecord) => void): () => void;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ mock.module('electron', () => ({
|
||||
dialog: {
|
||||
showOpenDialog: async () => ({ canceled: true, filePaths: [] }),
|
||||
},
|
||||
shell: {
|
||||
openPath: async () => '',
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('keytar', () => ({
|
||||
|
||||
Reference in New Issue
Block a user