summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-26 20:40:39 -0600
committerAdam <[email protected]>2026-02-26 20:40:39 -0600
commitb0b88f679216d1b7e16bb1098cef0d91f911d9dd (patch)
tree84ee9c9c55eb302d86e61895c1b0d43aa5d95b8d
parente9a7c7114184d0092c114ce7a7d9446cf0d366cc (diff)
downloadopencode-b0b88f679216d1b7e16bb1098cef0d91f911d9dd.tar.gz
opencode-b0b88f679216d1b7e16bb1098cef0d91f911d9dd.zip
fix(app): permission indicator
-rw-r--r--packages/app/src/pages/layout/helpers.test.ts24
-rw-r--r--packages/app/src/pages/layout/helpers.ts7
-rw-r--r--packages/app/src/pages/layout/sidebar-items.tsx33
3 files changed, 52 insertions, 12 deletions
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<T>(
+ request: Record<string, T[] | undefined>,
+ include: (item: T) => boolean = () => true,
+) {
+ return Object.values(request).some((list) => list?.some(include))
+}
+
export const childMapByParent = (sessions: Session[]) => {
const map = new Map<string, string[]>()
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 (
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
@@ -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() }}
/>
</div>
- <Show when={unseenCount() > 0 && props.notify}>
+ <Show when={notify()}>
<div
classList={{
"absolute top-px right-px size-1.5 rounded-full z-10": true,
- "bg-icon-critical-base": hasError(),
- "bg-text-interactive-base": !hasError(),
+ "bg-surface-warning-strong": hasPermissions(),
+ "bg-icon-critical-base": !hasPermissions() && hasError(),
+ "bg-text-interactive-base": !hasPermissions() && !hasError(),
}}
/>
</Show>
@@ -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