From 2aadf6fb687bd2cb9469b81fe0357d366efa538c Mon Sep 17 00:00:00 2001 From: anduimagui Date: Tue, 17 Mar 2026 21:29:36 +0000 Subject: [PATCH] feat(app): add project pinning to sidebar menu --- packages/app/src/context/server.tsx | 41 ++++++++++++++++++- packages/app/src/i18n/en.ts | 2 + .../app/src/pages/layout/sidebar-items.tsx | 26 ++++++++++++ .../app/src/pages/layout/sidebar-project.tsx | 3 +- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 1171ca90536..c953fcd03a5 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 @@ -276,9 +276,46 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( if (fromIndex === -1 || fromIndex === toIndex) return const result = [...current] const [item] = result.splice(fromIndex, 1) - result.splice(toIndex, 0, item) + if (!item) return + const pinned = result.filter((x) => !!x.pinned).length + const index = item.pinned + ? Math.max(0, Math.min(toIndex, pinned)) + : Math.max(pinned, Math.min(toIndex, result.length)) + result.splice(index, 0, item) setStore("projects", key, result) }, + pin(directory: string) { + const key = origin() + if (!key) return + const current = store.projects[key] ?? [] + const index = current.findIndex((x) => x.worktree === directory) + if (index === -1) return + if (current[index]?.pinned) return + const item = { ...current[index], pinned: true } + const rest = current.filter((_, i) => i !== index) + const pinned = rest.findLastIndex((x) => !!x.pinned) + const result = [...rest] + result.splice(pinned + 1, 0, item) + setStore("projects", key, result) + }, + unpin(directory: string) { + const key = origin() + if (!key) return + const current = store.projects[key] ?? [] + const index = current.findIndex((x) => x.worktree === directory) + if (index === -1) return + if (!current[index]?.pinned) return + const item = { ...current[index], pinned: false } + const rest = current.filter((_, i) => i !== index) + const pinned = rest.filter((x) => !!x.pinned).length + const result = [...rest] + result.splice(pinned, 0, item) + setStore("projects", key, result) + }, + isPinned(directory: string) { + const current = store.projects[origin()] ?? [] + return current.find((x) => x.worktree === directory)?.pinned ?? false + }, last() { const key = origin() if (!key) return diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index ad12e1e0de5..32e91e64b8c 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -666,6 +666,8 @@ export const dict = { "sidebar.nav.projectsAndSessions": "Projects and sessions", "sidebar.settings": "Settings", "sidebar.help": "Help", + "sidebar.project.pin": "Pin to top", + "sidebar.project.unpin": "Unpin", "sidebar.workspaces.enable": "Enable workspaces", "sidebar.workspaces.disable": "Disable workspaces", "sidebar.gettingStarted.title": "Getting started", diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index f8e16f3e122..130e3484616 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -1,5 +1,6 @@ import type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client" import { Avatar } from "@opencode-ai/ui/avatar" +import { ContextMenu } from "@opencode-ai/ui/context-menu" import { HoverCard } from "@opencode-ai/ui/hover-card" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -15,6 +16,7 @@ import { useLanguage } from "@/context/language" import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" +import { useServer } from "@/context/server" import { messageAgentColor } from "@/utils/agent" import { sessionPermissionRequest } from "../session/composer/session-request-tree" import { hasProjectPermissions } from "./helpers" @@ -65,6 +67,30 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti ) } +export const ProjectPinItem = (props: { project: LocalProject }): JSX.Element => { + const language = useLanguage() + const server = useServer() + return ( + { + if (server.projects.isPinned(props.project.worktree)) { + server.projects.unpin(props.project.worktree) + return + } + server.projects.pin(props.project.worktree) + }} + > + + {server.projects.isPinned(props.project.worktree) + ? language.t("sidebar.project.unpin") + : language.t("sidebar.project.pin")} + + + ) +} + export type SessionItemProps = { session: Session list: Session[] diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index a26bc183118..5fb4cbae144 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -10,7 +10,7 @@ import { useLayout, type LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" -import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items" +import { ProjectIcon, ProjectPinItem, SessionItem, type SessionItemProps } from "./sidebar-items" import { childMapByParent, displayName, sortedRootSessions } from "./helpers" export type ProjectSidebarContext = { @@ -146,6 +146,7 @@ const ProjectTile = (props: { props.showEditProjectDialog(props.project)}> {props.language.t("common.edit")} +