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
20 changes: 5 additions & 15 deletions src-tauri/src/onboarding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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(())
Expand Down
9 changes: 7 additions & 2 deletions src/onboarding/setup-wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,20 @@ export function SetupWizard(props: Props) {
const installAgent = async (key: string) => {
setInstallingAgent(key); setAgentInstallError(''); setAgentInstallLog([]);
const unlisten = await listen<string>('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 () => {
setSetupStatus('running'); setSetupLog([]);
const unlisten = await listen<string>('setup-output', (event) => setSetupLog((prev) => [...prev, event.payload]));
try {
const jsonStr = await invoke<string>('run_openacp_setup', { workspace: workspace, agent: selectedAgent });
setSetupStatus('starting'); await invoke('start_server');
setSetupStatus('starting');
await invoke<string>('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' };
Expand Down
19 changes: 19 additions & 0 deletions src/openacp/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -82,6 +83,7 @@ export function OpenACPApp() {
const [showSettings, setShowSettings] = useState(false);
const [settingsPage, setSettingsPage] = useState<SettingsPage>("general");
const [pluginsOpen, setPluginsOpen] = useState(false);
const [setupInfo, setSetupInfo] = useState<{ path: string; instanceId: string } | null>(null);

const retryRef = useRef<ReturnType<typeof setInterval>>();
const retryCountRef = useRef(0);
Expand Down Expand Up @@ -501,11 +503,28 @@ export function OpenACPApp() {
{showAddWorkspace && (
<AddWorkspaceModal
onAdd={handleAddWorkspace}
onSetup={(path, instanceId) => {
setShowAddWorkspace(false)
setSetupInfo({ path, instanceId })
}}
onClose={closeAddWorkspaceModal}
existingIds={workspaces.map((w) => w.id)}
defaultTab={addWorkspaceDefaultTab}
/>
)}
{setupInfo && (
<SetupModal
open
path={setupInfo.path}
instanceId={setupInfo.instanceId}
onComplete={(entry) => {
setSetupInfo(null)
addWorkspace(entry)
showToast({ description: `Workspace "${entry.name}" ready.`, variant: "success" })
}}
onClose={() => setSetupInfo(null)}
/>
)}
<SettingsDialog
open={showSettings}
onOpenChange={setShowSettings}
Expand Down
9 changes: 2 additions & 7 deletions src/openacp/components/add-workspace/create-instance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,8 @@ export function CreateInstance(props: CreateInstanceProps) {
} catch { /* server may take a moment to start */ }
props.onAdd({ id: data.id, name: data.name ?? data.id, directory: data.directory, type: 'local' })
} else {
// New instance needs onboarding (agent setup etc.)
if (props.onSetup) {
props.onSetup(props.path, data.id)
} else {
// Fallback: add without starting
props.onAdd({ id: data.id, name: data.name ?? data.id, directory: data.directory, type: 'local' })
}
// New — needs onboarding (agent setup, then start)
props.onSetup?.(props.path, data.id)
}
} catch (e: any) {
const msg = typeof e === 'string' ? e : e?.message ?? 'Failed to create instance'
Expand Down
3 changes: 2 additions & 1 deletion src/openacp/components/add-workspace/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Button } from "../ui/button";

interface AddWorkspaceModalProps {
onAdd: (entry: WorkspaceEntry) => void;
onSetup?: (path: string, instanceId: string) => void;
onClose: () => void;
existingIds: string[];
defaultTab?: "local" | "remote";
Expand Down Expand Up @@ -64,7 +65,7 @@ export function AddWorkspaceModal(props: AddWorkspaceModalProps) {
</TabsTrigger>
</TabsList>
<TabsContent value="local" className="p-6">
<LocalTab onAdd={props.onAdd} existingIds={props.existingIds} />
<LocalTab onAdd={props.onAdd} onSetup={props.onSetup} existingIds={props.existingIds} />
</TabsContent>
<TabsContent value="remote" className="p-6">
<RemoteTab onAdd={props.onAdd} />
Expand Down
157 changes: 157 additions & 0 deletions src/openacp/components/add-workspace/setup-modal.tsx
Original file line number Diff line number Diff line change
@@ -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<AgentEntry[]>([])
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<string>("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<string>("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<string>("invoke_cli", {
args: ["config", "set", "defaultAgent", selectedAgent, "--dir", props.path, "--json"],
}).catch(() => {})
}
// Start server
await invoke<string>("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 (
<Dialog open={props.open} onOpenChange={(open) => { if (!open) props.onClose() }}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Set up {folderName}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<p className="text-sm text-muted-foreground mb-3">Select an AI agent for this workspace</p>
{loading ? (
<p className="text-sm text-muted-foreground py-4">Loading agents...</p>
) : (
<div className="flex flex-col gap-1.5 max-h-48 overflow-y-auto">
{agents.map((agent) => (
<button
key={agent.key}
type="button"
className={`flex items-center gap-3 rounded-lg border px-3 py-2.5 text-left transition-colors ${
selectedAgent === agent.key
? "border-foreground bg-accent"
: "border-border-weak hover:bg-accent"
} ${!agent.installed ? "opacity-60" : ""}`}
onClick={() => agent.installed && setSelectedAgent(agent.key)}
>
<div className={`flex size-4 shrink-0 items-center justify-center rounded-full border ${
selectedAgent === agent.key ? "border-foreground bg-foreground" : "border-muted-foreground"
}`}>
{selectedAgent === agent.key && (
<div className="size-1.5 rounded-full bg-background" />
)}
</div>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-foreground">{agent.name}</span>
</div>
{agent.installed ? (
<span className="text-2xs text-muted-foreground">Installed</span>
) : agent.available ? (
<Button
variant="outline"
size="sm"
className="h-6 text-xs"
disabled={installingAgent === agent.key}
onClick={(e) => { e.stopPropagation(); installAgent(agent.key) }}
>
{installingAgent === agent.key ? "..." : "Install"}
</Button>
) : null}
</button>
))}
</div>
)}
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="flex justify-end gap-2 pt-2">
<Button variant="ghost" onClick={props.onClose} disabled={starting}>
Cancel
</Button>
<Button onClick={handleStart} disabled={!selectedAgent || starting}>
{starting ? "Starting..." : "Start workspace"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}
11 changes: 9 additions & 2 deletions src/openacp/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,17 @@ function App() {
invoke<boolean>('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)
})()
}, [])

Expand Down