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 72caed40ad9..80c42121277 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -667,6 +667,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 25282645615..215be626845 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 = {
@@ -147,6 +147,7 @@ const ProjectTile = (props: {
props.showEditProjectDialog(props.project)}>
{props.language.t("common.edit")}
+