Merge origin/main into feature/shared-packages

This commit is contained in:
Kacper
2025-12-20 01:06:05 +01:00
17 changed files with 11254 additions and 11941 deletions

View File

@@ -12,7 +12,7 @@ import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Resolve workspace root (apps/app/scripts -> workspace root)
// Resolve workspace root (apps/ui/scripts -> workspace root)
const WORKSPACE_ROOT = path.resolve(__dirname, "../../..");
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, "test/fixtures/projectA");
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, ".automaker/app_spec.txt");

View File

@@ -2,7 +2,6 @@ import { useState, useMemo, useEffect, useCallback, useRef } from "react";
import { useNavigate, useLocation } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { useAppStore, formatShortcut, type ThemeMode } from "@/store/app-store";
import { CoursePromoBadge } from "@/components/ui/course-promo-badge";
import {
FolderOpen,
Plus,
@@ -1942,8 +1941,6 @@ export function Sidebar() {
"bg-gradient-to-t from-background/10 via-sidebar/50 to-transparent"
)}
>
{/* Course Promo Badge */}
<CoursePromoBadge sidebarOpen={sidebarOpen} />
{/* Wiki Link */}
{!hideWiki && (
<div className="p-2 pb-0">

View File

@@ -1,90 +0,0 @@
import * as React from "react";
import { Sparkles, X } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAppStore } from "@/store/app-store";
interface CoursePromoBadgeProps {
sidebarOpen?: boolean;
}
export function CoursePromoBadge({ sidebarOpen = true }: CoursePromoBadgeProps) {
const [dismissed, setDismissed] = React.useState(false);
const hideMarketingContent = useAppStore((state) => state.hideMarketingContent);
// If marketing content is hidden globally or dismissed locally, don't render
if (hideMarketingContent || dismissed) {
return null;
}
// Collapsed state - show only icon with tooltip
if (!sidebarOpen) {
return (
<div className="p-2 pb-0 flex justify-center">
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://agenticjumpstart.com"
target="_blank"
rel="noopener noreferrer"
className="group cursor-pointer flex items-center justify-center w-10 h-10 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-all border border-primary/30"
data-testid="course-promo-badge-collapsed"
>
<Sparkles className="size-4 shrink-0" />
</a>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-2">
<span>Become a 10x Dev</span>
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDismissed(true);
}}
className="p-0.5 rounded-full hover:bg-primary/30 transition-colors cursor-pointer"
aria-label="Dismiss"
>
<X className="size-3" />
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
// Expanded state - show full badge
return (
<div className="p-2 pb-0">
<a
href="https://agenticjumpstart.com"
target="_blank"
rel="noopener noreferrer"
className="group cursor-pointer flex items-center justify-between w-full px-2 lg:px-3 py-2.5 bg-primary/10 text-primary rounded-lg font-medium text-sm hover:bg-primary/20 transition-all border border-primary/30"
data-testid="course-promo-badge"
>
<div className="flex items-center gap-2">
<Sparkles className="size-4 shrink-0" />
<span className="hidden lg:block">Become a 10x Dev</span>
</div>
<span
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDismissed(true);
}}
className="hidden lg:block p-1 rounded-full hover:bg-primary/30 transition-colors cursor-pointer"
aria-label="Dismiss"
>
<X className="size-3.5" />
</span>
</a>
</div>
);
}

View File

