From 721477c6f23984c0fb6fea6599000e1fdc5ce90a Mon Sep 17 00:00:00 2001 From: Yash Dewasthale <72986960+yashdev9274@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:56:05 +0530 Subject: [PATCH 1/2] feat(terminal): detect and open URLs from terminal output - Added functionality to extract URLs from terminal output during the 'run' phase. - Implemented a dropdown to select and open detected URLs in the browser. - Introduced a function to standardize URL formats. - Updated electron API types to include for URL handling. --- src/renderer/components/TaskTerminalPanel.tsx | 86 ++++++++++++++++++- src/renderer/types/electron-api.d.ts | 8 ++ src/shared/urls.ts | 12 +++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/TaskTerminalPanel.tsx b/src/renderer/components/TaskTerminalPanel.tsx index d8f6b3219..21ec184fe 100644 --- a/src/renderer/components/TaskTerminalPanel.tsx +++ b/src/renderer/components/TaskTerminalPanel.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import { TerminalPane } from './TerminalPane'; -import { Plus, Play, RotateCw, Square, X } from 'lucide-react'; +import { Plus, Play, RotateCw, Square, X, ExternalLink, Globe } from 'lucide-react'; import { useTheme } from '../hooks/useTheme'; import { useTaskTerminals } from '@/lib/taskTerminalsStore'; import { useTerminalSelection } from '../hooks/useTerminalSelection'; @@ -28,6 +28,7 @@ import { formatLifecycleLogLine, } from '@shared/lifecycle'; import { shouldDisablePlay } from '../lib/lifecycleUi'; +import { normalizeUrl } from '@shared/urls'; interface Task { id: string; @@ -64,6 +65,9 @@ const TaskTerminalPanelComponent: React.FC = ({ const { effectiveTheme } = useTheme(); const { toast } = useToast(); + const [detectedUrls, setDetectedUrls] = useState([]); + const [selectedUrl, setSelectedUrl] = useState(null); + // Use path in the key to differentiate multi-agent variants that share the same task.id const taskKey = task ? `${task.id}::${task.path}` : 'task-placeholder'; const taskTerminals = useTaskTerminals(taskKey, task?.path); @@ -100,6 +104,40 @@ const TaskTerminalPanelComponent: React.FC = ({ return () => clearTimeout(timer); }, [selection.activeTerminalId]); + useEffect(() => { + // Reset URLs when task changes + setDetectedUrls([]); + setSelectedUrl(null); + + if (!task?.id) return; + + const off = window.electronAPI?.onLifecycleEvent?.((data: any) => { + if (data?.taskId !== task.id) return; + + // Extract URL from run line output + if (data?.phase === 'run' && data?.status === 'line' && data?.line) { + const url = normalizeUrl(data.line); + if (url) { + setDetectedUrls((prev) => { + if (prev.includes(url)) return prev; + setSelectedUrl((current) => current || url); + return [...prev, url]; + }); + } + } + + // Reset URLs when run exits + if (data?.phase === 'run' && (data?.status === 'done' || data?.status === 'error')) { + setDetectedUrls([]); + setSelectedUrl(null); + } + }); + + return () => { + off?.(); + }; + }, [task?.id]); + const [runStatus, setRunStatus] = useState('idle'); const [setupStatus, setSetupStatus] = useState('idle'); const [teardownStatus, setTeardownStatus] = useState('idle'); @@ -637,6 +675,52 @@ const TaskTerminalPanelComponent: React.FC = ({ )} + {task && detectedUrls.length > 0 && ( + + + + {detectedUrls.length > 1 ? ( + + ) : ( + + )} + + +

+ {selectedUrl ? `Open ${selectedUrl} in browser` : 'No URL detected yet'} +

+
+
+
+ )} + {(() => { const canDelete = selection.parsed?.mode === 'task' diff --git a/src/renderer/types/electron-api.d.ts b/src/renderer/types/electron-api.d.ts index 81550d56d..5e160c78a 100644 --- a/src/renderer/types/electron-api.d.ts +++ b/src/renderer/types/electron-api.d.ts @@ -622,6 +622,14 @@ declare global { branches?: Array<{ ref: string; remote: string; branch: string; label: string }>; error?: string; }>; + onHostPreviewEvent: ( + listener: (data: { + type: 'url' | 'setup' | 'exit'; + taskId: string; + url?: string; + status?: string; + }) => void + ) => () => void; openExternal: (url: string) => Promise<{ success: boolean; error?: string }>; clipboardWriteText: (text: string) => Promise<{ success: boolean; error?: string }>; paste: () => Promise<{ success: boolean; error?: string }>; diff --git a/src/shared/urls.ts b/src/shared/urls.ts index c4ac05a4e..b967ceb26 100644 --- a/src/shared/urls.ts +++ b/src/shared/urls.ts @@ -1,2 +1,14 @@ export const EMDASH_RELEASES_URL = 'https://github.com/generalaction/emdash/releases'; export const EMDASH_DOCS_URL = 'https://docs.emdash.sh'; +export function normalizeUrl(u: string): string { + try { + const re = /(https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]):\d{2,5}(?:\/\S*)?)/i; + const m = u.match(re); + if (!m) return ''; + const url = new URL(m[1]); + url.hostname = 'localhost'; + return url.toString(); + } catch { + return ''; + } +} From 930af6db1c60bc734af0c09b8e9a13cd82eaa2da Mon Sep 17 00:00:00 2001 From: Yash Dewasthale <72986960+yashdev9274@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:04:56 +0530 Subject: [PATCH 2/2] fix(terminal): ensure selected URL is set correctly when detecting URLs - Updated logic to set the selected URL when a new URL is detected. - Wrapped URL selection dropdown in a span for better layout control. - Improved URL formatting in the dropdown to handle both HTTP and HTTPS protocols. --- src/renderer/components/TaskTerminalPanel.tsx | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/src/renderer/components/TaskTerminalPanel.tsx b/src/renderer/components/TaskTerminalPanel.tsx index 21ec184fe..28d787aa7 100644 --- a/src/renderer/components/TaskTerminalPanel.tsx +++ b/src/renderer/components/TaskTerminalPanel.tsx @@ -120,9 +120,11 @@ const TaskTerminalPanelComponent: React.FC = ({ if (url) { setDetectedUrls((prev) => { if (prev.includes(url)) return prev; - setSelectedUrl((current) => current || url); return [...prev, url]; }); + if (!selectedUrl) { + setSelectedUrl(url); + } } } @@ -679,38 +681,40 @@ const TaskTerminalPanelComponent: React.FC = ({ - {detectedUrls.length > 1 ? ( - - ) : ( - - )} + + {detectedUrls.length > 1 ? ( + + ) : ( + + )} +