feat: fix tooltip shortcuts and add dev server config dialog

Tooltip fixes (PR #177 follow-up):
- Remove duplicate title attr on Settings button that caused double-tooltip
- Restore keyboard shortcut hints in tooltip text: Settings (,), Reset (R)
- Clean up spurious peer markers in package-lock.json

Dev server config dialog:
- Add DevServerConfigDialog component for custom dev commands
- Open config dialog automatically when start fails with "no dev command"
- Add useDevServerConfig/useUpdateDevServerConfig hooks
- Add updateDevServerConfig API function
- Add config gear button next to dev server start

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-02-08 15:29:44 +02:00
parent 38fc8788a2
commit 1925818d49
6 changed files with 297 additions and 39 deletions

View File

@@ -0,0 +1,182 @@
import { useState, useEffect } from 'react'
import { Loader2, RotateCcw, Terminal } from 'lucide-react'
import { useQueryClient } from '@tanstack/react-query'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useDevServerConfig, useUpdateDevServerConfig } from '@/hooks/useProjects'
import { startDevServer } from '@/lib/api'
interface DevServerConfigDialogProps {
projectName: string
isOpen: boolean
onClose: () => void
autoStartOnSave?: boolean
}
export function DevServerConfigDialog({
projectName,
isOpen,
onClose,
autoStartOnSave = false,
}: DevServerConfigDialogProps) {
const { data: config } = useDevServerConfig(isOpen ? projectName : null)
const updateConfig = useUpdateDevServerConfig(projectName)
const queryClient = useQueryClient()
const [command, setCommand] = useState('')
const [error, setError] = useState<string | null>(null)
const [isSaving, setIsSaving] = useState(false)
// Sync input with config when dialog opens or config loads
useEffect(() => {
if (isOpen && config) {
setCommand(config.custom_command ?? config.effective_command ?? '')
setError(null)
}
}, [isOpen, config])
const hasCustomCommand = !!config?.custom_command
const handleSaveAndStart = async () => {
const trimmed = command.trim()
if (!trimmed) {
setError('Please enter a dev server command.')
return
}
setIsSaving(true)
setError(null)
try {
await updateConfig.mutateAsync(trimmed)
if (autoStartOnSave) {
await startDevServer(projectName)
queryClient.invalidateQueries({ queryKey: ['dev-server-status', projectName] })
}
onClose()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save configuration')
} finally {
setIsSaving(false)
}
}
const handleClear = async () => {
setIsSaving(true)
setError(null)
try {
await updateConfig.mutateAsync(null)
setCommand(config?.detected_command ?? '')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to clear configuration')
} finally {
setIsSaving(false)
}
}
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10 text-primary">
<Terminal size={20} />
</div>
<DialogTitle>Dev Server Configuration</DialogTitle>
</div>
</DialogHeader>
<DialogDescription asChild>
<div className="space-y-4">
{/* Detection info */}
<div className="rounded-lg border-2 border-border bg-muted/50 p-3 text-sm">
{config?.detected_type ? (
<p>
Detected project type: <strong className="text-foreground">{config.detected_type}</strong>
{config.detected_command && (
<span className="text-muted-foreground"> {config.detected_command}</span>
)}
</p>
) : (
<p className="text-muted-foreground">
No project type detected. Enter a custom command below.
</p>
)}
</div>
{/* Command input */}
<div className="space-y-2">
<Label htmlFor="dev-command" className="text-foreground">Dev server command</Label>
<Input
id="dev-command"
value={command}
onChange={(e) => {
setCommand(e.target.value)
setError(null)
}}
placeholder="npm run dev"
onKeyDown={(e) => {
if (e.key === 'Enter' && !isSaving) {
handleSaveAndStart()
}
}}
/>
<p className="text-xs text-muted-foreground">
Allowed runners: npm, npx, pnpm, yarn, python, uvicorn, flask, poetry, cargo, go
</p>
</div>
{/* Clear custom command button */}
{hasCustomCommand && (
<Button
variant="outline"
size="sm"
onClick={handleClear}
disabled={isSaving}
className="gap-1.5"
>
<RotateCcw size={14} />
Clear custom command (use auto-detection)
</Button>
)}
{/* Error display */}
{error && (
<p className="text-sm font-mono text-destructive">{error}</p>
)}
</div>
</DialogDescription>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={onClose} disabled={isSaving}>
Cancel
</Button>
<Button onClick={handleSaveAndStart} disabled={isSaving}>
{isSaving ? (
<>
<Loader2 size={16} className="animate-spin mr-1.5" />
Saving...
</>
) : autoStartOnSave ? (
'Save & Start'
) : (
'Save'
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}