feat: add configurable CLI command and UI improvements

Add support for alternative CLI commands via CLI_COMMAND environment
variable, allowing users to use CLIs other than 'claude' (e.g., 'glm').
This change affects all server services and the main CLI launcher.

Key changes:

- Configurable CLI command via CLI_COMMAND env var (defaults to 'claude')
- Configurable Playwright headless mode via PLAYWRIGHT_HEADLESS env var
- Pin claude-agent-sdk version to <0.2.0 for stability
- Use tail -500 for progress notes to avoid context overflow
- Add project delete functionality with confirmation dialog
- Replace single-line input with resizable textarea in spec chat
- Add coder agent configuration for code implementation tasks
- Ignore issues/ directory in git

Files modified:
- client.py: CLI command and Playwright headless configuration
- server/main.py, server/services/*: CLI command configuration
- start.py: CLI command configuration and error messages
- .env.example: Document new environment variables
- .gitignore: Ignore issues/ directory
- requirements.txt: Pin SDK version
- .claude/templates/*: Use tail -500 for progress notes
- ui/src/components/ProjectSelector.tsx: Add delete button
- ui/src/components/SpecCreationChat.tsx: Auto-resizing textarea
- ui/src/components/ConfirmDialog.tsx: New reusable dialog
- .claude/agents/coder.md: New coder agent configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-10 13:19:49 +02:00
parent a0f7e72361
commit 117ca89f08
16 changed files with 496 additions and 49 deletions

View File

@@ -1,7 +1,9 @@
import { useState } from 'react'
import { ChevronDown, Plus, FolderOpen, Loader2 } from 'lucide-react'
import { ChevronDown, Plus, FolderOpen, Loader2, Trash2 } from 'lucide-react'
import type { ProjectSummary } from '../lib/types'
import { NewProjectModal } from './NewProjectModal'
import { ConfirmDialog } from './ConfirmDialog'
import { useDeleteProject } from '../hooks/useProjects'
interface ProjectSelectorProps {
projects: ProjectSummary[]
@@ -18,12 +20,42 @@ export function ProjectSelector({
}: ProjectSelectorProps) {
const [isOpen, setIsOpen] = useState(false)
const [showNewProjectModal, setShowNewProjectModal] = useState(false)
const [projectToDelete, setProjectToDelete] = useState<string | null>(null)
const deleteProject = useDeleteProject()
const handleProjectCreated = (projectName: string) => {
onSelectProject(projectName)
setIsOpen(false)
}
const handleDeleteClick = (e: React.MouseEvent, projectName: string) => {
// Prevent the click from selecting the project
e.stopPropagation()
setProjectToDelete(projectName)
}
const handleConfirmDelete = async () => {
if (!projectToDelete) return
try {
await deleteProject.mutateAsync(projectToDelete)
// If the deleted project was selected, clear the selection
if (selectedProject === projectToDelete) {
onSelectProject(null)
}
setProjectToDelete(null)
} catch (error) {
// Error is handled by the mutation, just close the dialog
console.error('Failed to delete project:', error)
setProjectToDelete(null)
}
}
const handleCancelDelete = () => {
setProjectToDelete(null)
}
const selectedProjectData = projects.find(p => p.name === selectedProject)
return (
@@ -70,28 +102,39 @@ export function ProjectSelector({
{projects.length > 0 ? (
<div className="max-h-[300px] overflow-auto">
{projects.map(project => (
<button
<div
key={project.name}
onClick={() => {
onSelectProject(project.name)
setIsOpen(false)
}}
className={`w-full neo-dropdown-item flex items-center justify-between ${
className={`flex items-center ${
project.name === selectedProject
? 'bg-[var(--color-neo-pending)]'
: ''
}`}
>
<span className="flex items-center gap-2">
<FolderOpen size={16} />
{project.name}
</span>
{project.stats.total > 0 && (
<span className="text-sm font-mono">
{project.stats.passing}/{project.stats.total}
<button
onClick={() => {
onSelectProject(project.name)
setIsOpen(false)
}}
className="flex-1 neo-dropdown-item flex items-center justify-between"
>
<span className="flex items-center gap-2">
<FolderOpen size={16} />
{project.name}
</span>
)}
</button>
{project.stats.total > 0 && (
<span className="text-sm font-mono">
{project.stats.passing}/{project.stats.total}
</span>
)}
</button>
<button
onClick={(e) => handleDeleteClick(e, project.name)}
className="p-2 mr-2 text-[var(--color-neo-text-secondary)] hover:text-[var(--color-neo-danger)] hover:bg-[var(--color-neo-danger)]/10 transition-colors rounded"
title={`Delete ${project.name}`}
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
) : (
@@ -124,6 +167,19 @@ export function ProjectSelector({
onClose={() => setShowNewProjectModal(false)}
onProjectCreated={handleProjectCreated}
/>
{/* Delete Confirmation Dialog */}
<ConfirmDialog
isOpen={projectToDelete !== null}
title="Delete Project"
message={`Are you sure you want to remove "${projectToDelete}" from the registry? This will unregister the project but preserve its files on disk.`}
confirmLabel="Delete"
cancelLabel="Cancel"
variant="danger"
isLoading={deleteProject.isPending}
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
/>
</div>
)
}