From d80fb696edb890fb32e94e6998e8cfb6e3b551f0 Mon Sep 17 00:00:00 2001 From: lngdao Date: Mon, 6 Apr 2026 18:15:18 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20workspace=20enhancements=20=E2=80=94=20?= =?UTF-8?q?onboarding,=20lifecycle,=20chat=20UX=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace Management: - SetupModal for new workspace creation (agent selection + auto-start) - Clone workspace auto-starts server after creation - Context menu: Start/Stop server, Copy path, Reconnect, Remove - Status dot badges persist across workspace switches - invoke_cli includes stdout in error messages Onboarding: - Setup wizard uses invoke_cli for server start (fixes sidecar timeout) - Agent install updates UI state immediately without reload - dev_reset uses find_openacp_binary for reliable binary removal - Startup debug logging Chat & Tool Cards: - Copy button on assistant messages next to usage bar - Tool title rebuilds from tool_update events (fixes Read file name) - Thinking blocks: Markdown rendering with muted style - Thinking blocks split correctly across tool calls - Tool card IN/OUT toggle with animation --- src-tauri/src/onboarding.rs | 20 +-- src/onboarding/setup-wizard.tsx | 9 +- src/openacp/app.tsx | 19 +++ .../add-workspace/create-instance.tsx | 9 +- .../components/add-workspace/index.tsx | 3 +- .../components/add-workspace/setup-modal.tsx | 157 ++++++++++++++++++ src/openacp/main.tsx | 11 +- 7 files changed, 201 insertions(+), 27 deletions(-) create mode 100644 src/openacp/components/add-workspace/setup-modal.tsx diff --git a/src-tauri/src/onboarding.rs b/src-tauri/src/onboarding.rs index 20dba8d..064e3b8 100644 --- a/src-tauri/src/onboarding.rs +++ b/src-tauri/src/onboarding.rs @@ -317,7 +317,7 @@ pub async fn run_openacp_agent_install( /// Used to reset onboarding state during development. #[allow(dead_code)] #[tauri::command] -pub async fn dev_reset_openacp(app: tauri::AppHandle) -> Result<(), String> { +pub async fn dev_reset_openacp(_app: tauri::AppHandle) -> Result<(), String> { // Remove ~/.openacp if let Some(home) = dirs::home_dir() { let openacp_dir = home.join(".openacp"); @@ -326,20 +326,10 @@ pub async fn dev_reset_openacp(app: tauri::AppHandle) -> Result<(), String> { } } - // Remove openacp binary via `which openacp` - let which = app - .shell() - .command("which") - .args(["openacp"]) - .output() - .await - .map_err(|e| e.to_string())?; - - if which.status.success() { - let bin_path = String::from_utf8_lossy(&which.stdout).trim().to_string(); - if !bin_path.is_empty() { - std::fs::remove_file(&bin_path).ok(); // best-effort - } + // Remove openacp binary using same discovery as the rest of the app + if let Some((bin, _)) = find_openacp_binary_pub() { + std::fs::remove_file(&bin).ok(); // best-effort + tracing::info!("dev_reset: removed binary at {}", bin.display()); } Ok(()) diff --git a/src/onboarding/setup-wizard.tsx b/src/onboarding/setup-wizard.tsx index 6b992de..4e550f7 100644 --- a/src/onboarding/setup-wizard.tsx +++ b/src/onboarding/setup-wizard.tsx @@ -44,7 +44,11 @@ export function SetupWizard(props: Props) { const installAgent = async (key: string) => { setInstallingAgent(key); setAgentInstallError(''); setAgentInstallLog([]); const unlisten = await listen('agent-install-output', (event) => setAgentInstallLog((prev) => [...prev, event.payload])); - try { await invoke('run_openacp_agent_install', { agentKey: key }); setSelectedAgent(key); /* refetch would go here */ } catch (err) { setAgentInstallError(`Failed to install ${key}: ${String(err)}`); } finally { setInstallingAgent(''); unlisten(); } + try { + await invoke('run_openacp_agent_install', { agentKey: key }); + setSelectedAgent(key); + setAgents((prev) => prev.map((a) => a.key === key ? { ...a, installed: true } : a)); + } catch (err) { setAgentInstallError(`Failed to install ${key}: ${String(err)}`); } finally { setInstallingAgent(''); unlisten(); } }; const runSetup = async () => { @@ -52,7 +56,8 @@ export function SetupWizard(props: Props) { const unlisten = await listen('setup-output', (event) => setSetupLog((prev) => [...prev, event.payload])); try { const jsonStr = await invoke('run_openacp_setup', { workspace: workspace, agent: selectedAgent }); - setSetupStatus('starting'); await invoke('start_server'); + setSetupStatus('starting'); + await invoke('invoke_cli', { args: ['start', '--global', '--daemon'] }); const parsed = JSON.parse(jsonStr) as { success: boolean; data?: { instanceId?: string; name?: string; directory?: string } }; const data = parsed?.data ?? {}; const entry: WorkspaceEntry = { id: data.instanceId ?? 'main', name: data.name ?? 'Main', directory: data.directory ?? workspace, type: 'local' }; diff --git a/src/openacp/app.tsx b/src/openacp/app.tsx index 8ad5a9b..a2c23bb 100644 --- a/src/openacp/app.tsx +++ b/src/openacp/app.tsx @@ -22,6 +22,7 @@ import { SettingsDialog, type SettingsPage, } from "./components/settings/settings-dialog"; +import { SetupModal } from "./components/add-workspace/setup-modal"; import { showToast } from "./lib/toast"; import { Toaster } from "./components/ui/toaster"; import { @@ -82,6 +83,7 @@ export function OpenACPApp() { const [showSettings, setShowSettings] = useState(false); const [settingsPage, setSettingsPage] = useState("general"); const [pluginsOpen, setPluginsOpen] = useState(false); + const [setupInfo, setSetupInfo] = useState<{ path: string; instanceId: string } | null>(null); const retryRef = useRef>(); const retryCountRef = useRef(0); @@ -501,11 +503,28 @@ export function OpenACPApp() { {showAddWorkspace && ( { + setShowAddWorkspace(false) + setSetupInfo({ path, instanceId }) + }} onClose={closeAddWorkspaceModal} existingIds={workspaces.map((w) => w.id)} defaultTab={addWorkspaceDefaultTab} /> )} + {setupInfo && ( + { + setSetupInfo(null) + addWorkspace(entry) + showToast({ description: `Workspace "${entry.name}" ready.`, variant: "success" }) + }} + onClose={() => setSetupInfo(null)} + /> + )} void; + onSetup?: (path: string, instanceId: string) => void; onClose: () => void; existingIds: string[]; defaultTab?: "local" | "remote"; @@ -64,7 +65,7 @@ export function AddWorkspaceModal(props: AddWorkspaceModalProps) { - + diff --git a/src/openacp/components/add-workspace/setup-modal.tsx b/src/openacp/components/add-workspace/setup-modal.tsx new file mode 100644 index 0000000..cf2555c --- /dev/null +++ b/src/openacp/components/add-workspace/setup-modal.tsx @@ -0,0 +1,157 @@ +import React, { useState, useEffect, useMemo } from "react" +import { invoke } from "@tauri-apps/api/core" +import { listen } from "@tauri-apps/api/event" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog" +import { Button } from "../ui/button" +import { Input } from "../ui/input" +import { showToast } from "../../lib/toast" +import type { WorkspaceEntry } from "../../api/workspace-store" + +interface AgentEntry { + key: string + name: string + version: string + installed: boolean + available: boolean + description: string +} + +interface SetupModalProps { + open: boolean + path: string + instanceId: string + onComplete: (entry: WorkspaceEntry) => void + onClose: () => void +} + +export function SetupModal(props: SetupModalProps) { + const [agents, setAgents] = useState([]) + const [loading, setLoading] = useState(true) + const [selectedAgent, setSelectedAgent] = useState("") + const [installingAgent, setInstallingAgent] = useState("") + const [starting, setStarting] = useState(false) + const [error, setError] = useState("") + + const folderName = props.path.split("/").pop() ?? "workspace" + + useEffect(() => { + if (!props.open) return + invoke("run_openacp_agents_list").then((result) => { + const raw = typeof result === "string" ? JSON.parse(result) : result + let list: AgentEntry[] + if (Array.isArray(raw)) list = raw + else if (raw?.data?.agents) list = raw.data.agents + else list = [] + const claude = list.find((a) => a.key === "claude" && a.installed) + if (claude) setSelectedAgent("claude") + setAgents(list) + setLoading(false) + }).catch(() => setLoading(false)) + }, [props.open]) + + const installAgent = async (key: string) => { + setInstallingAgent(key) + const unlisten = await listen("agent-install-output", () => {}) + try { + await invoke("run_openacp_agent_install", { agentKey: key }) + setSelectedAgent(key) + setAgents((prev) => prev.map((a) => a.key === key ? { ...a, installed: true } : a)) + } catch (err) { + showToast({ description: `Failed to install ${key}`, variant: "error" }) + } finally { + setInstallingAgent("") + unlisten() + } + } + + const handleStart = async () => { + setStarting(true) + setError("") + try { + // Update config with selected agent + if (selectedAgent) { + await invoke("invoke_cli", { + args: ["config", "set", "defaultAgent", selectedAgent, "--dir", props.path, "--json"], + }).catch(() => {}) + } + // Start server + await invoke("invoke_cli", { args: ["start", "--dir", props.path, "--daemon"] }) + props.onComplete({ + id: props.instanceId, + name: folderName, + directory: props.path, + type: "local", + }) + } catch (e: any) { + const msg = typeof e === "string" ? e : e?.message ?? "Failed to start" + setError(msg) + setStarting(false) + } + } + + return ( + { if (!open) props.onClose() }}> + + + Set up {folderName} + +
+
+

Select an AI agent for this workspace

+ {loading ? ( +

Loading agents...

+ ) : ( +
+ {agents.map((agent) => ( + + ) : null} + + ))} +
+ )} +
+ {error &&

{error}

} +
+ + +
+
+
+
+ ) +} diff --git a/src/openacp/main.tsx b/src/openacp/main.tsx index fab65f3..988c7b7 100644 --- a/src/openacp/main.tsx +++ b/src/openacp/main.tsx @@ -25,10 +25,17 @@ function App() { invoke('check_openacp_config').catch(() => false), ]), ]) - setScreen(determineStartupScreen({ + const screen = determineStartupScreen({ installed: installedResult !== null, configExists: Boolean(configResult), - })) + }) + console.log('[onboard]', { + installed: installedResult !== null, + version: installedResult, + configExists: Boolean(configResult), + screen, + }) + setScreen(screen) })() }, [])