Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand Down
12 changes: 11 additions & 1 deletion apps/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -1165,6 +1174,7 @@ export function App() {
<RemoveProject
name={label(removing, hello.host)}
path={removing.path}
channels={held(removing)}
onRemove={async (ask) => {
setAskRemove(ask);
await forget(removing);
Expand Down
80 changes: 52 additions & 28 deletions apps/app/src/remove-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
onCancel: () => void;
}) {
Expand All @@ -34,35 +39,54 @@ export function RemoveProject({ name, path, onRemove, onCancel }: {
<Dialog open onOpenChange={next => (next || busy ? undefined : onCancel())}>
<DialogContent className="gap-3 sm:max-w-80" showCloseButton={false}>
<DialogHeader>
<DialogTitle className="break-all">Remove {name}?</DialogTitle>
<DialogTitle className="break-all">
{channels
? `${name} still has ${channels === 1 ? "a channel" : "channels"}`
: `Remove ${name}?`}
</DialogTitle>
<DialogDescription className="break-words">
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.`}
</DialogDescription>
</DialogHeader>
<label htmlFor={id} className="flex items-center gap-2 text-xs text-muted-foreground">
<input
id={id}
type="checkbox"
checked={skip}
disabled={busy}
onChange={(event) => setSkip(event.target.checked)}
/>
Don't ask again
</label>
<DialogFooter>
<Button type="button" variant="outline" disabled={busy} onClick={onCancel}>
Cancel
</Button>
<Button
type="button"
variant="destructive"
disabled={busy}
onClick={() => void confirm()}
>
{busy ? "Removing…" : "Remove project"}
</Button>
</DialogFooter>
{channels > 0
? (
<DialogFooter>
<Button type="button" variant="outline" onClick={onCancel}>OK</Button>
</DialogFooter>
)
: (
<>
<label htmlFor={id} className="flex items-center gap-2 text-xs text-muted-foreground">
<input
id={id}
type="checkbox"
checked={skip}
disabled={busy}
onChange={(event) => setSkip(event.target.checked)}
/>
Don't ask again
</label>
<DialogFooter>
<Button type="button" variant="outline" disabled={busy} onClick={onCancel}>
Cancel
</Button>
<Button
type="button"
variant="destructive"
disabled={busy}
onClick={() => void confirm()}
>
{busy ? "Removing…" : "Remove project"}
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
);
Expand Down