Skip to content

project: no API or UI to remove projects and recently-closed project entries #51197

Closed as duplicate

Description

@telles0808

Description

The desktop app listed two phantom projects (gabri, Default Project) under "Projetos fechados recentemente". There is no supported way to remove them:

  • the API exposes only GET /api/project and PATCH /api/project/{projectID} - no delete;
  • the app command catalog has no remove action (only project.select and project.copyID);
  • the entries persist in drafts.sqlite -> state -> name='opencode.global.dat', key='server' -> recentlyClosed.local and cannot be cleared from the UI.

They could only be removed by editing persistent state directly: deleting rows from the project table in opencode.db and setting recentlyClosed.local = [] in drafts.sqlite (with the app closed, otherwise it rewrites from memory).

Plugins

None.

OpenCode version

2.0.16

Steps to reproduce

  1. Let the app auto-register the home dir and Default Project as projects (happens after the persisted app state is cleared).
  2. GET /api/project returns 3 projects; the sidebar shows the extras under "Projetos fechados recentemente".
  3. Try to remove them: there is no UI action and no API endpoint.

Screenshot and/or share link

N/A - described above.

Operating System

Windows 11 - Windows NT 10.0.26200 (win32 x64)

Terminal

OpenCode Desktop (Electron 44 / Chrome 152), not a terminal.

Additional context

Suggested fix: expose DELETE /api/project/{projectID} (and/or a supported location removal) and add a "remove project" action in the app for both open and recently-closed projects.

