diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 2f85652a93e..79d3251797a 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -372,11 +372,13 @@ export const PromptInput: Component = (props) => { type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string } - const agentList = createMemo(() => - sync.data.agent + const agentList = createMemo(() => { + const agents = sync.data.agent + if (!Array.isArray(agents)) return [] + return agents .filter((agent) => !agent.hidden && agent.mode !== "primary") - .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })), - ) + .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })) + }) const handleAtSelect = (option: AtOption | undefined) => { if (!option) return @@ -1547,8 +1549,8 @@ export const PromptInput: Component = (props) => { -
-
+
+
@@ -1601,7 +1603,7 @@ export const PromptInput: Component = (props) => { >
-
+
- + {(summary) => (
@@ -1141,7 +1141,12 @@ export default function Layout(props: ParentProps) {
+ {/* Scroll to bottom button */} + +
+ +
+
+ {/* Prompt input */}
(promptDock = el)} diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 1e3cc0b2921..e6048919712 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -6,6 +6,7 @@ display: flex; align-items: flex-start; justify-content: flex-start; + overflow-x: hidden; [data-slot="session-turn-content"] { flex-grow: 1; @@ -13,6 +14,7 @@ height: 100%; min-width: 0; overflow-y: auto; + overflow-x: hidden; scrollbar-width: none; } @@ -28,37 +30,6 @@ min-width: 0; gap: 28px; overflow-anchor: none; - - [data-slot="session-turn-user-badges"] { - position: absolute; - right: 0; - display: flex; - gap: 6px; - padding-left: 16px; - background: linear-gradient(to right, transparent, var(--background-stronger) 12px); - opacity: 0; - transition: opacity 0.15s ease; - pointer-events: none; - } - - &:hover [data-slot="session-turn-user-badges"] { - opacity: 1; - pointer-events: auto; - } - - [data-slot="session-turn-badge"] { - display: inline-flex; - align-items: center; - padding: 2px 6px; - border-radius: 4px; - font-family: var(--font-family-mono); - font-size: var(--font-size-x-small); - font-weight: var(--font-weight-medium); - line-height: var(--line-height-normal); - white-space: nowrap; - color: var(--text-base); - background: var(--surface-raised-base); - } } [data-slot="session-turn-sticky-title"] { @@ -83,6 +54,7 @@ [data-slot="session-turn-message-header"] { display: flex; align-items: center; + gap: 8px; align-self: stretch; height: 32px; } diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index ae1321bac14..55991f39117 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -337,7 +337,20 @@ export function SessionTurn( const handleCopyResponse = async () => { const content = response() if (!content) return - await navigator.clipboard.writeText(content) + try { + await navigator.clipboard.writeText(content) + } catch { + // Fallback for iOS Safari + const textarea = document.createElement("textarea") + textarea.value = content + textarea.style.position = "fixed" + textarea.style.opacity = "0" + document.body.appendChild(textarea) + textarea.focus() + textarea.select() + document.execCommand("copy") + document.body.removeChild(textarea) + } setResponseCopied(true) setTimeout(() => setResponseCopied(false), 2000) } diff --git a/packages/ui/src/components/toast.css b/packages/ui/src/components/toast.css index 1459bb18903..fe8f1101aa4 100644 --- a/packages/ui/src/components/toast.css +++ b/packages/ui/src/components/toast.css @@ -1,15 +1,25 @@ [data-component="toast-region"] { position: fixed; bottom: 48px; - right: 32px; z-index: 1000; display: flex; flex-direction: column; gap: 8px; max-width: 400px; - width: 100%; + width: calc(100% - 32px); pointer-events: none; + /* Center on mobile, right-aligned on desktop */ + left: 50%; + transform: translateX(-50%); + + @media (min-width: 768px) { + left: auto; + right: 32px; + transform: none; + width: 100%; + } + [data-slot="toast-list"] { display: flex; flex-direction: column; diff --git a/packages/ui/src/hooks/create-auto-scroll.tsx b/packages/ui/src/hooks/create-auto-scroll.tsx index b9eae54881d..6835e99e394 100644 --- a/packages/ui/src/hooks/create-auto-scroll.tsx +++ b/packages/ui/src/hooks/create-auto-scroll.tsx @@ -17,6 +17,7 @@ export function createAutoScroll(options: AutoScrollOptions) { const [store, setStore] = createStore({ contentRef: undefined as HTMLElement | undefined, userScrolled: false, + notAtBottom: false, }) const active = () => options.working() || settling @@ -83,10 +84,18 @@ export function createAutoScroll(options: AutoScrollOptions) { } const handleScroll = () => { - if (!active()) return if (!scroll) return - if (distanceFromBottom() < 10) { + // Always track if we're at the bottom for the scroll button + const distance = distanceFromBottom() + const atBottom = distance < 50 + if (store.notAtBottom !== !atBottom) { + setStore("notAtBottom", !atBottom) + } + + if (!active()) return + + if (distance < 10) { if (store.userScrolled) setStore("userScrolled", false) return } @@ -113,7 +122,11 @@ export function createAutoScroll(options: AutoScrollOptions) { if (settleTimer) clearTimeout(settleTimer) settleTimer = undefined - setStore("userScrolled", false) + // Only reset userScrolled if we're actually at the bottom + // This preserves the scroll button when returning to a scrolled conversation + if (distanceFromBottom() < 50) { + setStore("userScrolled", false) + } if (working) { scrollToBottom(true) @@ -149,6 +162,11 @@ export function createAutoScroll(options: AutoScrollOptions) { el.addEventListener("pointerdown", handlePointerDown) el.addEventListener("touchstart", handleTouchStart, { passive: true }) + // Check initial scroll position after layout is complete + requestAnimationFrame(() => { + handleScroll() + }) + cleanup = () => { el.removeEventListener("wheel", handleWheel) el.removeEventListener("pointerdown", handlePointerDown) @@ -163,5 +181,6 @@ export function createAutoScroll(options: AutoScrollOptions) { scrollToBottom: () => scrollToBottom(false), forceScrollToBottom: () => scrollToBottom(true), userScrolled: () => store.userScrolled, + notAtBottom: () => store.notAtBottom, } }