Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,13 @@ export const PromptInput: Component<PromptInputProps> = (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
Expand Down Expand Up @@ -1547,8 +1549,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
</Show>
</div>
<div class="relative p-3 flex items-center justify-between">
<div class="flex items-center justify-start gap-0.5">
<div class="relative p-2 md:p-3 flex items-center justify-between gap-1.5 md:gap-2">
<div class="flex items-center justify-start gap-0 min-w-0 overflow-hidden">
<Switch>
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
Expand Down Expand Up @@ -1601,7 +1603,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
>
<Button
variant="ghost"
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
class="text-text-base capitalize text-12-regular"
onClick={() => local.model.variant.cycle()}
>
{local.model.variant.current() ?? "Default"}
Expand All @@ -1618,7 +1620,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
classList={{
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
"flex size-6 items-center justify-center": true,
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
}}
Expand All @@ -1634,7 +1636,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Match>
</Switch>
</div>
<div class="flex items-center gap-3 absolute right-2 bottom-2">
<div class="flex items-center gap-1.5 md:gap-3 absolute right-1.5 md:right-2 bottom-1.5 md:bottom-2">
<input
ref={fileInputRef}
type="file"
Expand Down
9 changes: 7 additions & 2 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,7 @@ export default function Layout(props: ParentProps) {
stopPropagation
/>
</Tooltip>
<Show when={props.session.summary}>
<Show when={!props.mobile && props.session.summary}>
{(summary) => (
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
<DiffChanges changes={summary()} />
Expand All @@ -1141,7 +1141,12 @@ export default function Layout(props: ParentProps) {
</div>
</A>
<div
class={`hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"}`}
classList={{
"text-text-base gap-1 items-center absolute": true,
[props.dense ? "top-0.5 right-0.5" : "top-1 right-1"]: true,
flex: props.mobile,
"hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex": !props.mobile,
}}
>
<TooltipKeybind
placement={props.mobile ? "bottom" : "right"}
Expand Down
14 changes: 14 additions & 0 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,20 @@ export default function Page() {
</Switch>
</div>

{/* Scroll to bottom button */}
<Show when={autoScroll.notAtBottom() && params.id}>
<div class="absolute inset-x-0 bottom-[calc(var(--prompt-height,8rem)+16px)] md:bottom-[calc(var(--prompt-height,10rem)+32px)] flex justify-center z-50 pointer-events-none">
<button
type="button"
onClick={() => autoScroll.forceScrollToBottom()}
class="pointer-events-auto flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-float-base border border-border-weak-base shadow-md text-text-base text-12-medium hover:bg-surface-float-strong transition-colors"
>
<Icon name="chevron-down" class="size-3.5" />
<span>Scroll to bottom</span>
</button>
</div>
</Show>

{/* Prompt input */}
<div
ref={(el) => (promptDock = el)}
Expand Down
34 changes: 3 additions & 31 deletions packages/ui/src/components/session-turn.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
display: flex;
align-items: flex-start;
justify-content: flex-start;
overflow-x: hidden;

[data-slot="session-turn-content"] {
flex-grow: 1;
width: 100%;
height: 100%;
min-width: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
}

Expand All @@ -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"] {
Expand All @@ -83,6 +54,7 @@
[data-slot="session-turn-message-header"] {
display: flex;
align-items: center;
gap: 8px;
align-self: stretch;
height: 32px;
}
Expand Down
15 changes: 14 additions & 1 deletion packages/ui/src/components/session-turn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/components/toast.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
25 changes: 22 additions & 3 deletions packages/ui/src/hooks/create-auto-scroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -163,5 +181,6 @@ export function createAutoScroll(options: AutoScrollOptions) {
scrollToBottom: () => scrollToBottom(false),
forceScrollToBottom: () => scrollToBottom(true),
userScrolled: () => store.userScrolled,
notAtBottom: () => store.notAtBottom,
}
}