From 27fc3ce7f2a979f604d06e1dad3b395bcca58e5d Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Mon, 19 Oct 2026 16:51:14 -0400 Subject: [PATCH 1/4] fix(app): walk a collaborator through cloning a project onto their machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The empty state and Set up dialog say the project isn't on this Mac yet instead of listing other hosts. The dialog's primary action is "Clone to ", "Choose another folder…" opens the native folder picker for the clone's parent folder, and an existing checkout opened instead of cloned is reported only when the host finds one. Fixes #502 Why: apps/app/src/setup-project.tsx#function SetupProject | a picked folder is the clone's parent; the clone's own folder still follows the repository name --- apps/app/src/app.tsx | 43 ++++++++++++-------- apps/app/src/setup-project.tsx | 72 ++++++++++++++++++++++++---------- apps/host/src/projects.ts | 9 +++-- 3 files changed, 83 insertions(+), 41 deletions(-) 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.`, ); From c4ea3969364cd86df095908ca6b526628cc3c030 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Mon, 19 Oct 2026 16:51:26 -0400 Subject: [PATCH 2/4] chore: record lane notes for setup-project --- ...AYXJQNG05XSJ4MD06CY2TF-a-picked-folder-is-the-clone.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .lane/memory/apps/app/src/setup-project.tsx/01M5AYXJQNG05XSJ4MD06CY2TF-a-picked-folder-is-the-clone.md 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..e8ad8aec --- /dev/null +++ b/.lane/memory/apps/app/src/setup-project.tsx/01M5AYXJQNG05XSJ4MD06CY2TF-a-picked-folder-is-the-clone.md @@ -0,0 +1,8 @@ +--- +id: 01M5AYXJQNG05XSJ4MD06CY2TF +anchor: function SetupProject +created: 2026-10-19T20:51:15Z +norm: '1' +--- + +a picked folder is the clone's parent; the clone's own folder still follows the repository name From 717963f7139fd548b10a039317d482e944af3241 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Mon, 19 Oct 2026 16:51:30 -0400 Subject: [PATCH 3/4] lane: sync clone-walkthrough memory --- ...01M5AYXJQNG05XSJ4MD06CY2TF-a-picked-folder-is-the-clone.md | 4 ++++ 1 file changed, 4 insertions(+) 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 index e8ad8aec..686b0bed 100644 --- 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 @@ -3,6 +3,10 @@ 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 From 9ff0902f498c9946ad208f24197513c339a792e4 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Mon, 19 Oct 2026 16:54:46 -0400 Subject: [PATCH 4/4] chore: confirm app.tsx note after rebase --- ...M4YT6XB7NQ89E6F4P375REE7-all-projects-and-project-vie.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 ---