diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 7627d9ba1..29517b624 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -5,6 +5,7 @@ import { displayName, errorMessage, getDraggableId, + hasProjectPermissions, latestRootSession, syncWorkspaceOrder, workspaceKey, @@ -116,6 +117,29 @@ describe("layout workspace helpers", () => { expect(result?.id).toBe("workspace") }) + test("detects project permissions with a filter", () => { + const result = hasProjectPermissions( + { + root: [{ id: "perm-root" }, { id: "perm-hidden" }], + child: [{ id: "perm-child" }], + }, + (item) => item.id === "perm-child", + ) + + expect(result).toBe(true) + }) + + test("ignores project permissions filtered out", () => { + const result = hasProjectPermissions( + { + root: [{ id: "perm-root" }], + }, + () => false, + ) + + expect(result).toBe(false) + }) + test("ignores archived and child sessions when finding latest root session", () => { const result = latestRootSession( [ diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index be4297fbe..2c4b834be 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -33,6 +33,13 @@ export const latestRootSession = (stores: { session: Session[]; path: { director .flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory))) .sort(sortSessions(now))[0] +export function hasProjectPermissions( + request: Record, + include: (item: T) => boolean = () => true, +) { + return Object.values(request).some((list) => list?.some(include)) +} + export const childMapByParent = (sessions: Session[]) => { const map = new Map() for (const session of sessions) { diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 194f75f81..eecfd17b5 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -3,6 +3,7 @@ import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useLayout, type LocalProject, getAvatarColors } from "@/context/layout" import { useNotification } from "@/context/notification" +import { usePermission } from "@/context/permission" import { base64Encode } from "@opencode-ai/util/encode" import { Avatar } from "@opencode-ai/ui/avatar" import { DiffChanges } from "@opencode-ai/ui/diff-changes" @@ -16,16 +17,27 @@ import { getFilename } from "@opencode-ai/util/path" import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client" import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js" import { agentColor } from "@/utils/agent" +import { hasProjectPermissions } from "./helpers" +import { sessionPermissionRequest } from "../session/composer/session-request-tree" const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => { + const globalSync = useGlobalSync() const notification = useNotification() + const permission = usePermission() const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) const unseenCount = createMemo(() => dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) + const hasPermissions = createMemo(() => + dirs().some((directory) => { + const [store] = globalSync.child(directory, { bootstrap: false }) + return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory)) + }), + ) + const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0)) const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) return (
@@ -37,15 +49,16 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti } {...getAvatarColors(props.project.icon?.color)} class="size-full rounded" - classList={{ "badge-mask": unseenCount() > 0 && props.notify }} + classList={{ "badge-mask": notify() }} />
- 0 && props.notify}> +
@@ -186,19 +199,15 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { const layout = useLayout() const language = useLanguage() const notification = useNotification() + const permission = usePermission() const globalSync = useGlobalSync() const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.session.id)) const [sessionStore] = globalSync.child(props.session.directory) const hasPermissions = createMemo(() => { - const permissions = sessionStore.permission?.[props.session.id] ?? [] - if (permissions.length > 0) return true - - for (const id of props.children.get(props.session.id) ?? []) { - const childPermissions = sessionStore.permission?.[id] ?? [] - if (childPermissions.length > 0) return true - } - return false + return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => { + return !permission.autoResponds(item, props.session.directory) + }) }) const isWorking = createMemo(() => { if (hasPermissions()) return false