scroll fixes + activity bar position
- Activity bar moved to bottom of message list (before messagesEnd) - Smart scroll: auto-scroll only during streaming or when near bottom - Scrolling up pauses auto-scroll until next send - Removed duplicate $effect block - Plan: tool timeline in sidebar (plans/2026-07-14-tool-timeline-sidebar.md)
This commit is contained in:
@@ -20,6 +20,31 @@
|
||||
|
||||
let input = $state('')
|
||||
let messagesEnd = $state<HTMLDivElement | null>(null)
|
||||
let scrolledUp = $state(false)
|
||||
let container = $state<HTMLDivElement | null>(null)
|
||||
|
||||
function isNearBottom(): boolean {
|
||||
if (!container) return true
|
||||
const { scrollTop, scrollHeight, clientHeight } = container
|
||||
return scrollHeight - scrollTop - clientHeight < 80
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
scrolledUp = !isNearBottom()
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom on new messages — unless user scrolled up to read.
|
||||
$effect(() => {
|
||||
void $messages
|
||||
if ($streaming || !scrolledUp) {
|
||||
setTimeout(() => messagesEnd?.scrollIntoView({ behavior: 'smooth' }), 50)
|
||||
}
|
||||
})
|
||||
|
||||
// Reset scroll lock when user sends a message.
|
||||
function submitFollows() {
|
||||
scrolledUp = false
|
||||
}
|
||||
|
||||
const MAX_INLINE_CARDS = 5
|
||||
|
||||
@@ -63,12 +88,6 @@
|
||||
window.addEventListener('pointerup', up)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void $messages
|
||||
void $streaming
|
||||
setTimeout(() => messagesEnd?.scrollIntoView({ behavior: 'smooth' }), 50)
|
||||
})
|
||||
|
||||
function render(text: string): string {
|
||||
return DOMPurify.sanitize(marked.parse(text, { async: false }) as string)
|
||||
}
|
||||
@@ -77,6 +96,7 @@
|
||||
const text = input.trim()
|
||||
if (!text || $streaming) return
|
||||
input = ''
|
||||
scrolledUp = false
|
||||
sendMessage(text)
|
||||
}
|
||||
|
||||
@@ -113,7 +133,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
||||
<div class="mx-auto flex max-w-3xl flex-col gap-5 p-4">
|
||||
{#if $messages.length === 0}
|
||||
<div class="flex flex-col items-center gap-6 pt-24 text-center">
|
||||
@@ -131,30 +151,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $currentSession && $messages.length > 0}
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{#if $streaming}
|
||||
<LoaderCircleIcon class="size-3 shrink-0 animate-spin text-primary" />
|
||||
<span>Agent is responding…</span>
|
||||
{:else if liveStatus === 'executing'}
|
||||
<LoaderCircleIcon class="size-3 shrink-0 animate-spin text-warning" />
|
||||
<span>Working — {statusLabel(liveStatus)}</span>
|
||||
<Button size="xs" variant="outline" class="ml-auto h-6 text-[11px]" onclick={async () => {
|
||||
if ($currentSession) { await resumeSession($currentSession) }
|
||||
}}>Continue</Button>
|
||||
{:else if liveStatus === 'awaiting_input'}
|
||||
<span class="text-warning">Waiting for your answer</span>
|
||||
{:else if liveStatus}
|
||||
<span>Status: {statusLabel(liveStatus)}</span>
|
||||
{:else}
|
||||
<span class="text-muted-foreground">Session ended</span>
|
||||
{/if}
|
||||
{#if $currentTask?.goal}
|
||||
<span class="text-muted-foreground">· {$currentTask.goal.slice(0, 60)}{$currentTask.goal.length > 60 ? '…' : ''}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each $messages as msg, i (msg.id)}
|
||||
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
|
||||
{#if msg.role === 'user'}
|
||||
@@ -191,6 +187,27 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $currentSession && $messages.length > 0}
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground py-1">
|
||||
{#if $streaming}
|
||||
<LoaderCircleIcon class="size-3 shrink-0 animate-spin text-primary" />
|
||||
<span>Agent is responding…</span>
|
||||
{:else if liveStatus === 'executing'}
|
||||
<LoaderCircleIcon class="size-3 shrink-0 animate-spin text-warning" />
|
||||
<span>Working — {statusLabel(liveStatus)}</span>
|
||||
<Button size="xs" variant="outline" class="ml-auto h-6 text-[11px]" onclick={async () => {
|
||||
if ($currentSession) { await resumeSession($currentSession) }
|
||||
}}>Continue</Button>
|
||||
{:else if liveStatus === 'awaiting_input'}
|
||||
<span class="text-warning">Waiting for your answer</span>
|
||||
{:else if liveStatus}
|
||||
<span>Status: {statusLabel(liveStatus)}</span>
|
||||
{/if}
|
||||
{#if $currentTask?.goal}
|
||||
<span class="text-muted-foreground">· {$currentTask.goal.slice(0, 60)}{$currentTask.goal.length > 60 ? '…' : ''}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div bind:this={messagesEnd}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user