feat(ui): add per-project themes and fix scrolling/styling issues

- 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 <noreply@anthropic.com>
This commit is contained in:
Cody Seibert
2025-12-10 12:56:24 -05:00
parent 9251411da9
commit f9ba1c260a
20 changed files with 868 additions and 90 deletions

View File

@@ -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<T extends { price: number }>(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<PaymentResult>;
}
class StripeProcessor implements PaymentProcessor {
async processPayment(
amount: number,
details: PaymentDetails
): Promise<PaymentResult> {
// 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<void>;
}
class EmailChannel implements NotificationChannel {
async send(user: User): Promise<void> {
// Implementation
}
}
class SMSChannel implements NotificationChannel {
async send(user: User): Promise<void> {
// Implementation
}
}
class NotificationService {
constructor(private channels: NotificationChannel[]) {}
async send(user: User): Promise<void> {
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.

View File

@@ -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.10PM.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.15PM.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.47PM.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"
}
]

View File

@@ -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 */

View File

@@ -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) {

View File

@@ -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 && (
<span
className="flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-sidebar-accent/10 border border-sidebar-border text-muted-foreground"
className="flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
data-testid={`project-hotkey-${index + 1}`}
>
{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"}{" "}
<span
className="ml-1 px-1 py-0.5 bg-sidebar-accent/10 rounded text-[10px] font-mono"
className="ml-1 px-1 py-0.5 bg-brand-500/10 border border-brand-500/30 rounded text-[10px] font-mono text-brand-400/70"
data-testid="sidebar-toggle-shortcut"
>
{UI_SHORTCUTS.toggleSidebar}
@@ -700,7 +739,7 @@ export function Sidebar() {
data-testid="open-project-button"
>
<FolderOpen className="w-4 h-4 shrink-0" />
<span className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500 ml-2">
<span className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70 ml-2">
{ACTION_SHORTCUTS.openProject}
</span>
</button>
@@ -740,7 +779,7 @@ export function Sidebar() {
</div>
<div className="flex items-center gap-1">
<span
className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-sidebar-accent/10 border border-sidebar-border text-muted-foreground"
className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
data-testid="project-picker-shortcut"
>
{ACTION_SHORTCUTS.projectPicker}
@@ -780,38 +819,92 @@ export function Sidebar() {
</DropdownMenuContent>
</DropdownMenu>
{/* Project History Menu - only show when there's history */}
{projectHistory.length > 1 && (
{/* Project Options Menu - theme and history */}
{currentProject && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="hidden lg:flex items-center justify-center w-8 h-[42px] rounded-lg text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border transition-all titlebar-no-drag"
title="Project history"
data-testid="project-history-menu"
title="Project options"
data-testid="project-options-menu"
>
<MoreVertical className="w-4 h-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={cyclePrevProject} data-testid="cycle-prev-project">
<Undo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Previous</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2">
{ACTION_SHORTCUTS.cyclePrevProject}
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={cycleNextProject} data-testid="cycle-next-project">
<Redo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Next</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2">
{ACTION_SHORTCUTS.cycleNextProject}
</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={clearProjectHistory} data-testid="clear-project-history">
<RotateCcw className="w-4 h-4 mr-2" />
<span>Clear history</span>
</DropdownMenuItem>
<DropdownMenuContent align="end" className="w-56">
{/* Project Theme Submenu */}
<DropdownMenuSub>
<DropdownMenuSubTrigger data-testid="project-theme-trigger">
<Palette className="w-4 h-4 mr-2" />
<span className="flex-1">Project Theme</span>
{currentProject.theme && (
<span className="text-[10px] text-muted-foreground ml-2 capitalize">
{currentProject.theme}
</span>
)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-48" data-testid="project-theme-menu">
<DropdownMenuLabel className="text-xs text-muted-foreground">
Select theme for this project
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={currentProject.theme || ""}
onValueChange={(value) => {
if (currentProject) {
setProjectTheme(currentProject.id, value === "" ? null : value as any);
}
}}
>
{PROJECT_THEME_OPTIONS.map((option) => {
const Icon = option.icon;
return (
<DropdownMenuRadioItem
key={option.value}
value={option.value}
data-testid={`project-theme-${option.value || 'global'}`}
>
<Icon className="w-4 h-4 mr-2" />
<span>{option.label}</span>
{option.value === "" && (
<span className="text-[10px] text-muted-foreground ml-1 capitalize">
({globalTheme})
</span>
)}
</DropdownMenuRadioItem>
);
})}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
{/* Project History Section - only show when there's history */}
{projectHistory.length > 1 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">
Project History
</DropdownMenuLabel>
<DropdownMenuItem onClick={cyclePrevProject} data-testid="cycle-prev-project">
<Undo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Previous</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2">
{ACTION_SHORTCUTS.cyclePrevProject}
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={cycleNextProject} data-testid="cycle-next-project">
<Redo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Next</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2">
{ACTION_SHORTCUTS.cycleNextProject}
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={clearProjectHistory} data-testid="clear-project-history">
<RotateCcw className="w-4 h-4 mr-2" />
<span>Clear history</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
@@ -887,9 +980,9 @@ export function Sidebar() {
{item.shortcut && sidebarOpen && (
<span
className={cn(
"hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500",
"hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70",
isActive &&
"bg-brand-500/10 border-brand-500/20 text-brand-400"
"bg-brand-500/20 border-brand-500/50 text-brand-400"
)}
data-testid={`shortcut-${item.id}`}
>
@@ -953,9 +1046,9 @@ export function Sidebar() {
{sidebarOpen && (
<span
className={cn(
"hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500",
"hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70",
isActiveRoute("settings") &&
"bg-brand-500/10 border-brand-500/20 text-brand-400"
"bg-brand-500/20 border-brand-500/50 text-brand-400"
)}
data-testid="shortcut-settings"
>

View File

@@ -250,7 +250,7 @@ export function SessionManager({
>
<Plus className="w-4 h-4 mr-1" />
New
<span className="ml-1.5 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/20 text-white/80">
<span className="ml-1.5 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground">
{ACTION_SHORTCUTS.newSession}
</span>
</Button>

View File

@@ -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: {

View File

@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground hover:border-primary/80",
className
)}
{...props}

View File

@@ -81,7 +81,7 @@ function DialogContent({
<DialogPrimitive.Close
data-slot="dialog-close"
className={cn(
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity cursor-pointer hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
compact ? "top-2 right-2" : "top-4 right-4"
)}
>

View File

@@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
"flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent hover:bg-accent",
inset && "pl-8",
className
)}
@@ -81,7 +81,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
inset && "pl-8",
className
)}
@@ -97,7 +97,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
className
)}
checked={checked}
@@ -121,7 +121,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
className
)}
{...props}
@@ -172,7 +172,7 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
className={cn("ml-auto text-xs tracking-widest text-brand-400/70", className)}
{...props}
/>
)

View File

@@ -458,7 +458,7 @@ export function GitDiffPanel({
return (
<div
className={cn(
"rounded-xl border border-border bg-card backdrop-blur-sm overflow-hidden flex flex-col h-full",
"rounded-xl border border-border bg-card backdrop-blur-sm overflow-hidden",
className
)}
data-testid="git-diff-panel"
@@ -497,7 +497,7 @@ export function GitDiffPanel({
{/* Content */}
{isExpanded && (
<div className="border-t border-border flex-1 min-h-0 overflow-hidden flex flex-col">
<div className="border-t border-border">
{isLoading ? (
<div className="flex items-center justify-center gap-2 py-8 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin" />
@@ -522,9 +522,9 @@ export function GitDiffPanel({
<span className="text-sm">No changes detected</span>
</div>
) : (
<div className="flex flex-col flex-1 min-h-0">
{/* Summary bar - fixed at top */}
<div className="p-4 pb-2 flex-shrink-0 border-b border-border-glass">
<div>
{/* Summary bar */}
<div className="p-4 pb-2 border-b border-border-glass">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-wrap">
{(() => {
@@ -610,8 +610,8 @@ export function GitDiffPanel({
</div>
</div>
{/* File diffs - scrollable */}
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-visible p-4 space-y-3">
{/* File diffs */}
<div className="p-4 space-y-3">
{parsedDiffs.map((fileDiff) => (
<FileDiffSection
key={fileDiff.filePath}

View File

@@ -16,10 +16,10 @@ const Slider = React.forwardRef<
)}
{...props}
>
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer">
<SliderPrimitive.Range className="slider-range absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent" />
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;

View File

@@ -26,7 +26,7 @@ function TabsList({
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px] border border-border",
className
)}
{...props}
@@ -42,7 +42,12 @@ function TabsTrigger({
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all duration-200 cursor-pointer",
"text-foreground/70 hover:text-foreground hover:bg-accent",
"data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:shadow-md data-[state=active]:border-primary/50",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:outline-1",
"disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}

View File

@@ -301,14 +301,14 @@ export function AgentOutputModal({
</DialogHeader>
{viewMode === "changes" ? (
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-hidden">
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-y-auto scrollbar-visible">
{projectPath ? (
<GitDiffPanel
projectPath={projectPath}
featureId={featureId}
compact={false}
useWorktrees={useWorktrees}
className="border-0 rounded-lg h-full"
className="border-0 rounded-lg"
/>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">

View File

@@ -268,6 +268,8 @@ export function BoardView() {
// Track previous project to detect switches
const prevProjectPathRef = useRef<string | null>(null);
const isSwitchingProjectRef = useRef<boolean>(false);
// Track if this is the initial load (to avoid showing loading spinner on subsequent reloads)
const isInitialLoadRef = useRef<boolean>(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<ColumnId, Feature[]> = {
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() {
<Plus className="w-4 h-4 mr-2" />
Add Feature
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-accent border border-border-glass"
className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground inline-flex items-center justify-center"
data-testid="shortcut-add-feature"
>
{ACTION_SHORTCUTS.addFeature}
@@ -2037,10 +2064,11 @@ export function BoardView() {
>
Add Feature
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20"
className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20 inline-flex items-center gap-1.5"
data-testid="shortcut-confirm-add-feature"
>
<span className="leading-none flex items-center justify-center"></span>
<span className="leading-none flex items-center justify-center"></span>
</span>
</Button>
</DialogFooter>

View File

@@ -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 && (
<div
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-muted border border-border text-muted-foreground z-10"
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70 z-10"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
@@ -869,4 +869,4 @@ export function KanbanCard({
</Dialog>
</Card>
);
}
});

View File

@@ -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({
</div>
</div>
);
}
});

View File

@@ -548,7 +548,7 @@ export function ProfilesView() {
<Button onClick={() => setShowAddDialog(true)} data-testid="add-profile-button" className="relative">
<Plus className="w-4 h-4 mr-2" />
New Profile
<span className="hidden lg:flex items-center justify-center ml-2 px-2 py-0.5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500">
<span className="hidden lg:flex items-center justify-center ml-2 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground">
{ACTION_SHORTCUTS.addProfile}
</span>
</Button>

View File

@@ -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 {

View File

@@ -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<AppState & AppActions>()(
// 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 }),