From 722437617096fc698858526af52db3ee9c9c045e Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Mon, 5 Jan 2026 18:56:04 +0000 Subject: [PATCH] feat: override auto-detected preview url (#1573) * override auto-detected preview url * i18n * remove redundant useMemo --- .../src/components/panels/PreviewPanel.tsx | 25 +++-- .../TaskDetails/preview/PreviewToolbar.tsx | 98 +++++++++++++++++-- frontend/src/i18n/locales/en/tasks.json | 4 +- frontend/src/i18n/locales/es/tasks.json | 4 +- frontend/src/i18n/locales/ja/tasks.json | 4 +- frontend/src/i18n/locales/ko/tasks.json | 4 +- frontend/src/i18n/locales/zh-Hans/tasks.json | 4 +- 7 files changed, 121 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/panels/PreviewPanel.tsx b/frontend/src/components/panels/PreviewPanel.tsx index e7e34561..07c45992 100644 --- a/frontend/src/components/panels/PreviewPanel.tsx +++ b/frontend/src/components/panels/PreviewPanel.tsx @@ -23,6 +23,7 @@ export function PreviewPanel() { const [showHelp, setShowHelp] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const [showLogs, setShowLogs] = useState(false); + const [customUrl, setCustomUrl] = useState(null); const listenerRef = useRef(null); const { t } = useTranslation('tasks'); @@ -51,6 +52,9 @@ export function PreviewPanel() { lastKnownUrl, }); + // Compute effective URL - custom URL overrides auto-detected + const effectiveUrl = customUrl ?? previewState.url; + const handleRefresh = () => { setIframeError(false); setRefreshKey((prev) => prev + 1); @@ -62,8 +66,8 @@ export function PreviewPanel() { const { addElement } = useClickedElements(); const handleCopyUrl = async () => { - if (previewState.url) { - await navigator.clipboard.writeText(previewState.url); + if (effectiveUrl) { + await navigator.clipboard.writeText(effectiveUrl); } }; @@ -117,12 +121,12 @@ export function PreviewPanel() { }, [loadingTimeFinished, isReady, latestDevServerProcess, runningDevServer]); const isPreviewReady = - previewState.status === 'ready' && - Boolean(previewState.url) && - !iframeError; + (previewState.status === 'ready' && Boolean(previewState.url)) || + (customUrl !== null && runningDevServer); + const isPreviewReadyWithoutError = isPreviewReady && !iframeError; const mode = iframeError ? 'error' - : isPreviewReady + : isPreviewReadyWithoutError ? 'ready' : runningDevServer ? 'searching' @@ -165,15 +169,18 @@ export function PreviewPanel() { <> diff --git a/frontend/src/components/tasks/TaskDetails/preview/PreviewToolbar.tsx b/frontend/src/components/tasks/TaskDetails/preview/PreviewToolbar.tsx index 576b6603..df59311b 100644 --- a/frontend/src/components/tasks/TaskDetails/preview/PreviewToolbar.tsx +++ b/frontend/src/components/tasks/TaskDetails/preview/PreviewToolbar.tsx @@ -1,4 +1,5 @@ -import { ExternalLink, RefreshCw, Copy, Loader2, Pause } from 'lucide-react'; +import { useState, useRef, useEffect } from 'react'; +import { ExternalLink, RefreshCw, Copy, Loader2, Pause, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { @@ -8,6 +9,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { NewCardHeader } from '@/components/ui/new-card'; +import { Input } from '@/components/ui/input'; interface PreviewToolbarProps { mode: 'noServer' | 'error' | 'ready'; @@ -16,6 +18,9 @@ interface PreviewToolbarProps { onCopyUrl: () => void; onStop: () => void; isStopping?: boolean; + customUrl: string | null; + detectedUrl: string | undefined; + onUrlChange: (url: string | null) => void; } export function PreviewToolbar({ @@ -25,8 +30,49 @@ export function PreviewToolbar({ onCopyUrl, onStop, isStopping, + customUrl, + detectedUrl, + onUrlChange, }: PreviewToolbarProps) { const { t } = useTranslation('tasks'); + const [isEditing, setIsEditing] = useState(false); + const [urlInput, setUrlInput] = useState(''); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [isEditing]); + + const handleStartEdit = () => { + setUrlInput(url ?? ''); + setIsEditing(true); + }; + + const handleSubmit = () => { + const trimmed = urlInput.trim(); + if (!trimmed || trimmed === detectedUrl) { + // Empty input or detected URL: reset to detected + onUrlChange(null); + } else { + onUrlChange(trimmed); + } + setIsEditing(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSubmit(); + } else if (e.key === 'Escape') { + setIsEditing(false); + } + }; + + const handleClearCustomUrl = () => { + onUrlChange(null); + }; const actions = mode !== 'noServer' ? ( @@ -119,13 +165,49 @@ export function PreviewToolbar({ return ( -
- - {url || } - +
+ {isEditing ? ( + setUrlInput(e.target.value)} + onBlur={handleSubmit} + onKeyDown={handleKeyDown} + className="h-7 text-sm font-mono flex-1" + placeholder="http://localhost:3000" + /> + ) : ( + <> + + {customUrl !== null && ( + + + + + + + {t('preview.toolbar.resetUrl')} + + + + )} + + )}
); diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json index 651e3782..7bac2b3d 100644 --- a/frontend/src/i18n/locales/en/tasks.json +++ b/frontend/src/i18n/locales/en/tasks.json @@ -102,7 +102,9 @@ "refresh": "Refresh preview", "copyUrl": "Copy URL", "openInTab": "Open in new tab", - "stopDevServer": "Stop dev server" + "stopDevServer": "Stop dev server", + "clickToEdit": "Click to edit URL", + "resetUrl": "Reset to detected URL" } }, "diff": { diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json index 6b29ab67..610eebb2 100644 --- a/frontend/src/i18n/locales/es/tasks.json +++ b/frontend/src/i18n/locales/es/tasks.json @@ -312,7 +312,9 @@ "copyUrl": "Copiar URL", "openInTab": "Abrir en nueva pestaña", "refresh": "Actualizar vista previa", - "stopDevServer": "Detener servidor de desarrollo" + "stopDevServer": "Detener servidor de desarrollo", + "clickToEdit": "Clic para editar URL", + "resetUrl": "Restablecer a URL detectada" }, "troubleAlert": { "item1": "¿Se inició correctamente el servidor de desarrollo? Puede haber un error que necesites resolver, o quizás sea necesario instalar dependencias.", diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json index 28ac7196..bcfafd5f 100644 --- a/frontend/src/i18n/locales/ja/tasks.json +++ b/frontend/src/i18n/locales/ja/tasks.json @@ -312,7 +312,9 @@ "copyUrl": "URLをコピー", "openInTab": "新しいタブで開く", "refresh": "プレビューを更新", - "stopDevServer": "開発サーバーを停止" + "stopDevServer": "開発サーバーを停止", + "clickToEdit": "クリックしてURLを編集", + "resetUrl": "検出されたURLにリセット" }, "troubleAlert": { "item1": "開発サーバーが正常に起動しましたか?解決すべきバグがあるか、依存関係のインストールが必要な可能性があります。", diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json index c77029af..c2107836 100644 --- a/frontend/src/i18n/locales/ko/tasks.json +++ b/frontend/src/i18n/locales/ko/tasks.json @@ -312,7 +312,9 @@ "copyUrl": "URL 복사", "openInTab": "새 탭에서 열기", "refresh": "미리보기 새로고침", - "stopDevServer": "개발 서버 중지" + "stopDevServer": "개발 서버 중지", + "clickToEdit": "URL 편집하려면 클릭", + "resetUrl": "감지된 URL로 재설정" }, "troubleAlert": { "item1": "개발 서버가 성공적으로 시작되었나요? 해결해야 할 버그가 있거나 종속성을 설치해야 할 수 있습니다.", diff --git a/frontend/src/i18n/locales/zh-Hans/tasks.json b/frontend/src/i18n/locales/zh-Hans/tasks.json index 3df16fde..4be3833b 100644 --- a/frontend/src/i18n/locales/zh-Hans/tasks.json +++ b/frontend/src/i18n/locales/zh-Hans/tasks.json @@ -115,7 +115,9 @@ "refresh": "刷新预览", "copyUrl": "复制 URL", "openInTab": "在新标签页中打开", - "stopDevServer": "停止开发服务器" + "stopDevServer": "停止开发服务器", + "clickToEdit": "点击编辑 URL", + "resetUrl": "重置为检测到的 URL" } }, "diff": {