From d9299b4680ddbf6d83c305cafdb6af6980f6e3aa Mon Sep 17 00:00:00 2001 From: webdevcody Date: Fri, 9 Jan 2026 23:02:57 -0500 Subject: [PATCH] feat: add command palette and dashboard view components - Introduced a command palette for enhanced navigation and command execution. - Added a new dashboard view with project management features, including project cards and an empty state for new users. - Updated routing to include the new dashboard view and integrated it with the existing layout. - Enhanced the app store to manage pinned projects and GitHub cache for issues and pull requests. These changes improve user experience by streamlining project management and navigation within the application. --- .../command-palette/command-palette.tsx | 163 ++++ .../src/components/command-palette/index.ts | 1 + .../dialogs/onboarding-wizard/index.ts | 1 + .../onboarding-wizard/onboarding-wizard.tsx | 386 ++++++++++ .../dialogs/settings-dialog/index.ts | 1 + .../settings-dialog/settings-dialog.tsx | 59 ++ .../layout/bottom-dock/bottom-dock.tsx | 564 ++++++++++++++ .../components/layout/bottom-dock/index.ts | 2 + .../bottom-dock/panels/agents-panel.tsx | 143 ++++ .../layout/bottom-dock/panels/chat-panel.tsx | 697 ++++++++++++++++++ .../bottom-dock/panels/context-panel.tsx | 441 +++++++++++ .../bottom-dock/panels/github-panel.tsx | 224 ++++++ .../bottom-dock/panels/ideation-panel.tsx | 592 +++++++++++++++ .../layout/bottom-dock/panels/index.ts | 7 + .../layout/bottom-dock/panels/spec-panel.tsx | 123 ++++ .../bottom-dock/panels/terminal-panel.tsx | 551 ++++++++++++++ .../ui/src/components/layout/top-bar/index.ts | 4 + .../layout/top-bar/pinned-projects.tsx | 128 ++++ .../layout/top-bar/project-switcher.tsx | 202 +++++ .../layout/top-bar/top-bar-actions.tsx | 389 ++++++++++ .../src/components/layout/top-bar/top-bar.tsx | 157 ++++ apps/ui/src/components/ui/context-menu.tsx | 186 +++++ .../shared/agent-model-selector.tsx | 18 +- apps/ui/src/components/views/board-view.tsx | 75 +- .../views/board-view/board-header.tsx | 130 +--- .../views/board-view/kanban-board.tsx | 57 +- .../worktree-panel/worktree-panel.tsx | 39 +- .../views/dashboard-view/dashboard-view.tsx | 320 ++++++++ .../views/dashboard-view/empty-state.tsx | 131 ++++ .../components/views/dashboard-view/index.ts | 3 + .../views/dashboard-view/project-card.tsx | 129 ++++ .../ui/src/components/views/settings-view.tsx | 217 +----- .../components/settings-navigation.tsx | 5 +- .../views/settings-view/settings-content.tsx | 229 ++++++ apps/ui/src/routes/__root.tsx | 51 +- apps/ui/src/routes/dashboard.tsx | 6 + apps/ui/src/store/app-store.ts | 99 +++ package-lock.json | 44 +- package.json | 2 + 39 files changed, 6131 insertions(+), 445 deletions(-) create mode 100644 apps/ui/src/components/command-palette/command-palette.tsx create mode 100644 apps/ui/src/components/command-palette/index.ts create mode 100644 apps/ui/src/components/dialogs/onboarding-wizard/index.ts create mode 100644 apps/ui/src/components/dialogs/onboarding-wizard/onboarding-wizard.tsx create mode 100644 apps/ui/src/components/dialogs/settings-dialog/index.ts create mode 100644 apps/ui/src/components/dialogs/settings-dialog/settings-dialog.tsx create mode 100644 apps/ui/src/components/layout/bottom-dock/bottom-dock.tsx create mode 100644 apps/ui/src/components/layout/bottom-dock/index.ts create mode 100644 apps/ui/src/components/layout/bottom-dock/panels/agents-panel.tsx create mode 100644 apps/ui/src/components/layout/bottom-dock/panels/chat-panel.tsx create mode 100644 apps/ui/src/components/layout/bottom-dock/panels/context-panel.tsx create mode 100644 apps/ui/src/components/layout/bottom-dock/panels/github-panel.tsx create mode 100644 apps/ui/src/components/layout/bottom-dock/panels/ideation-panel.tsx create mode 100644 apps/ui/src/components/layout/bottom-dock/panels/index.ts create mode 100644 apps/ui/src/components/layout/bottom-dock/panels/spec-panel.tsx create mode 100644 apps/ui/src/components/layout/bottom-dock/panels/terminal-panel.tsx create mode 100644 apps/ui/src/components/layout/top-bar/index.ts create mode 100644 apps/ui/src/components/layout/top-bar/pinned-projects.tsx create mode 100644 apps/ui/src/components/layout/top-bar/project-switcher.tsx create mode 100644 apps/ui/src/components/layout/top-bar/top-bar-actions.tsx create mode 100644 apps/ui/src/components/layout/top-bar/top-bar.tsx create mode 100644 apps/ui/src/components/ui/context-menu.tsx create mode 100644 apps/ui/src/components/views/dashboard-view/dashboard-view.tsx create mode 100644 apps/ui/src/components/views/dashboard-view/empty-state.tsx create mode 100644 apps/ui/src/components/views/dashboard-view/index.ts create mode 100644 apps/ui/src/components/views/dashboard-view/project-card.tsx create mode 100644 apps/ui/src/components/views/settings-view/settings-content.tsx create mode 100644 apps/ui/src/routes/dashboard.tsx diff --git a/apps/ui/src/components/command-palette/command-palette.tsx b/apps/ui/src/components/command-palette/command-palette.tsx new file mode 100644 index 00000000..fa7d36a1 --- /dev/null +++ b/apps/ui/src/components/command-palette/command-palette.tsx @@ -0,0 +1,163 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { useAppStore } from '@/store/app-store'; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command'; +import { + Plus, + Sparkles, + Play, + Square, + FileText, + FolderOpen, + Terminal, + Bot, + Settings, + Github, + BookOpen, + Wand2, + Search, + LayoutGrid, +} from 'lucide-react'; + +interface CommandPaletteProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { + const navigate = useNavigate(); + const { currentProject, getAutoModeState, setAutoModeRunning } = useAppStore(); + + const autoModeState = currentProject ? getAutoModeState(currentProject.id) : null; + const isAutoModeRunning = autoModeState?.isRunning ?? false; + + const runCommand = useCallback( + (command: () => void) => { + onOpenChange(false); + command(); + }, + [onOpenChange] + ); + + const handleNavigate = useCallback( + (path: string) => { + runCommand(() => navigate({ to: path })); + }, + [navigate, runCommand] + ); + + const handleToggleAutoMode = useCallback(() => { + if (currentProject) { + runCommand(() => setAutoModeRunning(currentProject.id, !isAutoModeRunning)); + } + }, [currentProject, isAutoModeRunning, setAutoModeRunning, runCommand]); + + return ( + + + + No results found. + + {currentProject && ( + <> + + handleNavigate('/board')}> + + Add Feature + + handleNavigate('/ideation')}> + + Generate Ideas + + + {isAutoModeRunning ? ( + <> + + Stop Auto Mode + + ) : ( + <> + + Start Auto Mode + + )} + + + + + + + handleNavigate('/board')}> + + Kanban Board + + handleNavigate('/running-agents')}> + + Running Agents + + handleNavigate('/terminal')}> + + Terminal + + + + + + + handleNavigate('/spec')}> + + App Specification + + handleNavigate('/context')}> + + Context Files + + handleNavigate('/github-issues')}> + + GitHub Issues + + handleNavigate('/github-prs')}> + + Pull Requests + + + + + + )} + + + handleNavigate('/profiles')}> + + AI Profiles + + handleNavigate('/settings')}> + + Settings + + handleNavigate('/wiki')}> + + Documentation + + + + + + + handleNavigate('/dashboard')}> + + All Projects + + + + + ); +} diff --git a/apps/ui/src/components/command-palette/index.ts b/apps/ui/src/components/command-palette/index.ts new file mode 100644 index 00000000..8ec0f783 --- /dev/null +++ b/apps/ui/src/components/command-palette/index.ts @@ -0,0 +1 @@ +export { CommandPalette } from './command-palette'; diff --git a/apps/ui/src/components/dialogs/onboarding-wizard/index.ts b/apps/ui/src/components/dialogs/onboarding-wizard/index.ts new file mode 100644 index 00000000..77f9c55b --- /dev/null +++ b/apps/ui/src/components/dialogs/onboarding-wizard/index.ts @@ -0,0 +1 @@ +export { OnboardingWizard } from './onboarding-wizard'; diff --git a/apps/ui/src/components/dialogs/onboarding-wizard/onboarding-wizard.tsx b/apps/ui/src/components/dialogs/onboarding-wizard/onboarding-wizard.tsx new file mode 100644 index 00000000..2805aa6d --- /dev/null +++ b/apps/ui/src/components/dialogs/onboarding-wizard/onboarding-wizard.tsx @@ -0,0 +1,386 @@ +import { useState, useCallback } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { cn } from '@/lib/utils'; +import { useAppStore, type ThemeMode } from '@/store/app-store'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { Slider } from '@/components/ui/slider'; +import { + FolderOpen, + FileText, + Sparkles, + ArrowRight, + ArrowLeft, + Check, + Loader2, +} from 'lucide-react'; +import { getElectronAPI } from '@/lib/electron'; +import { initializeProject, hasAutomakerDir, hasAppSpec } from '@/lib/project-init'; +import { toast } from 'sonner'; + +type OnboardingStep = 'select-folder' | 'project-name' | 'app-spec' | 'complete'; +type OnboardingMode = 'new' | 'existing'; + +interface OnboardingWizardProps { + open: boolean; + onOpenChange: (open: boolean) => void; + mode: OnboardingMode; + initialPath?: string; +} + +export function OnboardingWizard({ open, onOpenChange, mode, initialPath }: OnboardingWizardProps) { + const navigate = useNavigate(); + const { + upsertAndSetCurrentProject, + theme: globalTheme, + trashedProjects, + setSpecCreatingForProject, + } = useAppStore(); + + const [step, setStep] = useState(initialPath ? 'project-name' : 'select-folder'); + const [projectPath, setProjectPath] = useState(initialPath || ''); + const [projectName, setProjectName] = useState(''); + const [projectOverview, setProjectOverview] = useState(''); + const [generateFeatures, setGenerateFeatures] = useState(true); + const [featureCount, setFeatureCount] = useState(5); + const [isProcessing, setIsProcessing] = useState(false); + + const handleSelectFolder = useCallback(async () => { + const api = getElectronAPI(); + const result = await api.openDirectory(); + + if (!result.canceled && result.filePaths[0]) { + const path = result.filePaths[0]; + const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project'; + + setProjectPath(path); + setProjectName(name); + + // Check if it's an existing automaker project + const hadAutomakerDir = await hasAutomakerDir(path); + const specExists = await hasAppSpec(path); + + if (hadAutomakerDir && specExists) { + // Existing project with spec - skip to complete + try { + const initResult = await initializeProject(path); + if (initResult.success) { + const trashedProject = trashedProjects.find((p) => p.path === path); + const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || globalTheme; + upsertAndSetCurrentProject(path, name, effectiveTheme); + toast.success('Project opened', { description: `Opened ${name}` }); + onOpenChange(false); + navigate({ to: '/board' }); + } + } catch (error) { + toast.error('Failed to open project'); + } + } else { + setStep('project-name'); + } + } + }, [trashedProjects, globalTheme, upsertAndSetCurrentProject, onOpenChange, navigate]); + + const handleNext = useCallback(() => { + if (step === 'project-name') { + setStep('app-spec'); + } + }, [step]); + + const handleBack = useCallback(() => { + if (step === 'app-spec') { + setStep('project-name'); + } else if (step === 'project-name') { + setStep('select-folder'); + } + }, [step]); + + const handleSkipSpec = useCallback(async () => { + setIsProcessing(true); + try { + const initResult = await initializeProject(projectPath); + if (!initResult.success) { + toast.error('Failed to initialize project'); + return; + } + + const trashedProject = trashedProjects.find((p) => p.path === projectPath); + const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || globalTheme; + upsertAndSetCurrentProject(projectPath, projectName, effectiveTheme); + + toast.success('Project created', { description: `Created ${projectName}` }); + onOpenChange(false); + navigate({ to: '/board' }); + } finally { + setIsProcessing(false); + } + }, [ + projectPath, + projectName, + trashedProjects, + globalTheme, + upsertAndSetCurrentProject, + onOpenChange, + navigate, + ]); + + const handleGenerateSpec = useCallback(async () => { + setIsProcessing(true); + try { + const initResult = await initializeProject(projectPath); + if (!initResult.success) { + toast.error('Failed to initialize project'); + return; + } + + const trashedProject = trashedProjects.find((p) => p.path === projectPath); + const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || globalTheme; + upsertAndSetCurrentProject(projectPath, projectName, effectiveTheme); + + // Start spec generation in background + setSpecCreatingForProject(projectPath); + + onOpenChange(false); + navigate({ to: '/board' }); + + // Use the spec regeneration API + const api = getElectronAPI(); + if (api.specRegeneration && projectOverview.trim()) { + const result = await api.specRegeneration.create( + projectPath, + projectOverview.trim(), + generateFeatures, + true, // analyzeProject + generateFeatures ? featureCount : undefined + ); + + if (!result.success) { + setSpecCreatingForProject(null); + toast.error('Failed to create specification', { + description: result.error, + }); + } else { + toast.info('Generating app specification...', { + description: "This may take a minute. You'll be notified when complete.", + }); + } + } else { + toast.success('Project created', { description: `Created ${projectName}` }); + setSpecCreatingForProject(null); + } + } catch (error) { + setSpecCreatingForProject(null); + toast.error('Failed to create project', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsProcessing(false); + } + }, [ + projectPath, + projectName, + projectOverview, + generateFeatures, + featureCount, + trashedProjects, + globalTheme, + upsertAndSetCurrentProject, + setSpecCreatingForProject, + onOpenChange, + navigate, + ]); + + const renderStep = () => { + switch (step) { + case 'select-folder': + return ( +
+
+
+ +
+

Select Root Directory

+

+ Select the root directory of your project. This can be an empty directory for a new + project or an existing codebase. +

+
+ +
+ ); + + case 'project-name': + return ( +
+
+ + setProjectName(e.target.value)} + placeholder="My Awesome Project" + /> +
+
+ +

{projectPath}

+
+
+ + +
+
+ ); + + case 'app-spec': + return ( +
+
+ +