Activity

  1. milesbuckton commented on Oct 13, 2026

    @milesbuckton

    Confirming this from the desktop side, with the exact storage location and a workaround for anyone hitting it now.

    Reproduced on OpenCode.app 2.0.28. The "Recently closed" rows live entirely in persisted UI state:

    • ~/Library/Application Support/ai.opencode.desktop/drafts.sqlite
    • table state, row name='opencode.global.dat', key='server'
    • JSON field recentlyClosed.local: an array of absolute paths, newest first

    That matches the report above. Two things worth adding:

    1. There is no UI path at all, not even a partial one. I checked the 2.0.28 renderer bundle (assets/main-DChQ6T43.js) for every occurrence of recentlyClosed: five sites, all of them either read or the close()/open() pair. close() prepends a path when you close a project; open() removes that path when you reopen it. There is no dismiss, no "remove", no context menu, no clear — the row component is a plain button whose only handler is onAddProjects(server, [project.worktree]). So "forget this entry" is not reachable in the product, only accrual is. That is what makes a stale or wrong path permanent until it ages out of whatever window the list shows.

    2. The API gap is the same one, and it is worth naming which endpoint. GET /api/project and PATCH /api/project/{projectID} exist; nothing deletes. Since recentlyClosed.local is keyed by path and the project table is keyed by projectID, a DELETE /api/project/{projectID} that also removes that project's path from recentlyClosed.local is the single call that fixes both symptoms in this report. Without it the fix has to be "open the project again and close it properly", which does not help someone whose directory was renamed, moved, or deleted.

    I have a local script that edits the JSON directly (backs the DB up first, quits the app so the running app cannot re-flush its in-memory copy over the file) and it works, but it is the wrong shape for users — it needs the internal storage layout to stay stable, and it is per-machine shell surgery. A supported delete would be considerably better than everyone discovering drafts.sqlite.

    Happy to test a fix on this if it helps — I have a machine with both phantom and live entries in the list.

  2. milesbuckton commented on Oct 13, 2026

    @milesbuckton

    Confirmed this on the shipped desktop app (OpenCode.app 2.0.28, macOS) — same root cause, and I traced exactly where the entries live so a fix has a concrete target.

    Where the state actually is

    Not in opencode.db, not in the TUI's in-memory tab list — it is in the desktop app's Electron storage:

    ~/Library/Application Support/ai.opencode.desktop/drafts.sqlite
      table `state`
      row    name='opencode.global.dat', key='server'
      field  json -> recentlyClosed.local    (array of absolute paths)
    

    close(worktree) prepends to that array; open(worktree) removes the path. I checked every reference in the renderer bundle and there is no dismiss, remove, or clear action anywhere — the home-screen row component is a plain button whose only handler re-opens the path. So the entries are append-mostly with no deletion path, which is why list this grows without bound and why there is no way to reclaim it.

    What's missing on the API side

    Agreed with @reporter's assessment. Concretely, what would close this:

    1. DELETE /api/project/{projectID} — the obvious counterpart to the existing GET /api/project / PATCH /api/project/{projectID}
    2. A command-catalog action for removal, so it is reachable without a bespoke endpoint
    3. A dismiss affordance on the home-screen "Recently closed" row (an X per row, plus something for the phantom/nonexistent-directory case)

    Two gotchas worth documenting for anyone poking at this state

    Both of these cost me real debugging time, so recording them here in case they help:

    • The running app silently reverts an in-place DB edit. It holds this value in memory and rewrites the DB on quit, so an external edit is undone on shutdown. You must quit the app, edit, then relaunch.
    • A WAL database cannot be copied with cp. drafts.sqlite is in WAL mode; a bare file copy leaves the -wal/-shm behind, so the copy is not openable by anything — not even read-only. Backups and restores need VACUUM INTO, and deleting a database's sidecars to "clean up" leaves it permanently unopenable.

    Local workaround

    While this is unimplemented I wrote a CLI that edits that state safely: https://gist.github.com/milesbuckton/ee7627a8bfdcb3b3846579065d641e08

    It does list / rm <path|substring> / clear / restore <backup>, previews by default, refuses to write while the app is running (quits it, confirms the process is gone, writes, re-reads to verify, rolls back to the backup on mismatch, then relaunches), and can point at a copy of the DB via env var so the write path can be tested without touching the real install.

    Offered as-is, no warranty — and I'd much rather delete this repo once there's a real DELETE /api/project/{id}.

    Happy to PR a fix for the API + command-catalog side if a maintainer wants to point me at the right module.

  3. m4y0w4 commented on Oct 20, 2026

    @m4y0w4

    Confirmed the root cause on 2.0.32/macOS: GET /api/location persists a project row as a side effect — including a zero-session project, and even when the request is invalid.

    Minimal repro (isolated server, fresh DB, temporary HOME):

    1. Start opencode serve --port 4099 with cwd /tmp/projA.
    2. GET /api/project → [].
    3. GET /api/location (no params) → 200. GET /api/project now returns /tmp/projA with 0 sessions.
    4. GET /api/location?location[directory]=/tmp/projB → 200. /tmp/projB is added as a project with 0 sessions too.
    5. GET /api/location?location=/tmp/projB (wrong query shape) → 400, but a row for the server's cwd (/tmp/projA) is still persisted.

    The rows are ordinary project rows with deterministic IDs and survive server restarts. DELETE /api/debug/location with a JSON body returns 204 but only evicts the in-memory location; the DB row remains.

    How this produces the phantom home project: the desktop app's process cwd is $HOME when launched from Finder/Dock, and the background service's cwd is also $HOME (lsof -p <pid> -d cwd) even when the service was started from a project directory. On startup the app boots that location, and the server persists it as a zero-session project. It is hidden from the project list and cannot be removed (this issue). On 2.0.32 I observed the /Users/<user> row created in the same second the server logged location services booted directory=/Users/<user>.

    Impact: a permanent phantom project pointing at $HOME. Any session created there — for example running bare opencode from ~ — roots at the home directory and triggers home-wide scanning/indexing (see #32511). In my case that ended with gopls holding ~10k file descriptors until EMFILE.

    Suggested fix: don't persist a project as a side effect of GET /api/location; make location bootstrap ephemeral, or create the project only when a session is created or the folder is explicitly opened. Not retaining zero-session projects would also solve it. A desktop default-directory setting (#56932) would avoid booting $HOME in the first place.

    Environment: opencode 2.0.32 (desktop + background service), macOS 13.7.1 x64, zsh, no plugins. API calls authenticated with the service password (Basic opencode:<password>).

  4. neriousy commented on Oct 23, 2026

    @neriousy
    Member

    Preserving additional context from #58197: stale project directories also accumulate in the TUI Ctrl+O project picker. The reporter suggests considering automatic pruning or a hidden/archive state in addition to explicit project removal.

  5. neriousy commented on Oct 23, 2026

    @neriousy
    Member

    Preserving the TUI cleanup requirements from #49442: stale entries in /open and /project need a user-facing removal path. Suggestions include a Delete shortcut in the picker and CLI commands such as opencode project list, opencode project del <name>, and opencode project delsession <name>.

  6. wthsama commented on Oct 27, 2026

    @wthsama

    Confirmed on v2.2.0 (released 2026-10-27), self-hosted server in a Linux container.

    The server's own /openapi.json exposes only:

    /api/project        GET
    /api/project/{id}   PATCH   # body: canonical | name | icon | commands
    

    Project.Info.time.archived is present in the response schema and project.time_archived exists in SQLite, but no request body accepts it and there is no DELETE. A project therefore can never be archived or removed through the API.

    Local state, 31 rows in opencode.db:

    • 17 rows with time_archived = 0; all 17 are rendered in the app's project list.
    • 14 rows with a non-zero time_archived. All 14 share the same timestamp (2026-10-12 04:51), i.e. they were written directly to SQLite. They are correctly filtered out of the list, so the read side of the flag works and only the write path is missing.

    Directories that were never opened as a session cwd are among those 14 rows: /api/get_provider_by_id, /system-settings/security, /tmp/opencode/uploads/<uuid>, /tmp/opencode-browser-*/0.

    Two more shapes that the same cleanup path would need to cover:

    1. One physical directory registered as two projects. /workspaces is a bind mount of /vol1/1000/docker/opencode-v2/workspaces, so both spellings become rows:
    /workspaces/tmp
    /vol1/1000/docker/opencode-v2/workspaces/tmp
    
    1. cwd=/ creates a normal row next to the built-in global row, so / appears twice (same shape as sessions: folder-scoped list hides sessions when a worktree owns duplicate project rows #59371).

    Expected: DELETE /api/project/{id}, a time.archived field accepted by PATCH /api/project/{id}, and pruning of rows whose directory is missing and that own zero sessions.

  7. Drswith commented on Oct 28, 2026

    @Drswith

    Confirming this on the current desktop release (2.2.0, macOS arm64), with a bit more detail on where the gap is:

    • UI: Settings → Projects lists projects; a project only exposes "Edit project" (name / icon / color / worktree startup command). The command catalog has project.open and project.copyID, nothing else. "Hide from project list" exists only for remote server entries, not for local projects.
    • API: GET /api/project (with ?archived=true|false) and PATCH /api/project/{projectID} (canonical / name / icon / commands). No DELETE, and PATCH cannot set an archived flag.
    • Schema: project.time_archived exists and GET /api/project already filters on it, so the data model supports hiding a project — there is simply no write path (API or UI) to reach that state.

    Net effect: every folder ever opened stays listed forever; the only way out is DELETE FROM project in ~/.local/share/opencode/opencode.db, which cascades to project_directory, worktree, permission and session_v2 (i.e. chat history is lost as a side effect of cleaning up the list).

    Suggested minimal fix (also in the spirit of #37280):

    1. PATCH /api/project/{projectID} accepting archived: boolean, plus a "Remove project" / "Archive project" action in the sidebar and in Settings → Projects.
    2. Or DELETE /api/project/{projectID} with an explicit sessions=true|false opt-in.
    3. In both cases the on-disk folder must be left untouched, and the entry should also disappear from "Recently closed".

    Environment: OpenCode Desktop 2.2.0 (Electron 44.9.1), macOS 27.0.0 arm64, no plugins configured.

  8. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed write-up! I think this is a duplicate of #37280, which asks for the same thing: a supported way to remove a project from OpenCode (and optionally its sessions) without touching the folder on disk.

    Possible duplicates and related issues, ordered from most to least likely:

    There is also an open pull request, #59580, that adds DELETE /api/project/:projectID and delete actions in Settings → Projects and the Home project menu. It's still under review.

    The recently-closed part has also changed in newer versions. From v2.2.0 the app no longer keeps the old recentlyClosed list, and each project on Home has a Close action in its menu that removes it from the list. It may be worth updating from 2.0.16.

    The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.

  9. opencodejr commented on Oct 29, 2026

    @opencodejr
    Contributor

    Closing this as a duplicate of #37280, which asks for the same thing: a supported way to remove projects from OpenCode. A day has passed without a reply saying how this one differs. Please follow #37280 for updates. Related:

    If your case turns out to be different, for example the extra projects still show up on v2.2.0 or later and the Close action doesn't remove them, reply here with the details and we can reopen. Thanks again for the thorough report!

  10. m4y0w4 commented on Oct 29, 2026

    @m4y0w4

    Reopening request: 2.2.1 still reproduces the creation-path bug, and this was closed in a duplicate loop.

    I wrote the 2026-10-20 comment above. Two points.

    1. The duplicate decision is circular. On 2026-10-28 23:01 UTC the bot commented on #37280
    that it was a duplicate of this issue ("#51197 ... is where removing projects from OpenCode is
    being tracked") and labelled #37280 pending close + triaging. Six and a half hours later, at
    2026-10-29 05:33 UTC, the same bot closed this issue as a duplicate of #37280. Neither request is
    implemented, and #37280 still carries pending close as of this writing. Please keep one
    canonical tracker and link the other instead of closing both against each other.

    2. This thread is not only a removal request. The original report and six of the seven human
    comments are about deleting an existing entry; #37280 covers that. My comment is about why the
    entry exists at all: GET /api/location writes a project row as a side effect of reading. That is
    a separate bug with its own impact, and it is still present in the latest release.

    Reproduced today on the opencode-darwin-x64.zip asset from the v2.2.1 release (fresh HOME,
    cwd .../projA):

    • GET /api/project before any location request -> []
    • GET /api/location -> 200; GET /api/project now lists projA, with 0 sessions
    • GET /api/location?location[directory]=.../projB -> 200; projB listed, 0 sessions
    • GET /api/location?location[directory]=.../missing -> 404, and a row for the missing
      directory is persisted anyway
      (same 0 sessions; separate fresh profile)
    • DELETE /api/project/{id} -> 404 (no such route); PATCH {"archived":true} -> 200 but
      time.archived stays null in the response and in the DB

    /openapi.json on 2.2.1, and packages/protocol/openapi.json on dev today, expose only
    GET /api/project and PATCH /api/project/{projectID} for projects. There is no delete route and
    no writable archive field.

    So the removal half is #37280; the open PR #59580 implements that half and targets this issue.
    The creation half is #57988, still open and labelled reproduced; PR #59001 fixes only the
    missing-directory case. It still saves a project when the folder exists, because LocationProject
    calls project.resolve(location.directory) without persist: false — so the $HOME case in my
    report is unchanged. A GET on a directory the user never opened should not make it a project; if
    the intent is to create rows only on explicit open or session creation, the location boot needs to
    resolve with persist: false and save later.

    Could a human reopen this, or at minimum route the creation-path work to #57988 and leave this
    issue open as the removal tracker rather than closing it against #37280? @neriousy, since you
    folded #58197 and #49442 in here, your call on which issue owns which half.

    On my machine the /Users/<user> row is still present: 0 sessions, time_archived null,
    time_active 2026-10-29T14:35Z.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions