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 ba2cd533..6170e58a 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: 247d8e002cbe5eea -raw_hash: ff8ae1733758ca88 -vouched: 2026-10-22T00:00:58Z +body_hash: baf242069dc38c51 +raw_hash: 04ec83f8b4b4da88 +vouched: 2026-10-22T14:33:26Z lines: 235-824 --- 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 82937f16..56e8b4e1 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: 0636687429b37a91 -raw_hash: 71b29c34ea043161 -vouched: 2026-10-21T22:00:14Z +body_hash: 3dc6d02ecfe40716 +raw_hash: d2e36ee5c66a68da +vouched: 2026-10-22T14:33:26Z lines: 256-606 --- diff --git a/.lane/memory/apps/app/src/projects.ts/01M4YQHHKT4G697PSH2EAWHFCK-a-peer-answers-project-repo.md b/.lane/memory/apps/app/src/projects.ts/01M4YQHHKT4G697PSH2EAWHFCK-a-peer-answers-project-repo.md index 4db65d31..8f6b299c 100644 --- a/.lane/memory/apps/app/src/projects.ts/01M4YQHHKT4G697PSH2EAWHFCK-a-peer-answers-project-repo.md +++ b/.lane/memory/apps/app/src/projects.ts/01M4YQHHKT4G697PSH2EAWHFCK-a-peer-answers-project-repo.md @@ -4,9 +4,9 @@ anchor: function projects created: 2026-10-15T02:51:27Z norm: '1' sig: a2ec110fa31d81ee -body_hash: 708f362a469da056 -raw_hash: ba42a00a04cb6a48 -vouched: 2026-10-15T07:04:36Z +body_hash: b6503a52b484f8c1 +raw_hash: 512eae2c23a06106 +vouched: 2026-10-22T14:33:26Z lines: 42-91 --- diff --git a/.lane/memory/apps/host/src/directory.ts/01M4YVZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md b/.lane/memory/apps/host/src/directory.ts/01M4YVZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md index 973fc286..32e2574f 100644 --- a/.lane/memory/apps/host/src/directory.ts/01M4YVZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md +++ b/.lane/memory/apps/host/src/directory.ts/01M4YVZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md @@ -4,9 +4,9 @@ anchor: function configure created: 2026-10-15T04:08:53Z norm: '1' sig: f3e1bf286b59976d -body_hash: b0b93d13e4ac000b -raw_hash: 46e2e589b2f3a6ff -vouched: 2026-10-19T20:58:11Z +body_hash: 6b5edb5d1c039626 +raw_hash: 5154b99e7146ea7b +vouched: 2026-10-22T14:33:26Z lines: 46-54 --- diff --git a/.lane/memory/apps/host/src/directory.ts/01M4YX72YG7JQ8ACP15VM1V6XB-every-directory-request-must.md b/.lane/memory/apps/host/src/directory.ts/01M4YX72YG7JQ8ACP15VM1V6XB-every-directory-request-must.md index 90e49b6a..0b67a279 100644 --- a/.lane/memory/apps/host/src/directory.ts/01M4YX72YG7JQ8ACP15VM1V6XB-every-directory-request-must.md +++ b/.lane/memory/apps/host/src/directory.ts/01M4YX72YG7JQ8ACP15VM1V6XB-every-directory-request-must.md @@ -4,9 +4,9 @@ anchor: function call created: 2026-10-15T04:30:36Z norm: '1' sig: 3b9d360bb018f164 -body_hash: 420cd0f59f331b59 -raw_hash: cd5a1656a1e060aa -vouched: 2026-10-16T16:31:35Z +body_hash: ed032412ce991b6b +raw_hash: 17ca2b8a23ffb09d +vouched: 2026-10-22T14:33:26Z lines: 97-125 --- diff --git a/.lane/memory/apps/host/src/gateway-client.ts/01M4YQRMKVKT45GACS27J2TXWY-channel-listings-must-outliv.md b/.lane/memory/apps/host/src/gateway-client.ts/01M4YQRMKVKT45GACS27J2TXWY-channel-listings-must-outliv.md index 072712ea..fde304a4 100644 --- a/.lane/memory/apps/host/src/gateway-client.ts/01M4YQRMKVKT45GACS27J2TXWY-channel-listings-must-outliv.md +++ b/.lane/memory/apps/host/src/gateway-client.ts/01M4YQRMKVKT45GACS27J2TXWY-channel-listings-must-outliv.md @@ -4,9 +4,9 @@ anchor: class GatewayClient created: 2026-10-15T02:55:20Z norm: '1' sig: 41366b9e88f0e890 -body_hash: 0ea1b5a225ceb8b0 -raw_hash: 99b659f96ee9516e -vouched: 2026-10-22T00:00:58Z +body_hash: 37f27c37825b3457 +raw_hash: c990e819f8c5c8e5 +vouched: 2026-10-22T14:33:27Z lines: 33-240 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md b/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md index 739b43af..179d65da 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md @@ -4,9 +4,9 @@ anchor: function serve created: 2026-10-15T03:39:45Z norm: '1' sig: 171797818ef4d836 -body_hash: 347ec5128df25eff -raw_hash: 4f8782ceb985ad81 -vouched: 2026-10-21T23:57:08Z +body_hash: 50b83460ae5ee675 +raw_hash: f38372c6ca8389a3 +vouched: 2026-10-22T14:33:27Z lines: 668-751 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md b/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md index 49171b86..4644a5ac 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-15T04:08:49Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 14495715208898b4 -raw_hash: 2426d3718700367c -vouched: 2026-10-22T00:00:58Z +body_hash: 7148e9daddb2ba29 +raw_hash: 1839c37c0e9b7635 +vouched: 2026-10-22T14:33:27Z lines: 266-519 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md b/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md index c843f55c..35ae0bad 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-15T06:00:20Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 14495715208898b4 -raw_hash: 2426d3718700367c -vouched: 2026-10-22T00:00:58Z +body_hash: 7148e9daddb2ba29 +raw_hash: 1839c37c0e9b7635 +vouched: 2026-10-22T14:33:27Z lines: 281-561 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md b/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md index 29d8e176..0cb7805d 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md @@ -4,9 +4,9 @@ anchor: function serve created: 2026-10-15T10:17:10Z norm: '1' sig: 171797818ef4d836 -body_hash: 347ec5128df25eff -raw_hash: 4f8782ceb985ad81 -vouched: 2026-10-21T23:57:08Z +body_hash: 50b83460ae5ee675 +raw_hash: f38372c6ca8389a3 +vouched: 2026-10-22T14:33:27Z lines: 830-948 supersedes: 01M4Z2R1HEVDKEA8VEJ7XRJQ8F --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M50KBA3ME0Q9274A639MK6KJ-gateway-sockets-must-never-d.md b/.lane/memory/apps/host/src/gateway.ts/01M50KBA3ME0Q9274A639MK6KJ-gateway-sockets-must-never-d.md index 59596158..1cb9aef6 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M50KBA3ME0Q9274A639MK6KJ-gateway-sockets-must-never-d.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M50KBA3ME0Q9274A639MK6KJ-gateway-sockets-must-never-d.md @@ -4,9 +4,9 @@ anchor: function websocket created: 2026-10-15T20:16:37Z norm: '1' sig: 1b462e5d3bc33728 -body_hash: e4839023c586ff4f -raw_hash: cda22d4980cf5a80 -vouched: 2026-10-22T00:00:58Z +body_hash: 39ee5954e9b924c2 +raw_hash: 0abdf1cfc47ee529 +vouched: 2026-10-22T14:33:27Z lines: 673-737 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md b/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md index 8cef3bb9..3fafa5e9 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-19T21:09:57Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 14495715208898b4 -raw_hash: 2426d3718700367c -vouched: 2026-10-22T00:00:58Z +body_hash: 7148e9daddb2ba29 +raw_hash: 1839c37c0e9b7635 +vouched: 2026-10-22T14:33:27Z lines: 384-700 supersedes: 01M5APK9YCQH467YNMKAC8BT5B --- diff --git a/.lane/memory/apps/host/src/move.ts/01M5314FQ002GKKHTQ5G9Q4NZJ-the-directory-s-hosting-serv.md b/.lane/memory/apps/host/src/move.ts/01M5314FQ002GKKHTQ5G9Q4NZJ-the-directory-s-hosting-serv.md index cb9eb7f1..ab0da2eb 100644 --- a/.lane/memory/apps/host/src/move.ts/01M5314FQ002GKKHTQ5G9Q4NZJ-the-directory-s-hosting-serv.md +++ b/.lane/memory/apps/host/src/move.ts/01M5314FQ002GKKHTQ5G9Q4NZJ-the-directory-s-hosting-serv.md @@ -4,9 +4,9 @@ anchor: function targets created: 2026-10-16T18:56:03Z norm: '1' sig: 002f9c457b031ab4 -body_hash: bab5cddb9dfa6ae8 -raw_hash: 73b7e221730eeb13 -vouched: 2026-10-19T20:56:20Z +body_hash: 3301aca426267a1e +raw_hash: 9b7c37665b5ffcf0 +vouched: 2026-10-22T14:33:27Z lines: 64-79 --- diff --git a/.lane/memory/apps/host/src/peers.ts/01M4YX4FKJR9W30M9Y5HVTKBW4-a-peer-s-hello-can-arrive-be.md b/.lane/memory/apps/host/src/peers.ts/01M4YX4FKJR9W30M9Y5HVTKBW4-a-peer-s-hello-can-arrive-be.md index 6e81e1c7..5274adf3 100644 --- a/.lane/memory/apps/host/src/peers.ts/01M4YX4FKJR9W30M9Y5HVTKBW4-a-peer-s-hello-can-arrive-be.md +++ b/.lane/memory/apps/host/src/peers.ts/01M4YX4FKJR9W30M9Y5HVTKBW4-a-peer-s-hello-can-arrive-be.md @@ -4,8 +4,9 @@ anchor: function discover created: 2026-10-15T04:29:11Z norm: '1' sig: ab69f97386d0db50 -body_hash: 75be26d25d6bd1b9 -raw_hash: 08d9946a3a985c23 +body_hash: 3a50f94087c324bd +raw_hash: c8bb63059457b3f4 +vouched: 2026-10-22T14:33:27Z lines: 32-70 --- 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 new file mode 100644 index 00000000..578a82f0 --- /dev/null +++ b/.lane/memory/apps/host/src/projects.ts/01M5J0F003CDDGXJ61TYSB9NQ1-open-accepts-only-absolute-o.md @@ -0,0 +1,12 @@ +--- +id: 01M5J0F003CDDGXJ61TYSB9NQ1 +anchor: function open +created: 2026-10-22T14:32:55Z +norm: '1' +sig: 69741dfdfc216b17 +body_hash: 70f23e213716ef8e +raw_hash: ee7a4de150371af3 +lines: 131-142 +--- + +open accepts only absolute or ~ paths; a relative one would resolve against Ace Helper's working directory diff --git a/.lane/memory/apps/host/src/protocol.ts/01M5GEJ77XBRPQN89DW0CKNAWP-hello-answers-any-tailnet-pe.md b/.lane/memory/apps/host/src/protocol.ts/01M5GEJ77XBRPQN89DW0CKNAWP-hello-answers-any-tailnet-pe.md index 81affd80..9d7c46b8 100644 --- a/.lane/memory/apps/host/src/protocol.ts/01M5GEJ77XBRPQN89DW0CKNAWP-hello-answers-any-tailnet-pe.md +++ b/.lane/memory/apps/host/src/protocol.ts/01M5GEJ77XBRPQN89DW0CKNAWP-hello-answers-any-tailnet-pe.md @@ -3,9 +3,10 @@ id: 01M5GEJ77XBRPQN89DW0CKNAWP anchor: type Hello created: 2026-10-22T00:00:52Z norm: '1' -sig: a2275d05c454362d -body_hash: e3b0c44298fc1c14 -raw_hash: 7276efa4e6f4c00c +sig: 9b11d87fd756be4e +body_hash: 8af1972a8512b60d +raw_hash: 639013cb459f1ef3 +vouched: 2026-10-22T14:33:27Z lines: 157-157 --- diff --git a/.lane/memory/apps/host/src/team.ts/01M5FZ7YE9SPKEVN8YC2WGRKEM-ace-helper-runs-wrangler-as.md b/.lane/memory/apps/host/src/team.ts/01M5FZ7YE9SPKEVN8YC2WGRKEM-ace-helper-runs-wrangler-as.md index c9b4710b..795aa3b4 100644 --- a/.lane/memory/apps/host/src/team.ts/01M5FZ7YE9SPKEVN8YC2WGRKEM-ace-helper-runs-wrangler-as.md +++ b/.lane/memory/apps/host/src/team.ts/01M5FZ7YE9SPKEVN8YC2WGRKEM-ace-helper-runs-wrangler-as.md @@ -4,8 +4,9 @@ anchor: '@file' created: 2026-10-21T19:33:07Z norm: '1' sig: e3b0c44298fc1c14 -body_hash: a4c52bd75b46f573 -raw_hash: 1915cbbdad9adc90 +body_hash: affa0ee3ca60b97f +raw_hash: e0450bcaa3536482 +vouched: 2026-10-22T14:33:27Z lines: 1-98 --- diff --git a/.lane/memory/services/deploy.ts/01M5FXFQ3K4PKHS0PB2W3HQWNJ-the-secret-is-generated-here.md b/.lane/memory/services/deploy.ts/01M5FXFQ3K4PKHS0PB2W3HQWNJ-the-secret-is-generated-here.md index 2d46303b..d14caf07 100644 --- a/.lane/memory/services/deploy.ts/01M5FXFQ3K4PKHS0PB2W3HQWNJ-the-secret-is-generated-here.md +++ b/.lane/memory/services/deploy.ts/01M5FXFQ3K4PKHS0PB2W3HQWNJ-the-secret-is-generated-here.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-21T19:02:24Z norm: '1' sig: 061da171c15167fe -body_hash: 25ad1835bce04814 -raw_hash: 8a4a43bed92aa15a -vouched: 2026-10-21T19:33:27Z +body_hash: 92d7bd3ba1b688fb +raw_hash: edb10ffb7d852295 +vouched: 2026-10-22T14:33:27Z lines: 1-111 --- diff --git a/apps/app/src/app.tsx b/apps/app/src/app.tsx index 3d1eaef7..04fd5671 100644 --- a/apps/app/src/app.tsx +++ b/apps/app/src/app.tsx @@ -53,6 +53,7 @@ import { Palette } from "./palette"; import { placement } from "./placement"; import { present } from "./presence"; import { type AppProject, label, projects, title } from "./projects"; +import { RemoveProject } from "./remove-project"; import { type Section, Settings } from "./settings"; import { SetupProject } from "./setup-project"; import { ShareProject, useCanShare } from "./share-project"; @@ -305,6 +306,8 @@ export function App() { const [renaming, setRenaming] = useState<{ id: string; name: string }>(); const [deleting, setDeleting] = useState<{ id: string; name: string }>(); const [sharing, setSharing] = useState(false); + const [removing, setRemoving] = useState(); + const [askRemove, setAskRemove] = useLocalStorage("ace:remove-project-ask", true); const [moving, setMoving] = useState<{ item: SidebarRow; target: MoveTarget }>(); const [left, setLeft] = useLocalStorage("panel:left", true); const [width, setWidth] = useLocalStorage("panel:left:width", 200); @@ -318,13 +321,16 @@ export function App() { const [dismissed, setDismissed] = useLocalStorage("ace:directory-card", false); const directory = useDirectory(); const card = !!directory && !directory.url && !dismissed; - const [collapsed, setCollapsed] = useState>({ - pinned: false, - mine: false, - team: false, - inactive: true, - archived: true, - }); + const [collapsed, setCollapsed] = useLocalStorage>( + "ace:collapsed-groups", + { + pinned: false, + mine: false, + team: false, + inactive: true, + archived: true, + }, + ); const [now, setNow] = useState(Date.now); const picking = useRef(false); const creating = useRef(false); @@ -438,6 +444,7 @@ export function App() { name: label(value, hello.host), org: value.repo?.split("/")[0] || "", title: title(value, hello.host), + removable: removable(value), })), ]; const { attention, pulse } = useAttention( @@ -448,6 +455,11 @@ export function App() { const online = (channel: Listing) => present(presence[channelProject(channel)], people[hello.user], channel.id); let groups = projectGroups(visible, hello, people, now, collapsed, attention, pins, online); + if (!allProjects && current && removable(current)) { + groups = groups.map((group) => + group.id === "mine" ? { ...group, onRemove: () => remove(current) } : group + ); + } if (allProjects) { const byProject = new Map(); @@ -494,9 +506,7 @@ export function App() { onNewSession: connected && (value.host === hello.host || value.repo) ? () => void create(undefined, undefined, value) : undefined, - onRemove: connected && opened.some((checkout) => isLocal(checkout, value)) - ? () => void remove(value) - : undefined, + onRemove: removable(value) ? () => remove(value) : undefined, }, ...[...team].map(([owner, rows]) => ({ owner, name: person(people, owner), rows })) .sort((a, b) => a.name.localeCompare(b.name)) @@ -539,7 +549,17 @@ export function App() { return projectId(hello.host, checkout.path, checkout.repo) === value.id; } - async function remove(value: AppProject) { + // Declared, not a const: the picker and All Projects groups above call it during render. + function removable(value: AppProject) { + return connected && opened.some((checkout) => isLocal(checkout, value)); + } + + function remove(value: AppProject) { + if (askRemove) setRemoving(value); + else void forget(value); + } + + async function forget(value: AppProject) { try { for (const checkout of opened.filter((checkout) => isLocal(checkout, value))) { await host.request({ op: "project-remove", path: checkout.path }); @@ -549,6 +569,11 @@ export function App() { } } + function removeRepo(repo: SessionSidebarRepo) { + const value = available.find((project) => project.id === repo.id); + if (value) remove(value); + } + async function choose() { if (!connected || picking.current) return; if (!desktop) return setAdding(true); @@ -566,6 +591,9 @@ export function App() { async function init(path: string) { try { await host.request({ op: "project-init", path }); + toast.success("Initialized Git", { + description: "Committed the folder's files as Initial commit. Channels can start here now.", + }); } catch (error) { toast.error("Could not initialize Git", { description: (error as Error).message }); } @@ -816,6 +844,7 @@ export function App() { selectedUid={channel?.id} loading={status === "connecting" && !channels.length} onRepoChange={(repo) => chooseProject(repo.id)} + onRepoRemove={removeRepo} onAddRepo={connected ? () => void choose() : undefined} onCloneRepo={connected ? cloneUrl : undefined} onSelect={(item) => { @@ -853,7 +882,7 @@ export function App() { onArchiveInactive={!allProjects && local && connected ? () => void archiveInactive(current.path) : undefined} - onShareProject={!allProjects && canShare && connected + onShareProject={!allProjects && canShare && connected && directory?.url ? () => setSharing(true) : undefined} onCompute={connected @@ -882,6 +911,7 @@ export function App() { repos={repos} connected={connected} onProject={chooseProject} + onRemove={removeRepo} onOpen={() => void choose()} onClone={cloneUrl} /> @@ -931,6 +961,7 @@ export function App() { onSetup={!local && current.repo ? () => startSetup(current) : undefined} onInit={() => init(current.path)} onProject={chooseProject} + onRemove={removeRepo} onOpen={() => void choose()} onClone={cloneUrl} onChannel={select} @@ -987,6 +1018,7 @@ export function App() {
init(current.path)} /> @@ -1061,6 +1093,18 @@ export function App() { onClose={() => setSharing(false)} /> )} + {removing && ( + { + setAskRemove(ask); + await forget(removing); + setRemoving(undefined); + }} + onCancel={() => setRemoving(undefined)} + /> + )} {deleting && ( void; onInit: () => Promise; onProject: (id: string) => void; + onRemove: (repo: SessionSidebarRepo) => void; onOpen: () => void; onClone: () => void; onChannel: (channel: Listing) => void; @@ -381,6 +383,7 @@ export function Dashboard( repos={repos} selectedRepoId={project.id} onRepoChange={(repo) => onProject(repo.id)} + onRepoRemove={onRemove} onAddRepo={connected ? onOpen : undefined} onCloneRepo={connected ? onClone : undefined} trigger={ @@ -414,7 +417,12 @@ export function Dashboard( {needsGit && (
- +
)} {creatable && ( diff --git a/apps/app/src/github.tsx b/apps/app/src/github.tsx index 827e2c76..530db01d 100644 --- a/apps/app/src/github.tsx +++ b/apps/app/src/github.tsx @@ -72,15 +72,18 @@ function useGithubFilter(kind: GithubKind, project: AppProject) { ); } -export function GithubSidebar({ kind, project, repos, connected, onProject, onOpen, onClone }: { - kind: GithubKind; - project: AppProject; - repos: SessionSidebarRepo[]; - connected: boolean; - onProject: (id: string) => void; - onOpen: () => void; - onClone: () => void; -}) { +export function GithubSidebar( + { kind, project, repos, connected, onProject, onRemove, onOpen, onClone }: { + kind: GithubKind; + project: AppProject; + repos: SessionSidebarRepo[]; + connected: boolean; + onProject: (id: string) => void; + onRemove: (repo: SessionSidebarRepo) => void; + onOpen: () => void; + onClone: () => void; + }, +) { const [filter, setFilter] = useGithubFilter(kind, project); const left = useLayoutLeft(); const phone = useMedia("(width < 40rem)"); @@ -91,6 +94,7 @@ export function GithubSidebar({ kind, project, repos, connected, onProject, onOp repos={repos} selectedRepoId={project.id} onRepoChange={(repo) => onProject(repo.id)} + onRepoRemove={onRemove} onAddRepo={connected ? onOpen : undefined} onCloneRepo={connected ? onClone : undefined} > diff --git a/apps/app/src/open-project.tsx b/apps/app/src/open-project.tsx index ddeab402..507778fb 100644 --- a/apps/app/src/open-project.tsx +++ b/apps/app/src/open-project.tsx @@ -48,8 +48,10 @@ 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, disabled, onInit }: { +export function InitGit({ name, ignored, disabled, onInit }: { name: string; + /** Ace won't commit under a folder's own `.gitignore`, so the person commits it themselves. */ + ignored?: boolean; disabled: boolean; onInit: () => Promise; }) { @@ -68,17 +70,31 @@ export function InitGit({ name, disabled, onInit }: {

Initialize a Git repository to get started

-

- Channels work in lanes, which are Git worktrees. Ace will run git init in{" "} - {name}, add a .gitignore that leaves out secrets such as{" "} - .env, dependencies, and build output, and commit the rest. -

- + {ignored + ? ( +

+ Channels work in lanes, which are Git worktrees. {name} already has a{" "} + .gitignore, so Ace won't commit under its rules. Run git init + {" "} + and commit its files yourself, then channels can start here. +

+ ) + : ( + <> +

+ Channels work in lanes, which are Git worktrees. Ace will run git init in + {" "} + {name}, add a .gitignore that leaves out secrets such as{" "} + .env, dependencies, and build output, and commit the rest. +

+ + + )}
); } diff --git a/apps/app/src/projects.ts b/apps/app/src/projects.ts index 391d78a1..b686368d 100644 --- a/apps/app/src/projects.ts +++ b/apps/app/src/projects.ts @@ -71,7 +71,7 @@ export function projects(opened: Project[], channels: Listing[], host: string): name: string, reachable: boolean, repo?: string, - git?: false, + opened?: Project, ) { const id = projectId(at, path, repo); const checkout = checkoutId(at, path); @@ -88,7 +88,8 @@ export function projects(opened: Project[], channels: Listing[], host: string): checkouts: new Set([checkout]), }; if (repo) created.repo = repo; - if (git === false) created.git = git; + if (opened?.git === false) created.git = false; + if (opened?.ignored) created.ignored = true; return void values.set(id, created); } value.hosts.add(at); @@ -100,7 +101,7 @@ export function projects(opened: Project[], channels: Listing[], host: string): } if (host) { for (const project of opened) { - add(host, project.path, project.name, true, project.repo, project.git); + add(host, project.path, project.name, true, project.repo, project); } } for (const channel of channels) { diff --git a/apps/app/src/remove-project.tsx b/apps/app/src/remove-project.tsx new file mode 100644 index 00000000..4a63a7c7 --- /dev/null +++ b/apps/app/src/remove-project.tsx @@ -0,0 +1,69 @@ +import { useId, useState } from "react"; + +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@ace/ui"; + +/** Removing only forgets the folder, so a person may turn the question off. */ +export function RemoveProject({ name, path, onRemove, onCancel }: { + name: string; + path: string; + onRemove: (ask: boolean) => Promise; + onCancel: () => void; +}) { + const id = useId(); + const [busy, setBusy] = useState(false); + const [skip, setSkip] = useState(false); + + async function confirm() { + setBusy(true); + try { + await onRemove(!skip); + } finally { + setBusy(false); + } + } + + return ( + (next || busy ? undefined : onCancel())}> + + + Remove {name}? + + Ace forgets the project. Its files and lanes stay in{" "} + {path}, and opening the folder adds it back. + + + + + + + + + + ); +} diff --git a/apps/host/src/projects.ts b/apps/host/src/projects.ts index d64edf2d..a19140c1 100644 --- a/apps/host/src/projects.ts +++ b/apps/host/src/projects.ts @@ -96,7 +96,10 @@ async function project(path: string): Promise { const { root, repo, commit } = await checkout(path); const value: Project = { path: root, name: repo?.split("/")[1] || basename(root) || root }; if (repo) value.repo = repo; - if (!commit) value.git = false; + if (!commit) { + value.git = false; + if (existsSync(join(root, ".gitignore"))) value.ignored = true; + } return value; } @@ -126,7 +129,11 @@ const expand = (value: string) => /** Opening a folder must not require model credentials or create a channel. */ export async function open(value: string): Promise { - let path = realpathSync(resolve(expand(value))); + // A relative path would resolve against Ace Helper's working directory, which no one sees. + const expanded = expand(value.trim()); + if (!isAbsolute(expanded)) throw new Error("Enter a full folder path, such as ~/code/project"); + if (!existsSync(expanded)) throw new Error(`There's no folder at ${expanded}. Check the path.`); + let path = realpathSync(expanded); if (!statSync(path).isDirectory()) throw new Error("Choose a folder to open as a project"); path = realpathSync((await checkout(path)).root); const opened = paths(); diff --git a/apps/host/src/protocol.ts b/apps/host/src/protocol.ts index 5e680662..b9bc3135 100644 --- a/apps/host/src/protocol.ts +++ b/apps/host/src/protocol.ts @@ -244,6 +244,8 @@ export type Project = { repo?: string; /** False until the folder is a Git repository with a commit, which lanes start from. */ git?: false; + /** The folder without a commit already has a `.gitignore`, so Ace leaves initializing it to its owner. */ + ignored?: true; }; export type ProviderId = "anthropic" | "openai"; diff --git a/packages/ui/src/components/session-sidebar/session-options.tsx b/packages/ui/src/components/session-sidebar/session-options.tsx index 2f3e9bab..35c757fd 100644 --- a/packages/ui/src/components/session-sidebar/session-options.tsx +++ b/packages/ui/src/components/session-sidebar/session-options.tsx @@ -8,14 +8,17 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; import { IconDots } from "../../icons"; -/** Additional ways to start a repository session, and sharing the project. */ -export function SessionOptions({ className, onContinuePr, onStartIssue, onShareProject }: { - className?: string; - onContinuePr?: () => void; - onStartIssue?: () => void; - onShareProject?: () => void; -}) { - if (!onContinuePr && !onStartIssue && !onShareProject) return null; +/** Additional ways to start a repository session, and sharing or removing the project. */ +export function SessionOptions( + { className, onContinuePr, onStartIssue, onShareProject, onRemoveProject }: { + className?: string; + onContinuePr?: () => void; + onStartIssue?: () => void; + onShareProject?: () => void; + onRemoveProject?: () => void; + }, +) { + if (!onContinuePr && !onStartIssue && !onShareProject && !onRemoveProject) return null; return ( @@ -47,6 +50,9 @@ export function SessionOptions({ className, onContinuePr, onStartIssue, onShareP {onShareProject && ( Share project… )} + {onRemoveProject && ( + Remove project… + )} ); diff --git a/packages/ui/src/components/session-sidebar/session-sidebar.tsx b/packages/ui/src/components/session-sidebar/session-sidebar.tsx index 7b2bf958..7f6a5f89 100644 --- a/packages/ui/src/components/session-sidebar/session-sidebar.tsx +++ b/packages/ui/src/components/session-sidebar/session-sidebar.tsx @@ -73,6 +73,8 @@ export type SessionSidebarRepo = { avatar?: string; /** Shown on hover, such as the project's full path. */ title?: string; + /** The picker offers removing it when `onRepoRemove` is provided. */ + removable?: boolean; }; export type SessionSidebarProps = Omit, "children" | "onSelect"> & { @@ -362,7 +364,7 @@ export function ProjectPicker( repo={repo} selected={repo.id === selected.id} onRepoChange={onRepoChange} - onRepoRemove={onRepoRemove} + onRepoRemove={repo.removable ? onRepoRemove : undefined} /> ))} @@ -671,6 +673,7 @@ function Group({ onContinuePr={onContinuePr} onStartIssue={onStartIssue} onShareProject={shareProject} + onRemoveProject={group.onRemove} /> )}
@@ -692,7 +695,7 @@ function Group({ {group.onRemove && ( - Remove project + Remove project… )}