@@ -37,8 +37,6 @@ export function SettingsView() {
setShowProfilesOnly,
muteDoneSound,
setMuteDoneSound,
hideMarketingContent,
setHideMarketingContent,
currentProject,
moveProjectToTrash,
defaultPlanningMode,
@@ -104,9 +102,7 @@ export function SettingsView() {
<AppearanceSection
effectiveTheme={effectiveTheme}
currentProject={settingsProject}
hideMarketingContent={hideMarketingContent}
onThemeChange={handleSetTheme}
onHideMarketingContentChange={setHideMarketingContent}
/>
);
case "keyboard":

View File

@@ -1,6 +1,5 @@
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Palette, Megaphone } from "lucide-react";
import { Palette } from "lucide-react";
import { themeOptions } from "@/config/theme-options";
import { cn } from "@/lib/utils";
import type { Theme, Project } from "../shared/types";
@@ -8,17 +7,13 @@ import type { Theme, Project } from "../shared/types";
interface AppearanceSectionProps {
effectiveTheme: Theme;
currentProject: Project | null;
hideMarketingContent: boolean;
onThemeChange: (theme: Theme) => void;
onHideMarketingContentChange: (hide: boolean) => void;
}
export function AppearanceSection({
effectiveTheme,
currentProject,
hideMarketingContent,
onThemeChange,
onHideMarketingContentChange,
}: AppearanceSectionProps) {
return (
<div
@@ -85,35 +80,6 @@ export function AppearanceSection({
})}
</div>
</div>
{/* Separator */}
<div className="border-t border-border/30 my-4" />
{/* Hide Marketing Content Setting */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="hide-marketing-content"
checked={hideMarketingContent}
onCheckedChange={(checked) =>
onHideMarketingContentChange(checked === true)
}
className="mt-1"
data-testid="hide-marketing-content-checkbox"
/>
<div className="space-y-1.5">
<Label
htmlFor="hide-marketing-content"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<Megaphone className="w-4 h-4 text-brand-500" />
Hide marketing content
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
When enabled, hides promotional content like the &quot;Become a 10x Dev&quot; badge
in the sidebar. This setting persists across sessions.
</p>
</div>
</div>
</div>
</div>
);

View File

@@ -1,4 +1,3 @@
import { useState } from "react";
import { cn } from "@/lib/utils";
import {
@@ -53,7 +52,9 @@ function CollapsibleSection({
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-brand-500/10 text-brand-500">
<Icon className="w-4 h-4" />
</div>
<span className="flex-1 font-medium text-foreground">{section.title}</span>
<span className="flex-1 font-medium text-foreground">
{section.title}
</span>
{isOpen ? (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
) : (
@@ -86,19 +87,30 @@ function CodeBlock({ children, title }: { children: string; title?: string }) {
);
}
function FeatureList({ items }: { items: { icon: React.ElementType; title: string; description: string }[] }) {
function FeatureList({
items,
}: {
items: { icon: React.ElementType; title: string; description: string }[];
}) {
return (
<div className="grid gap-3 mt-3">
{items.map((item, index) => {
const ItemIcon = item.icon;
return (
<div key={index} className="flex items-start gap-3 p-3 rounded-lg bg-muted/30 border border-border/50">
<div
key={index}
className="flex items-start gap-3 p-3 rounded-lg bg-muted/30 border border-border/50"
>
<div className="flex items-center justify-center w-6 h-6 rounded bg-brand-500/10 text-brand-500 shrink-0 mt-0.5">
<ItemIcon className="w-3.5 h-3.5" />
</div>
<div>
<div className="font-medium text-foreground text-sm">{item.title}</div>
<div className="text-xs text-muted-foreground mt-0.5">{item.description}</div>
<div className="font-medium text-foreground text-sm">
{item.title}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{item.description}
</div>
</div>
</div>
);
@@ -108,7 +120,9 @@ function FeatureList({ items }: { items: { icon: React.ElementType; title: strin
}
export function WikiView() {
const [openSections, setOpenSections] = useState<Set<string>>(new Set(["overview"]));
const [openSections, setOpenSections] = useState<Set<string>>(
new Set(["overview"])
);
const toggleSection = (id: string) => {
setOpenSections((prev) => {
@@ -138,14 +152,21 @@ export function WikiView() {
content: (
<div className="space-y-3">
<p>
<strong className="text-foreground">Automaker</strong> is an autonomous AI development studio that helps developers build software faster using AI agents.
<strong className="text-foreground">Automaker</strong> is an
autonomous AI development studio that helps developers build
software faster using AI agents.
</p>
<p>
At its core, Automaker provides a visual Kanban board to manage features. When you're ready, AI agents automatically implement those features in your codebase, complete with git worktree isolation for safe parallel development.
At its core, Automaker provides a visual Kanban board to manage
features. When you're ready, AI agents automatically implement those
features in your codebase, complete with git worktree isolation for
safe parallel development.
</p>
<div className="p-3 rounded-lg bg-brand-500/10 border border-brand-500/20 mt-4">
<p className="text-brand-400 text-sm">
Think of it as having a team of AI developers that can work on multiple features simultaneously while you focus on the bigger picture.
Think of it as having a team of AI developers that can work on
multiple features simultaneously while you focus on the bigger
picture.
</p>
</div>
</div>
@@ -160,17 +181,21 @@ export function WikiView() {
<p>Automaker is built as a monorepo with two main applications:</p>
<ul className="list-disc list-inside space-y-2 ml-2">
<li>
<strong className="text-foreground">apps/app</strong> - Next.js + Electron frontend for the desktop application
<strong className="text-foreground">apps/ui</strong> - Next.js +
Electron frontend for the desktop application
</li>
<li>
<strong className="text-foreground">apps/server</strong> - Express backend handling API requests and agent orchestration
<strong className="text-foreground">apps/server</strong> - Express
backend handling API requests and agent orchestration
</li>
</ul>
<div className="mt-4 space-y-2">
<p className="font-medium text-foreground">Key Technologies:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>Electron wraps Next.js for cross-platform desktop support</li>
<li>Real-time communication via WebSocket for live agent updates</li>
<li>
Real-time communication via WebSocket for live agent updates
</li>
<li>State management with Zustand for reactive UI updates</li>
<li>Claude Agent SDK for AI capabilities</li>
</ul>
@@ -189,42 +214,50 @@ export function WikiView() {
{
icon: LayoutGrid,
title: "Kanban Board",
description: "4 columns: Backlog, In Progress, Waiting Approval, Verified. Drag and drop to manage feature lifecycle.",
description:
"4 columns: Backlog, In Progress, Waiting Approval, Verified. Drag and drop to manage feature lifecycle.",
},
{
icon: Bot,
title: "AI Agent Integration",
description: "Powered by Claude via the Agent SDK with full file, bash, and git access.",
description:
"Powered by Claude via the Agent SDK with full file, bash, and git access.",
},
{
icon: Cpu,
title: "Multi-Model Support",
description: "Claude Haiku/Sonnet/Opus models. Choose the right model for each task.",
description:
"Claude Haiku/Sonnet/Opus models. Choose the right model for each task.",
},
{
icon: Brain,
title: "Extended Thinking",
description: "Configurable thinking levels (none, low, medium, high, ultrathink) for complex tasks.",
description:
"Configurable thinking levels (none, low, medium, high, ultrathink) for complex tasks.",
},
{
icon: Zap,
title: "Real-time Streaming",
description: "Watch AI agents work in real-time with live output streaming.",
description:
"Watch AI agents work in real-time with live output streaming.",
},
{
icon: GitBranch,
title: "Git Worktree Isolation",
description: "Each feature runs in its own git worktree for safe parallel development.",
description:
"Each feature runs in its own git worktree for safe parallel development.",
},
{
icon: Users,
title: "AI Profiles",
description: "Pre-configured model + thinking level combinations for different task types.",
description:
"Pre-configured model + thinking level combinations for different task types.",
},
{
icon: Terminal,
title: "Integrated Terminal",
description: "Built-in terminal with tab support and split panes.",
description:
"Built-in terminal with tab support and split panes.",
},
{
icon: Keyboard,
@@ -234,7 +267,8 @@ export function WikiView() {
{
icon: Palette,
title: "14 Themes",
description: "From light to dark, retro to synthwave - pick your style.",
description:
"From light to dark, retro to synthwave - pick your style.",
},
{
icon: Image,
@@ -244,7 +278,8 @@ export function WikiView() {
{
icon: TestTube,
title: "Test Integration",
description: "Automatic test running and TDD support for quality assurance.",
description:
"Automatic test running and TDD support for quality assurance.",
},
]}
/>
@@ -257,39 +292,63 @@ export function WikiView() {
icon: GitBranch,
content: (
<div className="space-y-3">
<p>Here's what happens when you use Automaker to implement a feature:</p>
<p>
Here's what happens when you use Automaker to implement a feature:
</p>
<ol className="list-decimal list-inside space-y-3 ml-2 mt-4">
<li className="text-foreground">
<strong>Create Feature</strong>
<p className="text-muted-foreground ml-5 mt-1">Add a new feature card to the Kanban board with description and steps</p>
<p className="text-muted-foreground ml-5 mt-1">
Add a new feature card to the Kanban board with description and
steps
</p>
</li>
<li className="text-foreground">
<strong>Feature Saved</strong>
<p className="text-muted-foreground ml-5 mt-1">Feature saved to <code className="px-1 py-0.5 bg-muted rounded text-xs">.automaker/features/&#123;id&#125;/feature.json</code></p>
<p className="text-muted-foreground ml-5 mt-1">
Feature saved to{" "}
<code className="px-1 py-0.5 bg-muted rounded text-xs">
.automaker/features/&#123;id&#125;/feature.json
</code>
</p>
</li>
<li className="text-foreground">
<strong>Start Work</strong>
<p className="text-muted-foreground ml-5 mt-1">Drag to "In Progress" or enable auto mode to start implementation</p>
<p className="text-muted-foreground ml-5 mt-1">
Drag to "In Progress" or enable auto mode to start
implementation
</p>
</li>
<li className="text-foreground">
<strong>Git Worktree Created</strong>
<p className="text-muted-foreground ml-5 mt-1">Backend AutoModeService creates isolated git worktree (if enabled)</p>
<p className="text-muted-foreground ml-5 mt-1">
Backend AutoModeService creates isolated git worktree (if
enabled)
</p>
</li>
<li className="text-foreground">
<strong>Agent Executes</strong>
<p className="text-muted-foreground ml-5 mt-1">Claude Agent SDK runs with file/bash/git tool access</p>
<p className="text-muted-foreground ml-5 mt-1">
Claude Agent SDK runs with file/bash/git tool access
</p>
</li>
<li className="text-foreground">
<strong>Progress Streamed</strong>
<p className="text-muted-foreground ml-5 mt-1">Real-time updates via WebSocket as agent works</p>
<p className="text-muted-foreground ml-5 mt-1">
Real-time updates via WebSocket as agent works
</p>
</li>
<li className="text-foreground">
<strong>Completion</strong>
<p className="text-muted-foreground ml-5 mt-1">On success, feature moves to "waiting_approval" for your review</p>
<p className="text-muted-foreground ml-5 mt-1">
On success, feature moves to "waiting_approval" for your review
</p>
</li>
<li className="text-foreground">
<strong>Verify</strong>
<p className="text-muted-foreground ml-5 mt-1">Review changes and move to "verified" when satisfied</p>
<p className="text-muted-foreground ml-5 mt-1">
Review changes and move to "verified" when satisfied
</p>
</li>
</ol>
</div>
@@ -301,9 +360,11 @@ export function WikiView() {
icon: FolderTree,
content: (
<div>
<p className="mb-3">The Automaker codebase is organized as follows:</p>
<p className="mb-3">
The Automaker codebase is organized as follows:
</p>
<CodeBlock title="Directory Structure">
{`/automaker/
{`/automaker/
├── apps/
│ ├── app/ # Frontend (Next.js + Electron)
│ │ ├── electron/ # Electron main process
@@ -332,18 +393,46 @@ export function WikiView() {
<p>The main UI components that make up Automaker:</p>
<div className="grid gap-2 mt-4">
{[
{ file: "sidebar.tsx", desc: "Main navigation with project picker and view switching" },
{ file: "board-view.tsx", desc: "Kanban board with drag-and-drop cards" },
{ file: "agent-view.tsx", desc: "AI chat interface for conversational development" },
{
file: "sidebar.tsx",
desc: "Main navigation with project picker and view switching",
},
{
file: "board-view.tsx",
desc: "Kanban board with drag-and-drop cards",
},
{
file: "agent-view.tsx",
desc: "AI chat interface for conversational development",
},
{ file: "spec-view.tsx", desc: "Project specification editor" },
{ file: "context-view.tsx", desc: "Context file manager for AI context" },
{ file: "terminal-view.tsx", desc: "Integrated terminal with splits and tabs" },
{ file: "profiles-view.tsx", desc: "AI profile management (model + thinking presets)" },
{ file: "app-store.ts", desc: "Central Zustand state management" },
{
file: "context-view.tsx",
desc: "Context file manager for AI context",
},
{
file: "terminal-view.tsx",
desc: "Integrated terminal with splits and tabs",
},
{
file: "profiles-view.tsx",
desc: "AI profile management (model + thinking presets)",
},
{
file: "app-store.ts",
desc: "Central Zustand state management",
},
].map((item) => (
<div key={item.file} className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50">
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">{item.file}</code>
<span className="text-xs text-muted-foreground">{item.desc}</span>
<div
key={item.file}
className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50"
>
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">
{item.file}
</code>
<span className="text-xs text-muted-foreground">
{item.desc}
</span>
</div>
))}
</div>
@@ -356,21 +445,45 @@ export function WikiView() {
icon: Settings,
content: (
<div className="space-y-3">
<p>Automaker stores project configuration in the <code className="px-1 py-0.5 bg-muted rounded text-xs">.automaker/</code> directory:</p>
<p>
Automaker stores project configuration in the{" "}
<code className="px-1 py-0.5 bg-muted rounded text-xs">
.automaker/
</code>{" "}
directory:
</p>
<div className="grid gap-2 mt-4">
{[
{ file: "app_spec.txt", desc: "Project specification describing your app for AI context" },
{ file: "context/", desc: "Additional context files (docs, examples) for AI" },
{ file: "features/", desc: "Feature definitions with descriptions and steps" },
{
file: "app_spec.txt",
desc: "Project specification describing your app for AI context",
},
{
file: "context/",
desc: "Additional context files (docs, examples) for AI",
},
{
file: "features/",
desc: "Feature definitions with descriptions and steps",
},
].map((item) => (
<div key={item.file} className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50">
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">{item.file}</code>
<span className="text-xs text-muted-foreground">{item.desc}</span>
<div
key={item.file}
className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50"
>
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">
{item.file}
</code>
<span className="text-xs text-muted-foreground">
{item.desc}
</span>
</div>
))}
</div>
<div className="mt-4 p-3 rounded-lg bg-muted/30 border border-border/50">
<p className="text-sm text-foreground font-medium mb-2">Tip: App Spec Best Practices</p>
<p className="text-sm text-foreground font-medium mb-2">
Tip: App Spec Best Practices
</p>
<ul className="list-disc list-inside space-y-1 text-xs text-muted-foreground">
<li>Include your tech stack and key dependencies</li>
<li>Describe the project structure and conventions</li>
@@ -391,39 +504,68 @@ export function WikiView() {
<ol className="list-decimal list-inside space-y-4 ml-2 mt-4">
<li className="text-foreground">
<strong>Create or Open a Project</strong>
<p className="text-muted-foreground ml-5 mt-1">Use the sidebar to create a new project or open an existing folder</p>
<p className="text-muted-foreground ml-5 mt-1">
Use the sidebar to create a new project or open an existing
folder
</p>
</li>
<li className="text-foreground">
<strong>Write an App Spec</strong>
<p className="text-muted-foreground ml-5 mt-1">Go to Spec Editor and describe your project. This helps AI understand your codebase.</p>
<p className="text-muted-foreground ml-5 mt-1">
Go to Spec Editor and describe your project. This helps AI
understand your codebase.
</p>
</li>
<li className="text-foreground">
<strong>Add Context (Optional)</strong>
<p className="text-muted-foreground ml-5 mt-1">Add relevant documentation or examples to the Context view for better AI results</p>
<p className="text-muted-foreground ml-5 mt-1">
Add relevant documentation or examples to the Context view for
better AI results
</p>
</li>
<li className="text-foreground">
<strong>Create Features</strong>
<p className="text-muted-foreground ml-5 mt-1">Add feature cards to your Kanban board with clear descriptions and implementation steps</p>
<p className="text-muted-foreground ml-5 mt-1">
Add feature cards to your Kanban board with clear descriptions
and implementation steps
</p>
</li>
<li className="text-foreground">
<strong>Configure AI Profile</strong>
<p className="text-muted-foreground ml-5 mt-1">Choose an AI profile or customize model/thinking settings per feature</p>
<p className="text-muted-foreground ml-5 mt-1">
Choose an AI profile or customize model/thinking settings per
feature
</p>
</li>
<li className="text-foreground">
<strong>Start Implementation</strong>
<p className="text-muted-foreground ml-5 mt-1">Drag features to "In Progress" or enable auto mode to let AI work</p>
<p className="text-muted-foreground ml-5 mt-1">
Drag features to "In Progress" or enable auto mode to let AI
work
</p>
</li>
<li className="text-foreground">
<strong>Review and Verify</strong>
<p className="text-muted-foreground ml-5 mt-1">Check completed features, review changes, and mark as verified</p>
<p className="text-muted-foreground ml-5 mt-1">
Check completed features, review changes, and mark as verified
</p>
</li>
</ol>
<div className="mt-6 p-4 rounded-lg bg-brand-500/10 border border-brand-500/20">
<p className="text-brand-400 text-sm font-medium mb-2">Pro Tips:</p>
<ul className="list-disc list-inside space-y-1 text-xs text-brand-400/80">
<li>Use keyboard shortcuts for faster navigation (press <code className="px-1 py-0.5 bg-brand-500/20 rounded">?</code> to see all)</li>
<li>Enable git worktree isolation for parallel feature development</li>
<li>Start with "Quick Edit" profile for simple tasks, use "Heavy Task" for complex work</li>
<li>
Use keyboard shortcuts for faster navigation (press{" "}
<code className="px-1 py-0.5 bg-brand-500/20 rounded">?</code>{" "}
to see all)
</li>
<li>
Enable git worktree isolation for parallel feature development
</li>
<li>
Start with "Quick Edit" profile for simple tasks, use "Heavy
Task" for complex work
</li>
<li>Keep your app spec up to date as your project evolves</li>
</ul>
</div>

View File

@@ -459,9 +459,6 @@ export interface AppState {
// Audio Settings
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
// Marketing Settings
hideMarketingContent: boolean; // When true, hide marketing content like the "Become a 10x Dev" badge (default: false)
// Enhancement Model Settings
enhancementModel: AgentModel; // Model used for feature enhancement (default: sonnet)
@@ -673,9 +670,6 @@ export interface AppActions {
// Audio Settings actions
setMuteDoneSound: (muted: boolean) => void;
// Marketing Settings actions
setHideMarketingContent: (hide: boolean) => void;
// Enhancement Model actions
setEnhancementModel: (model: AgentModel) => void;
@@ -830,7 +824,6 @@ const initialState: AppState = {
showProfilesOnly: false, // Default to showing all options (not profiles only)
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
muteDoneSound: false, // Default to sound enabled (not muted)
hideMarketingContent: false, // Default to showing marketing content
enhancementModel: "sonnet", // Default to sonnet for feature enhancement
aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null,
@@ -1494,9 +1487,6 @@ export const useAppStore = create<AppState & AppActions>()(
// Audio Settings actions
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
// Marketing Settings actions
setHideMarketingContent: (hide) => set({ hideMarketingContent: hide }),
// Enhancement Model actions
setEnhancementModel: (model) => set({ enhancementModel: model }),
@@ -2341,7 +2331,6 @@ export const useAppStore = create<AppState & AppActions>()(
showProfilesOnly: state.showProfilesOnly,
keyboardShortcuts: state.keyboardShortcuts,
muteDoneSound: state.muteDoneSound,
hideMarketingContent: state.hideMarketingContent,
enhancementModel: state.enhancementModel,
// Profiles and sessions
aiProfiles: state.aiProfiles,

View File

@@ -110,8 +110,8 @@ test.describe("Kanban Responsive Scaling Tests", () => {
expect(Math.abs(columnWidth - baseWidth)).toBeLessThan(2);
}
// Column width should be within expected bounds (240px min, 360px max)
expect(baseWidth).toBeGreaterThanOrEqual(240);
// Column width should be within expected bounds (280px min, 360px max)
expect(baseWidth).toBeGreaterThanOrEqual(280);
expect(baseWidth).toBeLessThanOrEqual(360);
// Columns should not overlap (check x positions)
@@ -149,9 +149,30 @@ test.describe("Kanban Responsive Scaling Tests", () => {
expect(verifiedBox).not.toBeNull();
if (backlogBox && verifiedBox) {
// Calculate the left and right margins
const leftMargin = backlogBox.x;
const rightMargin = 1600 - (verifiedBox.x + verifiedBox.width);
// Get the actual container width (accounting for sidebar)
// The board-view container is inside a flex container that accounts for sidebar
const containerWidth = await page.evaluate(() => {
const boardView = document.querySelector('[data-testid="board-view"]');
if (!boardView) return window.innerWidth;
const parent = boardView.parentElement;
return parent ? parent.clientWidth : window.innerWidth;
});
// Calculate the left and right margins relative to the container
// The bounding box x is relative to the viewport, so we need to find where
// the container starts relative to the viewport
const containerLeft = await page.evaluate(() => {
const boardView = document.querySelector('[data-testid="board-view"]');
if (!boardView) return 0;
const parent = boardView.parentElement;
if (!parent) return 0;
const rect = parent.getBoundingClientRect();
return rect.left;
});
// Calculate margins relative to the container
const leftMargin = backlogBox.x - containerLeft;
const rightMargin = containerWidth - (verifiedBox.x + verifiedBox.width - containerLeft);
// The margins should be roughly equal (columns are centered)
// Allow for some tolerance due to padding and gaps

View File

@@ -1,184 +0,0 @@
/**
* Settings Marketing Content Toggle Tests
*
* Tests for the "Hide marketing content" setting in the Appearance section.
*/
import { test, expect } from "@playwright/test";
import * as fs from "fs";
import {
waitForNetworkIdle,
createTestGitRepo,
cleanupTempDir,
createTempDirPath,
setupProjectWithPathNoWorktrees,
navigateToSettings,
} from "./utils";
// Create unique temp dir for this test run
const TEST_TEMP_DIR = createTempDirPath("settings-marketing-tests");
interface TestRepo {
path: string;
cleanup: () => Promise<void>;
}
// Configure all tests to run serially
test.describe.configure({ mode: "serial" });
test.describe("Settings Marketing Content Tests", () => {
let testRepo: TestRepo;
test.beforeAll(async () => {
// Create test temp directory
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
});
test.beforeEach(async () => {
// Create a fresh test repo for each test
testRepo = await createTestGitRepo(TEST_TEMP_DIR);
});
test.afterEach(async () => {
// Cleanup test repo after each test
if (testRepo) {
await testRepo.cleanup();
}
});
test.afterAll(async () => {
// Cleanup temp directory
cleanupTempDir(TEST_TEMP_DIR);
});
test("should show course promo badge by default", async ({ page }) => {
// Setup project without worktrees for simpler testing
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
// Wait for sidebar to load
await expect(page.locator('[data-testid="sidebar"]')).toBeVisible({
timeout: 10000,
});
// Course promo badge should be visible by default
const promoBadge = page.locator('[data-testid="course-promo-badge"]');
await expect(promoBadge).toBeVisible({ timeout: 5000 });
});
test("should hide course promo badge when setting is enabled", async ({
page,
}) => {
// Setup project
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
// Navigate to settings
await navigateToSettings(page);
// Click on Appearance tab in settings navigation
const appearanceTab = page.getByRole("button", { name: /appearance/i });
await appearanceTab.click();
// Find and click the hide marketing content checkbox
const hideMarketingCheckbox = page.locator(
'[data-testid="hide-marketing-content-checkbox"]'
);
await expect(hideMarketingCheckbox).toBeVisible({ timeout: 5000 });
await hideMarketingCheckbox.click();
// Navigate back to board to see the sidebar
await page.goto("/board");
await waitForNetworkIdle(page);
// Wait for Zustand store to rehydrate from localStorage
await page.waitForFunction(() => {
const storage = localStorage.getItem('automaker-storage');
if (!storage) return false;
const parsed = JSON.parse(storage);
return parsed.state?.hideMarketingContent === true;
});
// Course promo badge should now be hidden
const promoBadge = page.locator('[data-testid="course-promo-badge"]');
await expect(promoBadge).not.toBeVisible({ timeout: 5000 });
});
test("should persist hide marketing setting across page reloads", async ({
page,
}) => {
// Setup project
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
// Navigate to settings and enable hide marketing
await navigateToSettings(page);
const appearanceTab = page.getByRole("button", { name: /appearance/i });
await appearanceTab.click();
const hideMarketingCheckbox = page.locator(
'[data-testid="hide-marketing-content-checkbox"]'
);
await hideMarketingCheckbox.click();
// Reload the page
await page.reload();
await waitForNetworkIdle(page);
// Course promo badge should still be hidden after reload
const promoBadge = page.locator('[data-testid="course-promo-badge"]');
await expect(promoBadge).not.toBeVisible({ timeout: 5000 });
});
test("should show course promo badge again when setting is disabled", async ({
page,
}) => {
// Setup project with hide marketing already enabled via localStorage
await page.addInitScript(() => {
const state = {
state: {
hideMarketingContent: true,
projects: [],
currentProject: null,
theme: "dark",
sidebarOpen: true,
},
version: 2,
};
localStorage.setItem("automaker-storage", JSON.stringify(state));
});
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await waitForNetworkIdle(page);
// Verify promo is hidden initially
const promoBadge = page.locator('[data-testid="course-promo-badge"]');
await expect(promoBadge).not.toBeVisible({ timeout: 5000 });
// Navigate to settings and disable hide marketing
await navigateToSettings(page);
const appearanceTab = page.getByRole("button", { name: /appearance/i });
await appearanceTab.click();
const hideMarketingCheckbox = page.locator(
'[data-testid="hide-marketing-content-checkbox"]'
);
await hideMarketingCheckbox.click(); // Uncheck
// Navigate back to board
await page.goto("/board");
await waitForNetworkIdle(page);
// Course promo badge should now be visible again
await expect(promoBadge).toBeVisible({ timeout: 5000 });
});
});

View File

@@ -799,10 +799,14 @@ test.describe("Worktree Integration Tests", () => {
await clickAddFeature(page);
// Fill in the feature details with the new branch
await fillAddFeatureDialog(page, "Feature that should auto-create worktree", {
branch: branchName,
category: "Testing",
});
await fillAddFeatureDialog(
page,
"Feature that should auto-create worktree",
{
branch: branchName,
category: "Testing",
}
);
// Confirm
await confirmAddFeature(page);
@@ -835,7 +839,7 @@ test.describe("Worktree Integration Tests", () => {
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Verify branch name is stored
expect(featureData.branchName).toBe(branchName);
@@ -900,7 +904,7 @@ test.describe("Worktree Integration Tests", () => {
let featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Verify feature was created with the branch name stored
expect(featureData.branchName).toBe(branchName);
// Verify worktreePath is NOT set (worktrees are created at execution time, not when adding)
@@ -1084,7 +1088,9 @@ test.describe("Worktree Integration Tests", () => {
// When a worktree is selected, "Use current selected branch" should be selected
// and the branch name should be shown in the label
const currentBranchLabel = page.locator('label[for="feature-current"]');
await expect(currentBranchLabel).toContainText(branchName, { timeout: 5000 });
await expect(currentBranchLabel).toContainText(branchName, {
timeout: 5000,
});
// Close dialog
await page.keyboard.press("Escape");
@@ -1275,11 +1281,7 @@ test.describe("Worktree Integration Tests", () => {
expect(featureDir).toBeDefined();
// Read the feature data
const featureFilePath = path.join(
featuresDir,
featureDir!,
"feature.json"
);
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(featureData.status).toBe("backlog");
@@ -1296,9 +1298,7 @@ test.describe("Worktree Integration Tests", () => {
// Wait for the feature to move to in_progress column
await expect(async () => {
const updatedData = JSON.parse(
fs.readFileSync(featureFilePath, "utf-8")
);
const updatedData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(updatedData.status).toBe("in_progress");
}).toPass({ timeout: 10000 });
@@ -1911,7 +1911,10 @@ test.describe("Worktree Integration Tests", () => {
await apiCreateWorktree(page, testRepo.path, branchName);
// Add a file and commit in the worktree
fs.writeFileSync(path.join(worktreePath, "merge-file.txt"), "merge content");
fs.writeFileSync(
path.join(worktreePath, "merge-file.txt"),
"merge content"
);
await execAsync("git add merge-file.txt", { cwd: worktreePath });
await execAsync('git commit -m "Add file for merge test"', {
cwd: worktreePath,
@@ -2065,9 +2068,9 @@ test.describe("Worktree Integration Tests", () => {
// Verify the worktree has the file from develop
const worktreePath = getWorktreePath(testRepo.path, "feature/from-develop");
expect(
fs.existsSync(path.join(worktreePath, "develop-only.txt"))
).toBe(true);
expect(fs.existsSync(path.join(worktreePath, "develop-only.txt"))).toBe(
true
);
const content = fs.readFileSync(
path.join(worktreePath, "develop-only.txt"),
"utf-8"
@@ -2100,10 +2103,9 @@ test.describe("Worktree Integration Tests", () => {
// Verify the worktree starts from the same commit as main
const worktreePath = getWorktreePath(testRepo.path, "feature/from-head");
const { stdout: worktreeHash } = await execAsync(
"git rev-parse HEAD~0",
{ cwd: worktreePath }
);
const { stdout: worktreeHash } = await execAsync("git rev-parse HEAD~0", {
cwd: worktreePath,
});
// The worktree's initial commit should be the same as main's HEAD
// (Since it was just created, we check the parent commit)
@@ -2395,9 +2397,9 @@ test.describe("Worktree Integration Tests", () => {
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Initially, the feature should be on main or have no branch set
expect(
!featureData.branchName || featureData.branchName === "main"
).toBe(true);
expect(!featureData.branchName || featureData.branchName === "main").toBe(
true
);
// The new branch we want to assign
const newBranchName = "feature/edited-branch";
@@ -2428,7 +2430,7 @@ test.describe("Worktree Integration Tests", () => {
await page.waitForTimeout(300);
// Type the new branch name
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill(newBranchName);
// Press Enter to select/create the branch
@@ -2519,7 +2521,7 @@ test.describe("Worktree Integration Tests", () => {
await page.waitForTimeout(300);
// Type "main" to change to main branch
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill("main");
await commandInput.press("Enter");
await page.waitForTimeout(200);
@@ -2577,7 +2579,7 @@ test.describe("Worktree Integration Tests", () => {
await branchInput.click();
await page.waitForTimeout(300);
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill(existingBranch);
await commandInput.press("Enter");
await page.waitForTimeout(200);