Skip to content

Commit b0b88f6

Browse files
committed
fix(app): permission indicator
1 parent e9a7c71 commit b0b88f6

3 files changed

Lines changed: 52 additions & 12 deletions

File tree

packages/app/src/pages/layout/helpers.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
displayName,
66
errorMessage,
77
getDraggableId,
8+
hasProjectPermissions,
89
latestRootSession,
910
syncWorkspaceOrder,
1011
workspaceKey,
@@ -116,6 +117,29 @@ describe("layout workspace helpers", () => {
116117
expect(result?.id).toBe("workspace")
117118
})
118119

120+
test("detects project permissions with a filter", () => {
121+
const result = hasProjectPermissions(
122+
{
123+
root: [{ id: "perm-root" }, { id: "perm-hidden" }],
124+
child: [{ id: "perm-child" }],
125+
},
126+
(item) => item.id === "perm-child",
127+
)
128+
129+
expect(result).toBe(true)
130+
})
131+
132+
test("ignores project permissions filtered out", () => {
133+
const result = hasProjectPermissions(
134+
{
135+
root: [{ id: "perm-root" }],
136+
},
137+
() => false,
138+
)
139+
140+
expect(result).toBe(false)
141+
})
142+
119143
test("ignores archived and child sessions when finding latest root session", () => {
120144
const result = latestRootSession(
121145
[

packages/app/src/pages/layout/helpers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ export const latestRootSession = (stores: { session: Session[]; path: { director
3333
.flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)))
3434
.sort(sortSessions(now))[0]
3535

36+
export function hasProjectPermissions<T>(
37+
request: Record<string, T[] | undefined>,
38+
include: (item: T) => boolean = () => true,
39+
) {
40+
return Object.values(request).some((list) => list?.some(include))
41+
}
42+
3643
export const childMapByParent = (sessions: Session[]) => {
3744
const map = new Map<string, string[]>()
3845
for (const session of sessions) {

packages/app/src/pages/layout/sidebar-items.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useGlobalSync } from "@/context/global-sync"
33
import { useLanguage } from "@/context/language"
44
import { useLayout, type LocalProject, getAvatarColors } from "@/context/layout"
55
import { useNotification } from "@/context/notification"
6+
import { usePermission } from "@/context/permission"
67
import { base64Encode } from "@opencode-ai/util/encode"
78
import { Avatar } from "@opencode-ai/ui/avatar"
89
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
@@ -16,16 +17,27 @@ import { getFilename } from "@opencode-ai/util/path"
1617
import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client"
1718
import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
1819
import { agentColor } from "@/utils/agent"
20+
import { hasProjectPermissions } from "./helpers"
21+
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
1922

2023
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
2124

2225
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
26+
const globalSync = useGlobalSync()
2327
const notification = useNotification()
28+
const permission = usePermission()
2429
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
2530
const unseenCount = createMemo(() =>
2631
dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
2732
)
2833
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
34+
const hasPermissions = createMemo(() =>
35+
dirs().some((directory) => {
36+
const [store] = globalSync.child(directory, { bootstrap: false })
37+
return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory))
38+
}),
39+
)
40+
const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0))
2941
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
3042
return (
3143
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
@@ -37,15 +49,16 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
3749
}
3850
{...getAvatarColors(props.project.icon?.color)}
3951
class="size-full rounded"
40-
classList={{ "badge-mask": unseenCount() > 0 && props.notify }}
52+
classList={{ "badge-mask": notify() }}
4153
/>
4254
</div>
43-
<Show when={unseenCount() > 0 && props.notify}>
55+
<Show when={notify()}>
4456
<div
4557
classList={{
4658
"absolute top-px right-px size-1.5 rounded-full z-10": true,
47-
"bg-icon-critical-base": hasError(),
48-
"bg-text-interactive-base": !hasError(),
59+
"bg-surface-warning-strong": hasPermissions(),
60+
"bg-icon-critical-base": !hasPermissions() && hasError(),
61+
"bg-text-interactive-base": !hasPermissions() && !hasError(),
4962
}}
5063
/>
5164
</Show>
@@ -186,19 +199,15 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
186199
const layout = useLayout()
187200
const language = useLanguage()
188201
const notification = useNotification()
202+
const permission = usePermission()
189203
const globalSync = useGlobalSync()
190204
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
191205
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
192206
const [sessionStore] = globalSync.child(props.session.directory)
193207
const hasPermissions = createMemo(() => {
194-
const permissions = sessionStore.permission?.[props.session.id] ?? []
195-
if (permissions.length > 0) return true
196-
197-
for (const id of props.children.get(props.session.id) ?? []) {
198-
const childPermissions = sessionStore.permission?.[id] ?? []
199-
if (childPermissions.length > 0) return true
200-
}
201-
return false
208+
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
209+
return !permission.autoResponds(item, props.session.directory)
210+
})
202211
})
203212
const isWorking = createMemo(() => {
204213
if (hasPermissions()) return false

0 commit comments

Comments
 (0)