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) + }) +}