diff --git a/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx b/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx index 4803d365..52b4c9db 100644 --- a/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx +++ b/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import WYSIWYGEditor from '@/components/ui/wysiwyg'; import { @@ -27,6 +28,7 @@ import { Settings, Terminal, User, + Wrench, } from 'lucide-react'; import RawLogText from '../common/RawLogText'; import UserMessage from './UserMessage'; @@ -34,6 +36,12 @@ import PendingApprovalEntry from './PendingApprovalEntry'; import { NextActionCard } from './NextActionCard'; import { cn } from '@/lib/utils'; import { useRetryUi } from '@/contexts/RetryUiContext'; +import { Button } from '@/components/ui/button'; +import { + ScriptFixerDialog, + type ScriptType, +} from '@/components/dialogs/scripts/ScriptFixerDialog'; +import { useAttemptRepo } from '@/hooks/useAttemptRepo'; type Props = { entry: NormalizedEntry | ProcessStartPayload; @@ -582,6 +590,80 @@ const ToolCallCard: React.FC<{ ); }; +// Script tool names that can be fixed +const SCRIPT_TOOL_NAMES = [ + 'Setup Script', + 'Cleanup Script', + 'Tool Install Script', +]; + +const getScriptType = (toolName: string): ScriptType => { + if (toolName === 'Setup Script') return 'setup'; + if (toolName === 'Cleanup Script') return 'cleanup'; + return 'dev_server'; // Tool Install Script +}; + +const ScriptToolCallCard: React.FC<{ + entry: NormalizedEntry | ProcessStartPayload; + expansionKey: string; + taskAttemptId?: string; + sessionId?: string; + isFailed: boolean; + toolName: string; + forceExpanded?: boolean; +}> = ({ + entry, + expansionKey, + taskAttemptId, + sessionId, + isFailed, + toolName, + forceExpanded = false, +}) => { + const { t } = useTranslation('common'); + const { repos } = useAttemptRepo(taskAttemptId); + + const handleFix = useCallback(() => { + if (!taskAttemptId || repos.length === 0) return; + + const scriptType = getScriptType(toolName); + + ScriptFixerDialog.show({ + scriptType, + repos, + workspaceId: taskAttemptId, + sessionId, + initialRepoId: repos.length === 1 ? repos[0].id : undefined, + }); + }, [toolName, taskAttemptId, sessionId, repos]); + + const canFix = taskAttemptId && repos.length > 0 && isFailed; + + return ( +
+
+ +
+ {canFix && ( + + )} +
+ ); +}; + const LoadingCard = () => { return (
@@ -734,6 +816,31 @@ function DisplayConversationEntry({ ); } + // Script entries (Setup Script, Cleanup Script, Tool Install Script) + if ( + toolEntry.action_type.action === 'command_run' && + SCRIPT_TOOL_NAMES.includes(toolEntry.tool_name) + ) { + const actionType = toolEntry.action_type; + const exitCode = + actionType.result?.exit_status?.type === 'exit_code' + ? actionType.result.exit_status.code + : null; + const isFailed = exitCode !== null && exitCode !== 0; + + return ( + + ); + } + return ( ; + +const ScriptFixerDialogImpl = NiceModal.create( + ({ scriptType, repos, workspaceId, sessionId, initialRepoId }) => { + const modal = useModal(); + const { t } = useTranslation(['tasks', 'common']); + const queryClient = useQueryClient(); + + // State + const [selectedRepoId, setSelectedRepoId] = useState( + initialRepoId || repos[0]?.id || '' + ); + const [script, setScript] = useState(''); + const [originalScript, setOriginalScript] = useState(''); + const [isLoadingRepo, setIsLoadingRepo] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isTesting, setIsTesting] = useState(false); + const [error, setError] = useState(null); + + // Get execution processes for the session to find latest script process + const { executionProcesses } = useExecutionProcesses(sessionId); + + // Find the latest process for this script type + const latestProcess = useMemo(() => { + const runReason = + scriptType === 'setup' + ? 'setupscript' + : scriptType === 'cleanup' + ? 'cleanupscript' + : 'devserver'; + const filtered = executionProcesses.filter( + (p) => p.run_reason === runReason && !p.dropped + ); + // Sort by created_at descending and return the first one + return filtered.sort( + (a, b) => + new Date(b.created_at as unknown as string).getTime() - + new Date(a.created_at as unknown as string).getTime() + )[0]; + }, [executionProcesses, scriptType]); + + // Stream logs for the latest process + const { logs: rawLogs, error: logsError } = useLogStream( + latestProcess?.id ?? '' + ); + const logs: LogEntry[] = rawLogs.filter( + (l): l is LogEntry => l.type === 'STDOUT' || l.type === 'STDERR' + ); + + // Compute status for the latest process + const isProcessRunning = latestProcess?.status === 'running'; + const isProcessCompleted = latestProcess?.status === 'completed'; + const isProcessKilled = latestProcess?.status === 'killed'; + const isProcessFailed = latestProcess?.status === 'failed'; + // exit_code can be null, number, or BigInt - convert to Number for comparison + const exitCode = latestProcess?.exit_code; + const isExitCodeZero = exitCode == null || Number(exitCode) === 0; + const isProcessSuccessful = isProcessCompleted && isExitCodeZero; + const hasProcessError = + isProcessFailed || (isProcessCompleted && !isExitCodeZero); + + // Fetch the selected repo's script + useEffect(() => { + if (!selectedRepoId) return; + + let cancelled = false; + setIsLoadingRepo(true); + setError(null); + + (async () => { + try { + const repo = await repoApi.getById(selectedRepoId); + if (cancelled) return; + + const scriptContent = + scriptType === 'setup' + ? (repo.setup_script ?? '') + : scriptType === 'cleanup' + ? (repo.cleanup_script ?? '') + : (repo.dev_server_script ?? ''); + + setScript(scriptContent); + setOriginalScript(scriptContent); + } catch (err) { + if (cancelled) return; + setError( + err instanceof Error ? err.message : t('common:error.generic') + ); + } finally { + if (!cancelled) setIsLoadingRepo(false); + } + })(); + + return () => { + cancelled = true; + }; + }, [selectedRepoId, scriptType, t]); + + const hasChanges = script !== originalScript; + + const handleClose = useCallback(() => { + modal.resolve({ action: 'canceled' } as ScriptFixerDialogResult); + modal.hide(); + }, [modal]); + + const handleOpenChange = (open: boolean) => { + if (!open) { + handleClose(); + } + }; + + const handleSave = useCallback(async () => { + if (!selectedRepoId) return; + + setIsSaving(true); + setError(null); + + try { + // Build update data with all required fields + const selectedRepo = repos.find((r) => r.id === selectedRepoId); + const updateData: UpdateRepo = { + display_name: selectedRepo?.display_name ?? null, + setup_script: + scriptType === 'setup' + ? script.trim() || null + : (selectedRepo?.setup_script ?? null), + cleanup_script: + scriptType === 'cleanup' + ? script.trim() || null + : (selectedRepo?.cleanup_script ?? null), + copy_files: selectedRepo?.copy_files ?? null, + parallel_setup_script: selectedRepo?.parallel_setup_script ?? null, + dev_server_script: + scriptType === 'dev_server' + ? script.trim() || null + : (selectedRepo?.dev_server_script ?? null), + }; + + await repoApi.update(selectedRepoId, updateData); + + // Invalidate repos cache + queryClient.invalidateQueries({ queryKey: ['repos'] }); + + setOriginalScript(script); + modal.resolve({ action: 'saved' } as ScriptFixerDialogResult); + modal.hide(); + } catch (err) { + setError( + err instanceof Error ? err.message : t('common:error.generic') + ); + } finally { + setIsSaving(false); + } + }, [selectedRepoId, script, scriptType, queryClient, modal, t, repos]); + + const handleSaveAndTest = useCallback(async () => { + if (!selectedRepoId) return; + + setIsTesting(true); + setError(null); + + try { + // First save the script + const selectedRepo = repos.find((r) => r.id === selectedRepoId); + const updateData: UpdateRepo = { + display_name: selectedRepo?.display_name ?? null, + setup_script: + scriptType === 'setup' + ? script.trim() || null + : (selectedRepo?.setup_script ?? null), + cleanup_script: + scriptType === 'cleanup' + ? script.trim() || null + : (selectedRepo?.cleanup_script ?? null), + copy_files: selectedRepo?.copy_files ?? null, + parallel_setup_script: selectedRepo?.parallel_setup_script ?? null, + dev_server_script: + scriptType === 'dev_server' + ? script.trim() || null + : (selectedRepo?.dev_server_script ?? null), + }; + + await repoApi.update(selectedRepoId, updateData); + + // Invalidate repos cache + queryClient.invalidateQueries({ queryKey: ['repos'] }); + + setOriginalScript(script); + + // Then run the script + if (scriptType === 'setup') { + await attemptsApi.runSetupScript(workspaceId); + } else if (scriptType === 'cleanup') { + await attemptsApi.runCleanupScript(workspaceId); + } else { + // Start the dev server + await attemptsApi.startDevServer(workspaceId); + } + + // Keep dialog open so user can see the new execution logs + // The logs will update automatically via useLogStream/useExecutionProcesses + } catch (err) { + setError( + err instanceof Error ? err.message : t('common:error.generic') + ); + } finally { + setIsTesting(false); + } + }, [ + selectedRepoId, + script, + scriptType, + workspaceId, + queryClient, + t, + repos, + ]); + + const dialogTitle = + scriptType === 'setup' + ? t('scriptFixer.setupScriptTitle') + : scriptType === 'cleanup' + ? t('scriptFixer.cleanupScriptTitle') + : t('scriptFixer.devServerTitle'); + + return ( + + + + {dialogTitle} + + +
+ {/* Repo selector (only show if multiple repos) */} + {repos.length > 1 && ( +
+ + +
+ )} + + {/* Script editor */} +
+ +
+ {isLoadingRepo ? ( +
+ +
+ ) : ( + setScript(e.target.value)} + className="font-mono text-sm p-3 border-0 min-h-full bg-panel" + placeholder={ + scriptType === 'setup' + ? '#!/bin/bash\nnpm install' + : scriptType === 'cleanup' + ? '#!/bin/bash\nrm -rf node_modules' + : '#!/bin/bash\nnpm run dev' + } + disableInternalScroll + /> + )} +
+
+ + {/* Logs section */} +
+
+ + {/* Status indicator */} + {latestProcess && ( +
+ {isProcessRunning ? ( + <> + + + {t('scriptFixer.statusRunning')} + + + ) : isProcessSuccessful ? ( + <> + + + {t('scriptFixer.statusSuccess')} + + + ) : hasProcessError ? ( + <> + + + {t('scriptFixer.statusFailed', { + exitCode: Number(latestProcess.exit_code ?? 0), + })} + + + ) : isProcessKilled ? ( + <> + + + {t('scriptFixer.statusKilled')} + + + ) : null} +
+ )} +
+
+ {latestProcess ? ( + + ) : ( +
+ {t('scriptFixer.noLogs')} +
+ )} +
+
+ + {/* Error display */} + {error &&
{error}
} +
+ + + + + + +
+
+ ); + } +); + +export const ScriptFixerDialog = defineModal< + ScriptFixerDialogProps, + ScriptFixerDialogResult +>(ScriptFixerDialogImpl); diff --git a/frontend/src/components/panels/PreviewPanel.tsx b/frontend/src/components/panels/PreviewPanel.tsx index 571a93d2..7ce9fbdb 100644 --- a/frontend/src/components/panels/PreviewPanel.tsx +++ b/frontend/src/components/panels/PreviewPanel.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Loader2, X } from 'lucide-react'; +import { Loader2, X, Wrench } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useDevserverPreview } from '@/hooks/useDevserverPreview'; import { useDevServer } from '@/hooks/useDevServer'; @@ -16,6 +16,8 @@ import { DevServerLogsView } from '@/components/tasks/TaskDetails/preview/DevSer import { PreviewToolbar } from '@/components/tasks/TaskDetails/preview/PreviewToolbar'; import { NoServerContent } from '@/components/tasks/TaskDetails/preview/NoServerContent'; import { ReadyContent } from '@/components/tasks/TaskDetails/preview/ReadyContent'; +import { ScriptFixerDialog } from '@/components/dialogs/scripts/ScriptFixerDialog'; +import { useAttemptRepo } from '@/hooks/useAttemptRepo'; export function PreviewPanel() { const [iframeError, setIframeError] = useState(false); @@ -35,6 +37,7 @@ export function PreviewPanel() { rawAttemptId && rawAttemptId !== 'latest' ? rawAttemptId : undefined; const { data: projectHasDevScript = false } = useHasDevServerScript(projectId); + const { repos } = useAttemptRepo(attemptId); const { start: startDevServer, @@ -112,6 +115,14 @@ export function PreviewPanel() { const hasRunningDevServer = runningDevServers.length > 0; + // Detect failed dev server process (failed status or completed with non-zero exit code) + const failedDevServerProcess = devServerProcesses.find( + (p) => + p.status === 'failed' || + (p.status === 'completed' && p.exit_code !== null && p.exit_code !== 0n) + ); + const hasFailedDevServer = Boolean(failedDevServerProcess); + useEffect(() => { if ( loadingTimeFinished && @@ -161,6 +172,22 @@ export function PreviewPanel() { }); }; + const handleFixDevScript = () => { + if (!attemptId || repos.length === 0) return; + + const sessionId = devServerProcesses[0]?.session_id; + + ScriptFixerDialog.show({ + scriptType: 'dev_server', + repos, + workspaceId: attemptId, + sessionId, + initialRepoId: repos.length === 1 ? repos[0].id : undefined, + }); + }; + + const canFixDevScript = attemptId && repos.length > 0; + if (!attemptId) { return (
@@ -202,6 +229,8 @@ export function PreviewPanel() { startDevServer={handleStartDevServer} stopDevServer={stopDevServer} project={project} + hasFailedDevServer={hasFailedDevServer} + onFixDevScript={canFixDevScript ? handleFixDevScript : undefined} /> )} @@ -229,16 +258,28 @@ export function PreviewPanel() { . - + {canFixDevScript && ( + )} - {t('preview.noServer.stopAndEditButton')} - +
)} + + {hasFailedDevServer && onFixDevScript && ( + + )}
diff --git a/frontend/src/components/ui-new/NewDisplayConversationEntry.tsx b/frontend/src/components/ui-new/NewDisplayConversationEntry.tsx index e7b84dc1..dcf63e38 100644 --- a/frontend/src/components/ui-new/NewDisplayConversationEntry.tsx +++ b/frontend/src/components/ui-new/NewDisplayConversationEntry.tsx @@ -7,6 +7,7 @@ import { ToolStatus, TodoItem, type TaskWithAttemptStatus, + type RepoWithTargetBranch, } from 'shared/types'; import type { WorkspaceWithSession } from '@/types/attempt'; import { DiffLineType, parseInstance } from '@git-diff-view/react'; @@ -18,7 +19,12 @@ import DisplayConversationEntry from '@/components/NormalizedConversation/Displa import { useMessageEditContext } from '@/contexts/MessageEditContext'; import { useFileNavigation } from '@/contexts/FileNavigationContext'; import { useLogNavigation } from '@/contexts/LogNavigationContext'; +import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; import { cn } from '@/lib/utils'; +import { + ScriptFixerDialog, + type ScriptType, +} from '@/components/dialogs/scripts/ScriptFixerDialog'; import { ChatToolSummary, ChatTodoList, @@ -215,11 +221,13 @@ function renderToolUseEntry( : null; return ( - ); } @@ -628,6 +636,72 @@ function SystemMessageEntry({ ); } +/** + * Script entry with fix button for failed scripts + */ +function ScriptEntryWithFix({ + title, + processId, + exitCode, + status, + workspaceId, + sessionId, +}: { + title: string; + processId: string; + exitCode: number | null; + status: ToolStatus; + workspaceId?: string; + sessionId?: string; +}) { + // Try to get repos from workspace context - may not be available in all contexts + let repos: RepoWithTargetBranch[] = []; + try { + const workspaceContext = useWorkspaceContext(); + repos = workspaceContext.repos; + } catch { + // Context not available, fix button won't be shown + } + + // Use ref to access current repos without causing callback recreation + const reposRef = useRef(repos); + reposRef.current = repos; + + const handleFix = useCallback(() => { + const currentRepos = reposRef.current; + if (!workspaceId || currentRepos.length === 0) return; + + // Determine script type based on title + const scriptType: ScriptType = + title === 'Setup Script' + ? 'setup' + : title === 'Cleanup Script' + ? 'cleanup' + : 'dev_server'; + + ScriptFixerDialog.show({ + scriptType, + repos: currentRepos, + workspaceId, + sessionId, + initialRepoId: currentRepos.length === 1 ? currentRepos[0].id : undefined, + }); + }, [title, workspaceId, sessionId]); + + // Only show fix button if we have the necessary context + const canFix = workspaceId && repos.length > 0; + + return ( + + ); +} + /** * Error message entry with expandable content */ diff --git a/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx b/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx index da6831b4..55c0572a 100644 --- a/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx +++ b/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx @@ -6,6 +6,7 @@ import { useLogStream } from '@/hooks/useLogStream'; import { useLayoutStore } from '@/stores/useLayoutStore'; import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; import { useNavigate } from 'react-router-dom'; +import { ScriptFixerDialog } from '@/components/dialogs/scripts/ScriptFixerDialog'; interface PreviewBrowserContainerProps { attemptId?: string; @@ -20,7 +21,7 @@ export function PreviewBrowserContainer({ const previewRefreshKey = useLayoutStore((s) => s.previewRefreshKey); const { repos } = useWorkspaceContext(); - const { start, isStarting, runningDevServers } = + const { start, isStarting, runningDevServers, devServerProcesses } = usePreviewDevServer(attemptId); const primaryDevServer = runningDevServers[0]; @@ -44,6 +45,21 @@ export function PreviewBrowserContainer({ } }; + const handleFixDevScript = useCallback(() => { + if (!attemptId || repos.length === 0) return; + + // Get session ID from the latest dev server process + const sessionId = devServerProcesses[0]?.session_id; + + ScriptFixerDialog.show({ + scriptType: 'dev_server', + repos, + workspaceId: attemptId, + sessionId, + initialRepoId: repos.length === 1 ? repos[0].id : undefined, + }); + }, [attemptId, repos, devServerProcesses]); + return ( 0} repos={repos} handleEditDevScript={handleEditDevScript} + handleFixDevScript={ + attemptId && repos.length > 0 ? handleFixDevScript : undefined + } className={className} /> ); diff --git a/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx b/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx index 8ceb191e..4259c3e3 100644 --- a/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx +++ b/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx @@ -5,6 +5,7 @@ import { usePreviewUrl } from '../hooks/usePreviewUrl'; import { useLogStream } from '@/hooks/useLogStream'; import { useLayoutStore } from '@/stores/useLayoutStore'; import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; +import { ScriptFixerDialog } from '@/components/dialogs/scripts/ScriptFixerDialog'; interface PreviewControlsContainerProps { attemptId?: string; @@ -88,10 +89,29 @@ export function PreviewControlsContainer({ } }, [urlInfo?.url]); + const handleFixScript = useCallback(() => { + if (!attemptId || repos.length === 0) return; + + // Get session ID from the latest dev server process + const sessionId = devServerProcesses[0]?.session_id; + + ScriptFixerDialog.show({ + scriptType: 'dev_server', + repos, + workspaceId: attemptId, + sessionId, + initialRepoId: repos.length === 1 ? repos[0].id : undefined, + }); + }, [attemptId, repos, devServerProcesses]); + const hasDevScript = repos.some( (repo) => repo.dev_server_script && repo.dev_server_script.trim() !== '' ); + // Only show "Fix Script" button when the latest dev server process failed + const latestDevServerFailed = + devServerProcesses.length > 0 && devServerProcesses[0]?.status === 'failed'; + // Don't render if no repos have dev server scripts configured if (!hasDevScript) { return null; @@ -111,6 +131,11 @@ export function PreviewControlsContainer({ onRefresh={handleRefresh} onCopyUrl={handleCopyUrl} onOpenInNewTab={handleOpenInNewTab} + onFixScript={ + attemptId && repos.length > 0 && latestDevServerFailed + ? handleFixScript + : undefined + } isStarting={isStarting} isStopping={isStopping} isServerRunning={runningDevServers.length > 0} diff --git a/frontend/src/components/ui-new/primitives/conversation/ChatScriptEntry.tsx b/frontend/src/components/ui-new/primitives/conversation/ChatScriptEntry.tsx index 16a6d859..4bfb0488 100644 --- a/frontend/src/components/ui-new/primitives/conversation/ChatScriptEntry.tsx +++ b/frontend/src/components/ui-new/primitives/conversation/ChatScriptEntry.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { TerminalIcon } from '@phosphor-icons/react'; +import { TerminalIcon, WrenchIcon } from '@phosphor-icons/react'; import { cn } from '@/lib/utils'; import { ToolStatus } from 'shared/types'; import { ToolStatusDot } from './ToolStatusDot'; @@ -11,6 +11,7 @@ interface ChatScriptEntryProps { exitCode?: number | null; className?: string; status: ToolStatus; + onFix?: () => void; } export function ChatScriptEntry({ @@ -19,6 +20,7 @@ export function ChatScriptEntry({ exitCode, className, status, + onFix, }: ChatScriptEntryProps) { const { t } = useTranslation('tasks'); const { viewProcessInPanel } = useLogNavigation(); @@ -26,6 +28,11 @@ export function ChatScriptEntry({ const isSuccess = status.status === 'success'; const isFailed = status.status === 'failed'; + const handleFixClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onFix?.(); + }; + const handleClick = () => { viewProcessInPanel(processId); }; @@ -66,10 +73,21 @@ export function ChatScriptEntry({ className="absolute -bottom-0.5 -left-0.5" /> -
+
{title} {getSubtitle()}
+ {isFailed && onFix && ( + + )}
); } diff --git a/frontend/src/components/ui-new/views/PreviewBrowser.tsx b/frontend/src/components/ui-new/views/PreviewBrowser.tsx index c2101fdc..ce005f67 100644 --- a/frontend/src/components/ui-new/views/PreviewBrowser.tsx +++ b/frontend/src/components/ui-new/views/PreviewBrowser.tsx @@ -1,4 +1,4 @@ -import { PlayIcon, SpinnerIcon } from '@phosphor-icons/react'; +import { PlayIcon, SpinnerIcon, WrenchIcon } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; import { PrimaryButton } from '../primitives/PrimaryButton'; @@ -11,6 +11,7 @@ interface PreviewBrowserProps { isServerRunning: boolean; repos: Repo[]; handleEditDevScript: () => void; + handleFixDevScript?: () => void; className?: string; } @@ -21,6 +22,7 @@ export function PreviewBrowser({ isServerRunning, repos, handleEditDevScript, + handleFixDevScript, className, }: PreviewBrowserProps) { const { t } = useTranslation(['tasks', 'common']); @@ -62,12 +64,22 @@ export function PreviewBrowser({ ) : hasDevScript ? ( <>

{t('preview.noServer.title')}

- +
+ + {handleFixDevScript && ( + + )} +
) : (
diff --git a/frontend/src/components/ui-new/views/PreviewControls.tsx b/frontend/src/components/ui-new/views/PreviewControls.tsx index f28963b8..31520cd0 100644 --- a/frontend/src/components/ui-new/views/PreviewControls.tsx +++ b/frontend/src/components/ui-new/views/PreviewControls.tsx @@ -5,6 +5,7 @@ import { ArrowClockwiseIcon, SpinnerIcon, CopyIcon, + WrenchIcon, } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; @@ -30,6 +31,7 @@ interface PreviewControlsProps { onRefresh: () => void; onCopyUrl: () => void; onOpenInNewTab: () => void; + onFixScript?: () => void; isStarting: boolean; isStopping: boolean; isServerRunning: boolean; @@ -49,6 +51,7 @@ export function PreviewControls({ onRefresh, onCopyUrl, onOpenInNewTab, + onFixScript, isStarting, isStopping, isServerRunning, @@ -118,6 +121,14 @@ export function PreviewControls({ disabled={isStarting} /> )} + {onFixScript && ( + + )}
diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 518ab60e..6349d00e 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -49,7 +49,8 @@ "ranCommand": "Ran command", "createdTask": "Created task: {{description}}", "todoOperation": "{{operation}} todos" - } + }, + "fixScript": "Fix Script" }, "folderPicker": { "legend": "Click folder names to navigate • Use action buttons to select", diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json index 73c2679f..82901c34 100644 --- a/frontend/src/i18n/locales/en/tasks.json +++ b/frontend/src/i18n/locales/en/tasks.json @@ -79,7 +79,8 @@ "item2": "Did your dev server print the URL and port to the terminal in the format", "item2Suffix": "? (this is how we know it's running)", "item3": "Have you installed the Web Companion (required for click-to-edit)? If not, please", - "item3Link": "follow the installation instructions here" + "item3Link": "follow the installation instructions here", + "fixScript": "Fix Dev Script" }, "noServer": { "title": "No dev server running", @@ -89,7 +90,8 @@ "companionLink": "View installation guide", "startButton": "Start Dev Server", "configureButton": "Configure", - "stopAndEditButton": "Stop Dev Server & Resolve Issues" + "stopAndEditButton": "Stop Dev Server & Resolve Issues", + "fixScript": "Fix Dev Script" }, "devScript": { "saveAndStart": "Save & Start", @@ -655,5 +657,24 @@ "buttons": { "retry": "Retry" } + }, + "scriptFixer": { + "title": "Fix Script", + "setupScriptTitle": "Fix Setup Script", + "cleanupScriptTitle": "Fix Cleanup Script", + "devServerTitle": "Fix Dev Server Script", + "scriptLabel": "Script (edit)", + "logsLabel": "Last Execution Logs", + "saveButton": "Save", + "saveAndTestButton": "Save and Test", + "saving": "Saving...", + "testing": "Testing...", + "noLogs": "No execution logs available", + "selectRepo": "Repository", + "fixScript": "Fix Script", + "statusRunning": "Running...", + "statusSuccess": "Completed successfully", + "statusFailed": "Failed with exit code {{exitCode}}", + "statusKilled": "Process was killed" } } diff --git a/frontend/src/i18n/locales/es/common.json b/frontend/src/i18n/locales/es/common.json index 4c447dcc..c559a9b9 100644 --- a/frontend/src/i18n/locales/es/common.json +++ b/frontend/src/i18n/locales/es/common.json @@ -60,7 +60,8 @@ "ranCommand": "Ejecutó comando", "createdTask": "Creó tarea: {{description}}", "todoOperation": "{{operation}} tareas pendientes" - } + }, + "fixScript": "Corregir Script" }, "language": { "browserDefault": "Predeterminado del navegador" diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json index 2087a854..3036b037 100644 --- a/frontend/src/i18n/locales/es/tasks.json +++ b/frontend/src/i18n/locales/es/tasks.json @@ -360,7 +360,8 @@ "startButton": "Iniciar Servidor de Desarrollo", "startPrompt": "Por favor inicia un servidor de desarrollo para ver la vista previa", "stopAndEditButton": "Detener Servidor de Desarrollo y Resolver Problemas", - "title": "No hay servidor de desarrollo en ejecución" + "title": "No hay servidor de desarrollo en ejecución", + "fixScript": "Corregir Script de Desarrollo" }, "selectAttempt": "Select an attempt to see preview", "title": "Preview", @@ -378,7 +379,8 @@ "item2Suffix": "? (así es como sabemos que está funcionando)", "item3": "¿Has instalado el Web Companion (requerido para hacer clic y editar)? Si no, por favor", "item3Link": "sigue las instrucciones de instalación aquí", - "title": "Tenemos problemas al previsualizar tu aplicación:" + "title": "Tenemos problemas al previsualizar tu aplicación:", + "fixScript": "Corregir Script de Desarrollo" } }, "processes": { @@ -655,5 +657,24 @@ "deleted": "Eliminado", "renamed": "Renombrado" } + }, + "scriptFixer": { + "title": "Corregir Script", + "setupScriptTitle": "Corregir Script de Configuración", + "cleanupScriptTitle": "Corregir Script de Limpieza", + "devServerTitle": "Corregir Script del Servidor de Desarrollo", + "scriptLabel": "Script (editar)", + "logsLabel": "Últimos Registros de Ejecución", + "saveButton": "Guardar", + "saveAndTestButton": "Guardar y Probar", + "saving": "Guardando...", + "testing": "Probando...", + "noLogs": "No hay registros de ejecución disponibles", + "selectRepo": "Repositorio", + "fixScript": "Corregir Script", + "statusRunning": "Ejecutando...", + "statusSuccess": "Completado exitosamente", + "statusFailed": "Falló con código de salida {{exitCode}}", + "statusKilled": "El proceso fue terminado" } } diff --git a/frontend/src/i18n/locales/ja/common.json b/frontend/src/i18n/locales/ja/common.json index 86cad25b..3932d811 100644 --- a/frontend/src/i18n/locales/ja/common.json +++ b/frontend/src/i18n/locales/ja/common.json @@ -60,7 +60,8 @@ "ranCommand": "コマンドを実行", "createdTask": "タスクを作成: {{description}}", "todoOperation": "{{operation}} Todo" - } + }, + "fixScript": "スクリプトを修正" }, "language": { "browserDefault": "ブラウザ設定" diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json index c0b0d133..4cd20ccc 100644 --- a/frontend/src/i18n/locales/ja/tasks.json +++ b/frontend/src/i18n/locales/ja/tasks.json @@ -360,7 +360,8 @@ "startButton": "開発サーバーを開始", "startPrompt": "プレビューを表示するには開発サーバーを起動してください", "stopAndEditButton": "開発サーバーを停止して問題を解決", - "title": "開発サーバーが実行されていません" + "title": "開発サーバーが実行されていません", + "fixScript": "開発スクリプトを修正" }, "selectAttempt": "Select an attempt to see preview", "title": "Preview", @@ -378,7 +379,8 @@ "item2Suffix": "?(これにより実行中であることを認識します)", "item3": "Web Companion(クリックして編集機能に必要)をインストールしましたか?インストールしていない場合は、", "item3Link": "こちらのインストール手順に従ってください", - "title": "アプリケーションのプレビューに問題があります:" + "title": "アプリケーションのプレビューに問題があります:", + "fixScript": "開発スクリプトを修正" } }, "processes": { @@ -655,5 +657,24 @@ "deleted": "削除済み", "renamed": "名前変更済み" } + }, + "scriptFixer": { + "title": "スクリプトを修正", + "setupScriptTitle": "セットアップスクリプトを修正", + "cleanupScriptTitle": "クリーンアップスクリプトを修正", + "devServerTitle": "開発サーバースクリプトを修正", + "scriptLabel": "スクリプト(編集)", + "logsLabel": "最後の実行ログ", + "saveButton": "保存", + "saveAndTestButton": "保存してテスト", + "saving": "保存中...", + "testing": "テスト中...", + "noLogs": "利用可能な実行ログがありません", + "selectRepo": "リポジトリ", + "fixScript": "スクリプトを修正", + "statusRunning": "実行中...", + "statusSuccess": "正常に完了しました", + "statusFailed": "終了コード {{exitCode}} で失敗しました", + "statusKilled": "プロセスが強制終了されました" } } diff --git a/frontend/src/i18n/locales/ko/common.json b/frontend/src/i18n/locales/ko/common.json index d881ef79..18218b40 100644 --- a/frontend/src/i18n/locales/ko/common.json +++ b/frontend/src/i18n/locales/ko/common.json @@ -60,7 +60,8 @@ "ranCommand": "명령 실행", "createdTask": "작업 생성: {{description}}", "todoOperation": "{{operation}} 할 일" - } + }, + "fixScript": "스크립트 수정" }, "language": { "browserDefault": "브라우저 기본값" diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json index 4f7cd622..a4b192e7 100644 --- a/frontend/src/i18n/locales/ko/tasks.json +++ b/frontend/src/i18n/locales/ko/tasks.json @@ -352,7 +352,8 @@ "startButton": "개발 서버 시작", "startPrompt": "미리보기를 보려면 개발 서버를 시작하세요", "stopAndEditButton": "개발 서버 중지 및 문제 해결", - "title": "실행 중인 개발 서버 없음" + "title": "실행 중인 개발 서버 없음", + "fixScript": "개발 스크립트 수정" }, "selectAttempt": "Select an attempt to see preview", "title": "Preview", @@ -370,7 +371,8 @@ "item2Suffix": "? (이것이 실행 중임을 아는 방법입니다)", "item3": "Web Companion(클릭하여 편집에 필요)을 설치했나요? 설치하지 않았다면", "item3Link": "여기의 설치 지침을 따르세요", - "title": "애플리케이션 미리보기에 문제가 발생했습니다:" + "title": "애플리케이션 미리보기에 문제가 발생했습니다:", + "fixScript": "개발 스크립트 수정" }, "browser": { "title": "개발 서버 미리보기", @@ -655,5 +657,24 @@ "deleted": "삭제됨", "renamed": "이름 변경됨" } + }, + "scriptFixer": { + "title": "스크립트 수정", + "setupScriptTitle": "설정 스크립트 수정", + "cleanupScriptTitle": "정리 스크립트 수정", + "devServerTitle": "개발 서버 스크립트 수정", + "scriptLabel": "스크립트 (편집)", + "logsLabel": "마지막 실행 로그", + "saveButton": "저장", + "saveAndTestButton": "저장 및 테스트", + "saving": "저장 중...", + "testing": "테스트 중...", + "noLogs": "실행 로그가 없습니다", + "selectRepo": "저장소", + "fixScript": "스크립트 수정", + "statusRunning": "실행 중...", + "statusSuccess": "성공적으로 완료됨", + "statusFailed": "종료 코드 {{exitCode}}(으)로 실패함", + "statusKilled": "프로세스가 종료되었습니다" } } diff --git a/frontend/src/i18n/locales/zh-Hans/common.json b/frontend/src/i18n/locales/zh-Hans/common.json index 824c1979..f77a740a 100644 --- a/frontend/src/i18n/locales/zh-Hans/common.json +++ b/frontend/src/i18n/locales/zh-Hans/common.json @@ -49,7 +49,8 @@ "ranCommand": "执行命令", "createdTask": "创建任务:{{description}}", "todoOperation": "{{operation}} 待办事项" - } + }, + "fixScript": "修复脚本" }, "folderPicker": { "legend": "点击文件夹名称进行导航 • 使用操作按钮进行选择", diff --git a/frontend/src/i18n/locales/zh-Hans/tasks.json b/frontend/src/i18n/locales/zh-Hans/tasks.json index dd63d9fd..c6416487 100644 --- a/frontend/src/i18n/locales/zh-Hans/tasks.json +++ b/frontend/src/i18n/locales/zh-Hans/tasks.json @@ -92,7 +92,8 @@ "item2": "您的开发服务器是否以格式打印 URL 和端口到终端", "item2Suffix": "?(这就是我们如何知道它正在运行)", "item3": "您是否安装了 Web Companion(点击编辑所需)?如果没有,请", - "item3Link": "按照此处的安装说明操作" + "item3Link": "按照此处的安装说明操作", + "fixScript": "修复开发脚本" }, "noServer": { "title": "没有运行开发服务器", @@ -102,7 +103,8 @@ "companionLink": "查看安装指南", "startButton": "启动开发服务器", "configureButton": "配置", - "stopAndEditButton": "停止开发服务器并解决问题" + "stopAndEditButton": "停止开发服务器并解决问题", + "fixScript": "修复开发脚本" }, "devScript": { "saveAndStart": "保存并启动", @@ -655,5 +657,24 @@ "updatedTodos": "更新的待办事项", "viewInChangesPanel": "在更改面板中查看", "unableToRenderDiff": "无法显示差异。" + }, + "scriptFixer": { + "title": "修复脚本", + "setupScriptTitle": "修复设置脚本", + "cleanupScriptTitle": "修复清理脚本", + "devServerTitle": "修复开发服务器脚本", + "scriptLabel": "脚本(编辑)", + "logsLabel": "上次执行日志", + "saveButton": "保存", + "saveAndTestButton": "保存并测试", + "saving": "保存中...", + "testing": "测试中...", + "noLogs": "没有可用的执行日志", + "selectRepo": "仓库", + "fixScript": "修复脚本", + "statusRunning": "运行中...", + "statusSuccess": "成功完成", + "statusFailed": "失败,退出代码 {{exitCode}}", + "statusKilled": "进程已被终止" } } diff --git a/frontend/src/i18n/locales/zh-Hant/common.json b/frontend/src/i18n/locales/zh-Hant/common.json index ea1136c4..4a2eaadf 100644 --- a/frontend/src/i18n/locales/zh-Hant/common.json +++ b/frontend/src/i18n/locales/zh-Hant/common.json @@ -49,7 +49,8 @@ "ranCommand": "執行命令", "createdTask": "建立任務:{{description}}", "todoOperation": "{{operation}} 待辦事項" - } + }, + "fixScript": "修復腳本" }, "folderPicker": { "legend": "點擊資料夾名稱進行導覽 • 使用操作按鈕進行選擇", diff --git a/frontend/src/i18n/locales/zh-Hant/tasks.json b/frontend/src/i18n/locales/zh-Hant/tasks.json index 5f14612b..eeeace27 100644 --- a/frontend/src/i18n/locales/zh-Hant/tasks.json +++ b/frontend/src/i18n/locales/zh-Hant/tasks.json @@ -92,7 +92,8 @@ "item2": "您的開發伺服器是否以格式將 URL 與連接埠輸出到終端機", "item2Suffix": "?(這是我們判斷它是否在執行的方式)", "item3": "您是否安裝了 Web Companion(點擊編輯所需)?如果沒有,請", - "item3Link": "依照此處的安裝說明操作" + "item3Link": "依照此處的安裝說明操作", + "fixScript": "修復開發腳本" }, "noServer": { "title": "沒有執行中的開發伺服器", @@ -102,7 +103,8 @@ "companionLink": "查看安裝指南", "startButton": "啟動開發伺服器", "configureButton": "設定", - "stopAndEditButton": "停止開發伺服器並解決問題" + "stopAndEditButton": "停止開發伺服器並解決問題", + "fixScript": "修復開發腳本" }, "devScript": { "saveAndStart": "儲存並啟動", @@ -655,5 +657,24 @@ "deleted": "已刪除", "renamed": "已重新命名" } + }, + "scriptFixer": { + "title": "修復腳本", + "setupScriptTitle": "修復設定腳本", + "cleanupScriptTitle": "修復清理腳本", + "devServerTitle": "修復開發伺服器腳本", + "scriptLabel": "腳本(編輯)", + "logsLabel": "上次執行日誌", + "saveButton": "儲存", + "saveAndTestButton": "儲存並測試", + "saving": "儲存中...", + "testing": "測試中...", + "noLogs": "沒有可用的執行日誌", + "selectRepo": "儲存庫", + "fixScript": "修復腳本", + "statusRunning": "執行中...", + "statusSuccess": "成功完成", + "statusFailed": "失敗,結束代碼 {{exitCode}}", + "statusKilled": "程序已被終止" } }