diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx
index b530aff532f..e7760c603c0 100644
--- a/packages/app/src/components/dialog-select-file.tsx
+++ b/packages/app/src/components/dialog-select-file.tsx
@@ -24,6 +24,7 @@ type Entry = {
type: EntryType
title: string
description?: string
+ keywords?: string
keybind?: string
category: string
option?: CommandOption
@@ -62,6 +63,7 @@ const createCommandEntry = (option: CommandOption, category: string): Entry => (
type: "command",
title: option.title,
description: option.description,
+ keywords: option.keywords,
keybind: option.keybind,
category,
option,
@@ -392,7 +394,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
loadingMessage={language.t("common.loading")}
items={items}
key={(item) => item.id}
- filterKeys={["title", "description", "category"]}
+ filterKeys={["title", "description", "keywords", "category"]}
groupBy={grouped() ? (item) => item.category : () => ""}
onMove={handleMove}
onSelect={handleSelect}
diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx
index 03bd6318dab..8a5568e2cf5 100644
--- a/packages/app/src/context/command.tsx
+++ b/packages/app/src/context/command.tsx
@@ -53,6 +53,7 @@ export interface CommandOption {
id: string
title: string
description?: string
+ keywords?: string
category?: string
keybind?: KeybindConfig
slash?: string
diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index c9b92db501d..00041f08e17 100644
--- a/packages/app/src/i18n/ar.ts
+++ b/packages/app/src/i18n/ar.ts
@@ -21,6 +21,7 @@ export const dict = {
"theme.scheme.dark": "داكن",
"command.sidebar.toggle": "تبديل الشريط الجانبي",
"command.project.open": "فتح مشروع",
+ "command.project.close": "إغلاق مشروع",
"command.provider.connect": "اتصال بموفر",
"command.server.switch": "تبديل الخادم",
"command.settings.open": "فتح الإعدادات",
diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts
index 951edf0a5c0..48fcd6ca542 100644
--- a/packages/app/src/i18n/br.ts
+++ b/packages/app/src/i18n/br.ts
@@ -21,6 +21,7 @@ export const dict = {
"theme.scheme.dark": "Escuro",
"command.sidebar.toggle": "Alternar barra lateral",
"command.project.open": "Abrir projeto",
+ "command.project.close": "Fechar projeto",
"command.provider.connect": "Conectar provedor",
"command.server.switch": "Trocar servidor",
"command.settings.open": "Abrir configurações",
diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts
index e8bdcde596e..a2ed2c4526c 100644
--- a/packages/app/src/i18n/bs.ts
+++ b/packages/app/src/i18n/bs.ts
@@ -23,6 +23,7 @@ export const dict = {
"command.sidebar.toggle": "Prikaži/sakrij bočnu traku",
"command.project.open": "Otvori projekat",
+ "command.project.close": "Zatvori projekat",
"command.provider.connect": "Poveži provajdera",
"command.server.switch": "Promijeni server",
"command.settings.open": "Otvori postavke",
diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts
index 5ea52a5c92c..da14fb8fc9b 100644
--- a/packages/app/src/i18n/da.ts
+++ b/packages/app/src/i18n/da.ts
@@ -23,6 +23,7 @@ export const dict = {
"command.sidebar.toggle": "Skift sidebjælke",
"command.project.open": "Åbn projekt",
+ "command.project.close": "Luk projekt",
"command.provider.connect": "Tilslut udbyder",
"command.server.switch": "Skift server",
"command.settings.open": "Åbn indstillinger",
diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts
index a6cf8045c09..74aec54b1e2 100644
--- a/packages/app/src/i18n/de.ts
+++ b/packages/app/src/i18n/de.ts
@@ -25,6 +25,7 @@ export const dict = {
"theme.scheme.dark": "Dunkel",
"command.sidebar.toggle": "Seitenleiste umschalten",
"command.project.open": "Projekt öffnen",
+ "command.project.close": "Projekt schließen",
"command.provider.connect": "Anbieter verbinden",
"command.server.switch": "Server wechseln",
"command.settings.open": "Einstellungen öffnen",
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 97a572f1cf2..9ddd9bbdea6 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -23,6 +23,8 @@ export const dict = {
"command.sidebar.toggle": "Toggle sidebar",
"command.project.open": "Open project",
+ "command.project.close": "Close project",
+ "command.project.close.description": "Close the current project",
"command.provider.connect": "Connect provider",
"command.server.switch": "Switch server",
"command.settings.open": "Open settings",
@@ -347,6 +349,11 @@ export const dict = {
"dialog.project.edit.worktree.startup": "Workspace startup script",
"dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "e.g. bun install",
+ "dialog.project.close.title": "Close project",
+ "dialog.project.close.confirm": 'Close project "{{name}}"?',
+ "dialog.project.close.sessions.one": "This project has 1 active session.",
+ "dialog.project.close.sessions.many": "This project has {{count}} active sessions.",
+ "dialog.project.close.note": "You can reopen the project later from the command palette or sidebar.",
"dialog.releaseNotes.action.getStarted": "Get started",
"dialog.releaseNotes.action.next": "Next",
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index 77ef7970c43..793b7640911 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -23,6 +23,7 @@ export const dict = {
"command.sidebar.toggle": "Alternar barra lateral",
"command.project.open": "Abrir proyecto",
+ "command.project.close": "Cerrar proyecto",
"command.provider.connect": "Conectar proveedor",
"command.server.switch": "Cambiar servidor",
"command.settings.open": "Abrir ajustes",
diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts
index c887f9ee8b2..9f05596fc22 100644
--- a/packages/app/src/i18n/fr.ts
+++ b/packages/app/src/i18n/fr.ts
@@ -21,6 +21,7 @@ export const dict = {
"theme.scheme.dark": "Sombre",
"command.sidebar.toggle": "Basculer la barre latérale",
"command.project.open": "Ouvrir un projet",
+ "command.project.close": "Fermer le projet",
"command.provider.connect": "Connecter un fournisseur",
"command.server.switch": "Changer de serveur",
"command.settings.open": "Ouvrir les paramètres",
diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts
index 9ddb6baf4a7..1f997cbb177 100644
--- a/packages/app/src/i18n/ja.ts
+++ b/packages/app/src/i18n/ja.ts
@@ -21,6 +21,7 @@ export const dict = {
"theme.scheme.dark": "ダーク",
"command.sidebar.toggle": "サイドバーの切り替え",
"command.project.open": "プロジェクトを開く",
+ "command.project.close": "プロジェクトを閉じる",
"command.provider.connect": "プロバイダーに接続",
"command.server.switch": "サーバーの切り替え",
"command.settings.open": "設定を開く",
diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts
index 1e35106d1bc..559c00a550b 100644
--- a/packages/app/src/i18n/ko.ts
+++ b/packages/app/src/i18n/ko.ts
@@ -25,6 +25,7 @@ export const dict = {
"theme.scheme.dark": "다크",
"command.sidebar.toggle": "사이드바 토글",
"command.project.open": "프로젝트 열기",
+ "command.project.close": "프로젝트 닫기",
"command.provider.connect": "공급자 연결",
"command.server.switch": "서버 전환",
"command.settings.open": "설정 열기",
diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts
index d9dac8ee550..d36fa304986 100644
--- a/packages/app/src/i18n/no.ts
+++ b/packages/app/src/i18n/no.ts
@@ -26,6 +26,7 @@ export const dict = {
"command.sidebar.toggle": "Veksle sidepanel",
"command.project.open": "Åpne prosjekt",
+ "command.project.close": "Lukk prosjekt",
"command.provider.connect": "Koble til leverandør",
"command.server.switch": "Bytt server",
"command.settings.open": "Åpne innstillinger",
diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts
index b63fe5ee409..901fa943ba6 100644
--- a/packages/app/src/i18n/pl.ts
+++ b/packages/app/src/i18n/pl.ts
@@ -21,6 +21,7 @@ export const dict = {
"theme.scheme.dark": "Ciemny",
"command.sidebar.toggle": "Przełącz pasek boczny",
"command.project.open": "Otwórz projekt",
+ "command.project.close": "Zamknij projekt",
"command.provider.connect": "Połącz dostawcę",
"command.server.switch": "Przełącz serwer",
"command.settings.open": "Otwórz ustawienia",
diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts
index aadb926d270..72d057df1ec 100644
--- a/packages/app/src/i18n/ru.ts
+++ b/packages/app/src/i18n/ru.ts
@@ -23,6 +23,7 @@ export const dict = {
"command.sidebar.toggle": "Переключить боковую панель",
"command.project.open": "Открыть проект",
+ "command.project.close": "Закрыть проект",
"command.provider.connect": "Подключить провайдера",
"command.server.switch": "Переключить сервер",
"command.settings.open": "Открыть настройки",
diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts
index 6a25a356a96..23cfebcdafe 100644
--- a/packages/app/src/i18n/th.ts
+++ b/packages/app/src/i18n/th.ts
@@ -23,6 +23,7 @@ export const dict = {
"command.sidebar.toggle": "สลับแถบข้าง",
"command.project.open": "เปิดโปรเจกต์",
+ "command.project.close": "ปิดโปรเจกต์",
"command.provider.connect": "เชื่อมต่อผู้ให้บริการ",
"command.server.switch": "สลับเซิร์ฟเวอร์",
"command.settings.open": "เปิดการตั้งค่า",
diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts
index 50e55983247..afc8abbe682 100644
--- a/packages/app/src/i18n/tr.ts
+++ b/packages/app/src/i18n/tr.ts
@@ -27,6 +27,7 @@ export const dict = {
"command.sidebar.toggle": "Kenar çubuğunu aç/kapat",
"command.project.open": "Proje aç",
+ "command.project.close": "Projeyi kapat",
"command.provider.connect": "Sağlayıcı bağla",
"command.server.switch": "Sunucu değiştir",
"command.settings.open": "Ayarları aç",
diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts
index 1f88a822235..cd9fd60f468 100644
--- a/packages/app/src/i18n/zh.ts
+++ b/packages/app/src/i18n/zh.ts
@@ -28,6 +28,7 @@ export const dict = {
"command.sidebar.toggle": "切换侧边栏",
"command.project.open": "打开项目",
+ "command.project.close": "关闭项目",
"command.provider.connect": "连接提供商",
diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts
index a75e8ef47a6..c22b3937ce5 100644
--- a/packages/app/src/i18n/zht.ts
+++ b/packages/app/src/i18n/zht.ts
@@ -27,6 +27,7 @@ export const dict = {
"command.sidebar.toggle": "切換側邊欄",
"command.project.open": "開啟專案",
+ "command.project.close": "關閉專案",
"command.provider.connect": "連接提供者",
"command.server.switch": "切換伺服器",
"command.settings.open": "開啟設定",
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 052a03c5491..0518dc99bff 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -83,6 +83,7 @@ import {
import { workspaceOpenState } from "./layout/sidebar-workspace-helpers"
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
import { SidebarContent } from "./layout/sidebar-shell"
+import { closeProject as runProjectClose } from "./layout/project-close"
export default function Layout(props: ParentProps) {
const [store, setStore, , ready] = persisted(
@@ -959,6 +960,19 @@ export default function Layout(props: ParentProps) {
keybind: "mod+o",
onSelect: () => chooseProject(),
},
+ {
+ id: "project.close",
+ title: language.t("command.project.close"),
+ description: language.t("command.project.close.description"),
+ keywords: "remove workspace",
+ category: language.t("command.category.project"),
+ disabled: !currentProject(),
+ onSelect: () => {
+ const project = currentProject()
+ if (!project) return
+ close(project.worktree)
+ },
+ },
{
id: "provider.connect",
title: language.t("command.provider.connect"),
@@ -1317,30 +1331,15 @@ export default function Layout(props: ParentProps) {
setWorkspaceName(directory, next, projectId, branch)
}
- function closeProject(directory: string) {
- const list = layout.projects.list()
- const index = list.findIndex((x) => x.worktree === directory)
- const active = currentProject()?.worktree === directory
- if (index === -1) return
- const next = list[index + 1]
-
- if (!active) {
- layout.projects.close(directory)
- return
- }
-
- if (!next) {
- layout.projects.close(directory)
- navigate("/")
- return
- }
-
- navigateWithSidebarReset(`/${base64Encode(next.worktree)}/session`)
- layout.projects.close(directory)
- queueMicrotask(() => {
- void navigateToProject(next.worktree)
+ const close = (directory: string) =>
+ runProjectClose({
+ directory,
+ list: layout.projects.list(),
+ current: currentProject()?.worktree,
+ close: layout.projects.close,
+ go: navigateWithSidebarReset,
+ open: navigateToProject,
})
- }
function toggleProjectWorkspaces(project: LocalProject) {
const enabled = layout.sidebar.workspaces(project.worktree)()
@@ -1882,7 +1881,7 @@ export default function Layout(props: ParentProps) {
onProjectFocus: (worktree) => aim.activate(worktree),
navigateToProject,
openSidebar: () => layout.sidebar.open(),
- closeProject,
+ closeProject: close,
showEditProjectDialog,
toggleProjectWorkspaces,
workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(),
@@ -2016,7 +2015,7 @@ export default function Layout(props: ParentProps) {
closeProject(p().worktree)}
+ onSelect={() => close(p().worktree)}
>
{language.t("common.close")}
diff --git a/packages/app/src/pages/layout/project-close.test.ts b/packages/app/src/pages/layout/project-close.test.ts
new file mode 100644
index 00000000000..4e140514c7d
--- /dev/null
+++ b/packages/app/src/pages/layout/project-close.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, test } from "bun:test"
+import { closeProject } from "./project-close"
+
+describe("closeProject", () => {
+ test("closes inactive project without navigation", () => {
+ const calls = { close: [] as string[], nav: [] as string[], open: [] as string[] }
+ closeProject({
+ directory: "/b",
+ list: [
+ { worktree: "/a", expanded: false },
+ { worktree: "/b", expanded: false },
+ ],
+ current: "/a",
+ close: (directory) => calls.close.push(directory),
+ navigate: (href) => calls.nav.push(href),
+ open: (directory) => {
+ calls.open.push(directory)
+ },
+ })
+ expect(calls).toEqual({ close: ["/b"], nav: [], open: [] })
+ })
+
+ test("navigates home when closing the active last project", () => {
+ const calls = { close: [] as string[], nav: [] as string[], open: [] as string[] }
+ closeProject({
+ directory: "/a",
+ list: [{ worktree: "/a", expanded: false }],
+ current: "/a",
+ close: (directory) => calls.close.push(directory),
+ navigate: (href) => calls.nav.push(href),
+ open: (directory) => {
+ calls.open.push(directory)
+ },
+ })
+ expect(calls).toEqual({ close: ["/a"], nav: ["/"], open: [] })
+ })
+})
diff --git a/packages/app/src/pages/layout/project-close.tsx b/packages/app/src/pages/layout/project-close.tsx
new file mode 100644
index 00000000000..8c7e0606604
--- /dev/null
+++ b/packages/app/src/pages/layout/project-close.tsx
@@ -0,0 +1,37 @@
+import { base64Encode } from "@opencode-ai/util/encode"
+import type { LocalProject } from "@/context/layout"
+
+type Nav = {
+ directory: string
+ list: LocalProject[]
+ current?: string
+ close: (directory: string) => void
+ go?: (href: string) => void
+ navigate?: (href: string) => void
+ open: (directory: string) => Promise | void
+}
+
+export function closeProject(input: Nav) {
+ const go = input.go ?? input.navigate
+ const index = input.list.findIndex((x) => x.worktree === input.directory)
+ const active = input.current === input.directory
+ if (index === -1) return
+ const next = input.list[index + 1]
+
+ if (!active) {
+ input.close(input.directory)
+ return
+ }
+
+ if (!next) {
+ input.close(input.directory)
+ go?.("/")
+ return
+ }
+
+ go?.(`/${base64Encode(next.worktree)}/session`)
+ input.close(input.directory)
+ queueMicrotask(() => {
+ void input.open(next.worktree)
+ })
+}