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 3adf223a..e70e40bb 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: c729811995ffcaa0 -raw_hash: 9fd83c4d176cf4c6 -vouched: 2026-10-22T20:47:29Z +body_hash: bcbd8e0b666b1d3c +raw_hash: 33b2e33890f7fef6 +vouched: 2026-10-22T20:54:01Z lines: 235-824 --- diff --git a/.lane/memory/apps/app/src/app.tsx/01M5JMVG9G87F31FGVV571Y6BJ-the-project-list-is-built-on.md b/.lane/memory/apps/app/src/app.tsx/01M5JMVG9G87F31FGVV571Y6BJ-the-project-list-is-built-on.md index b2e608ee..b69e1f72 100644 --- a/.lane/memory/apps/app/src/app.tsx/01M5JMVG9G87F31FGVV571Y6BJ-the-project-list-is-built-on.md +++ b/.lane/memory/apps/app/src/app.tsx/01M5JMVG9G87F31FGVV571Y6BJ-the-project-list-is-built-on.md @@ -4,9 +4,9 @@ anchor: function App created: 2026-10-22T20:29:16Z norm: '1' sig: 063d0f00fea02511 -body_hash: c729811995ffcaa0 -raw_hash: 9fd83c4d176cf4c6 -vouched: 2026-10-22T20:47:29Z +body_hash: bcbd8e0b666b1d3c +raw_hash: 33b2e33890f7fef6 +vouched: 2026-10-22T20:54:01Z lines: 330-1237 --- diff --git a/.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md b/.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md index 95cba7ae..319c8f41 100644 --- a/.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md +++ b/.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md @@ -4,9 +4,9 @@ anchor: function Dashboard created: 2026-10-19T20:51:03Z norm: '1' sig: 4bfb45497157c0e0 -body_hash: 97a54ddcbd872f30 -raw_hash: 758dbb3d2b2fb407 -vouched: 2026-10-22T15:14:58Z +body_hash: 7bc89e0d12e7b984 +raw_hash: cc8a4b9d6c396b11 +vouched: 2026-10-22T20:44:27Z lines: 256-606 --- diff --git a/.lane/memory/apps/app/src/open-project.tsx/01M5JNQ72H3WS44JG82D60Q37M-a-folder-without-a-commit-is.md b/.lane/memory/apps/app/src/open-project.tsx/01M5JNQ72H3WS44JG82D60Q37M-a-folder-without-a-commit-is.md new file mode 100644 index 00000000..ef9e2203 --- /dev/null +++ b/.lane/memory/apps/app/src/open-project.tsx/01M5JNQ72H3WS44JG82D60Q37M-a-folder-without-a-commit-is.md @@ -0,0 +1,12 @@ +--- +id: 01M5JNQ72H3WS44JG82D60Q37M +anchor: function InitGit +created: 2026-10-22T20:44:24Z +norm: '1' +sig: e05f5c81094473f3 +body_hash: faffdcfc736d2780 +raw_hash: e2bf3f4586e0df5c +lines: 52-111 +--- + +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 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 686b0bed..a0364cb7 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,9 +3,10 @@ id: 01M5AYXJQNG05XSJ4MD06CY2TF anchor: function SetupProject created: 2026-10-19T20:51:15Z norm: '1' -sig: 5fe4de63348584e9 -body_hash: 7f0b00583c29c9fd -raw_hash: 0faa3040f0175372 +sig: c2572cc83b5ddf2c +body_hash: f7b40f021d23c63c +raw_hash: 617255c5382e68e6 +vouched: 2026-10-22T20:44:27Z lines: 24-163 --- diff --git a/.lane/memory/apps/host/src/projects.ts/01M5J0F003CDDGXJ61TYSB9NQ1-open-accepts-only-absolute-o.md b/.lane/memory/apps/host/src/projects.ts/01M5J0F003CDDGXJ61TYSB9NQ1-open-accepts-only-absolute-o.md index 578a82f0..346eae01 100644 --- a/.lane/memory/apps/host/src/projects.ts/01M5J0F003CDDGXJ61TYSB9NQ1-open-accepts-only-absolute-o.md +++ b/.lane/memory/apps/host/src/projects.ts/01M5J0F003CDDGXJ61TYSB9NQ1-open-accepts-only-absolute-o.md @@ -3,9 +3,10 @@ id: 01M5J0F003CDDGXJ61TYSB9NQ1 anchor: function open created: 2026-10-22T14:32:55Z norm: '1' -sig: 69741dfdfc216b17 -body_hash: 70f23e213716ef8e -raw_hash: ee7a4de150371af3 +sig: 2a6b090bac83313c +body_hash: 9b3ecf598f795095 +raw_hash: b861580496c0d206 +vouched: 2026-10-22T20:44:27Z lines: 131-142 --- 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}

)} - +