diff --git a/.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md b/.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md index 8876bae3..ed139c92 100644 --- a/.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md +++ b/.lane/memory/apps/app/src/app.tsx/01M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md @@ -4,9 +4,9 @@ anchor: function App created: 2026-10-15T03:38:04Z norm: '1' sig: 063d0f00fea02511 -body_hash: 3ae5f8490cc90da0 -raw_hash: 7e04439bd64b4ba8 -vouched: 2026-10-19T20:51:12Z +body_hash: 7ffeaa95733dc612 +raw_hash: a73f79d41002c2a0 +vouched: 2026-10-19T20:54:33Z lines: 235-824 --- diff --git a/.lane/memory/apps/app/src/setup-project.tsx/01M5AYXJQNG05XSJ4MD06CY2TF-a-picked-folder-is-the-clone.md b/.lane/memory/apps/app/src/setup-project.tsx/01M5AYXJQNG05XSJ4MD06CY2TF-a-picked-folder-is-the-clone.md new file mode 100644 index 00000000..686b0bed --- /dev/null +++ b/.lane/memory/apps/app/src/setup-project.tsx/01M5AYXJQNG05XSJ4MD06CY2TF-a-picked-folder-is-the-clone.md @@ -0,0 +1,12 @@ +--- +id: 01M5AYXJQNG05XSJ4MD06CY2TF +anchor: function SetupProject +created: 2026-10-19T20:51:15Z +norm: '1' +sig: 5fe4de63348584e9 +body_hash: 7f0b00583c29c9fd +raw_hash: 0faa3040f0175372 +lines: 24-163 +--- + +a picked folder is the clone's parent; the clone's own folder still follows the repository name diff --git a/apps/app/src/app.tsx b/apps/app/src/app.tsx index f712708b..8276c608 100644 --- a/apps/app/src/app.tsx +++ b/apps/app/src/app.tsx @@ -514,9 +514,14 @@ export function App() { /** Channels only start on this host; setup may clone here first, never on the other host. */ async function finishSetup(repo: string, path: string) { if (!setup) return; - const value = await host.request({ op: "project-clone", repo, path }); + const value = await host.request({ + op: "project-clone", + repo, + path, + }); const id = projectId(hello.host, value.path, value.repo); setSetup(undefined); + if (value.found) toast.success(`Opened the checkout already at ${value.path}`); if (!setup.start) return chooseProject(id); const target: AppProject = { ...value, @@ -901,10 +906,9 @@ export function App() { )} {!allProjects && !local && (

- {current.label} is checked out on {[...current.hosts].join(", ")}.{" "} - {current.repo - ? `Set it up on ${here} to start channels here.` - : "Open a checkout on this host to start channels here."} + {current.label} isn't on {here} yet. {current.repo + ? "Set it up to start channels here." + : `Open a checkout of it on ${here} to start channels here.`}

)} {!allProjects && needsGit && ( @@ -918,18 +922,23 @@ export function App() { )} {!allProjects && creatable && (
- {!local && ( - - )} - + {local + ? ( + + ) + : ( + + )}
)} diff --git a/apps/app/src/setup-project.tsx b/apps/app/src/setup-project.tsx index f7900696..381ee810 100644 --- a/apps/app/src/setup-project.tsx +++ b/apps/app/src/setup-project.tsx @@ -12,12 +12,12 @@ import { } from "@ace/ui"; import { githubRepo } from "@ace/host/protocol"; -import { here } from "./desktop"; +import { desktop, here } from "./desktop"; import { useFolder } from "./folder"; import type { AppProject } from "./projects"; /** - * Clones a project known from other hosts, or a GitHub repository by URL when there is no + * Clones a project that isn't on this machine yet, or a GitHub repository by URL when there is no * project, or opens the matching checkout already at the chosen folder. Nothing is cloned until * the person confirms the destination. */ @@ -30,6 +30,8 @@ export function SetupProject({ project, starting, onSetup, onClose }: { const id = useId(); const { folder } = useFolder(); const [url, setUrl] = useState(""); + // A picked folder holds the clone; the clone's own folder still follows the repository's name. + const [parent, setParent] = useState(); // The folder follows the repository's name until the person edits it. const [edited, setEdited] = useState(); const [busy, setBusy] = useState(false); @@ -37,7 +39,7 @@ export function SetupProject({ project, starting, onSetup, onClose }: { const repo = project?.repo || url.trim(); const name = githubRepo(repo)?.split("/")[1]; // The host expands `~`, so the app never assumes where this machine's home folder is. - const base = folder?.replace(/\/$/, ""); + const base = (parent ?? folder)?.replace(/\/$/, ""); const path = edited ?? (base && name ? `${base}/${name}` : ""); const ready = !!repo && !!path.trim(); @@ -53,6 +55,18 @@ export function SetupProject({ project, starting, onSetup, onClose }: { } } + async function choose() { + if (!desktop) return; + try { + const chosen = await desktop.project(); + if (!chosen) return; + setParent(chosen); + setEdited(undefined); + } catch (error) { + setError((error as Error).message); + } + } + return ( {project - ? `${project.label} is checked out on ${ - [...project.hosts].join(", ") - }. Clone ${project.repo} into a folder on ${here}${ - starting ? " to start the channel here." : "." - } ` - : `Clone a GitHub repository into a folder on ${here} and add it as a project. `} - A checkout of it already in that folder is opened instead. + ? `${project.label} isn't on ${here} yet. Clone ${project.repo} from GitHub to get a copy here${ + starting ? ", then start the channel." : "." + }` + : `Clone a GitHub repository to ${here} and add it as a project.`}
Folder - setEdited(event.target.value)} - /> +
+ setEdited(event.target.value)} + /> + {desktop && ( + + )} +
{error && (

@@ -123,8 +145,16 @@ export function SetupProject({ project, starting, onSetup, onClose }: {

- diff --git a/apps/host/src/projects.ts b/apps/host/src/projects.ts index f2aa55fd..d48b56d8 100644 --- a/apps/host/src/projects.ts +++ b/apps/host/src/projects.ts @@ -220,9 +220,12 @@ const cloning = new Set(); /** * Sets up a GitHub repository on this host: clones it into `value`, or opens the checkout of it - * already there. Anything else at `value` is left untouched. + * already there, marked `found`. Anything else at `value` is left untouched. */ -export async function setup(source: string, value: string): Promise { +export async function setup( + source: string, + value: string, +): Promise { const repo = parse(source); const expanded = expand(value.trim()); if (!isAbsolute(expanded)) throw new Error("Enter a full folder path, such as ~/code/project"); @@ -238,7 +241,7 @@ export async function setup(source: string, value: string): Promise { if (found?.toLowerCase() === repo.toLowerCase()) { // An interrupted clone already names its remote but has no commit to work from. const head = await git(real, ["rev-parse", "--verify", "-q", "HEAD"]); - if (head.status === 0) return await open(real); + if (head.status === 0) return { ...(await open(real)), found: true }; throw new Error( `${path} has an incomplete clone of ${repo}. Remove it or choose another folder.`, );