fix(app): permission indicator
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
displayName,
|
displayName,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
getDraggableId,
|
getDraggableId,
|
||||||
|
hasProjectPermissions,
|
||||||
latestRootSession,
|
latestRootSession,
|
||||||
syncWorkspaceOrder,
|
syncWorkspaceOrder,
|
||||||
workspaceKey,
|
workspaceKey,
|
||||||
@@ -116,6 +117,29 @@ describe("layout workspace helpers", () => {
|
|||||||
expect(result?.id).toBe("workspace")
|
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", () => {
|
test("ignores archived and child sessions when finding latest root session", () => {
|
||||||
const result = latestRootSession(
|
const result = latestRootSession(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ export const latestRootSession = (stores: { session: Session[]; path: { director
|
|||||||
.flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)))
|
.flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)))
|
||||||
.sort(sortSessions(now))[0]
|
.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[]) => {
|
export const childMapByParent = (sessions: Session[]) => {
|
||||||
const map = new Map<string, string[]>()
|
const map = new Map<string, string[]>()
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useGlobalSync } from "@/context/global-sync"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout, type LocalProject, getAvatarColors } from "@/context/layout"
|
import { useLayout, type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
|
import { usePermission } from "@/context/permission"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
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 { 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 { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
|
||||||
import { agentColor } from "@/utils/agent"
|
import { agentColor } from "@/utils/agent"
|
||||||
|
import { hasProjectPermissions } from "./helpers"
|
||||||
|
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
||||||
|
|
||||||
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||||
|
|
||||||
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
||||||
|
const globalSync = useGlobalSync()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
|
const permission = usePermission()
|
||||||
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
|
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
|
||||||
const unseenCount = createMemo(() =>
|
const unseenCount = createMemo(() =>
|
||||||
dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
|
dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
|
||||||
)
|
)
|
||||||
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
|
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))
|
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||||
return (
|
return (
|
||||||
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
|
<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)}
|
{...getAvatarColors(props.project.icon?.color)}
|
||||||
class="size-full rounded"
|
class="size-full rounded"
|
||||||
classList={{ "badge-mask": unseenCount() > 0 && props.notify }}
|
classList={{ "badge-mask": notify() }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Show when={unseenCount() > 0 && props.notify}>
|
<Show when={notify()}>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"absolute top-px right-px size-1.5 rounded-full z-10": true,
|
"absolute top-px right-px size-1.5 rounded-full z-10": true,
|
||||||
"bg-icon-critical-base": hasError(),
|
"bg-surface-warning-strong": hasPermissions(),
|
||||||
"bg-text-interactive-base": !hasError(),
|
"bg-icon-critical-base": !hasPermissions() && hasError(),
|
||||||
|
"bg-text-interactive-base": !hasPermissions() && !hasError(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -186,19 +199,15 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
|
const permission = usePermission()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
|
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
|
||||||
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
|
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
|
||||||
const [sessionStore] = globalSync.child(props.session.directory)
|
const [sessionStore] = globalSync.child(props.session.directory)
|
||||||
const hasPermissions = createMemo(() => {
|
const hasPermissions = createMemo(() => {
|
||||||
const permissions = sessionStore.permission?.[props.session.id] ?? []
|
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
|
||||||
if (permissions.length > 0) return true
|
return !permission.autoResponds(item, props.session.directory)
|
||||||
|
})
|
||||||
for (const id of props.children.get(props.session.id) ?? []) {
|
|
||||||
const childPermissions = sessionStore.permission?.[id] ?? []
|
|
||||||
if (childPermissions.length > 0) return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
const isWorking = createMemo(() => {
|
const isWorking = createMemo(() => {
|
||||||
if (hasPermissions()) return false
|
if (hasPermissions()) return false
|
||||||
|
|||||||
Reference in New Issue
Block a user