From 9da3130d01fc401748a55e4723d644c8733be333 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 16:44:24 -0400 Subject: [PATCH 1/4] fix: say what Open folder and Clone from URL did with what you typed Open folder names the enclosing repository it opened instead of a folder inside it, and says when a checkout's repository is already a project. Clone from URL says why a non-GitHub URL can't clone and when the repository is already a project, clears errors once a field changes, and keeps long paths inside the dialog. The Initialize Git card reads the folder again when shown or refocused, so a commit made outside Ace clears it. Why: apps/app/src/open-project.tsx#function InitGit | a folder without a commit is re-read only by a listing that opening triggers; the card re-opens its folder on mount and focus so outside commits show --- apps/app/src/app.tsx | 22 ++++++++++++++++++-- apps/app/src/dashboard.tsx | 1 + apps/app/src/open-project.tsx | 24 ++++++++++++++++++---- apps/app/src/setup-project.tsx | 37 ++++++++++++++++++++++++++-------- apps/host/src/github.ts | 8 ++------ apps/host/src/projects.ts | 15 ++++++++------ apps/host/src/protocol.ts | 4 ++++ 7 files changed, 85 insertions(+), 26 deletions(-) diff --git a/apps/app/src/app.tsx b/apps/app/src/app.tsx index 7cda2f17..937b67a9 100644 --- a/apps/app/src/app.tsx +++ b/apps/app/src/app.tsx @@ -604,8 +604,24 @@ export function App() { } async function open(path: string) { - const value = await host.request({ op: "project-open", path }); - chooseProject(projectId(hello.host, value.path, value.repo)); + const value = await host.request({ op: "project-open", path }); + const id = projectId(hello.host, value.path, value.repo); + // A GitHub project is one project on this host however many checkouts of it are opened. + const other = value.repo + && opened?.find((checkout) => + checkout.path !== value.path && projectId(hello.host, checkout.path, checkout.repo) === id + ); + chooseProject(id); + if (other) { + toast(`${value.name} is already a project at ${other.path}`, { + description: "Channels in it keep starting from that checkout.", + }); + } else if (value.from) { + toast(`Opened ${value.name}`, { + description: + `${value.from} is inside the Git repository at ${value.path}, so Ace opened that.`, + }); + } } // Declared, not a const: the All Projects groups above call it during render. @@ -1096,6 +1112,7 @@ export function App() { name={current.label} ignored={current.ignored} disabled={!connected} + path={current.path} onInit={() => init(current.path)} /> @@ -1142,6 +1159,7 @@ export function App() { {setup && ( setSetup(undefined)} diff --git a/apps/app/src/dashboard.tsx b/apps/app/src/dashboard.tsx index 2ffb2045..19d21ff6 100644 --- a/apps/app/src/dashboard.tsx +++ b/apps/app/src/dashboard.tsx @@ -439,6 +439,7 @@ export function Dashboard( {needsGit && (
void; @@ -48,7 +49,8 @@ export function EmptyProjects({ onOpen, onClone, disabled }: { } /** Lanes are Git worktrees, so a folder needs a commit before it can start channels. */ -export function InitGit({ name, ignored, disabled, onInit }: { +export function InitGit({ path, name, ignored, disabled, onInit }: { + path: string; name: string; /** Ace won't commit under a folder's own `.gitignore`, so the person commits it themselves. */ ignored?: boolean; @@ -56,6 +58,15 @@ export function InitGit({ name, ignored, disabled, onInit }: { onInit: () => Promise; }) { const [busy, setBusy] = useState(false); + // A commit made outside Ace, say in a terminal, shows once the host reads the folder again, as + // opening it does; listings alone don't. + useEffect(() => { + if (disabled) return; + const check = () => void host.request({ op: "project-open", path }).catch(() => {}); + check(); + addEventListener("focus", check); + return () => removeEventListener("focus", check); + }, [path, disabled]); async function init() { setBusy(true); @@ -148,9 +159,14 @@ export function OpenProject({ onOpen, onClose }: { placeholder="~/code/project" value={path} disabled={busy} - onChange={(event) => setPath(event.target.value)} + onChange={(event) => { + setPath(event.target.value); + setError(""); + }} /> - {error &&

{error}

} + {error && ( +

{error}

+ )} diff --git a/apps/app/src/setup-project.tsx b/apps/app/src/setup-project.tsx index 381ee810..938e8f1e 100644 --- a/apps/app/src/setup-project.tsx +++ b/apps/app/src/setup-project.tsx @@ -10,7 +10,7 @@ import { DialogTitle, Input, } from "@ace/ui"; -import { githubRepo } from "@ace/host/protocol"; +import { githubRepo, NOT_GITHUB, type Project } from "@ace/host/protocol"; import { desktop, here } from "./desktop"; import { useFolder } from "./folder"; @@ -21,8 +21,10 @@ import type { AppProject } from "./projects"; * project, or opens the matching checkout already at the chosen folder. Nothing is cloned until * the person confirms the destination. */ -export function SetupProject({ project, starting, onSetup, onClose }: { +export function SetupProject({ project, opened, starting, onSetup, onClose }: { project?: AppProject & { repo: string }; + /** This host's projects, to say when a cloned repository already is one. */ + opened: Project[]; starting: boolean; onSetup: (repo: string, path: string) => Promise; onClose: () => void; @@ -37,7 +39,10 @@ export function SetupProject({ project, starting, onSetup, onClose }: { const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const repo = project?.repo || url.trim(); - const name = githubRepo(repo)?.split("/")[1]; + const full = githubRepo(repo); + const name = full?.split("/")[1]; + const existing = !project && full + && opened.find((value) => value.repo?.toLowerCase() === full.toLowerCase()); // The host expands `~`, so the app never assumes where this machine's home folder is. const base = (parent ?? folder)?.replace(/\/$/, ""); const path = edited ?? (base && name ? `${base}/${name}` : ""); @@ -62,6 +67,7 @@ export function SetupProject({ project, starting, onSetup, onClose }: { if (!chosen) return; setParent(chosen); setEdited(undefined); + setError(""); } catch (error) { setError((error as Error).message); } @@ -94,7 +100,7 @@ export function SetupProject({ project, starting, onSetup, onClose }: { event.preventDefault(); void setup(); }} - className="flex flex-col gap-3" + className="flex min-w-0 flex-col gap-3" > {!project && (
@@ -108,8 +114,20 @@ export function SetupProject({ project, starting, onSetup, onClose }: { disabled={busy} spellCheck={false} placeholder="https://github.com/owner/name" - onChange={(event) => setUrl(event.target.value)} + onChange={(event) => { + setUrl(event.target.value); + setError(""); + }} /> + {url.trim() && !full && ( +

{NOT_GITHUB}

+ )} + {existing && ( +

+ {full} is already a project at{" "} + {existing.path}. Another copy won't add a second project. +

+ )}
)}
@@ -123,7 +141,10 @@ export function SetupProject({ project, starting, onSetup, onClose }: { disabled={busy} spellCheck={false} placeholder={base && `${base}/name`} - onChange={(event) => setEdited(event.target.value)} + onChange={(event) => { + setEdited(event.target.value); + setError(""); + }} /> {desktop && (
{error && ( -

+

{error}

)} - +