Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLIFrameElement | null>(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);
Expand All @@ -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 <html lang="…"> in sync with the user's locale so screen readers
// and browser features (autotranslate, hyphenation) pick the right language.
useEffect(() => {
Expand Down
36 changes: 24 additions & 12 deletions src/components/convert-chip.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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 (
<div
Expand All @@ -63,14 +78,10 @@ export function ConvertChip() {
aria-label={t("convertChip.label")}
className="pointer-events-auto group relative flex items-center gap-2 rounded-full px-4 py-2.5 text-[12.5px] font-medium shadow-lg transition-all hover:scale-[1.02] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-40"
style={{
background: isRunning
? "var(--coral)"
: canConvert
? "var(--ink)"
: "var(--ink-mute)",
background: isRunning ? "var(--coral-hover)" : "var(--coral)",
color: "#fff",
border: "1px solid rgba(255,255,255,0.18)",
boxShadow: "0 4px 18px rgba(15, 14, 12, 0.18)",
boxShadow: "0 4px 18px rgba(201, 100, 66, 0.32)",
}}
>
{isRunning ? (
Expand All @@ -82,6 +93,7 @@ export function ConvertChip() {
<>
<span aria-hidden>⚡</span>
{t("convertChip.label")}
<span className="hidden text-[10.5px] opacity-70 sm:inline">⌘↵</span>
</>
)}
</button>
Expand Down
53 changes: 1 addition & 52 deletions src/components/toolbar.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
<header
Expand Down Expand Up @@ -103,35 +81,6 @@ export function Toolbar({
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
</button>
{status === "running" ? (
<button
onClick={() => cancel(activeTaskId)}
className="btn-ghost"
style={{ borderColor: "var(--coral)", color: "var(--coral)" }}
>
{t("toolbar.stop")}
</button>
) : (
<button
onClick={() =>
agent && run({ taskId: activeTaskId, agent, templateId: template, content, format, model })
}
disabled={!canConvert}
className="btn-primary"
title={
!agent
? t("toolbar.firstSelectAgent")
: agentInfo?.unsupported
? t("toolbar.unsupportedProtocol")
: !content.trim()
? t("toolbar.enterContent")
: t("toolbar.shortcutHint")
}
>
{t("toolbar.convert")}
<span className="hidden text-[11px] opacity-70 sm:inline">⌘↵</span>
</button>
)}
<ExportMenu iframeRef={iframeRef} />
</div>
</header>
Expand Down
5 changes: 4 additions & 1 deletion src/lib/agents/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,10 @@ export async function resolveOpenclawAgentId(bin: string): Promise<string> {
try {
const { spawn } = await import("node:child_process");
const out = await new Promise<string>((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));
Expand Down