From adfa92fce2acc9fabe36793adc8928cc84909e81 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 10 Dec 2025 12:33:19 -0500 Subject: [PATCH 1/3] fix(modal): autofocus description field on add feature modal open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add autoFocus prop to DescriptionImageDropZone component - Enable autoFocus in board-view for add feature modal - Ensure description textarea gets focus instead of prompt tab - Also improve scrollbar visibility in git diff panels ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 --- .automaker/feature_list.json | 29 +++++++++- app/electron/auto-mode-service.js | 27 +++++---- app/src/app/globals.css | 56 +++++++++++++++++++ .../ui/description-image-dropzone.tsx | 3 + app/src/components/ui/dialog.tsx | 46 ++++++++------- app/src/components/ui/git-diff-panel.tsx | 8 +-- .../components/views/agent-output-modal.tsx | 18 +++--- app/src/components/views/board-view.tsx | 1 + 8 files changed, 146 insertions(+), 42 deletions(-) diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 0637a088..d38c0cd7 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -1 +1,28 @@ -[] \ No newline at end of file +[ + { + "id": "feature-1765387670653-bl83444lj", + "category": "Kanban", + "description": "In the output logs of the proc agent output in the file diffs Can you add a scroll bar so it actually scroll to see all these new styles right now it seems like I can't scroll", + "steps": [], + "status": "in_progress", + "startedAt": "2025-12-10T17:32:30.636Z", + "imagePaths": [], + "skipTests": true, + "summary": "Added always-visible scrollbar to file diffs in agent output modal. Created new 'scrollbar-visible' CSS utility class in globals.css with theme support for all themes (light, dark, retro, etc.). Applied scrollbar-visible class to: git-diff-panel.tsx (FileDiffSection expanded content), agent-output-modal.tsx (changes view container and logs/raw view container). Scrollbars now remain visible even on macOS where they normally auto-hide.", + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1765387746902-na752mp1y", + "category": "Kanban", + "description": "When the add feature modal pops up, make sure that the description is always the main focus. When it first loads up. Do not focus the prompt tab, which is currently doing this.", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T17:29:13.854Z", + "imagePaths": [], + "skipTests": true, + "summary": "Added autoFocus prop to DescriptionImageDropZone component. Modified: description-image-dropzone.tsx (added autoFocus prop support), board-view.tsx (enabled autoFocus on add feature modal). Now the description textarea receives focus when the modal opens instead of the prompt tab.", + "model": "opus", + "thinkingLevel": "none" + } +] \ No newline at end of file diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 1ebe370e..054c3b03 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -235,14 +235,16 @@ class AutoModeService { // Update feature status based on result // For skipTests features, go to waiting_approval on success instead of verified - // On failure, skipTests features should also go to waiting_approval for user review + // On failure, ALL features go to waiting_approval so user can review and decide next steps + // This prevents infinite retry loops when the same issue keeps failing let newStatus; if (result.passes) { newStatus = feature.skipTests ? "waiting_approval" : "verified"; } else { - // For skipTests features, keep in waiting_approval so user can review - // For normal TDD features, move to backlog for retry - newStatus = feature.skipTests ? "waiting_approval" : "backlog"; + // On failure, go to waiting_approval for user review + // Don't automatically move back to backlog to avoid infinite retry loops + // (especially when hitting rate limits or persistent errors) + newStatus = "waiting_approval"; } await featureLoader.updateFeatureStatus( feature.id, @@ -507,11 +509,13 @@ class AutoModeService { // Update feature status based on final result // For skipTests features, go to waiting_approval on success instead of verified + // On failure, go to waiting_approval so user can review and decide next steps let newStatus; if (finalResult.passes) { newStatus = feature.skipTests ? "waiting_approval" : "verified"; } else { - newStatus = "in_progress"; + // On failure after all retry attempts, go to waiting_approval for user review + newStatus = "waiting_approval"; } await featureLoader.updateFeatureStatus( featureId, @@ -691,14 +695,16 @@ class AutoModeService { // Update feature status based on result // For skipTests features, go to waiting_approval on success instead of verified - // On failure, skipTests features should also go to waiting_approval for user review + // On failure, ALL features go to waiting_approval so user can review and decide next steps + // This prevents infinite retry loops when the same issue keeps failing let newStatus; if (result.passes) { newStatus = feature.skipTests ? "waiting_approval" : "verified"; } else { - // For skipTests features, keep in waiting_approval so user can review - // For normal TDD features, move to backlog for retry - newStatus = feature.skipTests ? "waiting_approval" : "backlog"; + // On failure, go to waiting_approval for user review + // Don't automatically move back to backlog to avoid infinite retry loops + // (especially when hitting rate limits or persistent errors) + newStatus = "waiting_approval"; } await featureLoader.updateFeatureStatus( feature.id, @@ -936,11 +942,12 @@ class AutoModeService { ); // For skipTests features, go to waiting_approval on success instead of verified + // On failure, go to waiting_approval so user can review and decide next steps const newStatus = result.passes ? feature.skipTests ? "waiting_approval" : "verified" - : "in_progress"; + : "waiting_approval"; await featureLoader.updateFeatureStatus( feature.id, diff --git a/app/src/app/globals.css b/app/src/app/globals.css index 09207187..4920fa6a 100644 --- a/app/src/app/globals.css +++ b/app/src/app/globals.css @@ -1039,6 +1039,62 @@ background: var(--background); } +/* Always visible scrollbar for file diffs and code blocks */ +.scrollbar-visible { + overflow-y: auto !important; + scrollbar-width: thin; + scrollbar-color: var(--muted-foreground) var(--muted); +} + +.scrollbar-visible::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.scrollbar-visible::-webkit-scrollbar-track { + background: var(--muted); + border-radius: 4px; +} + +.scrollbar-visible::-webkit-scrollbar-thumb { + background: var(--muted-foreground); + border-radius: 4px; + min-height: 40px; +} + +.scrollbar-visible::-webkit-scrollbar-thumb:hover { + background: var(--foreground-secondary); +} + +/* Force scrollbar to always be visible (not auto-hide) */ +.scrollbar-visible::-webkit-scrollbar-thumb { + visibility: visible; +} + +/* Light mode scrollbar-visible adjustments */ +.light .scrollbar-visible::-webkit-scrollbar-track { + background: oklch(0.95 0 0); +} + +.light .scrollbar-visible::-webkit-scrollbar-thumb { + background: oklch(0.7 0 0); +} + +.light .scrollbar-visible::-webkit-scrollbar-thumb:hover { + background: oklch(0.6 0 0); +} + +/* Retro mode scrollbar-visible adjustments */ +.retro .scrollbar-visible::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 0; +} + +.retro .scrollbar-visible::-webkit-scrollbar-track { + background: var(--background); + border-radius: 0; +} + /* Glass morphism utilities */ @layer utilities { .glass { diff --git a/app/src/components/ui/description-image-dropzone.tsx b/app/src/components/ui/description-image-dropzone.tsx index f483fb1a..7e11c9e5 100644 --- a/app/src/components/ui/description-image-dropzone.tsx +++ b/app/src/components/ui/description-image-dropzone.tsx @@ -30,6 +30,7 @@ interface DescriptionImageDropZoneProps { // Optional: pass preview map from parent to persist across tab switches previewMap?: ImagePreviewMap; onPreviewMapChange?: (map: ImagePreviewMap) => void; + autoFocus?: boolean; } const ACCEPTED_IMAGE_TYPES = [ @@ -53,6 +54,7 @@ export function DescriptionImageDropZone({ maxFileSize = DEFAULT_MAX_FILE_SIZE, previewMap, onPreviewMapChange, + autoFocus = false, }: DescriptionImageDropZoneProps) { const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); @@ -303,6 +305,7 @@ export function DescriptionImageDropZone({ value={value} onChange={(e) => onChange(e.target.value)} disabled={disabled} + autoFocus={autoFocus} className={cn( "min-h-[120px]", isProcessing && "opacity-50 pointer-events-none" diff --git a/app/src/components/ui/dialog.tsx b/app/src/components/ui/dialog.tsx index 57fbbc61..2438783e 100644 --- a/app/src/components/ui/dialog.tsx +++ b/app/src/components/ui/dialog.tsx @@ -1,33 +1,33 @@ -"use client" +"use client"; -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Dialog({ ...props }: React.ComponentProps) { - return + return ; } function DialogTrigger({ ...props }: React.ComponentProps) { - return + return ; } function DialogPortal({ ...props }: React.ComponentProps) { - return + return ; } function DialogClose({ ...props }: React.ComponentProps) { - return + return ; } function DialogOverlay({ @@ -43,7 +43,7 @@ function DialogOverlay({ )} {...props} /> - ) + ); } function DialogContent({ @@ -53,9 +53,13 @@ function DialogContent({ compact = false, ...props }: React.ComponentProps & { - showCloseButton?: boolean - compact?: boolean + showCloseButton?: boolean; + compact?: boolean; }) { + // Check if className contains a custom max-width + const hasCustomMaxWidth = + typeof className === "string" && className.includes("max-w-"); + return ( @@ -63,7 +67,11 @@ function DialogContent({ data-slot="dialog-content" className={cn( "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-4rem)]", - compact ? "max-w-2xl p-4" : "sm:max-w-2xl p-6", + compact + ? "max-w-4xl p-4" + : !hasCustomMaxWidth + ? "sm:max-w-2xl p-6" + : "p-6", className )} {...props} @@ -83,7 +91,7 @@ function DialogContent({ )} - ) + ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -93,7 +101,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex flex-col gap-2 text-center sm:text-left", className)} {...props} /> - ) + ); } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -106,7 +114,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { )} {...props} /> - ) + ); } function DialogTitle({ @@ -119,7 +127,7 @@ function DialogTitle({ className={cn("text-lg leading-none font-semibold", className)} {...props} /> - ) + ); } function DialogDescription({ @@ -132,7 +140,7 @@ function DialogDescription({ className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } export { @@ -146,4 +154,4 @@ export { DialogPortal, DialogTitle, DialogTrigger, -} +}; diff --git a/app/src/components/ui/git-diff-panel.tsx b/app/src/components/ui/git-diff-panel.tsx index ae734a9c..2ddd5015 100644 --- a/app/src/components/ui/git-diff-panel.tsx +++ b/app/src/components/ui/git-diff-panel.tsx @@ -333,7 +333,7 @@ function FileDiffSection({ {isExpanded && ( -
+
{fileDiff.hunks.map((hunk, hunkIndex) => (
{hunk.lines.map((line, lineIndex) => ( @@ -458,7 +458,7 @@ export function GitDiffPanel({ return (
setIsExpanded(!isExpanded)} - className="w-full px-4 py-3 flex items-center justify-between bg-card hover:bg-accent/50 transition-colors text-left" + className="w-full px-4 py-3 flex items-center justify-between bg-card hover:bg-accent/50 transition-colors text-left flex-shrink-0" data-testid="git-diff-panel-toggle" >
@@ -497,7 +497,7 @@ export function GitDiffPanel({ {/* Content */} {isExpanded && ( -
+
{isLoading ? (
diff --git a/app/src/components/views/agent-output-modal.tsx b/app/src/components/views/agent-output-modal.tsx index f463737b..a5f05585 100644 --- a/app/src/components/views/agent-output-modal.tsx +++ b/app/src/components/views/agent-output-modal.tsx @@ -154,29 +154,31 @@ export function AgentOutputModal({ case "auto_mode_ultrathink_preparation": // Format thinking level preparation information let prepContent = `\n๐Ÿง  Ultrathink Preparation\n`; - + if (event.warnings && event.warnings.length > 0) { prepContent += `\nโš ๏ธ Warnings:\n`; event.warnings.forEach((warning: string) => { prepContent += ` โ€ข ${warning}\n`; }); } - + if (event.recommendations && event.recommendations.length > 0) { prepContent += `\n๐Ÿ’ก Recommendations:\n`; event.recommendations.forEach((rec: string) => { prepContent += ` โ€ข ${rec}\n`; }); } - + if (event.estimatedCost !== undefined) { - prepContent += `\n๐Ÿ’ฐ Estimated Cost: ~$${event.estimatedCost.toFixed(2)} per execution\n`; + prepContent += `\n๐Ÿ’ฐ Estimated Cost: ~$${event.estimatedCost.toFixed( + 2 + )} per execution\n`; } - + if (event.estimatedTime) { prepContent += `\nโฑ๏ธ Estimated Time: ${event.estimatedTime}\n`; } - + newContent = prepContent; break; case "auto_mode_feature_complete": @@ -299,7 +301,7 @@ export function AgentOutputModal({ {viewMode === "changes" ? ( -
+
{projectPath ? ( {isLoading && !output ? (
diff --git a/app/src/components/views/board-view.tsx b/app/src/components/views/board-view.tsx index 150ec84f..480d3ae1 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -1738,6 +1738,7 @@ export function BoardView() { placeholder="Describe the feature..." previewMap={newFeaturePreviewMap} onPreviewMapChange={setNewFeaturePreviewMap} + autoFocus />
From 9251411da91230a76de6f008da7db0ce4c5cae68 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 10 Dec 2025 12:39:19 -0500 Subject: [PATCH 2/3] docs: add security disclaimer and improve README structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created comprehensive DISCLAIMER.md with security warnings about AI tooling risks and sandboxing recommendations. Updated README.md with project overview, security disclaimer section, and complete features list with emojis. Also improved git-diff-panel layout with better scrolling behavior. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 --- .automaker/feature_list.json | 21 ++- DISCLAIMER.md | 58 ++++++ app/README.md | 55 +++++- app/src/components/ui/git-diff-panel.tsx | 168 +++++++++--------- .../components/views/agent-output-modal.tsx | 4 +- 5 files changed, 208 insertions(+), 98 deletions(-) create mode 100644 DISCLAIMER.md diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index d38c0cd7..3d580337 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -4,11 +4,11 @@ "category": "Kanban", "description": "In the output logs of the proc agent output in the file diffs Can you add a scroll bar so it actually scroll to see all these new styles right now it seems like I can't scroll", "steps": [], - "status": "in_progress", - "startedAt": "2025-12-10T17:32:30.636Z", + "status": "waiting_approval", + "startedAt": "2025-12-10T17:36:31.716Z", "imagePaths": [], "skipTests": true, - "summary": "Added always-visible scrollbar to file diffs in agent output modal. Created new 'scrollbar-visible' CSS utility class in globals.css with theme support for all themes (light, dark, retro, etc.). Applied scrollbar-visible class to: git-diff-panel.tsx (FileDiffSection expanded content), agent-output-modal.tsx (changes view container and logs/raw view container). Scrollbars now remain visible even on macOS where they normally auto-hide.", + "summary": "Fixed file diffs scrolling in agent output modal. Restructured git-diff-panel.tsx with flex layout - summary bar stays fixed at top while file list scrolls with visible scrollbar. Previously added scrollbar-visible CSS utility class in globals.css.", "model": "opus", "thinkingLevel": "none" }, @@ -17,12 +17,25 @@ "category": "Kanban", "description": "When the add feature modal pops up, make sure that the description is always the main focus. When it first loads up. Do not focus the prompt tab, which is currently doing this.", "steps": [], - "status": "waiting_approval", + "status": "verified", "startedAt": "2025-12-10T17:29:13.854Z", "imagePaths": [], "skipTests": true, "summary": "Added autoFocus prop to DescriptionImageDropZone component. Modified: description-image-dropzone.tsx (added autoFocus prop support), board-view.tsx (enabled autoFocus on add feature modal). Now the description textarea receives focus when the modal opens instead of the prompt tab.", "model": "opus", "thinkingLevel": "none" + }, + { + "id": "feature-1765388139100-ln31jgp5n", + "category": "Uncategorized", + "description": "Can you add a disclaimer .md file to this project saying that this uses a bunch of AI related tooling which could have access to your operating system and change and delete files and so use at your own risk. We tried to check it for security of vulnerability to make sure it's good. But you assume the risk and you should be reviewing the code yourself before you try to run it. And also sandboxing this so it doesn't have access to your whole operating system like using Docker to sandbox before you run it or use a virtual machine to sandbox it. and that we do not recommend running locally on your computer due to the risk of it having access to everything on your computer.\n\nUpdate or read me with a short paragraph overview/description at the top followed by a disclaimer section in red that points to the disclaimer file with the same disclaimer information.\n\nThen a section that lists out all the features of cool emojis.", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T17:35:40.700Z", + "imagePaths": [], + "skipTests": true, + "summary": "Created DISCLAIMER.md with comprehensive security warnings about AI tooling risks and sandboxing recommendations. Updated README.md with project overview, red caution disclaimer section linking to DISCLAIMER.md, and features list with emojis covering all major functionality (Kanban, AI agents, multi-model support, etc.).", + "model": "opus", + "thinkingLevel": "none" } ] \ No newline at end of file diff --git a/DISCLAIMER.md b/DISCLAIMER.md new file mode 100644 index 00000000..c13a610b --- /dev/null +++ b/DISCLAIMER.md @@ -0,0 +1,58 @@ +# Security Disclaimer + +## Important Warning + +**Automaker uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.** + +## Risk Assessment + +This software utilizes AI agents (such as Claude) that can: + +- **Read files** from your file system +- **Write and modify files** in your projects +- **Delete files** when instructed +- **Execute commands** on your operating system +- **Access environment variables** and configuration files + +While we have made efforts to review this codebase for security vulnerabilities and implement safeguards, **you assume all risk** when running this software. + +## Recommendations + +### 1. Review the Code First +Before running Automaker, we strongly recommend reviewing the source code yourself to understand what operations it performs and ensure you are comfortable with its behavior. + +### 2. Use Sandboxing (Highly Recommended) +**We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Instead, consider: + +- **Docker**: Run Automaker in a Docker container to isolate it from your host system +- **Virtual Machine**: Use a VM (such as VirtualBox, VMware, or Parallels) to create an isolated environment +- **Cloud Development Environment**: Use a cloud-based development environment that provides isolation + +### 3. Limit Access +If you must run locally: +- Create a dedicated user account with limited permissions +- Only grant access to specific project directories +- Avoid running with administrator/root privileges +- Keep sensitive files and credentials outside of project directories + +### 4. Monitor Activity +- Review the agent's actions in the output logs +- Pay attention to file modifications and command executions +- Stop the agent immediately if you notice unexpected behavior + +## No Warranty + +This software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + +## Acknowledgment + +By using Automaker, you acknowledge that: + +1. You have read and understood this disclaimer +2. You accept full responsibility for any consequences of using this software +3. You understand the risks of AI agents having access to your operating system +4. You agree to take appropriate precautions as outlined above + +--- + +**If you are not comfortable with these risks, do not use this software.** diff --git a/app/README.md b/app/README.md index cddabe10..fc43a642 100644 --- a/app/README.md +++ b/app/README.md @@ -1,4 +1,41 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Automaker + +Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion. + +--- + +> **[!CAUTION]** +> ## Security Disclaimer +> +> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.** +> +> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it. +> +> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine. +> +> **[Read the full disclaimer](../DISCLAIMER.md)** + +--- + +## Features + +- ๐Ÿ“‹ **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages +- ๐Ÿค– **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress" +- ๐Ÿง  **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more +- ๐Ÿ’ญ **Extended Thinking** - Enable extended thinking modes for complex problem-solving +- ๐Ÿ“ก **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented +- ๐Ÿ” **Project Analysis** - AI-powered project structure analysis to understand your codebase +- ๐Ÿ“ **Context Management** - Add context files to help AI agents understand your project better +- ๐Ÿ’ก **Feature Suggestions** - AI-generated feature suggestions based on your project +- ๐Ÿ–ผ๏ธ **Image Support** - Attach images and screenshots to feature descriptions +- โšก **Concurrent Processing** - Configure concurrency to process multiple features simultaneously +- ๐Ÿงช **Test Integration** - Automatic test running and verification for implemented features +- ๐Ÿ”€ **Git Integration** - View git diffs and track changes made by AI agents +- ๐Ÿ‘ค **AI Profiles** - Create and manage different AI agent profiles for various tasks +- ๐Ÿ’ฌ **Chat History** - Keep track of conversations and interactions with AI agents +- โŒจ๏ธ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts +- ๐ŸŽจ **Dark/Light Theme** - Beautiful UI with theme support +- ๐Ÿ–ฅ๏ธ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux ## Getting Started @@ -16,9 +53,13 @@ bun dev Open [http://localhost:3007](http://localhost:3007) with your browser to see the result. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Tech Stack -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +- [Next.js](https://nextjs.org) - React framework +- [Electron](https://www.electronjs.org/) - Desktop application framework +- [Tailwind CSS](https://tailwindcss.com/) - Styling +- [Zustand](https://zustand-demo.pmnd.rs/) - State management +- [dnd-kit](https://dndkit.com/) - Drag and drop functionality ## Learn More @@ -27,10 +68,6 @@ To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## License -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +See [LICENSE](../LICENSE) for details. diff --git a/app/src/components/ui/git-diff-panel.tsx b/app/src/components/ui/git-diff-panel.tsx index 2ddd5015..5c99406b 100644 --- a/app/src/components/ui/git-diff-panel.tsx +++ b/app/src/components/ui/git-diff-panel.tsx @@ -497,7 +497,7 @@ export function GitDiffPanel({ {/* Content */} {isExpanded && ( -
+
{isLoading ? (
@@ -522,94 +522,96 @@ export function GitDiffPanel({ No changes detected
) : ( -
- {/* Summary bar */} -
-
- {(() => { - // Group files by status - const statusGroups = files.reduce((acc, file) => { - const status = file.status; - if (!acc[status]) { - acc[status] = { - count: 0, - statusText: getStatusDisplayName(status), - files: [] - }; - } - acc[status].count += 1; - acc[status].files.push(file.path); - return acc; - }, {} as Record); +
+ {/* Summary bar - fixed at top */} +
+
+
+ {(() => { + // Group files by status + const statusGroups = files.reduce((acc, file) => { + const status = file.status; + if (!acc[status]) { + acc[status] = { + count: 0, + statusText: getStatusDisplayName(status), + files: [] + }; + } + acc[status].count += 1; + acc[status].files.push(file.path); + return acc; + }, {} as Record); - return Object.entries(statusGroups).map(([status, group]) => ( -
- {getFileIcon(status)} - ( +
- {group.count} {group.statusText} - -
- )); - })()} + {getFileIcon(status)} + + {group.count} {group.statusText} + +
+ )); + })()} +
+
+ + + +
-
- - - + + {/* Stats */} +
+ + {files.length} {files.length === 1 ? "file" : "files"} changed + + {totalAdditions > 0 && ( + + +{totalAdditions} additions + + )} + {totalDeletions > 0 && ( + + -{totalDeletions} deletions + + )}
- {/* Stats */} -
- - {files.length} {files.length === 1 ? "file" : "files"} changed - - {totalAdditions > 0 && ( - - +{totalAdditions} additions - - )} - {totalDeletions > 0 && ( - - -{totalDeletions} deletions - - )} -
- - {/* File diffs */} -
+ {/* File diffs - scrollable */} +
{parsedDiffs.map((fileDiff) => ( {viewMode === "changes" ? ( -
+
{projectPath ? ( ) : (
From f9ba1c260a2c4f3b92f3c6f699ea7646a361ae5f Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 10 Dec 2025 12:56:24 -0500 Subject: [PATCH 3/3] feat(ui): add per-project themes and fix scrolling/styling issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add per-project theme support with theme selector in sidebar - Fix file diffs scrolling in agent output modal with proper overflow handling - Improve cursor styling across all interactive elements (buttons, tabs, checkboxes) - Enhance hotkey styling to use consistent theme colors - Fix Kanban board flash/refresh issues with React.memo optimizations - Update tab component for better theme integration with proper active states ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 --- .automaker/context/clean-code.md | 474 ++++++++++++++++++ .automaker/feature_list.json | 124 ++++- app/src/app/globals.css | 21 + app/src/app/page.tsx | 36 +- app/src/components/layout/sidebar.tsx | 157 ++++-- app/src/components/session-manager.tsx | 2 +- app/src/components/ui/button.tsx | 2 +- app/src/components/ui/checkbox.tsx | 2 +- app/src/components/ui/dialog.tsx | 2 +- app/src/components/ui/dropdown-menu.tsx | 10 +- app/src/components/ui/git-diff-panel.tsx | 14 +- app/src/components/ui/slider.tsx | 4 +- app/src/components/ui/tabs.tsx | 9 +- .../components/views/agent-output-modal.tsx | 4 +- app/src/components/views/board-view.tsx | 48 +- app/src/components/views/kanban-card.tsx | 8 +- app/src/components/views/kanban-column.tsx | 5 +- app/src/components/views/profiles-view.tsx | 2 +- app/src/lib/electron.ts | 1 + app/src/store/app-store.ts | 33 ++ 20 files changed, 868 insertions(+), 90 deletions(-) create mode 100644 .automaker/context/clean-code.md diff --git a/.automaker/context/clean-code.md b/.automaker/context/clean-code.md new file mode 100644 index 00000000..17c5eb16 --- /dev/null +++ b/.automaker/context/clean-code.md @@ -0,0 +1,474 @@ +# Clean Code Guidelines + +## Overview + +This document serves as a comprehensive guide for writing clean, maintainable, and extensible code. It outlines principles and practices that ensure code quality, reusability, and long-term maintainability. When writing or reviewing code, follow these guidelines to create software that is easy to understand, modify, and extend. This file is used by LLMs to understand and enforce coding standards throughout the codebase. + +--- + +## Core Principles + +### 1. DRY (Don't Repeat Yourself) + +**Principle**: Every piece of knowledge should have a single, unambiguous representation within a system. + +**Practices**: + +- Extract repeated logic into reusable functions, classes, or modules +- Use constants for repeated values +- Create shared utilities for common operations +- Avoid copy-pasting code blocks +- When you find yourself writing similar code more than twice, refactor it + +**Example - Bad**: + +```typescript +// Repeated validation logic +if (email.includes("@") && email.length > 5) { + // ... +} +if (email.includes("@") && email.length > 5) { + // ... +} +``` + +**Example - Good**: + +```typescript +function isValidEmail(email: string): boolean { + return email.includes("@") && email.length > 5; +} + +if (isValidEmail(email)) { + // ... +} +``` + +--- + +### 2. Code Reusability + +**Principle**: Write code that can be used in multiple contexts without modification or with minimal adaptation. + +**Practices**: + +- Create generic, parameterized functions instead of specific ones +- Use composition over inheritance where appropriate +- Design functions to be pure (no side effects) when possible +- Create utility libraries for common operations +- Use dependency injection to make components reusable +- Design APIs that are flexible and configurable + +**Example - Bad**: + +```typescript +function calculateUserTotal(userId: string) { + const user = getUser(userId); + return user.items.reduce((sum, item) => sum + item.price, 0); +} +``` + +**Example - Good**: + +```typescript +function calculateTotal(items: T[]): number { + return items.reduce((sum, item) => sum + item.price, 0); +} + +function calculateUserTotal(userId: string) { + const user = getUser(userId); + return calculateTotal(user.items); +} +``` + +--- + +### 3. Abstract Functions and Abstractions + +**Principle**: Create abstractions that hide implementation details and provide clear, simple interfaces. + +**Practices**: + +- Use interfaces and abstract classes to define contracts +- Create abstraction layers between different concerns +- Hide complex implementation behind simple function signatures +- Use dependency inversion - depend on abstractions, not concretions +- Create factory functions/classes for object creation +- Use strategy pattern for interchangeable algorithms + +**Example - Bad**: + +```typescript +function processPayment(amount: number, cardNumber: string, cvv: string) { + // Direct implementation tied to specific payment processor + fetch("https://stripe.com/api/charge", { + method: "POST", + body: JSON.stringify({ amount, cardNumber, cvv }), + }); +} +``` + +**Example - Good**: + +```typescript +interface PaymentProcessor { + processPayment( + amount: number, + details: PaymentDetails + ): Promise; +} + +class StripeProcessor implements PaymentProcessor { + async processPayment( + amount: number, + details: PaymentDetails + ): Promise { + // Implementation + } +} + +function processPayment( + processor: PaymentProcessor, + amount: number, + details: PaymentDetails +) { + return processor.processPayment(amount, details); +} +``` + +--- + +### 4. Extensibility + +**Principle**: Design code that can be easily extended with new features without modifying existing code. + +**Practices**: + +- Follow the Open/Closed Principle: open for extension, closed for modification +- Use plugin architectures and hooks for extensibility +- Design with future requirements in mind (but don't over-engineer) +- Use configuration over hardcoding +- Create extension points through interfaces and callbacks +- Use composition and dependency injection +- Design APIs that can accommodate new parameters/options + +**Example - Bad**: + +```typescript +function sendNotification(user: User, type: string) { + if (type === "email") { + sendEmail(user.email); + } else if (type === "sms") { + sendSMS(user.phone); + } + // Adding new notification types requires modifying this function +} +``` + +**Example - Good**: + +```typescript +interface NotificationChannel { + send(user: User): Promise; +} + +class EmailChannel implements NotificationChannel { + async send(user: User): Promise { + // Implementation + } +} + +class SMSChannel implements NotificationChannel { + async send(user: User): Promise { + // Implementation + } +} + +class NotificationService { + constructor(private channels: NotificationChannel[]) {} + + async send(user: User): Promise { + await Promise.all(this.channels.map((channel) => channel.send(user))); + } +} +// New notification types can be added without modifying existing code +``` + +--- + +### 5. Avoid Magic Numbers and Strings + +**Principle**: Use named constants instead of hardcoded values to improve readability and maintainability. + +**Practices**: + +- Extract all magic numbers into named constants +- Use enums for related constants +- Create configuration objects for settings +- Use constants for API endpoints, timeouts, limits, etc. +- Document why specific values are used + +**Example - Bad**: + +```typescript +if (user.age >= 18) { + // What does 18 mean? +} + +setTimeout(() => { + // What does 3000 mean? +}, 3000); + +if (status === "active") { + // What are the valid statuses? +} +``` + +**Example - Good**: + +```typescript +const MINIMUM_AGE_FOR_ADULTS = 18; +const SESSION_TIMEOUT_MS = 3000; + +enum UserStatus { + ACTIVE = "active", + INACTIVE = "inactive", + SUSPENDED = "suspended", +} + +if (user.age >= MINIMUM_AGE_FOR_ADULTS) { + // Clear intent +} + +setTimeout(() => { + // Clear intent +}, SESSION_TIMEOUT_MS); + +if (status === UserStatus.ACTIVE) { + // Type-safe and clear +} +``` + +--- + +## Additional Best Practices + +### 6. Single Responsibility Principle + +Each function, class, or module should have one reason to change. + +**Example**: + +```typescript +// Bad: Multiple responsibilities +class User { + save() { + /* database logic */ + } + sendEmail() { + /* email logic */ + } + validate() { + /* validation logic */ + } +} + +// Good: Single responsibility +class User { + validate() { + /* validation only */ + } +} + +class UserRepository { + save(user: User) { + /* database logic */ + } +} + +class EmailService { + sendToUser(user: User) { + /* email logic */ + } +} +``` + +### 7. Meaningful Names + +- Use descriptive names that reveal intent +- Avoid abbreviations unless they're widely understood +- Use verbs for functions, nouns for classes +- Be consistent with naming conventions + +**Example**: + +```typescript +// Bad +const d = new Date(); +const u = getUser(); +function calc(x, y) {} + +// Good +const currentDate = new Date(); +const currentUser = getUser(); +function calculateTotal(price: number, quantity: number): number {} +``` + +### 8. Small Functions + +- Functions should do one thing and do it well +- Keep functions short (ideally under 20 lines) +- Extract complex logic into separate functions +- Use descriptive function names instead of comments + +### 9. Error Handling + +- Handle errors explicitly +- Use appropriate error types +- Provide meaningful error messages +- Don't swallow errors silently +- Use try-catch appropriately + +**Example**: + +```typescript +// Bad +function divide(a: number, b: number) { + return a / b; // Can throw division by zero +} + +// Good +function divide(a: number, b: number): number { + if (b === 0) { + throw new Error("Division by zero is not allowed"); + } + return a / b; +} +``` + +### 10. Comments and Documentation + +- Write self-documenting code (code should explain itself) +- Use comments to explain "why", not "what" +- Document complex algorithms or business logic +- Keep comments up-to-date with code changes +- Use JSDoc/TSDoc for public APIs + +### 11. Type Safety + +- Use TypeScript types/interfaces effectively +- Avoid `any` type unless absolutely necessary +- Use union types and discriminated unions +- Leverage type inference where appropriate +- Create custom types for domain concepts + +**Example**: + +```typescript +// Bad +function processUser(data: any) { + return data.name; +} + +// Good +interface User { + id: string; + name: string; + email: string; +} + +function processUser(user: User): string { + return user.name; +} +``` + +### 12. Testing Considerations + +- Write testable code (pure functions, dependency injection) +- Keep functions small and focused +- Avoid hidden dependencies +- Use mocks and stubs appropriately +- Design for testability from the start + +### 13. Performance vs. Readability + +- Prefer readability over premature optimization +- Profile before optimizing +- Use clear algorithms first, optimize if needed +- Document performance-critical sections +- Balance between clean code and performance requirements + +### 14. Code Organization + +- Group related functionality together +- Use modules/packages to organize code +- Follow consistent file and folder structures +- Separate concerns (UI, business logic, data access) +- Use barrel exports (index files) appropriately + +### 15. Configuration Management + +- Externalize configuration values +- Use environment variables for environment-specific settings +- Create configuration objects/interfaces +- Validate configuration at startup +- Provide sensible defaults + +**Example**: + +```typescript +// Bad +const apiUrl = "https://api.example.com"; +const timeout = 5000; + +// Good +interface Config { + apiUrl: string; + timeout: number; + maxRetries: number; +} + +const config: Config = { + apiUrl: process.env.API_URL || "https://api.example.com", + timeout: parseInt(process.env.TIMEOUT || "5000"), + maxRetries: parseInt(process.env.MAX_RETRIES || "3"), +}; +``` + +--- + +## Code Review Checklist + +When reviewing code, check for: + +- [ ] No code duplication (DRY principle) +- [ ] Meaningful variable and function names +- [ ] No magic numbers or strings +- [ ] Functions are small and focused +- [ ] Proper error handling +- [ ] Type safety maintained +- [ ] Code is testable +- [ ] Documentation where needed +- [ ] Consistent code style +- [ ] Proper abstraction levels +- [ ] Extensibility considered +- [ ] Single responsibility principle followed + +--- + +## Summary + +Clean code is: + +- **Readable**: Easy to understand at a glance +- **Maintainable**: Easy to modify and update +- **Testable**: Easy to write tests for +- **Extensible**: Easy to add new features +- **Reusable**: Can be used in multiple contexts +- **Well-documented**: Clear intent and purpose +- **Type-safe**: Leverages type system effectively +- **DRY**: No unnecessary repetition +- **Abstracted**: Proper separation of concerns +- **Configurable**: Uses constants and configuration over hardcoding + +Remember: Code is read far more often than it is written. Write code for your future self and your teammates. \ No newline at end of file diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 3d580337..a797057a 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -5,10 +5,10 @@ "description": "In the output logs of the proc agent output in the file diffs Can you add a scroll bar so it actually scroll to see all these new styles right now it seems like I can't scroll", "steps": [], "status": "waiting_approval", - "startedAt": "2025-12-10T17:36:31.716Z", + "startedAt": "2025-12-10T17:42:09.158Z", "imagePaths": [], "skipTests": true, - "summary": "Fixed file diffs scrolling in agent output modal. Restructured git-diff-panel.tsx with flex layout - summary bar stays fixed at top while file list scrolls with visible scrollbar. Previously added scrollbar-visible CSS utility class in globals.css.", + "summary": "Fixed scrolling for file diffs in agent output modal. Changed approach: parent container (agent-output-modal.tsx) now handles scrolling with overflow-y-auto, while GitDiffPanel uses natural height without flex-based scrolling. Modified: agent-output-modal.tsx (line 304), git-diff-panel.tsx (lines 461, 500, 525, 614).", "model": "opus", "thinkingLevel": "none" }, @@ -30,12 +30,130 @@ "category": "Uncategorized", "description": "Can you add a disclaimer .md file to this project saying that this uses a bunch of AI related tooling which could have access to your operating system and change and delete files and so use at your own risk. We tried to check it for security of vulnerability to make sure it's good. But you assume the risk and you should be reviewing the code yourself before you try to run it. And also sandboxing this so it doesn't have access to your whole operating system like using Docker to sandbox before you run it or use a virtual machine to sandbox it. and that we do not recommend running locally on your computer due to the risk of it having access to everything on your computer.\n\nUpdate or read me with a short paragraph overview/description at the top followed by a disclaimer section in red that points to the disclaimer file with the same disclaimer information.\n\nThen a section that lists out all the features of cool emojis.", "steps": [], - "status": "waiting_approval", + "status": "verified", "startedAt": "2025-12-10T17:35:40.700Z", "imagePaths": [], "skipTests": true, "summary": "Created DISCLAIMER.md with comprehensive security warnings about AI tooling risks and sandboxing recommendations. Updated README.md with project overview, red caution disclaimer section linking to DISCLAIMER.md, and features list with emojis covering all major functionality (Kanban, AI agents, multi-model support, etc.).", "model": "opus", "thinkingLevel": "none" + }, + { + "id": "feature-1765388388144-oa1dewze9", + "category": "Uncategorized", + "description": "Please fix the styling of the hotkeys to be more using the theme colors. Notice that they're kind of gray. I would rather than have some type of like light green if they're not active and then the brighter green if they are active and also the add feature but in the top right it's not very legible. So fix the accessibility of the hotkey but also keep it within the theme. You might just have to change the text inside of it to be bright green.", + "steps": [], + "status": "verified", + "startedAt": "2025-12-10T17:40:02.745Z", + "imagePaths": [ + { + "id": "img-1765388352835-dgx4ishp0", + "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388352832-6jnbgw8kg_Screenshot_2025-12-10_at_12.39.10_PM.png", + "filename": "Screenshot 2025-12-10 at 12.39.10โ€ฏPM.png", + "mimeType": "image/png" + }, + { + "id": "img-1765388356955-a0gdovp5b", + "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388356954-d59a65nf9_Screenshot_2025-12-10_at_12.39.15_PM.png", + "filename": "Screenshot 2025-12-10 at 12.39.15โ€ฏPM.png", + "mimeType": "image/png" + } + ], + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1765388402095-x66aduwg3", + "category": "Uncategorized", + "description": "Can you please add some spacing and fix the styling of the hotkey with the command enter and make it so they're both vertically aligned for those icons?", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T17:44:08.667Z", + "imagePaths": [ + { + "id": "img-1765388390408-eefybe95t", + "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388390408-nn320yoyc_Screenshot_2025-12-10_at_12.39.47_PM.png", + "filename": "Screenshot 2025-12-10 at 12.39.47โ€ฏPM.png", + "mimeType": "image/png" + } + ], + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1765388662444-as3hqn7be", + "category": "Uncategorized", + "description": "Fix the styling on all the buttons when I hover over them with my mouse they never change to a click mouse cursor. In order they seem to show any type of like hover state changes, if they do, at least for the certain game I'm using, it's not very obvious that you're hovering over the button.", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T17:45:59.666Z", + "imagePaths": [], + "skipTests": true, + "summary": "Fixed hover cursor styling on all interactive elements. Modified: button.tsx (added cursor-pointer to base styles), dropdown-menu.tsx (added cursor-pointer to all menu items), checkbox.tsx (added cursor-pointer), tabs.tsx (added cursor-pointer to triggers), dialog.tsx (added cursor-pointer to close button), slider.tsx (added cursor-grab to thumb, cursor-pointer to track), globals.css (added global CSS rules for clickable elements to ensure consistent cursor behavior).", + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1765388693856-yx1dk1acj", + "category": "Kanban", + "description": "The tabs in the add new feature modal for the prompt model and testing tabs. They don't seem to look like tabs when I'm on a certain theme. Can you verify that those are hooked into the theme? And make sure that the active one is colored differently than the unactive ones. Keep the primary colors when doing this.", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T17:46:00.019Z", + "imagePaths": [], + "skipTests": true, + "summary": "Fixed tabs component theme integration. Modified: tabs.tsx. Changes: (1) Added visible border to TabsList container using theme's border color, (2) Changed inactive tab text to foreground/70 for better contrast, (3) Enhanced active tab with shadow-md and semi-transparent primary border, (4) Improved hover state with full accent background. Active tabs now properly use bg-primary/text-primary-foreground which adapts to each theme.", + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1765388754462-bek0flvkj", + "category": "Uncategorized", + "description": "There's a strange issue when I when when these agents are like doing things it seems like it completely refreshes the whole Kanban board and there's like a black flash. Can you verify that the data loading does not cause the entire component to refresh? Maybe there's an issue with the react effect or how the component is rendered maybe we need some used memos or something but it shouldn't refresh the whole page it should just like update the individual cards when they change.", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T17:47:20.170Z", + "imagePaths": [], + "skipTests": true, + "summary": "Fixed Kanban board flash/refresh issue. Changes: (1) board-view.tsx - Added isInitialLoadRef to only show loading spinner on initial load, not on feature reloads; memoized column features with useMemo to prevent recalculation on every render. (2) kanban-card.tsx - Wrapped with React.memo to prevent unnecessary re-renders. (3) kanban-column.tsx - Wrapped with React.memo for performance. The flash was caused by loadFeatures setting isLoading=true on every reload, which caused the entire board to unmount and show a loading spinner.", + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1765388793845-yhluf0sry", + "category": "Uncategorized", + "description": "Add in the ability so that every project can have its own selected theme. This will allow me to have different projects have different themes so I can easily differentiate when I have one project selected or not.", + "steps": [], + "status": "waiting_approval", + "startedAt": "2025-12-10T17:54:11.363Z", + "imagePaths": [], + "skipTests": true, + "summary": "Added per-project theme support. Modified: electron.ts (added theme property to Project interface), app-store.ts (added setProjectTheme and getEffectiveTheme actions), page.tsx (uses effectiveTheme for theme switching), sidebar.tsx (added project theme selector dropdown with all 13 themes + \"Use Global\" option). Users can now set unique themes for each project via the project options menu in the sidebar.", + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1765389333728-y74hmz2yp", + "category": "Agent Runner", + "description": "On the Agent Runner, I took a screenshot and dropped it into the text area and after a certain amount of time, it's like the image preview just completely went away. Can you debug and fix this on the Agent Runner?", + "steps": [], + "status": "backlog", + "imagePaths": [], + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" + }, + { + "id": "feature-1765389352488-j9bez5ztx", + "category": "Kanban", + "description": "It seems like the category typehead is no longer working. Can you double check that code didn't break? It should have kept track of categories inside of the categories.json file inside the .automaker folder when adding new features modal", + "steps": [], + "status": "backlog", + "imagePaths": [], + "skipTests": true, + "model": "opus", + "thinkingLevel": "none" } ] \ No newline at end of file diff --git a/app/src/app/globals.css b/app/src/app/globals.css index 4920fa6a..50379b13 100644 --- a/app/src/app/globals.css +++ b/app/src/app/globals.css @@ -1009,6 +1009,27 @@ @apply bg-background text-foreground; background-color: var(--background); } + + /* Ensure all clickable elements show pointer cursor */ + button:not(:disabled), + [role="button"]:not([aria-disabled="true"]), + a[href], + input[type="button"]:not(:disabled), + input[type="submit"]:not(:disabled), + input[type="reset"]:not(:disabled), + select:not(:disabled), + [tabindex]:not([tabindex="-1"]):not(:disabled) { + cursor: pointer; + } + + /* Disabled elements should show not-allowed cursor */ + button:disabled, + [role="button"][aria-disabled="true"], + input:disabled, + select:disabled, + textarea:disabled { + cursor: not-allowed; + } } /* Custom scrollbar for dark themes */ diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 27cc5e90..54665476 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -15,9 +15,13 @@ import { useAppStore } from "@/store/app-store"; import { getElectronAPI, isElectron } from "@/lib/electron"; export default function Home() { - const { currentView, setIpcConnected, theme } = useAppStore(); + const { currentView, setIpcConnected, theme, currentProject } = useAppStore(); const [isMounted, setIsMounted] = useState(false); + // Compute the effective theme: project theme takes priority over global theme + // This is reactive because it depends on currentProject and theme from the store + const effectiveTheme = currentProject?.theme || theme; + // Prevent hydration issues useEffect(() => { setIsMounted(true); @@ -39,7 +43,7 @@ export default function Home() { testConnection(); }, [setIpcConnected]); - // Apply theme class to document + // Apply theme class to document (uses effective theme - project-specific or global) useEffect(() => { const root = document.documentElement; root.classList.remove( @@ -57,31 +61,31 @@ export default function Home() { "synthwave" ); - if (theme === "dark") { + if (effectiveTheme === "dark") { root.classList.add("dark"); - } else if (theme === "retro") { + } else if (effectiveTheme === "retro") { root.classList.add("retro"); - } else if (theme === "dracula") { + } else if (effectiveTheme === "dracula") { root.classList.add("dracula"); - } else if (theme === "nord") { + } else if (effectiveTheme === "nord") { root.classList.add("nord"); - } else if (theme === "monokai") { + } else if (effectiveTheme === "monokai") { root.classList.add("monokai"); - } else if (theme === "tokyonight") { + } else if (effectiveTheme === "tokyonight") { root.classList.add("tokyonight"); - } else if (theme === "solarized") { + } else if (effectiveTheme === "solarized") { root.classList.add("solarized"); - } else if (theme === "gruvbox") { + } else if (effectiveTheme === "gruvbox") { root.classList.add("gruvbox"); - } else if (theme === "catppuccin") { + } else if (effectiveTheme === "catppuccin") { root.classList.add("catppuccin"); - } else if (theme === "onedark") { + } else if (effectiveTheme === "onedark") { root.classList.add("onedark"); - } else if (theme === "synthwave") { + } else if (effectiveTheme === "synthwave") { root.classList.add("synthwave"); - } else if (theme === "light") { + } else if (effectiveTheme === "light") { root.classList.add("light"); - } else if (theme === "system") { + } else if (effectiveTheme === "system") { // System theme const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; if (isDark) { @@ -90,7 +94,7 @@ export default function Home() { root.classList.add("light"); } } - }, [theme]); + }, [effectiveTheme]); const renderView = () => { switch (currentView) { diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index 5e80d017..77e92c28 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -25,6 +25,20 @@ import { Undo2, UserCircle, MoreVertical, + Palette, + Moon, + Sun, + Terminal, + Ghost, + Snowflake, + Flame, + Sparkles as TokyoNightIcon, + Eclipse, + Trees, + Cat, + Atom, + Radio, + Monitor, } from "lucide-react"; import { DropdownMenu, @@ -32,6 +46,12 @@ import { DropdownMenuTrigger, DropdownMenuItem, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuLabel, } from "@/components/ui/dropdown-menu"; import { Dialog, @@ -139,7 +159,7 @@ function SortableProjectItem({ {/* Hotkey indicator */} {index < 9 && ( {index + 1} @@ -161,6 +181,23 @@ function SortableProjectItem({ ); } +// Theme options for project theme selector +const PROJECT_THEME_OPTIONS = [ + { value: "", label: "Use Global", icon: Monitor }, + { value: "dark", label: "Dark", icon: Moon }, + { value: "light", label: "Light", icon: Sun }, + { value: "retro", label: "Retro", icon: Terminal }, + { value: "dracula", label: "Dracula", icon: Ghost }, + { value: "nord", label: "Nord", icon: Snowflake }, + { value: "monokai", label: "Monokai", icon: Flame }, + { value: "tokyonight", label: "Tokyo Night", icon: TokyoNightIcon }, + { value: "solarized", label: "Solarized", icon: Eclipse }, + { value: "gruvbox", label: "Gruvbox", icon: Trees }, + { value: "catppuccin", label: "Catppuccin", icon: Cat }, + { value: "onedark", label: "One Dark", icon: Atom }, + { value: "synthwave", label: "Synthwave", icon: Radio }, +] as const; + export function Sidebar() { const { projects, @@ -180,6 +217,8 @@ export function Sidebar() { cyclePrevProject, cycleNextProject, clearProjectHistory, + setProjectTheme, + theme: globalTheme, } = useAppStore(); // State for project picker dropdown @@ -640,7 +679,7 @@ export function Sidebar() { > {sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}{" "} {UI_SHORTCUTS.toggleSidebar} @@ -700,7 +739,7 @@ export function Sidebar() { data-testid="open-project-button" > - + {ACTION_SHORTCUTS.openProject} @@ -740,7 +779,7 @@ export function Sidebar() {
{ACTION_SHORTCUTS.projectPicker} @@ -780,38 +819,92 @@ export function Sidebar() { - {/* Project History Menu - only show when there's history */} - {projectHistory.length > 1 && ( + {/* Project Options Menu - theme and history */} + {currentProject && ( - - - - Previous - - {ACTION_SHORTCUTS.cyclePrevProject} - - - - - Next - - {ACTION_SHORTCUTS.cycleNextProject} - - - - - - Clear history - + + {/* Project Theme Submenu */} + + + + Project Theme + {currentProject.theme && ( + + {currentProject.theme} + + )} + + + + Select theme for this project + + + { + if (currentProject) { + setProjectTheme(currentProject.id, value === "" ? null : value as any); + } + }} + > + {PROJECT_THEME_OPTIONS.map((option) => { + const Icon = option.icon; + return ( + + + {option.label} + {option.value === "" && ( + + ({globalTheme}) + + )} + + ); + })} + + + + + {/* Project History Section - only show when there's history */} + {projectHistory.length > 1 && ( + <> + + + Project History + + + + Previous + + {ACTION_SHORTCUTS.cyclePrevProject} + + + + + Next + + {ACTION_SHORTCUTS.cycleNextProject} + + + + + Clear history + + + )} )} @@ -887,9 +980,9 @@ export function Sidebar() { {item.shortcut && sidebarOpen && ( @@ -953,9 +1046,9 @@ export function Sidebar() { {sidebarOpen && ( diff --git a/app/src/components/session-manager.tsx b/app/src/components/session-manager.tsx index 88144679..b5c9670a 100644 --- a/app/src/components/session-manager.tsx +++ b/app/src/components/session-manager.tsx @@ -250,7 +250,7 @@ export function SessionManager({ > New - + {ACTION_SHORTCUTS.newSession} diff --git a/app/src/components/ui/button.tsx b/app/src/components/ui/button.tsx index 3e9a1ffc..20b0aaa4 100644 --- a/app/src/components/ui/button.tsx +++ b/app/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { diff --git a/app/src/components/ui/checkbox.tsx b/app/src/components/ui/checkbox.tsx index 1a2a4593..69f4bf56 100644 --- a/app/src/components/ui/checkbox.tsx +++ b/app/src/components/ui/checkbox.tsx @@ -13,7 +13,7 @@ const Checkbox = React.forwardRef< diff --git a/app/src/components/ui/dropdown-menu.tsx b/app/src/components/ui/dropdown-menu.tsx index 54d8cb47..9a15f9e3 100644 --- a/app/src/components/ui/dropdown-menu.tsx +++ b/app/src/components/ui/dropdown-menu.tsx @@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef< ) => { return ( ) diff --git a/app/src/components/ui/git-diff-panel.tsx b/app/src/components/ui/git-diff-panel.tsx index 5c99406b..d6789547 100644 --- a/app/src/components/ui/git-diff-panel.tsx +++ b/app/src/components/ui/git-diff-panel.tsx @@ -458,7 +458,7 @@ export function GitDiffPanel({ return (
+
{isLoading ? (
@@ -522,9 +522,9 @@ export function GitDiffPanel({ No changes detected
) : ( -
- {/* Summary bar - fixed at top */} -
+
+ {/* Summary bar */} +
{(() => { @@ -610,8 +610,8 @@ export function GitDiffPanel({
- {/* File diffs - scrollable */} -
+ {/* File diffs */} +
{parsedDiffs.map((fileDiff) => ( - + - + )); Slider.displayName = SliderPrimitive.Root.displayName; diff --git a/app/src/components/ui/tabs.tsx b/app/src/components/ui/tabs.tsx index 497ba5ea..d849f038 100644 --- a/app/src/components/ui/tabs.tsx +++ b/app/src/components/ui/tabs.tsx @@ -26,7 +26,7 @@ function TabsList({ {viewMode === "changes" ? ( -
+
{projectPath ? ( ) : (
diff --git a/app/src/components/views/board-view.tsx b/app/src/components/views/board-view.tsx index 480d3ae1..84fcf2be 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -268,6 +268,8 @@ export function BoardView() { // Track previous project to detect switches const prevProjectPathRef = useRef(null); const isSwitchingProjectRef = useRef(false); + // Track if this is the initial load (to avoid showing loading spinner on subsequent reloads) + const isInitialLoadRef = useRef(true); // Auto mode hook const autoMode = useAutoMode(); @@ -367,11 +369,13 @@ export function BoardView() { const previousPath = prevProjectPathRef.current; // If project switched, clear features first to prevent cross-contamination + // Also treat this as an initial load for the new project if (previousPath !== null && currentPath !== previousPath) { console.log( `[BoardView] Project switch detected: ${previousPath} -> ${currentPath}, clearing features` ); isSwitchingProjectRef.current = true; + isInitialLoadRef.current = true; setFeatures([]); setPersistedCategories([]); // Also clear categories } @@ -379,7 +383,11 @@ export function BoardView() { // Update the ref to track current project prevProjectPathRef.current = currentPath; - setIsLoading(true); + // Only show loading spinner on initial load to prevent board flash during reloads + if (isInitialLoadRef.current) { + setIsLoading(true); + } + try { const api = getElectronAPI(); const result = await api.readFile( @@ -403,6 +411,7 @@ export function BoardView() { console.error("Failed to load features:", error); } finally { setIsLoading(false); + isInitialLoadRef.current = false; isSwitchingProjectRef.current = false; } }, [currentProject, setFeatures]); @@ -1270,17 +1279,35 @@ export function BoardView() { } }; - const getColumnFeatures = (columnId: ColumnId) => { - return features.filter((f) => { + // Memoize column features to prevent unnecessary re-renders + const columnFeaturesMap = useMemo(() => { + const map: Record = { + backlog: [], + in_progress: [], + waiting_approval: [], + verified: [], + }; + + features.forEach((f) => { // If feature has a running agent, always show it in "in_progress" const isRunning = runningAutoTasks.includes(f.id); if (isRunning) { - return columnId === "in_progress"; + map.in_progress.push(f); + } else { + // Otherwise, use the feature's status + map[f.status].push(f); } - // Otherwise, use the feature's status - return f.status === columnId; }); - }; + + return map; + }, [features, runningAutoTasks]); + + const getColumnFeatures = useCallback( + (columnId: ColumnId) => { + return columnFeaturesMap[columnId]; + }, + [columnFeaturesMap] + ); const handleViewOutput = (feature: Feature) => { setOutputFeature(feature); @@ -1537,7 +1564,7 @@ export function BoardView() { Add Feature {ACTION_SHORTCUTS.addFeature} @@ -2037,10 +2064,11 @@ export function BoardView() { > Add Feature - โŒ˜โ†ต + โŒ˜ + โ†ต diff --git a/app/src/components/views/kanban-card.tsx b/app/src/components/views/kanban-card.tsx index 714093a8..7a4c64b8 100644 --- a/app/src/components/views/kanban-card.tsx +++ b/app/src/components/views/kanban-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, memo } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { cn } from "@/lib/utils"; @@ -92,7 +92,7 @@ interface KanbanCardProps { summary?: string; } -export function KanbanCard({ +export const KanbanCard = memo(function KanbanCard({ feature, onEdit, onDelete, @@ -227,7 +227,7 @@ export function KanbanCard({ {/* Shortcut key badge for in-progress cards */} {shortcutKey && (
{shortcutKey} @@ -869,4 +869,4 @@ export function KanbanCard({ ); -} +}); diff --git a/app/src/components/views/kanban-column.tsx b/app/src/components/views/kanban-column.tsx index 95fac767..cbffc051 100644 --- a/app/src/components/views/kanban-column.tsx +++ b/app/src/components/views/kanban-column.tsx @@ -1,5 +1,6 @@ "use client"; +import { memo } from "react"; import { useDroppable } from "@dnd-kit/core"; import { cn } from "@/lib/utils"; import type { ReactNode } from "react"; @@ -13,7 +14,7 @@ interface KanbanColumnProps { headerAction?: ReactNode; } -export function KanbanColumn({ +export const KanbanColumn = memo(function KanbanColumn({ id, title, color, @@ -48,4 +49,4 @@ export function KanbanColumn({
); -} +}); diff --git a/app/src/components/views/profiles-view.tsx b/app/src/components/views/profiles-view.tsx index f7dd35d4..82bf811d 100644 --- a/app/src/components/views/profiles-view.tsx +++ b/app/src/components/views/profiles-view.tsx @@ -548,7 +548,7 @@ export function ProfilesView() { diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 91392894..6cb31885 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -1354,6 +1354,7 @@ export interface Project { name: string; path: string; lastOpened?: string; + theme?: string; // Per-project theme override (uses ThemeMode from app-store) } export interface TrashedProject extends Project { diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index bb4d52f3..c4be43af 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -248,6 +248,8 @@ export interface AppActions { // Theme actions setTheme: (theme: ThemeMode) => void; + setProjectTheme: (projectId: string, theme: ThemeMode | null) => void; // Set per-project theme (null to clear) + getEffectiveTheme: () => ThemeMode; // Get the effective theme (project or global) // Feature actions setFeatures: (features: Feature[]) => void; @@ -635,6 +637,37 @@ export const useAppStore = create()( // Theme actions setTheme: (theme) => set({ theme }), + setProjectTheme: (projectId, theme) => { + // Update the project's theme property + const projects = get().projects.map((p) => + p.id === projectId + ? { ...p, theme: theme === null ? undefined : theme } + : p + ); + set({ projects }); + + // Also update currentProject if it's the same project + const currentProject = get().currentProject; + if (currentProject?.id === projectId) { + set({ + currentProject: { + ...currentProject, + theme: theme === null ? undefined : theme, + }, + }); + } + }, + + getEffectiveTheme: () => { + const currentProject = get().currentProject; + // If current project has a theme set, use it + if (currentProject?.theme) { + return currentProject.theme as ThemeMode; + } + // Otherwise fall back to global theme + return get().theme; + }, + // Feature actions setFeatures: (features) => set({ features }),