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 6561a7b0..3adf223a 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: 543e14e26111c21c -raw_hash: 36221fa4f08e0523 -vouched: 2026-10-22T20:42:40Z +body_hash: c729811995ffcaa0 +raw_hash: 9fd83c4d176cf4c6 +vouched: 2026-10-22T20:47:29Z 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 36e47d2f..b2e608ee 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: 543e14e26111c21c -raw_hash: 36221fa4f08e0523 -vouched: 2026-10-22T20:42:40Z +body_hash: c729811995ffcaa0 +raw_hash: 9fd83c4d176cf4c6 +vouched: 2026-10-22T20:47:29Z lines: 330-1237 --- diff --git a/apps/app/src/app.tsx b/apps/app/src/app.tsx index 967ca5fe..7cda2f17 100644 --- a/apps/app/src/app.tsx +++ b/apps/app/src/app.tsx @@ -618,8 +618,17 @@ export function App() { return connected && !!opened?.some((checkout) => isLocal(checkout, value)); } + /** This host's channels in the project, archived ones too, which the host refuses to orphan. */ + function held(value: AppProject) { + const paths = new Set( + opened?.flatMap((checkout) => isLocal(checkout, value) ? [checkout.path] : []), + ); + return channels.filter((channel) => channel.host === hello.host && paths.has(channel.project)) + .length; + } + function remove(value: AppProject) { - if (askRemove) setRemoving(value); + if (askRemove || held(value)) setRemoving(value); else void forget(value); } @@ -1165,6 +1174,7 @@ export function App() { { setAskRemove(ask); await forget(removing); diff --git a/apps/app/src/remove-project.tsx b/apps/app/src/remove-project.tsx index 4a63a7c7..4c460ea6 100644 --- a/apps/app/src/remove-project.tsx +++ b/apps/app/src/remove-project.tsx @@ -10,10 +10,15 @@ import { DialogTitle, } from "@ace/ui"; -/** Removing only forgets the folder, so a person may turn the question off. */ -export function RemoveProject({ name, path, onRemove, onCancel }: { +/** + * Removing only forgets the folder, so a person may turn the question off. The host refuses while + * the project has channels, so then this says how to delete them instead of offering to remove. + */ +export function RemoveProject({ name, path, channels, onRemove, onCancel }: { name: string; path: string; + /** This host's channels in it, counting archived ones. */ + channels: number; onRemove: (ask: boolean) => Promise; onCancel: () => void; }) { @@ -34,35 +39,54 @@ export function RemoveProject({ name, path, onRemove, onCancel }: { (next || busy ? undefined : onCancel())}> - Remove {name}? + + {channels + ? `${name} still has ${channels === 1 ? "a channel" : "channels"}` + : `Remove ${name}?`} + - Ace forgets the project. Its files and lanes stay in{" "} - {path}, and opening the folder adds it back. + {channels + ? `It has ${ + channels === 1 ? "a channel" : `${channels} channels` + }, counting archived ones. Archive ${ + channels === 1 ? "it" : "each" + } from its ⋯ menu, delete it from the Archived group, then remove the project.` + : `Ace forgets the project. Its files and lanes stay in ${path}, and opening the folder adds it back.`} - - - - - + {channels > 0 + ? ( + + + + ) + : ( + <> + + + + + + + )} );