diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0b6e51f..e99a7e8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@ateam/desktop", - "version": "0.1.38", + "version": "0.1.39", "private": true, "main": "./out/main/bootstrap.js", "scripts": { diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 71fc15e..86c99ad 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -386,7 +386,8 @@ export function App() { if (e.dest === dest) onLog(e.chunk); }); try { - await window.ateamHost.install(dest); + // Return the box's status so the picker can show its readiness checklist. + return await window.ateamHost.install(dest); } finally { off(); } diff --git a/apps/desktop/src/renderer/src/components/BoxReadinessChecklist.tsx b/apps/desktop/src/renderer/src/components/BoxReadinessChecklist.tsx new file mode 100644 index 0000000..b7aa3c3 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/BoxReadinessChecklist.tsx @@ -0,0 +1,76 @@ +import { useCallback, useEffect, useState } from "react"; +import type { BoxReadiness } from "../../../shared/host"; + +// The set-up-so-far checklist shown after a box is created OR prepared over SSH: what's +// already task-ready (engine + Tailscale) vs the interactive steps left — GitHub sign-in +// (the clone needs it up front) and, if none is installed, a coding agent. Recheck +// re-probes; the git identity auto-derives once the box is signed into GitHub. Shared so +// every "add a box" flow ends the same way instead of on a cryptic clone error. +export function BoxReadinessChecklist({ + alias, + agents, + title, + onDone, +}: { + /** The connected box to probe (ssh_config alias). */ + alias: string; + /** Agents already installed on the box (from the create/install handshake). */ + agents: string[]; + title: string; + onDone: () => void; +}) { + const [ready, setReady] = useState(null); + const [checking, setChecking] = useState(false); + + const check = useCallback(async () => { + setChecking(true); + try { + setReady(await window.ateamHost.boxReadiness(alias)); + } catch { + // A probe failure just leaves the checklist partial — not worth blocking on. + } finally { + setChecking(false); + } + }, [alias]); + // Probe once when the box appears (and again if it changes). + useEffect(() => { + void check(); + }, [check]); + + return ( +
+
{title}
+ +
+ + +
+
+ ); +} diff --git a/apps/desktop/src/renderer/src/components/CreateBoxDialog.tsx b/apps/desktop/src/renderer/src/components/CreateBoxDialog.tsx index 54254a5..01976e7 100644 --- a/apps/desktop/src/renderer/src/components/CreateBoxDialog.tsx +++ b/apps/desktop/src/renderer/src/components/CreateBoxDialog.tsx @@ -1,8 +1,8 @@ import { X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import type { AgentDTO } from "@ateam/protocol"; -import type { BoxReadiness, ProviderOptions } from "../../../shared/host"; - +import type { ProviderOptions } from "../../../shared/host"; +import { BoxReadinessChecklist } from "./BoxReadinessChecklist"; import { HetznerLogo } from "./HetznerLogo"; // "Create a box" — Ateam stands up a fresh VPS at a provider, generates the SSH key, @@ -33,24 +33,12 @@ export function CreateBoxDialog({ const [error, setError] = useState(null); const [agents, setAgents] = useState([]); const [preinstall, setPreinstall] = useState([]); - // After a box is created + connected: its readiness (gh/identity) + installed agents. + // After a box is created + connected: the alias to probe + the agents it came with, + // which the readiness checklist below renders. const [readyAlias, setReadyAlias] = useState(null); - const [readyBox, setReadyBox] = useState(null); const [readyAgents, setReadyAgents] = useState([]); - const [checking, setChecking] = useState(false); const logRef = useRef(null); - const checkReadiness = async (alias: string) => { - setChecking(true); - try { - setReadyBox(await window.ateamHost.boxReadiness(alias)); - } catch { - // A probe failure just leaves the checklist partial — not worth blocking on. - } finally { - setChecking(false); - } - }; - const loadOptions = async () => { setLoadingOpts(true); setOptsError(null); @@ -120,7 +108,6 @@ export function CreateBoxDialog({ if (status.alias) { setReadyAlias(status.alias); setReadyAgents(status.info.agents); - void checkReadiness(status.alias); } } catch (e) { setError(e instanceof Error ? e.message : String(e)); @@ -162,53 +149,12 @@ export function CreateBoxDialog({ )} {error &&
{error}
} {!busy && readyAlias ? ( -
-
- Box created — finish these in the box’s terminal: -
-
    -
  • Engine + Tailscale
  • -
  • - {readyBox?.gh.signedIn - ? `GitHub signed in${readyBox.gh.login ? ` as ${readyBox.gh.login}` : ""}` - : "GitHub — sign in: "} - {!readyBox?.gh.signedIn ? gh auth login : null} -
  • -
  • - {readyBox?.gitName - ? `git identity (${readyBox.gitName})` - : "git identity — sets automatically after GitHub sign-in"} -
  • - {readyAgents.length === 0 ? ( -
  • - no coding agent yet — install one from the agent picker -
  • - ) : ( - readyAgents.map((a) => ( -
  • - {a} installed -
  • - )) - )} -
-
- - -
-
+ onDone(readyAlias)} + /> ) : !busy ? (
))} - {onAdd && - (adding ? ( + {(onAdd || onInstall) && + (addMode === "" ? ( + + ) : addMode === "menu" ? ( + <> + {onInstall && ( + + )} + {onInstall && ( + + )} + {onAdd && ( + + )} + + ) : addMode === "tailscale" ? (
{ if (e.key === "Enter") void submit(); if (e.key === "Escape") { - setAdding(false); + setAddMode("menu"); setError(null); } }} @@ -217,25 +294,16 @@ export function EnvironmentPicker({ {busy ? "Connecting…" : (error ?? "The box's Tailscale address")}
+ ) : addMode === "ready" ? ( + installedAlias && ( + + ) ) : ( - - ))} - {onInstall && - (setup ? (
{ if (e.key === "Enter") void submitInstall(); if (e.key === "Escape" && !installing) { - setSetup(false); + setAddMode("menu"); setInstallError(null); } }} @@ -274,42 +342,7 @@ export function EnvironmentPicker({ )}
- ) : ( - ))} - {onInstall && ( - - )}
, document.body, )} diff --git a/apps/desktop/src/renderer/src/components/NewTaskComposer.tsx b/apps/desktop/src/renderer/src/components/NewTaskComposer.tsx index b09ef09..a61c18a 100644 --- a/apps/desktop/src/renderer/src/components/NewTaskComposer.tsx +++ b/apps/desktop/src/renderer/src/components/NewTaskComposer.tsx @@ -1,6 +1,7 @@ import type { AgentDTO } from "@ateam/protocol"; import { ArrowUp, Paperclip, X, Zap } from "lucide-react"; import { useEffect, useState } from "react"; +import type { HostStatus } from "../../../shared/host"; import { AgentPicker } from "./AgentPicker"; import { type EnvOption, EnvironmentPicker } from "./EnvironmentPicker"; @@ -48,7 +49,7 @@ export function NewTaskComposer({ /** Connect a Tailscale endpoint typed into the picker. */ onAdd?: (endpoint: string) => Promise; /** Set up a fresh box over SSH (install engine + connect) from the picker. */ - onInstall?: (dest: string, onLog: (chunk: string) => void) => Promise; + onInstall?: (dest: string, onLog: (chunk: string) => void) => Promise; /** Install a coding agent's CLI on the selected box, streamed; returns the login step. */ onInstallAgent?: ( alias: string, @@ -71,10 +72,20 @@ export function NewTaskComposer({ const [yolo, setYolo] = useState(false); const [files, setFiles] = useState([]); const [dragging, setDragging] = useState(false); - // Default to the first runnable environment (Local, unless the repo isn't here). - const [alias, setAlias] = useState( - environments.find((e) => !e.disabled)?.alias ?? null, - ); + // Remember the last-picked environment across tasks (and app restarts) so it isn't + // re-selected every time; fall back to the first runnable one (Local, unless the repo + // isn't here) when nothing valid is saved for this project. "__local__" = the Mac. + const [alias, setAliasState] = useState(() => { + const saved = localStorage.getItem("ateam.runOn"); + const savedAlias = saved === "__local__" ? null : saved; + const usable = + saved !== null && environments.some((e) => e.alias === savedAlias && !e.disabled); + return usable ? savedAlias : (environments.find((e) => !e.disabled)?.alias ?? null); + }); + const setAlias = (a: string | null) => { + setAliasState(a); + localStorage.setItem("ateam.runOn", a === null ? "__local__" : a); + }; // Which agents the chosen environment actually has installed. Known only for a // connected engine; when unknown, fall back to the catalog's own `available` flag. diff --git a/apps/desktop/src/renderer/src/index.css b/apps/desktop/src/renderer/src/index.css index 85817a3..9259a6f 100644 --- a/apps/desktop/src/renderer/src/index.css +++ b/apps/desktop/src/renderer/src/index.css @@ -1968,16 +1968,17 @@ input { cursor: pointer; } -.cb-ready { +/* Shared box-readiness checklist (after Create-a-box or Set-up-over-SSH). */ +.box-ready { display: flex; flex-direction: column; gap: 10px; } -.cb-ready-title { +.box-ready-title { font-size: 13px; color: var(--text); } -.cb-ready-list { +.box-ready-list { list-style: none; margin: 0; padding: 0; @@ -1985,28 +1986,28 @@ input { flex-direction: column; gap: 6px; } -.cb-ready-list li { +.box-ready-list li { font-size: 13px; color: var(--text-dim); padding-left: 18px; position: relative; } -.cb-ready-list li.done { +.box-ready-list li.done { color: var(--text); } -.cb-ready-list li.done::before { +.box-ready-list li.done::before { content: "\2713"; position: absolute; left: 0; color: var(--text); } -.cb-ready-list li.todo::before { +.box-ready-list li.todo::before { content: "\2192"; position: absolute; left: 0; color: var(--text-dim); } -.cb-ready-list code { +.box-ready-list code { font-family: ui-monospace, Menlo, monospace; font-size: 12px; color: var(--text); @@ -2014,3 +2015,8 @@ input { padding: 1px 5px; border-radius: 4px; } +.box-ready-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +}