mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 14:22:04 +00:00
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:
103
ui/src/components/ConfirmDialog.tsx
Normal file
103
ui/src/components/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* ConfirmDialog Component
|
||||
*
|
||||
* A reusable confirmation dialog following the neobrutalism design system.
|
||||
* Used to confirm destructive actions like deleting projects.
|
||||
*/
|
||||
|
||||
import { AlertTriangle, X } from 'lucide-react'
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean
|
||||
title: string
|
||||
message: string
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
variant?: 'danger' | 'warning'
|
||||
isLoading?: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function ConfirmDialog({
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
variant = 'danger',
|
||||
isLoading = false,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDialogProps) {
|
||||
if (!isOpen) return null
|
||||
|
||||
const variantColors = {
|
||||
danger: {
|
||||
icon: 'var(--color-neo-danger)',
|
||||
button: 'neo-btn-danger',
|
||||
},
|
||||
warning: {
|
||||
icon: 'var(--color-neo-pending)',
|
||||
button: 'neo-btn-warning',
|
||||
},
|
||||
}
|
||||
|
||||
const colors = variantColors[variant]
|
||||
|
||||
return (
|
||||
<div className="neo-modal-backdrop" onClick={onCancel}>
|
||||
<div
|
||||
className="neo-modal w-full max-w-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b-3 border-[var(--color-neo-border)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="p-2 border-2 border-[var(--color-neo-border)] shadow-[2px_2px_0px_rgba(0,0,0,1)]"
|
||||
style={{ backgroundColor: colors.icon }}
|
||||
>
|
||||
<AlertTriangle size={20} className="text-white" />
|
||||
</div>
|
||||
<h2 className="font-display font-bold text-lg text-[#1a1a1a]">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="neo-btn neo-btn-ghost p-2"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<p className="text-[var(--color-neo-text-secondary)] mb-6">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="neo-btn"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className={`neo-btn ${colors.button}`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Deleting...' : confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function SpecCreationChat({
|
||||
const [yoloEnabled, setYoloEnabled] = useState(false)
|
||||
const [pendingAttachments, setPendingAttachments] = useState<ImageAttachment[]>([])
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const {
|
||||
@@ -98,6 +98,10 @@ export function SpecCreationChat({
|
||||
sendMessage(trimmed, pendingAttachments.length > 0 ? pendingAttachments : undefined)
|
||||
setInput('')
|
||||
setPendingAttachments([]) // Clear attachments after sending
|
||||
// Reset textarea height after sending
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.height = 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
@@ -355,11 +359,15 @@ export function SpecCreationChat({
|
||||
<Paperclip size={18} />
|
||||
</button>
|
||||
|
||||
<input
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value)
|
||||
// Auto-resize the textarea
|
||||
e.target.style.height = 'auto'
|
||||
e.target.style.height = `${Math.min(e.target.scrollHeight, 200)}px`
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
currentQuestions
|
||||
@@ -368,8 +376,9 @@ export function SpecCreationChat({
|
||||
? 'Add a message with your image(s)...'
|
||||
: 'Type your response... (or /exit to go to project)'
|
||||
}
|
||||
className="neo-input flex-1"
|
||||
className="neo-input flex-1 resize-none min-h-[46px] max-h-[200px] overflow-y-auto"
|
||||
disabled={(isLoading && !currentQuestions) || connectionStatus !== 'connected'}
|
||||
rows={1}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
@@ -386,7 +395,7 @@ export function SpecCreationChat({
|
||||
|
||||
{/* Help text */}
|
||||
<p className="text-xs text-[var(--color-neo-text-secondary)] mt-2">
|
||||
Press Enter to send. Drag & drop or click <Paperclip size={12} className="inline" /> to attach images (JPEG/PNG, max 5MB).
|
||||
Press Enter to send, Shift+Enter for new line. Drag & drop or click <Paperclip size={12} className="inline" /> to attach images (JPEG/PNG, max 5MB).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user