Skip to content

feat(frontend): remove a project from the sidebar#219

Open
codebanditssss wants to merge 1 commit into
mainfrom
feat/sidebar-remove-project
Open

feat(frontend): remove a project from the sidebar#219
codebanditssss wants to merge 1 commit into
mainfrom
feat/sidebar-remove-project

Conversation

@codebanditssss

Copy link
Copy Markdown
Collaborator

Closes #218.

Problem

The daemon and CLI both support removing a project (DELETE /api/v1/projects/{id} / ao project rm), but the renderer had no UI for it — once added, a project could only be removed from the CLI. Surfaced while click-testing the app.

Change

  • Sidebar: each project row gets a kebab (SidebarMenuAction with showOnHover, an absolute-positioned sibling of the row button — no nested-button issue). It reveals on hover where the session count fades, and opens a menu with Project settings and a destructive Remove project.
  • _shell.tsx: new removeProject callback calls apiClient.DELETE("/api/v1/projects/{id}"), then optimistically drops the row from the workspace cache. If the removed project is the active route, the sidebar navigates Home.

No backend changes — the endpoint already exists. ao project rm only archives the registration (repo on disk untouched, re-addable), so this is reversible.

Test

  • Sidebar.test.tsx (new): opens the row kebab and clicks Remove project, asserting onRemoveProject is called with the project id. (Router hooks stubbed, since the Sidebar reads selection from the router.)
  • npm run typecheck clean; prettier clean; the renderer vitest suite passes.

Notes

  • No confirmation dialog: the app has no AlertDialog/toast primitive yet, and the action is reversible — so it's a two-step intent (open kebab → click the red item). A confirm can follow when a dialog primitive lands.
  • On delete failure the row resyncs via the existing 15s workspace refetch (logged to console); a visible error needs a toast surface the app doesn't have yet.

The daemon and CLI already support DELETE /api/v1/projects/{id}, but the
renderer had no UI for it. Add a per-row kebab (SidebarMenuAction, revealed on
hover) with Project settings + a destructive Remove project, wired to the
existing endpoint with an optimistic sidebar update; if the removed project was
the active route, fall back Home. ao project rm only archives the registration,
so this is reversible.

Closes #218
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR surfaces the existing DELETE /api/v1/projects/{id} endpoint in the renderer by adding a per-project kebab menu (SidebarMenuAction) to each sidebar row. The removeProject callback lives in _shell.tsx and follows the same optimistic-cache-update pattern as createProject.

  • _shell.tsx: new removeProject callback — calls the DELETE endpoint, throws on API error, and synchronously filters the project out of the React Query workspace cache on success.
  • Sidebar.tsx: ProjectItem grows a DropdownMenu trigger (showOnHover) with Project settings and a destructive Remove project item; handleRemove awaits the prop callback then navigates home if the removed project was active.
  • Sidebar.test.tsx: new test confirms the kebab flow calls onRemoveProject with the correct id; the branch that navigates home when the active project is removed is not yet covered.

Confidence Score: 4/5

Safe to merge; the only unhandled case is the kebab appearing in the collapsed icon rail, which is a visual oddity rather than a data-loss risk.

The API call, cache update, and navigation are all wired correctly and mirror existing patterns. The kebab action is missing the group-data-[collapsible=icon]:hidden guard that the session-count span uses, so in icon/collapsed mode it can appear on hover and open a dropdown that overlaps the main panel. The test covers the happy path but skips the active-project-removal branch that calls goHome().

frontend/src/renderer/components/Sidebar.tsx — verify SidebarMenuAction behavior in the collapsed/icon rail before shipping.

Important Files Changed

