diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index bab3d39f386a..4ac001a84af7 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -51,7 +51,7 @@ type TabHandoff = { at: number } -export type LocalProject = Partial & { worktree: string; expanded: boolean } +export type LocalProject = Partial & { worktree: string; expanded: boolean; pinned?: boolean } export type ReviewDiffStyle = "unified" | "split" @@ -384,7 +384,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( return available[Math.floor(Math.random() * available.length)] } - function enrich(project: { worktree: string; expanded: boolean }) { + function enrich(project: { worktree: string; expanded: boolean; pinned?: boolean }) { const [childStore] = globalSync.child(project.worktree, { bootstrap: false }) const projectID = childStore.project const metadata = projectID @@ -597,6 +597,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( move(directory: string, toIndex: number) { server.projects.move(directory, toIndex) }, + pin(directory: string, pinned: boolean) { + server.projects.pin(directory, pinned) + }, }, sidebar: { opened: createMemo(() => store.sidebar.opened), diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 1204fba55710..7c53c4fb9eeb 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -4,7 +4,7 @@ import { createStore } from "solid-js/store" import { Persist, persisted } from "@/utils/persist" import { useCheckServerHealth } from "@/utils/server-health" -type StoredProject = { worktree: string; expanded: boolean } +type StoredProject = { worktree: string; expanded: boolean; pinned?: boolean } type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http const HEALTH_POLL_INTERVAL_MS = 10_000 @@ -33,6 +33,10 @@ function isLocalHost(url: string) { if (host === "localhost" || host === "127.0.0.1") return "local" } +function order(list: StoredProject[]) { + return [...list.filter((item) => item.pinned), ...list.filter((item) => !item.pinned)] +} + export namespace ServerConnection { type Base = { displayName?: string } @@ -250,7 +254,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( if (!key) return const current = store.projects[key] ?? [] if (current.find((x) => x.worktree === directory)) return - setStore("projects", key, [{ worktree: directory, expanded: true }, ...current]) + setStore("projects", key, order([{ worktree: directory, expanded: true }, ...current])) }, close(directory: string) { const key = origin() @@ -285,7 +289,25 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( const result = [...current] const [item] = result.splice(fromIndex, 1) result.splice(toIndex, 0, item) - setStore("projects", key, result) + setStore("projects", key, order(result)) + }, + pin(directory: string, pinned: boolean) { + const key = origin() + if (!key) return + const current = store.projects[key] ?? [] + const index = current.findIndex((x) => x.worktree === directory) + if (index === -1) return + const item = current[index] + if (!item || !!item.pinned === pinned) return + const rest = current.filter((x) => x.worktree !== directory) + const next = { ...item, pinned } + if (pinned) { + const split = rest.findIndex((x) => !x.pinned) + const at = split === -1 ? rest.length : split + setStore("projects", key, [...rest.slice(0, at), next, ...rest.slice(at)]) + return + } + setStore("projects", key, order([...rest, next])) }, last() { const key = origin() diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 6c3f3bb55ef4..2768d962070d 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -542,6 +542,8 @@ export const dict = { "sidebar.project.recentSessions": "الجلسات الحديثة", "sidebar.project.viewAllSessions": "عرض جميع الجلسات", "sidebar.project.clearNotifications": "مسح الإشعارات", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "سطح المكتب", "settings.section.server": "الخادم", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 63880462a467..e4de9d3d3cdb 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -549,6 +549,8 @@ export const dict = { "sidebar.project.recentSessions": "Sessões recentes", "sidebar.project.viewAllSessions": "Ver todas as sessões", "sidebar.project.clearNotifications": "Limpar notificações", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", "settings.section.server": "Servidor", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 2b589eb35f62..d5e547079684 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -610,6 +610,8 @@ export const dict = { "sidebar.project.recentSessions": "Nedavne sesije", "sidebar.project.viewAllSessions": "Prikaži sve sesije", "sidebar.project.clearNotifications": "Očisti obavijesti", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index b096d87b4b7b..1ea2a86fa17e 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -606,6 +606,8 @@ export const dict = { "sidebar.project.recentSessions": "Seneste sessioner", "sidebar.project.viewAllSessions": "Vis alle sessioner", "sidebar.project.clearNotifications": "Ryd notifikationer", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 6dc0b0497245..6d9e2b1141ec 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -558,6 +558,8 @@ export const dict = { "sidebar.project.recentSessions": "Letzte Sitzungen", "sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen", "sidebar.project.clearNotifications": "Benachrichtigungen löschen", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Desktop", "settings.section.server": "Server", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index c6bcc37b116f..a5760702ed63 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -681,6 +681,8 @@ export const dict = { "sidebar.project.recentSessions": "Recent sessions", "sidebar.project.viewAllSessions": "View all sessions", "sidebar.project.clearNotifications": "Clear notifications", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "sidebar.empty.title": "No projects open", "sidebar.empty.description": "Open a project to get started", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index c600232ef613..cdb443e70cbe 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -613,6 +613,8 @@ export const dict = { "sidebar.project.recentSessions": "Sesiones recientes", "sidebar.project.viewAllSessions": "Ver todas las sesiones", "sidebar.project.clearNotifications": "Borrar notificaciones", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index a140c1e3a123..3e47a93e3de8 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -556,6 +556,8 @@ export const dict = { "sidebar.project.recentSessions": "Sessions récentes", "sidebar.project.viewAllSessions": "Voir toutes les sessions", "sidebar.project.clearNotifications": "Effacer les notifications", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Bureau", "settings.section.server": "Serveur", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 3da1c4b43b58..2fdc603c0766 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -546,6 +546,8 @@ export const dict = { "sidebar.project.recentSessions": "最近のセッション", "sidebar.project.viewAllSessions": "すべてのセッションを表示", "sidebar.project.clearNotifications": "通知をクリア", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "デスクトップ", "settings.section.server": "サーバー", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 0f2f7647abf5..4b25ab3e0194 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -547,6 +547,8 @@ export const dict = { "sidebar.project.recentSessions": "최근 세션", "sidebar.project.viewAllSessions": "모든 세션 보기", "sidebar.project.clearNotifications": "알림 지우기", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "데스크톱", "settings.section.server": "서버", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index a0a968179cd0..eaf97e28ba06 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -613,6 +613,8 @@ export const dict = { "sidebar.project.recentSessions": "Nylige sesjoner", "sidebar.project.viewAllSessions": "Vis alle sesjoner", "sidebar.project.clearNotifications": "Fjern varsler", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 88d209f11ff2..1957e34674e1 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -547,6 +547,8 @@ export const dict = { "sidebar.project.recentSessions": "Ostatnie sesje", "sidebar.project.viewAllSessions": "Zobacz wszystkie sesje", "sidebar.project.clearNotifications": "Wyczyść powiadomienia", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Pulpit", "settings.section.server": "Serwer", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 688289b7e812..eb57c56fcafb 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -611,6 +611,8 @@ export const dict = { "sidebar.project.recentSessions": "Недавние сессии", "sidebar.project.viewAllSessions": "Посмотреть все сессии", "sidebar.project.clearNotifications": "Очистить уведомления", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "Приложение", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 5decf3adb531..5edf671b2848 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -604,6 +604,8 @@ export const dict = { "sidebar.project.recentSessions": "เซสชันล่าสุด", "sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด", "sidebar.project.clearNotifications": "ล้างการแจ้งเตือน", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 6a3ade0d0b07..39839366be88 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -616,6 +616,8 @@ export const dict = { "sidebar.project.recentSessions": "Son oturumlar", "sidebar.project.viewAllSessions": "Tüm oturumları görüntüle", "sidebar.project.clearNotifications": "Bildirimleri temizle", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Masaüstü", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 28231733eaba..a5c44c92dd29 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -603,6 +603,8 @@ export const dict = { "sidebar.project.recentSessions": "最近会话", "sidebar.project.viewAllSessions": "查看全部会话", "sidebar.project.clearNotifications": "清除通知", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 4abdf5db574d..aff76c57a8a4 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -600,6 +600,8 @@ export const dict = { "sidebar.project.recentSessions": "最近工作階段", "sidebar.project.viewAllSessions": "查看全部工作階段", "sidebar.project.clearNotifications": "清除通知", + "sidebar.project.pin": "Pin project", + "sidebar.project.unpin": "Unpin project", "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "桌面", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index f402f4bc04df..4659cf48f8b4 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1441,6 +1441,10 @@ export default function Layout(props: ParentProps) { layout.sidebar.toggleWorkspaces(project.worktree) } + function pinProject(directory: string, pinned: boolean) { + layout.projects.pin(directory, pinned) + } + const showEditProjectDialog = (project: LocalProject) => { const run = ++dialogRun void import("@/components/dialog-edit-project").then((x) => { @@ -1848,10 +1852,14 @@ export default function Layout(props: ParentProps) { const { draggable, droppable } = event if (draggable && droppable) { const projects = layout.projects.list() - const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString()) - const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString()) + const source = projects.find((p) => p.worktree === draggable.id.toString()) + const target = projects.find((p) => p.worktree === droppable.id.toString()) + if (!source || !target) return + if (!!source.pinned !== !!target.pinned) return + const fromIndex = projects.findIndex((p) => p.worktree === source.worktree) + const toIndex = projects.findIndex((p) => p.worktree === target.worktree) if (fromIndex !== toIndex && toIndex !== -1) { - layout.projects.move(draggable.id.toString(), toIndex) + layout.projects.move(source.worktree, toIndex) } } } @@ -2005,6 +2013,7 @@ export default function Layout(props: ParentProps) { navigateToProject, openSidebar: () => layout.sidebar.open(), closeProject, + pinProject, showEditProjectDialog, toggleProjectWorkspaces, workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(), @@ -2159,6 +2168,19 @@ export default function Layout(props: ParentProps) { > {language.t("common.edit")} + { + const item = project() + if (!item) return + pinProject(item.worktree, !item.pinned) + }} + > + + {project()?.pinned ? language.t("sidebar.project.unpin") : language.t("sidebar.project.pin")} + + void openSidebar: () => void closeProject: (directory: string) => void + pinProject: (directory: string, pinned: boolean) => void showEditProjectDialog: (project: LocalProject) => void toggleProjectWorkspaces: (project: LocalProject) => void workspacesEnabled: (project: LocalProject) => boolean @@ -64,6 +65,7 @@ const ProjectTile = (props: { onProjectFocus: (worktree: string) => void navigateToProject: (directory: string) => void showEditProjectDialog: (project: LocalProject) => void + pinProject: (directory: string, pinned: boolean) => void toggleProjectWorkspaces: (project: LocalProject) => void workspacesEnabled: (project: LocalProject) => boolean closeProject: (directory: string) => void @@ -143,13 +145,29 @@ const ProjectTile = (props: { }} onBlur={() => props.setOpen(false)} > - +
+ + +
+ +
+
+
props.showEditProjectDialog(props.project)}> {props.language.t("common.edit")} + props.pinProject(props.project.worktree, !props.project.pinned)} + > + + {props.project.pinned ? props.language.t("sidebar.project.unpin") : props.language.t("sidebar.project.pin")} + + { const expanded = createMemo(() => !!props.mobile || props.opened()) const placement = () => (props.mobile ? "bottom" : "right") + const first = createMemo(() => props.projects().findIndex((project) => !project.pinned)) let panel: HTMLDivElement | undefined createEffect(() => { @@ -64,7 +65,16 @@ export const SidebarContent = (props: {
p.worktree)}> - {(project) => props.renderProject(project)} + + {(project, index) => ( + <> + 0 && index() === first()}> +