mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-19 20:03:08 +00:00
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:
14
ui/package-lock.json
generated
14
ui/package-lock.json
generated
@@ -96,7 +96,6 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -2826,7 +2825,6 @@
|
|||||||
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -2836,7 +2834,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
||||||
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -2847,7 +2844,6 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -2903,7 +2899,6 @@
|
|||||||
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.51.0",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.51.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
@@ -3214,7 +3209,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3346,7 +3340,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -3618,7 +3611,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -3844,7 +3836,6 @@
|
|||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5845,7 +5836,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5961,7 +5951,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -5971,7 +5960,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -6436,7 +6424,6 @@
|
|||||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6690,7 +6677,6 @@
|
|||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|||||||
@@ -302,13 +302,12 @@ function App() {
|
|||||||
onClick={() => setShowSettings(true)}
|
onClick={() => setShowSettings(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Settings (,)"
|
|
||||||
aria-label="Open Settings"
|
aria-label="Open Settings"
|
||||||
>
|
>
|
||||||
<Settings size={18} />
|
<Settings size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Settings</TooltipContent>
|
<TooltipContent>Settings (,)</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -323,7 +322,7 @@ function App() {
|
|||||||
<RotateCcw size={18} />
|
<RotateCcw size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Reset</TooltipContent>
|
<TooltipContent>Reset (R)</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Ollama Mode Indicator */}
|
{/* Ollama Mode Indicator */}
|
||||||
|
|||||||
182
ui/src/components/DevServerConfigDialog.tsx
Normal file
182
ui/src/components/DevServerConfigDialog.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Globe, Square, Loader2, ExternalLink, AlertTriangle } from 'lucide-react'
|
import { useState } from 'react'
|
||||||
|
import { Globe, Square, Loader2, ExternalLink, AlertTriangle, Settings2 } from 'lucide-react'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import type { DevServerStatus } from '../lib/types'
|
import type { DevServerStatus } from '../lib/types'
|
||||||
import { startDevServer, stopDevServer } from '../lib/api'
|
import { startDevServer, stopDevServer } from '../lib/api'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { DevServerConfigDialog } from './DevServerConfigDialog'
|
||||||
|
|
||||||
// Re-export DevServerStatus from lib/types for consumers that import from here
|
// Re-export DevServerStatus from lib/types for consumers that import from here
|
||||||
export type { DevServerStatus }
|
export type { DevServerStatus }
|
||||||
@@ -59,17 +61,27 @@ interface DevServerControlProps {
|
|||||||
* - Shows loading state during operations
|
* - Shows loading state during operations
|
||||||
* - Displays clickable URL when server is running
|
* - Displays clickable URL when server is running
|
||||||
* - Uses neobrutalism design with cyan accent when running
|
* - Uses neobrutalism design with cyan accent when running
|
||||||
|
* - Config dialog for setting custom dev commands
|
||||||
*/
|
*/
|
||||||
export function DevServerControl({ projectName, status, url }: DevServerControlProps) {
|
export function DevServerControl({ projectName, status, url }: DevServerControlProps) {
|
||||||
const startDevServerMutation = useStartDevServer(projectName)
|
const startDevServerMutation = useStartDevServer(projectName)
|
||||||
const stopDevServerMutation = useStopDevServer(projectName)
|
const stopDevServerMutation = useStopDevServer(projectName)
|
||||||
|
const [showConfigDialog, setShowConfigDialog] = useState(false)
|
||||||
|
const [autoStartOnSave, setAutoStartOnSave] = useState(false)
|
||||||
|
|
||||||
const isLoading = startDevServerMutation.isPending || stopDevServerMutation.isPending
|
const isLoading = startDevServerMutation.isPending || stopDevServerMutation.isPending
|
||||||
|
|
||||||
const handleStart = () => {
|
const handleStart = () => {
|
||||||
// Clear any previous errors before starting
|
// Clear any previous errors before starting
|
||||||
stopDevServerMutation.reset()
|
stopDevServerMutation.reset()
|
||||||
startDevServerMutation.mutate()
|
startDevServerMutation.mutate(undefined, {
|
||||||
|
onError: (err) => {
|
||||||
|
if (err.message?.includes('No dev command available')) {
|
||||||
|
setAutoStartOnSave(true)
|
||||||
|
setShowConfigDialog(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const handleStop = () => {
|
const handleStop = () => {
|
||||||
// Clear any previous errors before stopping
|
// Clear any previous errors before stopping
|
||||||
@@ -77,6 +89,19 @@ export function DevServerControl({ projectName, status, url }: DevServerControlP
|
|||||||
stopDevServerMutation.mutate()
|
stopDevServerMutation.mutate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenConfig = () => {
|
||||||
|
setAutoStartOnSave(false)
|
||||||
|
setShowConfigDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseConfig = () => {
|
||||||
|
setShowConfigDialog(false)
|
||||||
|
// Clear the start error if config dialog was opened reactively
|
||||||
|
if (startDevServerMutation.error?.message?.includes('No dev command available')) {
|
||||||
|
startDevServerMutation.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Server is stopped when status is 'stopped' or 'crashed' (can restart)
|
// Server is stopped when status is 'stopped' or 'crashed' (can restart)
|
||||||
const isStopped = status === 'stopped' || status === 'crashed'
|
const isStopped = status === 'stopped' || status === 'crashed'
|
||||||
// Server is in a running state
|
// Server is in a running state
|
||||||
@@ -84,9 +109,14 @@ export function DevServerControl({ projectName, status, url }: DevServerControlP
|
|||||||
// Server has crashed
|
// Server has crashed
|
||||||
const isCrashed = status === 'crashed'
|
const isCrashed = status === 'crashed'
|
||||||
|
|
||||||
|
// Hide inline error when config dialog is handling it
|
||||||
|
const startError = startDevServerMutation.error
|
||||||
|
const showInlineError = startError && !startError.message?.includes('No dev command available')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isStopped ? (
|
{isStopped ? (
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleStart}
|
onClick={handleStart}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -103,6 +133,16 @@ export function DevServerControl({ projectName, status, url }: DevServerControlP
|
|||||||
<Globe size={18} />
|
<Globe size={18} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleOpenConfig}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
title="Configure Dev Server"
|
||||||
|
aria-label="Configure Dev Server"
|
||||||
|
>
|
||||||
|
<Settings2 size={16} />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
@@ -139,12 +179,20 @@ export function DevServerControl({ projectName, status, url }: DevServerControlP
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error display */}
|
{/* Error display (hide "no dev command" error when config dialog handles it) */}
|
||||||
{(startDevServerMutation.error || stopDevServerMutation.error) && (
|
{(showInlineError || stopDevServerMutation.error) && (
|
||||||
<span className="text-xs font-mono text-destructive ml-2">
|
<span className="text-xs font-mono text-destructive ml-2">
|
||||||
{String((startDevServerMutation.error || stopDevServerMutation.error)?.message || 'Operation failed')}
|
{String((showInlineError ? startError : stopDevServerMutation.error)?.message || 'Operation failed')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Dev Server Config Dialog */}
|
||||||
|
<DevServerConfigDialog
|
||||||
|
projectName={projectName}
|
||||||
|
isOpen={showConfigDialog}
|
||||||
|
onClose={handleCloseConfig}
|
||||||
|
autoStartOnSave={autoStartOnSave}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import * as api from '../lib/api'
|
import * as api from '../lib/api'
|
||||||
import type { FeatureCreate, FeatureUpdate, ModelsResponse, ProjectSettingsUpdate, ProvidersResponse, Settings, SettingsUpdate } from '../lib/types'
|
import type { DevServerConfig, FeatureCreate, FeatureUpdate, ModelsResponse, ProjectSettingsUpdate, ProvidersResponse, Settings, SettingsUpdate } from '../lib/types'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Projects
|
// Projects
|
||||||
@@ -345,3 +345,36 @@ export function useUpdateSettings() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Dev Server Config
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Default config for placeholder (until API responds)
|
||||||
|
const DEFAULT_DEV_SERVER_CONFIG: DevServerConfig = {
|
||||||
|
detected_type: null,
|
||||||
|
detected_command: null,
|
||||||
|
custom_command: null,
|
||||||
|
effective_command: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDevServerConfig(projectName: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['dev-server-config', projectName],
|
||||||
|
queryFn: () => api.getDevServerConfig(projectName!),
|
||||||
|
enabled: !!projectName,
|
||||||
|
staleTime: 30_000,
|
||||||
|
placeholderData: DEFAULT_DEV_SERVER_CONFIG,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateDevServerConfig(projectName: string) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (customCommand: string | null) =>
|
||||||
|
api.updateDevServerConfig(projectName, customCommand),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['dev-server-config', projectName] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -445,6 +445,16 @@ export async function getDevServerConfig(projectName: string): Promise<DevServer
|
|||||||
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/config`)
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/config`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateDevServerConfig(
|
||||||
|
projectName: string,
|
||||||
|
customCommand: string | null
|
||||||
|
): Promise<DevServerConfig> {
|
||||||
|
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/config`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ custom_command: customCommand }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Terminal API
|
// Terminal API
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user