feat: implement running agents view and enhance auto mode functionality

- Added a new `RunningAgentsView` component to display currently active agents working on features.
- Implemented auto-refresh functionality for the running agents list every 2 seconds.
- Enhanced the auto mode service to support project-specific operations, including starting and stopping auto mode for individual projects.
- Updated IPC handlers to manage auto mode status and running agents more effectively.
- Introduced audio settings to mute notifications when agents complete tasks.
- Refactored existing components to accommodate new features and improve overall user experience.
This commit is contained in:
Cody Seibert
2025-12-10 21:51:00 -05:00
parent 5ac81ce5a9
commit d08f922631
24 changed files with 1450 additions and 405 deletions

View File

@@ -592,7 +592,7 @@ export function AgentView() {
</div>
<Card
className={cn(
"max-w-[80%]",
"max-w-[80%] py-0",
message.role === "user"
? "bg-transparent border border-primary text-foreground"
: "border-l-4 border-primary bg-card"
@@ -628,7 +628,7 @@ export function AgentView() {
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<Bot className="w-4 h-4 text-primary" />
</div>
<Card className="border-l-4 border-primary bg-card">
<Card className="border-l-4 border-primary bg-card py-0">
<CardContent className="p-3">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-primary" />

View File

@@ -357,7 +357,10 @@ ${Object.entries(projectAnalysis.filesByExtension)
)
.sort((a: [string, number], b: [string, number]) => b[1] - a[1])
.slice(0, 5)
.map(([ext, count]: [string, number]) => ` <language ext=".${ext}" count="${count}" />`)
.map(
([ext, count]: [string, number]) =>
` <language ext=".${ext}" count="${count}" />`
)
.join("\n")}
</languages>
<frameworks>
@@ -756,6 +759,10 @@ ${Object.entries(projectAnalysis.filesByExtension)
}
// Create each feature using the features API
if (!api.features) {
throw new Error("Features API not available");
}
for (const feature of detectedFeatures) {
await api.features.create(currentProject.path, feature);
}
@@ -829,7 +836,9 @@ ${Object.entries(projectAnalysis.filesByExtension)
</div>
{node.isDirectory && isExpanded && node.children && (
<div>
{node.children.map((child: FileTreeNode) => renderNode(child, depth + 1))}
{node.children.map((child: FileTreeNode) =>
renderNode(child, depth + 1)
)}
</div>
)}
</div>
@@ -953,7 +962,10 @@ ${Object.entries(projectAnalysis.filesByExtension)
<CardContent>
<div className="space-y-2">
{Object.entries(projectAnalysis.filesByExtension)
.sort((a: [string, number], b: [string, number]) => b[1] - a[1])
.sort(
(a: [string, number], b: [string, number]) =>
b[1] - a[1]
)
.slice(0, 15)
.map(([ext, count]: [string, number]) => (
<div key={ext} className="flex justify-between text-sm">
@@ -1096,7 +1108,9 @@ ${Object.entries(projectAnalysis.filesByExtension)
data-testid="analysis-file-tree"
>
<div className="p-2">
{projectAnalysis.fileTree.map((node: FileTreeNode) => renderNode(node))}
{projectAnalysis.fileTree.map((node: FileTreeNode) =>
renderNode(node)
)}
</div>
</CardContent>
</Card>

View File

@@ -529,16 +529,22 @@ export function BoardView() {
const projectId = currentProject.id;
const unsubscribe = api.autoMode.onEvent((event) => {
// Use event's projectId if available, otherwise use current project
const eventProjectId = event.projectId || projectId;
// Use event's projectPath or projectId if available, otherwise use current project
// Board view only reacts to events for the currently selected project
const eventProjectId = ('projectId' in event && event.projectId) || projectId;
if (event.type === "auto_mode_feature_complete") {
// Reload features when a feature is completed
console.log("[Board] Feature completed, reloading features...");
loadFeatures();
// Play ding sound when feature is done
const audio = new Audio("/sounds/ding.mp3");
audio.play().catch((err) => console.warn("Could not play ding sound:", err));
// Play ding sound when feature is done (unless muted)
const { muteDoneSound } = useAppStore.getState();
if (!muteDoneSound) {
const audio = new Audio("/sounds/ding.mp3");
audio
.play()
.catch((err) => console.warn("Could not play ding sound:", err));
}
} else if (event.type === "auto_mode_error") {
// Reload features when an error occurs (feature moved to waiting_approval)
console.log(
@@ -580,22 +586,36 @@ export function BoardView() {
const api = getElectronAPI();
if (!api?.autoMode?.status) return;
const status = await api.autoMode.status();
if (status.success && status.runningFeatures) {
console.log(
"[Board] Syncing running tasks from backend:",
status.runningFeatures
);
// Clear existing running tasks for this project and add the actual running ones
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
const status = await api.autoMode.status(currentProject.path);
if (status.success) {
const projectId = currentProject.id;
clearRunningTasks(projectId);
const { clearRunningTasks, addRunningTask, setAutoModeRunning } =
useAppStore.getState();
// Add each running feature to the store
status.runningFeatures.forEach((featureId: string) => {
addRunningTask(projectId, featureId);
});
// Sync running features if available
if (status.runningFeatures) {
console.log(
"[Board] Syncing running tasks from backend:",
status.runningFeatures
);
// Clear existing running tasks for this project and add the actual running ones
clearRunningTasks(projectId);
// Add each running feature to the store
status.runningFeatures.forEach((featureId: string) => {
addRunningTask(projectId, featureId);
});
}
// Sync auto mode running state (backend returns autoLoopRunning, mock returns isRunning)
const isAutoModeRunning =
status.autoLoopRunning ?? status.isRunning ?? false;
console.log(
"[Board] Syncing auto mode running state:",
isAutoModeRunning
);
setAutoModeRunning(projectId, isAutoModeRunning);
}
} catch (error) {
console.error("[Board] Failed to sync running tasks:", error);
@@ -1899,7 +1919,7 @@ export function BoardView() {
data-testid="start-next-button"
>
<FastForward className="w-3 h-3 mr-1" />
Start Next
Pull Top
</HotkeyButton>
)}
</div>

View File

@@ -20,8 +20,11 @@ import {
StopCircle,
ChevronDown,
ChevronRight,
RefreshCw,
Shield,
Zap,
} from "lucide-react";
import { getElectronAPI, FeatureSuggestion, SuggestionsEvent } from "@/lib/electron";
import { getElectronAPI, FeatureSuggestion, SuggestionsEvent, SuggestionType } from "@/lib/electron";
import { useAppStore, Feature } from "@/store/app-store";
import { toast } from "sonner";
@@ -36,6 +39,39 @@ interface FeatureSuggestionsDialogProps {
setIsGenerating: (generating: boolean) => void;
}
// Configuration for each suggestion type
const suggestionTypeConfig: Record<SuggestionType, {
label: string;
icon: React.ComponentType<{ className?: string }>;
description: string;
color: string;
}> = {
features: {
label: "Feature Suggestions",
icon: Lightbulb,
description: "Discover missing features and improvements",
color: "text-yellow-500",
},
refactoring: {
label: "Refactoring Suggestions",
icon: RefreshCw,
description: "Find code smells and refactoring opportunities",
color: "text-blue-500",
},
security: {
label: "Security Suggestions",
icon: Shield,
description: "Identify security vulnerabilities and issues",
color: "text-red-500",
},
performance: {
label: "Performance Suggestions",
icon: Zap,
description: "Discover performance bottlenecks and optimizations",
color: "text-green-500",
},
};
export function FeatureSuggestionsDialog({
open,
onClose,
@@ -49,6 +85,7 @@ export function FeatureSuggestionsDialog({
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [isImporting, setIsImporting] = useState(false);
const [currentSuggestionType, setCurrentSuggestionType] = useState<SuggestionType | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const autoScrollRef = useRef(true);
@@ -87,7 +124,8 @@ export function FeatureSuggestionsDialog({
setSuggestions(event.suggestions);
// Select all by default
setSelectedIds(new Set(event.suggestions.map((s) => s.id)));
toast.success(`Generated ${event.suggestions.length} feature suggestions!`);
const typeLabel = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType].label.toLowerCase() : "suggestions";
toast.success(`Generated ${event.suggestions.length} ${typeLabel}!`);
} else {
toast.info("No suggestions generated. Try again.");
}
@@ -100,10 +138,10 @@ export function FeatureSuggestionsDialog({
return () => {
unsubscribe();
};
}, [open, setSuggestions, setIsGenerating]);
}, [open, setSuggestions, setIsGenerating, currentSuggestionType]);
// Start generating suggestions
const handleGenerate = useCallback(async () => {
// Start generating suggestions for a specific type
const handleGenerate = useCallback(async (suggestionType: SuggestionType) => {
const api = getElectronAPI();
if (!api?.suggestions) {
toast.error("Suggestions API not available");
@@ -114,9 +152,10 @@ export function FeatureSuggestionsDialog({
setProgress([]);
setSuggestions([]);
setSelectedIds(new Set());
setCurrentSuggestionType(suggestionType);
try {
const result = await api.suggestions.generate(projectPath);
const result = await api.suggestions.generate(projectPath, suggestionType);
if (!result.success) {
toast.error(result.error || "Failed to start generation");
setIsGenerating(false);
@@ -203,8 +242,10 @@ export function FeatureSuggestionsDialog({
}));
// Create each new feature using the features API
for (const feature of newFeatures) {
await api.features.create(projectPath, feature);
if (api.features) {
for (const feature of newFeatures) {
await api.features.create(projectPath, feature);
}
}
// Merge with existing features for store update
@@ -219,6 +260,7 @@ export function FeatureSuggestionsDialog({
setSuggestions([]);
setSelectedIds(new Set());
setProgress([]);
setCurrentSuggestionType(null);
onClose();
} catch (error) {
@@ -238,16 +280,17 @@ export function FeatureSuggestionsDialog({
autoScrollRef.current = isAtBottom;
};
// Reset state when dialog closes
useEffect(() => {
if (!open) {
// Don't reset immediately - allow re-open to see results
// Only reset if explicitly closed without importing
}
}, [open]);
// Go back to type selection
const handleBackToSelection = useCallback(() => {
setSuggestions([]);
setSelectedIds(new Set());
setProgress([]);
setCurrentSuggestionType(null);
}, [setSuggestions]);
const hasStarted = progress.length > 0 || suggestions.length > 0;
const hasSuggestions = suggestions.length > 0;
const currentConfig = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType] : null;
return (
<Dialog open={open} onOpenChange={onClose}>
@@ -257,31 +300,56 @@ export function FeatureSuggestionsDialog({
>
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<Lightbulb className="w-5 h-5 text-yellow-500" />
Feature Suggestions
{currentConfig ? (
<>
<currentConfig.icon className={`w-5 h-5 ${currentConfig.color}`} />
{currentConfig.label}
</>
) : (
<>
<Lightbulb className="w-5 h-5 text-yellow-500" />
AI Suggestions
</>
)}
</DialogTitle>
<DialogDescription>
Analyze your project to discover missing features and improvements.
The AI will scan your codebase and suggest features ordered by priority.
{currentConfig
? currentConfig.description
: "Analyze your project to discover improvements. Choose a suggestion type below."}
</DialogDescription>
</DialogHeader>
{!hasStarted ? (
// Initial state - show explanation and generate button
<div className="flex-1 flex flex-col items-center justify-center py-8 text-center">
<Lightbulb className="w-16 h-16 text-yellow-500/50 mb-4" />
<h3 className="text-lg font-semibold mb-2">
Discover Missing Features
</h3>
<p className="text-muted-foreground max-w-md mb-6">
Our AI will analyze your project structure, code patterns, and
existing features to generate a prioritized list of suggestions
for new features you could add.
// Initial state - show suggestion type buttons
<div className="flex-1 flex flex-col items-center justify-center py-8">
<p className="text-muted-foreground text-center max-w-lg mb-8">
Our AI will analyze your project and generate actionable suggestions.
Choose what type of analysis you want to perform:
</p>
<Button onClick={handleGenerate} size="lg">
<Lightbulb className="w-4 h-4 mr-2" />
Generate Suggestions
</Button>
<div className="grid grid-cols-2 gap-4 w-full max-w-2xl">
{(Object.entries(suggestionTypeConfig) as [SuggestionType, typeof suggestionTypeConfig[SuggestionType]][]).map(
([type, config]) => {
const Icon = config.icon;
return (
<Button
key={type}
variant="outline"
className="h-auto py-6 px-6 flex flex-col items-center gap-3 hover:border-primary/50 transition-colors"
onClick={() => handleGenerate(type)}
data-testid={`generate-${type}-btn`}
>
<Icon className={`w-8 h-8 ${config.color}`} />
<div className="text-center">
<div className="font-semibold">{config.label.replace(" Suggestions", "")}</div>
<div className="text-xs text-muted-foreground mt-1">
{config.description}
</div>
</div>
</Button>
);
}
)}
</div>
</div>
) : isGenerating ? (
// Generating state - show progress
@@ -410,20 +478,34 @@ export function FeatureSuggestionsDialog({
<p className="text-muted-foreground mb-4">
No suggestions were generated. Try running the analysis again.
</p>
<Button onClick={handleGenerate}>
<Lightbulb className="w-4 h-4 mr-2" />
Try Again
</Button>
<div className="flex gap-2">
<Button variant="outline" onClick={handleBackToSelection}>
Back to Selection
</Button>
{currentSuggestionType && (
<Button onClick={() => handleGenerate(currentSuggestionType)}>
<Lightbulb className="w-4 h-4 mr-2" />
Try Again
</Button>
)}
</div>
</div>
)}
<DialogFooter className="flex-shrink-0">
{hasSuggestions && (
<div className="flex gap-2 w-full justify-between">
<Button variant="outline" onClick={handleGenerate}>
<Lightbulb className="w-4 h-4 mr-2" />
Regenerate
</Button>
<div className="flex gap-2">
<Button variant="outline" onClick={handleBackToSelection}>
Back
</Button>
{currentSuggestionType && (
<Button variant="outline" onClick={() => handleGenerate(currentSuggestionType)}>
{currentConfig && <currentConfig.icon className="w-4 h-4 mr-2" />}
Regenerate
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="ghost" onClick={onClose}>
Cancel

View File

@@ -248,21 +248,12 @@ export const KanbanCard = memo(function KanbanCard({
{...attributes}
{...(isDraggable ? listeners : {})}
>
{/* 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-brand-500/10 border border-brand-500/30 text-brand-400/70 z-10"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
</div>
)}
{/* Skip Tests indicator badge */}
{feature.skipTests && !feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
shortcutKey ? "top-2 left-10" : "top-2 left-2",
"top-2 left-2",
"bg-orange-500/20 border border-orange-500/50 text-orange-400"
)}
data-testid={`skip-tests-badge-${feature.id}`}
@@ -277,7 +268,7 @@ export const KanbanCard = memo(function KanbanCard({
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
shortcutKey ? "top-2 left-10" : "top-2 left-2",
"top-2 left-2",
"bg-red-500/20 border border-red-500/50 text-red-400"
)}
data-testid={`error-badge-${feature.id}`}
@@ -299,9 +290,7 @@ export const KanbanCard = memo(function KanbanCard({
// Position below error badge if present, otherwise use normal position
feature.error || feature.skipTests
? "top-8 left-2"
: shortcutKey
? "top-2 left-10"
: "top-2 left-2"
: "top-2 left-2"
)}
data-testid={`branch-badge-${feature.id}`}
>
@@ -319,7 +308,7 @@ export const KanbanCard = memo(function KanbanCard({
className={cn(
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
// Add extra top padding when badges are present to prevent text overlap
(feature.skipTests || feature.error || shortcutKey) && "pt-10",
(feature.skipTests || feature.error) && "pt-10",
// Add even more top padding when both badges and branch are shown
hasWorktree && (feature.skipTests || feature.error) && "pt-14"
)}
@@ -613,6 +602,14 @@ export const KanbanCard = memo(function KanbanCard({
>
<FileText className="w-3 h-3 mr-1" />
Logs
{shortcutKey && (
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
</span>
)}
</Button>
)}
{onForceStop && (

View File

@@ -0,0 +1,210 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Bot, Folder, Loader2, RefreshCw, Square, Activity } from "lucide-react";
import { getElectronAPI, RunningAgent } from "@/lib/electron";
import { useAppStore } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function RunningAgentsView() {
const [runningAgents, setRunningAgents] = useState<RunningAgent[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const { setCurrentProject, projects, setCurrentView } = useAppStore();
const fetchRunningAgents = useCallback(async () => {
try {
const api = getElectronAPI();
if (api.runningAgents) {
const result = await api.runningAgents.getAll();
if (result.success && result.runningAgents) {
setRunningAgents(result.runningAgents);
}
}
} catch (error) {
console.error("[RunningAgentsView] Error fetching running agents:", error);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
// Initial fetch
useEffect(() => {
fetchRunningAgents();
}, [fetchRunningAgents]);
// Auto-refresh every 2 seconds
useEffect(() => {
const interval = setInterval(() => {
fetchRunningAgents();
}, 2000);
return () => clearInterval(interval);
}, [fetchRunningAgents]);
// Subscribe to auto-mode events to update in real-time
useEffect(() => {
const api = getElectronAPI();
if (!api.autoMode) return;
const unsubscribe = api.autoMode.onEvent((event) => {
// When a feature completes or errors, refresh the list
if (
event.type === "auto_mode_feature_complete" ||
event.type === "auto_mode_error"
) {
fetchRunningAgents();
}
});
return () => {
unsubscribe();
};
}, [fetchRunningAgents]);
const handleRefresh = useCallback(() => {
setRefreshing(true);
fetchRunningAgents();
}, [fetchRunningAgents]);
const handleStopAgent = useCallback(async (featureId: string) => {
try {
const api = getElectronAPI();
if (api.autoMode) {
await api.autoMode.stopFeature(featureId);
// Refresh list after stopping
fetchRunningAgents();
}
} catch (error) {
console.error("[RunningAgentsView] Error stopping agent:", error);
}
}, [fetchRunningAgents]);
const handleNavigateToProject = useCallback((agent: RunningAgent) => {
// Find the project by path
const project = projects.find((p) => p.path === agent.projectPath);
if (project) {
setCurrentProject(project);
setCurrentView("board");
}
}, [projects, setCurrentProject, setCurrentView]);
if (loading) {
return (
<div className="flex-1 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="flex-1 flex flex-col overflow-hidden p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-brand-500/10">
<Activity className="h-6 w-6 text-brand-500" />
</div>
<div>
<h1 className="text-2xl font-bold">Running Agents</h1>
<p className="text-sm text-muted-foreground">
{runningAgents.length === 0
? "No agents currently running"
: `${runningAgents.length} agent${runningAgents.length === 1 ? "" : "s"} running across all projects`}
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={refreshing}
>
<RefreshCw
className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")}
/>
Refresh
</Button>
</div>
{/* Content */}
{runningAgents.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-center">
<div className="p-4 rounded-full bg-muted/50 mb-4">
<Bot className="h-12 w-12 text-muted-foreground" />
</div>
<h2 className="text-lg font-medium mb-2">No Running Agents</h2>
<p className="text-muted-foreground max-w-md">
Agents will appear here when they are actively working on features.
Start an agent from the Kanban board by dragging a feature to "In Progress".
</p>
</div>
) : (
<div className="flex-1 overflow-auto">
<div className="space-y-3">
{runningAgents.map((agent) => (
<div
key={`${agent.projectPath}-${agent.featureId}`}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card hover:bg-accent/50 transition-colors"
>
<div className="flex items-center gap-4 min-w-0">
{/* Status indicator */}
<div className="relative">
<Bot className="h-8 w-8 text-brand-500" />
<span className="absolute -top-1 -right-1 flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
</span>
</div>
{/* Agent info */}
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium truncate">
{agent.featureId}
</span>
{agent.isAutoMode && (
<span className="px-2 py-0.5 text-[10px] font-medium rounded-full bg-brand-500/10 text-brand-500 border border-brand-500/30">
AUTO
</span>
)}
</div>
<button
onClick={() => handleNavigateToProject(agent)}
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<Folder className="h-3.5 w-3.5" />
<span className="truncate">{agent.projectName}</span>
</button>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="ghost"
size="sm"
onClick={() => handleNavigateToProject(agent)}
className="text-muted-foreground hover:text-foreground"
>
View Project
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleStopAgent(agent.featureId)}
>
<Square className="h-3.5 w-3.5 mr-1.5" />
Stop
</Button>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -42,6 +42,8 @@ import {
RefreshCw,
Info,
RotateCcw,
Volume2,
VolumeX,
} from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { Checkbox } from "@/components/ui/checkbox";
@@ -62,6 +64,7 @@ const NAV_ITEMS = [
{ id: "codex", label: "Codex", icon: Atom },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
{ id: "audio", label: "Audio", icon: Volume2 },
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
{ id: "danger", label: "Danger Zone", icon: Trash2 },
@@ -83,6 +86,8 @@ export function SettingsView() {
setUseWorktrees,
showProfilesOnly,
setShowProfilesOnly,
muteDoneSound,
setMuteDoneSound,
currentProject,
moveProjectToTrash,
keyboardShortcuts,
@@ -2132,6 +2137,55 @@ export function SettingsView() {
</div>
</div>
{/* Audio Section */}
<div
id="audio"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Volume2 className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Audio
</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure audio and notification settings.
</p>
</div>
<div className="p-6 space-y-4">
{/* Mute Done Sound Setting */}
<div className="space-y-3">
<div className="flex items-start space-x-3">
<Checkbox
id="mute-done-sound"
checked={muteDoneSound}
onCheckedChange={(checked) =>
setMuteDoneSound(checked === true)
}
className="mt-0.5"
data-testid="mute-done-sound-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="mute-done-sound"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<VolumeX className="w-4 h-4 text-brand-500" />
Mute notification sound when agents complete
</Label>
<p className="text-xs text-muted-foreground">
When enabled, disables the &quot;ding&quot; sound that
plays when an agent completes a feature. The feature
will still move to the completed column, but without
audio notification.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Feature Defaults Section */}
<div
id="defaults"