Filename Overview
frontend/src/renderer/routes/_shell.tsx Adds removeProject callback: calls DELETE endpoint, throws on error, then optimistically filters the project out of the React Query cache. Clean implementation mirroring createProject's pattern.
frontend/src/renderer/components/Sidebar.tsx Adds per-project kebab menu with Project settings and Remove project actions; SidebarMenuAction lacks group-data-[collapsible=icon]:hidden so the kebab remains interactive in icon/collapsed rail mode where it may overlap the icon tile.
frontend/src/renderer/components/Sidebar.test.tsx New test exercises the happy-path: opens the kebab and asserts onRemoveProject is called with the correct id. Router hooks are stubbed. Does not verify the goHome navigation branch when the removed project is the active route.

Sequence Diagram

sequenceDiagram
    actor User
    participant Sidebar
    participant Shell as _shell removeProject
    participant API
    participant Cache as React Query Cache
    participant Router

    User->>Sidebar: hover project row, kebab appears
    User->>Sidebar: click kebab, click Remove project
    Sidebar->>Shell: await onRemoveProject(projectId)
    Shell->>API: "DELETE /api/v1/projects/{id}"
    API-->>Shell: 200 OK
    Shell->>Cache: updateWorkspaces, filter out projectId
    Shell-->>Sidebar: resolves
    Sidebar->>Sidebar: "check activeProjectId === projectId"
    Sidebar->>Router: selection.goHome() if active
    Router-->>User: renders home route
Loading

Reviews (1): Last reviewed commit: "feat(frontend): remove a project from th..." | Re-trigger Greptile

Comment on lines +329 to +341
const handleRemove = () => {
void (async () => {
try {
await onRemoveProject(workspace.id);
// The route for a removed project no longer resolves; fall back home.
if (selection.activeProjectId === workspace.id) selection.goHome();
} catch (err) {
// The app has no toast surface yet; the 15s workspace refetch resyncs
// the sidebar, so log and let the row reappear if the delete failed.
console.error("remove project failed", err);
}
})();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Navigation fires after the cache update, not atomically with it. onRemoveProject (in _shell.tsx) removes the project from the React Query cache before it returns, which schedules a re-render. selection.goHome() is then called in the next microtask. In React 18's automatic-batching model both state changes are normally merged into one paint, but if the router renders the active project route before the navigation lands, the outlet may briefly see a missing project and throw/404. Moving goHome() into removeProject (in _shell.tsx, where it already owns the cache mutation) and accepting a navigate param would make the two operations atomic from the shell's perspective — but even as-is this is unlikely to surface visually due to batching.

Comment on lines +36 to +44
describe("Sidebar", () => {
it("removes a project from the row kebab menu", async () => {
const user = userEvent.setup();
const onRemoveProject = renderSidebar();

await user.click(screen.getByRole("button", { name: "Actions for my-app" }));
await user.click(await screen.findByText("Remove project"));

expect(onRemoveProject).toHaveBeenCalledWith("proj-1");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 goHome navigation branch isn't exercised by any test. The test stubs useParams to return {}, so activeProjectId is always undefined and the if (selection.activeProjectId === workspace.id) selection.goHome() branch in handleRemove is never reached. A second test case that sets useParams to return { projectId: "proj-1" } would cover the navigation on active-project removal and catch any regression in that branch.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +391 to +396
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover aria-label={`Actions for ${workspace.name}`}>
<MoreHorizontal aria-hidden="true" />
</SidebarMenuAction>
</DropdownMenuTrigger>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The SidebarMenuAction doesn't carry the group-data-[collapsible=icon]:hidden class that the session-count <span> uses to hide itself in the icon/collapsed rail. When the sidebar is collapsed to the 48 px letter rail, hovering the icon tile will still reveal the kebab button — and since DropdownMenuContent opens side="right", the menu will pop out from an already-narrow icon rail, likely overlapping the main content area.

Suggested change
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover aria-label={`Actions for ${workspace.name}`}>
<MoreHorizontal aria-hidden="true" />
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover aria-label={`Actions for ${workspace.name}`} className="group-data-[collapsible=icon]:hidden">
<MoreHorizontal aria-hidden="true" />
</SidebarMenuAction>
</DropdownMenuTrigger>

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frontend: no way to remove a project from the UI (daemon/CLI already support it)

1 participant