feat: implement E2E testing workflow and enhance XML syntax editor

- Added a new GitHub Actions workflow for end-to-end (E2E) testing, including setup for Node.js, Playwright, and server initialization.
- Introduced a setup script for E2E test fixtures to create necessary directories and files.
- Integrated CodeMirror for XML syntax editing in the XmlSyntaxEditor component, improving code highlighting and editing experience.
- Updated package dependencies in package.json and package-lock.json to include new libraries for XML handling and theming.
- Refactored various components for improved readability and consistency, including the sidebar and file browser dialog.
- Added tests for spec editor persistence to ensure data integrity across sessions.
This commit is contained in:
Cody Seibert
2025-12-14 14:12:38 -05:00
parent ae13551033
commit 20a7c8b5a8
16 changed files with 2132 additions and 893 deletions

View File

@@ -207,6 +207,16 @@ export function Sidebar() {
moveProjectToTrash,
} = useAppStore();
// Environment variable flags for hiding sidebar items
// Note: Next.js requires static access to process.env variables (no dynamic keys)
const hideTerminal = process.env.NEXT_PUBLIC_HIDE_TERMINAL === "true";
const hideWiki = process.env.NEXT_PUBLIC_HIDE_WIKI === "true";
const hideRunningAgents =
process.env.NEXT_PUBLIC_HIDE_RUNNING_AGENTS === "true";
const hideContext = process.env.NEXT_PUBLIC_HIDE_CONTEXT === "true";
const hideSpecEditor = process.env.NEXT_PUBLIC_HIDE_SPEC_EDITOR === "true";
const hideAiProfiles = process.env.NEXT_PUBLIC_HIDE_AI_PROFILES === "true";
// Get customizable keyboard shortcuts
const shortcuts = useKeyboardShortcutsConfig();
@@ -590,54 +600,75 @@ export function Sidebar() {
}
}, [emptyTrash, trashedProjects.length]);
const navSections: NavSection[] = [
{
label: "Project",
items: [
{
id: "board",
label: "Kanban Board",
icon: LayoutGrid,
shortcut: shortcuts.board,
},
{
id: "agent",
label: "Agent Runner",
icon: Bot,
shortcut: shortcuts.agent,
},
],
},
{
label: "Tools",
items: [
{
id: "spec",
label: "Spec Editor",
icon: FileText,
shortcut: shortcuts.spec,
},
{
id: "context",
label: "Context",
icon: BookOpen,
shortcut: shortcuts.context,
},
{
id: "profiles",
label: "AI Profiles",
icon: UserCircle,
shortcut: shortcuts.profiles,
},
{
id: "terminal",
label: "Terminal",
icon: Terminal,
shortcut: shortcuts.terminal,
},
],
},
];
const navSections: NavSection[] = useMemo(() => {
const allToolsItems: NavItem[] = [
{
id: "spec",
label: "Spec Editor",
icon: FileText,
shortcut: shortcuts.spec,
},
{
id: "context",
label: "Context",
icon: BookOpen,
shortcut: shortcuts.context,
},
{
id: "profiles",
label: "AI Profiles",
icon: UserCircle,
shortcut: shortcuts.profiles,
},
{
id: "terminal",
label: "Terminal",
icon: Terminal,
shortcut: shortcuts.terminal,
},
];
// Filter out hidden items
const visibleToolsItems = allToolsItems.filter((item) => {
if (item.id === "spec" && hideSpecEditor) {
return false;
}
if (item.id === "context" && hideContext) {
return false;
}
if (item.id === "profiles" && hideAiProfiles) {
return false;
}
if (item.id === "terminal" && hideTerminal) {
return false;
}
return true;
});
return [
{
label: "Project",
items: [
{
id: "board",
label: "Kanban Board",
icon: LayoutGrid,
shortcut: shortcuts.board,
},
{
id: "agent",
label: "Agent Runner",
icon: Bot,
shortcut: shortcuts.agent,
},
],
},
{
label: "Tools",
items: visibleToolsItems,
},
];
}, [shortcuts, hideSpecEditor, hideContext, hideTerminal, hideAiProfiles]);
// Handle selecting the currently highlighted project
const selectHighlightedProject = useCallback(() => {
@@ -1268,108 +1299,112 @@ export function Sidebar() {
{/* Course Promo Badge */}
<CoursePromoBadge sidebarOpen={sidebarOpen} />
{/* Wiki Link */}
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("wiki")}
className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
isActiveRoute("wiki")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? "Wiki" : undefined}
data-testid="wiki-link"
>
{isActiveRoute("wiki") && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
)}
<BookOpen
{!hideWiki && (
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("wiki")}
className={cn(
"w-4 h-4 shrink-0 transition-colors",
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
isActiveRoute("wiki")
? "text-brand-500"
: "group-hover:text-brand-400"
)}
/>
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? "Wiki" : undefined}
data-testid="wiki-link"
>
Wiki
</span>
{!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
Wiki
</span>
)}
</button>
</div>
{/* Running Agents Link */}
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("running-agents")}
className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
isActiveRoute("running-agents")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? "Running Agents" : undefined}
data-testid="running-agents-link"
>
{isActiveRoute("running-agents") && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
)}
<div className="relative">
<Activity
{isActiveRoute("wiki") && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
)}
<BookOpen
className={cn(
"w-4 h-4 shrink-0 transition-colors",
isActiveRoute("running-agents")
isActiveRoute("wiki")
? "text-brand-500"
: "group-hover:text-brand-400"
)}
/>
{/* Running agents count badge - shown in collapsed state */}
{!sidebarOpen && runningAgentsCount > 0 && (
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
Wiki
</span>
{!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
Wiki
</span>
)}
</button>
</div>
)}
{/* Running Agents Link */}
{!hideRunningAgents && (
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("running-agents")}
className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
isActiveRoute("running-agents")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? "Running Agents" : undefined}
data-testid="running-agents-link"
>
{isActiveRoute("running-agents") && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
)}
<div className="relative">
<Activity
className={cn(
"w-4 h-4 shrink-0 transition-colors",
isActiveRoute("running-agents")
? "text-brand-500"
: "group-hover:text-brand-400"
)}
/>
{/* Running agents count badge - shown in collapsed state */}
{!sidebarOpen && runningAgentsCount > 0 && (
<span
className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-semibold rounded-full bg-brand-500 text-white"
data-testid="running-agents-count-collapsed"
>
{runningAgentsCount > 99 ? "99" : runningAgentsCount}
</span>
)}
</div>
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
Running Agents
</span>
{/* Running agents count badge - shown in expanded state */}
{sidebarOpen && runningAgentsCount > 0 && (
<span
className="absolute -top-1.5 -right-1.5 flex items-center justify-center min-w-5 h-5 px-1 text-[10px] font-semibold rounded-full bg-brand-500 text-white"
data-testid="running-agents-count-collapsed"
className={cn(
"hidden lg:flex items-center justify-center min-w-6 h-6 px-1.5 text-xs font-semibold rounded-full bg-brand-500 text-white",
isActiveRoute("running-agents") && "bg-brand-600"
)}
data-testid="running-agents-count"
>
{runningAgentsCount > 99 ? "99" : runningAgentsCount}
</span>
)}
</div>
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
{!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
Running Agents
</span>
)}
>
Running Agents
</span>
{/* Running agents count badge - shown in expanded state */}
{sidebarOpen && runningAgentsCount > 0 && (
<span
className={cn(
"hidden lg:flex items-center justify-center min-w-6 h-6 px-1.5 text-xs font-semibold rounded-full bg-brand-500 text-white",
isActiveRoute("running-agents") && "bg-brand-600"
)}
data-testid="running-agents-count"
>
{runningAgentsCount > 99 ? "99" : runningAgentsCount}
</span>
)}
{!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
Running Agents
</span>
)}
</button>
</div>
</button>
</div>
)}
{/* Settings Link */}
<div className="p-2">
<button