diff --git a/src/app/page.tsx b/src/app/page.tsx index fad9574..2dc7b64 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,12 +8,13 @@ import { TasksSidebar } from "@/components/tasks-sidebar"; import { WelcomeModal } from "@/components/welcome-modal"; import { SettingsModal } from "@/components/settings-modal"; import { ConvertChip } from "@/components/convert-chip"; -import { useStore } from "@/lib/store"; +import { useStore, type AgentInfo } from "@/lib/store"; export default function Home() { const iframeRef = useRef(null); const welcomeAck = useStore((s) => s.welcomeAck); const selectedAgent = useStore((s) => s.selectedAgent); + const setAgents = useStore((s) => s.setAgents); const locale = useStore((s) => s.locale); const layoutMode = useStore((s) => s.layoutMode); const [welcomeOpen, setWelcomeOpen] = useState(false); @@ -24,6 +25,29 @@ export default function Home() { setHydrated(true); }, []); + // Detect agents on mount so the toolbar's agent chip can resolve the + // persisted `selectedAgent` to a label without waiting for the user to + // open Settings or Welcome. Without this, after a hard reload the chip + // briefly (or permanently) shows "Select agent" even though selection + // is intact in localStorage. + useEffect(() => { + if (!hydrated) return; + let cancelled = false; + (async () => { + try { + const res = await fetch("/api/agents", { cache: "no-store" }); + if (!res.ok) return; + const data = (await res.json()) as { agents: AgentInfo[] }; + if (!cancelled) setAgents(data.agents); + } catch { + // Settings / Welcome modals will retry on open. + } + })(); + return () => { + cancelled = true; + }; + }, [hydrated, setAgents]); + // Keep in sync with the user's locale so screen readers // and browser features (autotranslate, hyphenation) pick the right language. useEffect(() => { diff --git a/src/components/convert-chip.tsx b/src/components/convert-chip.tsx index fd68795..14a5703 100644 --- a/src/components/convert-chip.tsx +++ b/src/components/convert-chip.tsx @@ -1,14 +1,14 @@ "use client"; +import { useCallback, useEffect } from "react"; import { useStore, selectActiveTask } from "@/lib/store"; import { useT } from "@/lib/i18n"; import { useConvert } from "@/lib/use-convert"; /** - * Floating chip pinned to the editor / preview divider that runs the same - * Convert action as the toolbar button. Mirrors `Toolbar`'s logic but stays - * close to the editor so users notice it after editing without traveling - * back up to the top bar. Toolbar's own button (and ⌘+Enter shortcut) stay. + * Floating chip pinned to the editor / preview divider — the single Convert + * entry point. The toolbar no longer has its own Convert button (removed to + * avoid duplication), so this chip also owns the ⌘/Ctrl+Enter shortcut. */ export function ConvertChip() { const agent = useStore((s) => s.selectedAgent); @@ -42,14 +42,29 @@ export function ConvertChip() { ? t("toolbar.enterContent") : t("convertChip.tooltip"); - const onClick = () => { + const onClick = useCallback(() => { if (isRunning) { cancel(activeTaskId); return; } if (!canConvert) return; run({ taskId: activeTaskId, agent: agent!, templateId: template, content, format, model }); - }; + }, [isRunning, canConvert, cancel, run, activeTaskId, agent, template, content, format, model]); + + // ⌘/Ctrl + Enter — global shortcut, fires Convert from anywhere on the page. + // Lives here (not in Toolbar) because the chip is the single source of + // Convert truth after the toolbar button was removed. + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { + if (isRunning || !canConvert) return; + e.preventDefault(); + onClick(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClick, isRunning, canConvert]); return (
{isRunning ? ( @@ -82,6 +93,7 @@ export function ConvertChip() { <> {t("convertChip.label")} + ⌘↵ )} diff --git a/src/components/toolbar.tsx b/src/components/toolbar.tsx index 605967b..c2f4c2b 100644 --- a/src/components/toolbar.tsx +++ b/src/components/toolbar.tsx @@ -1,9 +1,7 @@ "use client"; -import { useEffect } from "react"; -import { useStore, selectActiveTask } from "@/lib/store"; +import { useStore } from "@/lib/store"; import { useT } from "@/lib/i18n"; -import { useConvert } from "@/lib/use-convert"; import { TemplatePicker } from "./template-picker"; import { ExportMenu } from "./export-menu"; import { LayoutModeToggle } from "./layout-mode-toggle"; @@ -20,30 +18,10 @@ export function Toolbar({ const agent = useStore((s) => s.selectedAgent); const agents = useStore((s) => s.agents); const agentModels = useStore((s) => s.agentModels); - const activeTaskId = useStore((s) => s.activeTaskId); - const template = useStore((s) => selectActiveTask(s)?.templateId ?? "article-magazine"); - const content = useStore((s) => selectActiveTask(s)?.content ?? ""); - const format = useStore((s) => selectActiveTask(s)?.format ?? "text"); - const status = useStore((s) => selectActiveTask(s)?.status ?? "idle"); - const { run, cancel } = useConvert(); const t = useT(); const agentInfo = agents.find((a) => a.id === agent); const model = agent ? agentModels[agent] ?? "default" : "default"; - const canConvert = - !!agent && !!content.trim() && status !== "running" && !agentInfo?.unsupported; - - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { - e.preventDefault(); - if (canConvert) - run({ taskId: activeTaskId, agent: agent!, templateId: template, content, format, model }); - } - }; - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [canConvert, agent, template, content, format, model, run, activeTaskId]); return (
- {status === "running" ? ( - - ) : ( - - )}
diff --git a/src/lib/agents/detect.ts b/src/lib/agents/detect.ts index 8e4fd77..c72c8db 100644 --- a/src/lib/agents/detect.ts +++ b/src/lib/agents/detect.ts @@ -360,7 +360,10 @@ export async function resolveOpenclawAgentId(bin: string): Promise { try { const { spawn } = await import("node:child_process"); const out = await new Promise((res, rej) => { - const child = spawn(bin, ["agents", "list"], { stdio: ["ignore", "pipe", "pipe"] }); + const child = spawn(bin, ["agents", "list"], { + stdio: ["ignore", "pipe", "pipe"], + shell: process.platform === "win32", + }); let buf = ""; child.stdout.setEncoding("utf8"); child.stdout.on("data", (c) => (buf += c));