diff --git a/src/components/WindowToolbar.tsx b/src/components/WindowToolbar.tsx index 9ffa13f..aef1e35 100644 --- a/src/components/WindowToolbar.tsx +++ b/src/components/WindowToolbar.tsx @@ -6,6 +6,7 @@ import { Copy, CopyCheck, ArrowRightFromLine, + ExternalLink, } from "lucide-react"; import { useSetAtom } from "jotai"; import { useState } from "react"; @@ -91,6 +92,21 @@ export function WindowToolbar({ setShowSpellDialog(true); }; + const handlePopOut = () => { + if (!window) return; + + // Get command string (existing or reconstructed) + const commandString = window.commandString || reconstructCommand(window); + + // Construct the /run URL with the command as a query parameter + const popOutUrl = `/run?cmd=${encodeURIComponent(commandString)}`; + + // Open in a new window/tab + globalThis.window.open(popOutUrl, "_blank"); + + toast.success("Window popped out"); + }; + // Copy functionality for NIPs const { copy, copied } = useCopy(); const isNipWindow = window?.appId === "nip"; @@ -139,6 +155,18 @@ export function WindowToolbar({ + {/* Pop Out button */} + + + + {/* Copy button for NIPs */} {isNipWindow && ( + * + * Executes a command and displays the result without affecting the workspace layout. + * This allows windows to be "popped out" into separate browser windows/tabs. + */ +export default function RunCommandPage() { + const [searchParams] = useSearchParams(); + const { state } = useGrimoire(); + const [parsed, setParsed] = useState(null); + const [loading, setLoading] = useState(true); + + const cmdParam = searchParams.get("cmd"); + + useEffect(() => { + async function parseCommand() { + if (!cmdParam) { + setParsed({ + commandName: "", + args: [], + fullInput: "", + error: "No command provided", + }); + setLoading(false); + return; + } + + try { + const result = await parseAndExecuteCommand( + cmdParam, + state.activeAccount?.pubkey, + ); + setParsed(result); + } catch (error) { + setParsed({ + commandName: "", + args: [], + fullInput: cmdParam, + error: + error instanceof Error ? error.message : "Failed to parse command", + }); + } finally { + setLoading(false); + } + } + + parseCommand(); + }, [cmdParam, state.activeAccount?.pubkey]); + + if (loading) { + return ( + + Loading command... + + ); + } + + if (!parsed || parsed.error || !parsed.command || !parsed.props) { + return ( + + + + Command Error + + + {parsed?.error || "Unknown error"} + + {cmdParam && ( + + Command: {cmdParam} + + )} + + + ); + } + + const title = + parsed.globalFlags?.windowProps?.title || parsed.command.name.toUpperCase(); + + // Construct a minimal WindowInstance for rendering + const windowInstance = { + id: "pop-out", + appId: parsed.command.appId, + props: parsed.props, + customTitle: parsed.globalFlags?.windowProps?.title, + commandString: parsed.fullInput, + }; + + return ( + + {/* Header */} + + + + {title} + + + {parsed.fullInput} + + + + {/* Content */} + + window.close()} + /> + + + ); +} diff --git a/src/root.tsx b/src/root.tsx index 9aa0cf0..023c315 100644 --- a/src/root.tsx +++ b/src/root.tsx @@ -3,6 +3,7 @@ import { AppShell } from "./components/layouts/AppShell"; import DashboardPage from "./components/pages/DashboardPage"; import SpellbookPage from "./components/pages/SpellbookPage"; import Nip19PreviewRouter from "./components/pages/Nip19PreviewRouter"; +import RunCommandPage from "./components/pages/RunCommandPage"; const router = createBrowserRouter([ { @@ -13,6 +14,10 @@ const router = createBrowserRouter([ ), }, + { + path: "/run", + element: , + }, { path: "/preview/:actor/:identifier", element: (
+ {parsed?.error || "Unknown error"} +
+ Command: {cmdParam} +