Skip to content

Commit 942bd41

Browse files
committed
fix(ui): keep permission dock buttons in view on long requests
Cap the dock with a measured max-height matching the available slot under the session header, and make the patterns row a flex grid track with min-height: 0 so long content scrolls inside the dock with a fading bottom mask instead of pushing the action buttons off-screen.
1 parent ba43706 commit 942bd41

2 files changed

Lines changed: 86 additions & 2 deletions

File tree

packages/app/src/pages/session/composer/session-permission-dock.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { For, Show } from "solid-js"
1+
import { For, Show, onCleanup, onMount } from "solid-js"
22
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
33
import { Button } from "@opencode-ai/ui/button"
44
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
55
import { Icon } from "@opencode-ai/ui/icon"
6+
import { makeEventListener } from "@solid-primitives/event-listener"
7+
import { createResizeObserver } from "@solid-primitives/resize-observer"
68
import { useLanguage } from "@/context/language"
79

810
export function SessionPermissionDock(props: {
@@ -19,9 +21,57 @@ export function SessionPermissionDock(props: {
1921
return value
2022
}
2123

24+
let root: HTMLDivElement | undefined
25+
26+
const measure = () => {
27+
if (!root) return
28+
29+
const scroller = document.querySelector(".scroll-view__viewport")
30+
const head = scroller instanceof HTMLElement ? scroller.firstElementChild : undefined
31+
const top =
32+
head instanceof HTMLElement && head.classList.contains("sticky") ? head.getBoundingClientRect().bottom : 0
33+
if (!top) {
34+
root.style.removeProperty("--permission-prompt-max-height")
35+
return
36+
}
37+
38+
const dock = root.closest('[data-component="session-prompt-dock"]')
39+
if (!(dock instanceof HTMLElement)) return
40+
41+
const dockBottom = dock.getBoundingClientRect().bottom
42+
const below = Math.max(0, dockBottom - root.getBoundingClientRect().bottom)
43+
const gap = 8
44+
const max = Math.max(240, Math.floor(dockBottom - top - gap - below))
45+
root.style.setProperty("--permission-prompt-max-height", `${max}px`)
46+
}
47+
48+
onMount(() => {
49+
let raf: number | undefined
50+
const update = () => {
51+
if (raf !== undefined) cancelAnimationFrame(raf)
52+
raf = requestAnimationFrame(() => {
53+
raf = undefined
54+
measure()
55+
})
56+
}
57+
58+
update()
59+
60+
makeEventListener(window, "resize", update)
61+
62+
const dock = root?.closest('[data-component="session-prompt-dock"]')
63+
const scroller = document.querySelector(".scroll-view__viewport")
64+
createResizeObserver([dock, scroller], update)
65+
66+
onCleanup(() => {
67+
if (raf !== undefined) cancelAnimationFrame(raf)
68+
})
69+
})
70+
2271
return (
2372
<DockPrompt
2473
kind="permission"
74+
ref={(el) => (root = el)}
2575
header={
2676
<div data-slot="permission-row" data-variant="header">
2777
<span data-slot="permission-icon">

packages/ui/src/components/message-part.css

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@
77
gap: 12px;
88
}
99

10+
@property --permission-bottom-fade {
11+
syntax: "<length>";
12+
inherits: false;
13+
initial-value: 0px;
14+
}
15+
16+
@keyframes permission-scroll-fade {
17+
0% {
18+
--permission-bottom-fade: 24px;
19+
}
20+
90% {
21+
--permission-bottom-fade: 24px;
22+
}
23+
100% {
24+
--permission-bottom-fade: 0px;
25+
}
26+
}
27+
1028
[data-component="user-message"] {
1129
font-family: var(--font-family-sans);
1230
font-size: var(--font-size-base);
@@ -720,7 +738,7 @@
720738
flex-direction: column;
721739
gap: 0;
722740
min-height: 0;
723-
max-height: 100dvh;
741+
max-height: var(--permission-prompt-max-height, 100dvh);
724742

725743
[data-slot="permission-body"] {
726744
display: flex;
@@ -746,6 +764,17 @@
746764
align-items: center;
747765
}
748766

767+
[data-slot="permission-row"]:has(> [data-slot="permission-patterns"]) {
768+
flex: 1;
769+
min-height: 0;
770+
grid-template-rows: minmax(0, 1fr);
771+
align-items: stretch;
772+
}
773+
774+
[data-slot="permission-row"]:has(> [data-slot="permission-patterns"]) [data-slot="permission-spacer"] {
775+
align-self: start;
776+
}
777+
749778
[data-slot="permission-icon"] {
750779
display: inline-flex;
751780
align-items: center;
@@ -792,6 +821,11 @@
792821
flex: 1;
793822
min-height: 0;
794823
overflow-y: auto;
824+
overscroll-behavior: contain;
825+
mask: linear-gradient(to bottom, #ffff calc(100% - var(--permission-bottom-fade)), #0000);
826+
animation: permission-scroll-fade;
827+
animation-timeline: --permission-scroll;
828+
scroll-timeline: --permission-scroll y;
795829
scrollbar-width: none;
796830
-ms-overflow-style: none;
797831

0 commit comments

Comments
 (0)