summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-27 20:40:25 -0600
committerAdam <[email protected]>2025-12-27 20:40:25 -0600
commit1b5bf32ce560f44dd558f3951121c3f4b0560e85 (patch)
tree14a0f32dacd9deaf6bdd57bf5ccf03d6a9b8747e
parent2e972b3fdc4b7f3ccb2224f693391a8f11572c3c (diff)
downloadopencode-1b5bf32ce560f44dd558f3951121c3f4b0560e85.tar.gz
opencode-1b5bf32ce560f44dd558f3951121c3f4b0560e85.zip
chore: permissions ux
-rw-r--r--packages/app/src/pages/layout.tsx38
-rw-r--r--packages/ui/src/components/session-turn.tsx6
-rw-r--r--packages/ui/src/components/toast.tsx5
-rw-r--r--packages/ui/src/hooks/create-auto-scroll.tsx17
4 files changed, 56 insertions, 10 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index bd368bb6c..0b4e040b7 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -41,7 +41,7 @@ import {
} from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import { useProviders } from "@/hooks/use-providers"
-import { showToast, Toast } from "@opencode-ai/ui/toast"
+import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
@@ -118,13 +118,15 @@ export default function Layout(props: ParentProps) {
})
onMount(() => {
- const seenPermissions = new Set<string>()
+ const seenSessions = new Set<string>()
+ const toastBySession = new Map<string, number>()
const unsub = globalSDK.event.listen((e) => {
if (e.details?.type !== "permission.updated") return
const directory = e.name
const permission = e.details.properties
- if (seenPermissions.has(permission.id)) return
- seenPermissions.add(permission.id)
+ const sessionKey = `${directory}:${permission.sessionID}`
+ if (seenSessions.has(sessionKey)) return
+ seenSessions.add(sessionKey)
const currentDir = params.dir ? base64Decode(params.dir) : undefined
const currentSession = params.id
if (directory === currentDir && permission.sessionID === currentSession) return
@@ -133,7 +135,7 @@ export default function Layout(props: ParentProps) {
if (directory === currentDir && session?.parentID === currentSession) return
const sessionTitle = session?.title ?? "New session"
const projectName = getFilename(directory)
- showToast({
+ const toastId = showToast({
persistent: true,
icon: "checklist",
title: "Permission required",
@@ -144,7 +146,6 @@ export default function Layout(props: ParentProps) {
onClick: () => {
navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`)
},
- dismissAfter: true,
},
{
label: "Dismiss",
@@ -152,8 +153,33 @@ export default function Layout(props: ParentProps) {
},
],
})
+ toastBySession.set(sessionKey, toastId)
})
onCleanup(unsub)
+
+ createEffect(() => {
+ const currentDir = params.dir ? base64Decode(params.dir) : undefined
+ const currentSession = params.id
+ if (!currentDir || !currentSession) return
+ const sessionKey = `${currentDir}:${currentSession}`
+ const toastId = toastBySession.get(sessionKey)
+ if (toastId !== undefined) {
+ toaster.dismiss(toastId)
+ toastBySession.delete(sessionKey)
+ seenSessions.delete(sessionKey)
+ }
+ const [store] = globalSync.child(currentDir)
+ const childSessions = store.session.filter((s) => s.parentID === currentSession)
+ for (const child of childSessions) {
+ const childKey = `${currentDir}:${child.id}`
+ const childToastId = toastBySession.get(childKey)
+ if (childToastId !== undefined) {
+ toaster.dismiss(childToastId)
+ toastBySession.delete(childKey)
+ seenSessions.delete(childKey)
+ }
+ }
+ })
})
function sortSessions(a: Session, b: Session) {
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index ce4845a71..8cb426387 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -329,6 +329,12 @@ export function SessionTurn(
})
createEffect(() => {
+ if (permissionParts().length > 0) {
+ autoScroll.forceScrollToBottom()
+ }
+ })
+
+ createEffect(() => {
if (working() || !isLastUserMessage()) return
const diffs = message()?.summary?.diffs
diff --git a/packages/ui/src/components/toast.tsx b/packages/ui/src/components/toast.tsx
index 7e90e9f2f..f34c46d42 100644
--- a/packages/ui/src/components/toast.tsx
+++ b/packages/ui/src/components/toast.tsx
@@ -92,7 +92,6 @@ export type ToastVariant = "default" | "success" | "error" | "loading"
export interface ToastAction {
label: string
onClick: "dismiss" | (() => void)
- dismissAfter?: boolean
}
export interface ToastOptions {
@@ -132,10 +131,8 @@ export function showToast(options: ToastOptions | string) {
onClick={() => {
if (typeof action.onClick === "function") {
action.onClick()
- if (action.dismissAfter) toaster.dismiss(props.toastId)
- } else {
- toaster.dismiss(props.toastId)
}
+ toaster.dismiss(props.toastId)
}}
>
{action.label}
diff --git a/packages/ui/src/hooks/create-auto-scroll.tsx b/packages/ui/src/hooks/create-auto-scroll.tsx
index e262b7c69..b780f47c6 100644
--- a/packages/ui/src/hooks/create-auto-scroll.tsx
+++ b/packages/ui/src/hooks/create-auto-scroll.tsx
@@ -35,6 +35,22 @@ export function createAutoScroll(options: AutoScrollOptions) {
})
}
+ function forceScrollToBottom() {
+ if (!scrollRef) return
+
+ setStore("userScrolled", false)
+ isAutoScrolling = true
+ if (autoScrollTimeout) clearTimeout(autoScrollTimeout)
+ autoScrollTimeout = setTimeout(() => {
+ isAutoScrolling = false
+ }, 1000)
+
+ scrollRef.scrollTo({
+ top: scrollRef.scrollHeight,
+ behavior: "smooth",
+ })
+ }
+
function handleScroll() {
if (!scrollRef) return
@@ -161,6 +177,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
handleScroll,
handleInteraction,
scrollToBottom,
+ forceScrollToBottom,
userScrolled: () => store.userScrolled,
}
}