mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-30 06:27:13 +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 { 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 {
|
import type {
|
||||||
AgentActivityEvent,
|
AgentActivityEvent,
|
||||||
@@ -159,6 +160,28 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
void this.secretStore;
|
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> {
|
async addProject(): Promise<WorkspaceState> {
|
||||||
const workspace = await this.loadWorkspace();
|
const workspace = await this.loadWorkspace();
|
||||||
const result = await dialog.showOpenDialog({
|
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.selectProject, (_event, projectId?: string) => service.selectProject(projectId));
|
||||||
ipcMain.handle(ipcChannels.selectPattern, (_event, patternId?: string) => service.selectPattern(patternId));
|
ipcMain.handle(ipcChannels.selectPattern, (_event, patternId?: string) => service.selectPattern(patternId));
|
||||||
ipcMain.handle(ipcChannels.selectSession, (_event, sessionId?: string) => service.selectSession(sessionId));
|
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) => {
|
service.on('workspace-updated', (workspace) => {
|
||||||
window.webContents.send(ipcChannels.workspaceUpdated, workspace);
|
window.webContents.send(ipcChannels.workspaceUpdated, workspace);
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ const api: ElectronApi = {
|
|||||||
selectProject: (projectId) => ipcRenderer.invoke(ipcChannels.selectProject, projectId),
|
selectProject: (projectId) => ipcRenderer.invoke(ipcChannels.selectProject, projectId),
|
||||||
selectPattern: (patternId) => ipcRenderer.invoke(ipcChannels.selectPattern, patternId),
|
selectPattern: (patternId) => ipcRenderer.invoke(ipcChannels.selectPattern, patternId),
|
||||||
selectSession: (sessionId) => ipcRenderer.invoke(ipcChannels.selectSession, sessionId),
|
selectSession: (sessionId) => ipcRenderer.invoke(ipcChannels.selectSession, sessionId),
|
||||||
|
openAppDataFolder: () => ipcRenderer.invoke(ipcChannels.openAppDataFolder),
|
||||||
|
resetLocalWorkspace: () => ipcRenderer.invoke(ipcChannels.resetLocalWorkspace),
|
||||||
onWorkspaceUpdated:(listener) => {
|
onWorkspaceUpdated:(listener) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, workspace: Awaited<ReturnType<ElectronApi['loadWorkspace']>>) =>
|
const handler = (_event: Electron.IpcRendererEvent, workspace: Awaited<ReturnType<ElectronApi['loadWorkspace']>>) =>
|
||||||
listener(workspace);
|
listener(workspace);
|
||||||
|
|||||||
@@ -306,6 +306,10 @@ export default function App() {
|
|||||||
await api.savePattern({ pattern });
|
await api.savePattern({ pattern });
|
||||||
}}
|
}}
|
||||||
onSetTheme={(theme) => void api.setTheme(theme)}
|
onSetTheme={(theme) => void api.setTheme(theme)}
|
||||||
|
onOpenAppDataFolder={() => void api.openAppDataFolder()}
|
||||||
|
onResetLocalWorkspace={async () => {
|
||||||
|
await api.resetLocalWorkspace();
|
||||||
|
}}
|
||||||
patterns={workspace.patterns}
|
patterns={workspace.patterns}
|
||||||
sidecarCapabilities={sidecarCapabilities}
|
sidecarCapabilities={sidecarCapabilities}
|
||||||
theme={workspace.settings.theme}
|
theme={workspace.settings.theme}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, type ReactNode } from 'react';
|
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 { CopilotStatusCard } from '@renderer/components/CopilotStatusCard';
|
||||||
import { PatternEditor } from '@renderer/components/PatternEditor';
|
import { PatternEditor } from '@renderer/components/PatternEditor';
|
||||||
@@ -36,9 +36,11 @@ interface SettingsPanelProps {
|
|||||||
onDeleteLspProfile: (profileId: string) => Promise<void>;
|
onDeleteLspProfile: (profileId: string) => Promise<void>;
|
||||||
onNewLspProfile: () => LspProfileDefinition;
|
onNewLspProfile: () => LspProfileDefinition;
|
||||||
onSetTheme: (theme: AppearanceTheme) => void;
|
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 {
|
interface NavItem {
|
||||||
id: SettingsSection;
|
id: SettingsSection;
|
||||||
@@ -77,6 +79,12 @@ const navGroups: NavGroup[] = [
|
|||||||
{ id: 'lsp-profiles', label: 'LSP Profiles', icon: <Code className="size-3.5" /> },
|
{ 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) {
|
function modeBadgeClasses(pattern: PatternDefinition) {
|
||||||
@@ -103,6 +111,8 @@ export function SettingsPanel({
|
|||||||
onDeleteLspProfile,
|
onDeleteLspProfile,
|
||||||
onNewLspProfile,
|
onNewLspProfile,
|
||||||
onSetTheme,
|
onSetTheme,
|
||||||
|
onOpenAppDataFolder,
|
||||||
|
onResetLocalWorkspace,
|
||||||
}: SettingsPanelProps) {
|
}: SettingsPanelProps) {
|
||||||
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
|
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
|
||||||
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
|
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
|
||||||
@@ -268,6 +278,12 @@ export function SettingsPanel({
|
|||||||
profiles={toolingSettings.lspProfiles}
|
profiles={toolingSettings.lspProfiles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{activeSection === 'troubleshooting' && (
|
||||||
|
<TroubleshootingSection
|
||||||
|
onOpenAppDataFolder={onOpenAppDataFolder}
|
||||||
|
onResetLocalWorkspace={onResetLocalWorkspace}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -562,3 +578,112 @@ function EmptyState({ children }: { children: ReactNode }) {
|
|||||||
</div>
|
</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',
|
selectProject: 'selection:project',
|
||||||
selectPattern: 'selection:pattern',
|
selectPattern: 'selection:pattern',
|
||||||
selectSession: 'selection:session',
|
selectSession: 'selection:session',
|
||||||
|
openAppDataFolder: 'troubleshooting:open-app-data-folder',
|
||||||
|
resetLocalWorkspace: 'troubleshooting:reset-local-workspace',
|
||||||
workspaceUpdated: 'workspace:updated',
|
workspaceUpdated: 'workspace:updated',
|
||||||
sessionEvent: 'sessions:event',
|
sessionEvent: 'sessions:event',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ export interface ElectronApi {
|
|||||||
selectSession(sessionId?: string): Promise<WorkspaceState>;
|
selectSession(sessionId?: string): Promise<WorkspaceState>;
|
||||||
setPatternFavorite(input: SetPatternFavoriteInput): Promise<WorkspaceState>;
|
setPatternFavorite(input: SetPatternFavoriteInput): Promise<WorkspaceState>;
|
||||||
setTheme(theme: AppearanceTheme): Promise<WorkspaceState>;
|
setTheme(theme: AppearanceTheme): Promise<WorkspaceState>;
|
||||||
|
openAppDataFolder(): Promise<void>;
|
||||||
|
resetLocalWorkspace(): Promise<WorkspaceState>;
|
||||||
onWorkspaceUpdated(listener: (workspace: WorkspaceState) => void): () => void;
|
onWorkspaceUpdated(listener: (workspace: WorkspaceState) => void): () => void;
|
||||||
onSessionEvent(listener: (event: SessionEventRecord) => void): () => void;
|
onSessionEvent(listener: (event: SessionEventRecord) => void): () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ mock.module('electron', () => ({
|
|||||||
dialog: {
|
dialog: {
|
||||||
showOpenDialog: async () => ({ canceled: true, filePaths: [] }),
|
showOpenDialog: async () => ({ canceled: true, filePaths: [] }),
|
||||||
},
|
},
|
||||||
|
shell: {
|
||||||
|
openPath: async () => '',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
mock.module('keytar', () => ({
|
mock.module('keytar', () => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user