fix(ui): chat keeps the bottom pinned while async markdown lands
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
let showResults = $state(false);
|
let showResults = $state(false);
|
||||||
let loadedAt = $state(new Date(0).toISOString());
|
let loadedAt = $state(new Date(0).toISOString());
|
||||||
let scroller = $state<HTMLDivElement | null>(null);
|
let scroller = $state<HTMLDivElement | null>(null);
|
||||||
|
let body = $state<HTMLDivElement | null>(null);
|
||||||
let now = $state(Date.now());
|
let now = $state(Date.now());
|
||||||
let pinned = true;
|
let pinned = true;
|
||||||
|
|
||||||
@@ -90,6 +91,21 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Markdown lands asynchronously (Obsidian renders after the DOM exists),
|
||||||
|
// so follow the content's height, not just the message count.
|
||||||
|
$effect(() => {
|
||||||
|
if (!(scroller && body)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
if (pinned && scroller) {
|
||||||
|
scroller.scrollTop = scroller.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(body);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
if (model.running) {
|
if (model.running) {
|
||||||
@@ -124,13 +140,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-3 py-3 @md:px-6"
|
class="min-h-0 flex-1 overflow-y-auto px-3 py-3 @md:px-6"
|
||||||
onscroll={() => {
|
onscroll={() => {
|
||||||
pinned = nearBottom();
|
pinned = nearBottom();
|
||||||
}}
|
}}
|
||||||
bind:this={scroller}
|
bind:this={scroller}
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-2 self-end text-muted-foreground text-xs">
|
<div class="flex flex-col gap-3" bind:this={body}>
|
||||||
|
<span
|
||||||
|
class="flex items-center gap-2 self-end text-muted-foreground text-xs"
|
||||||
|
>
|
||||||
<Switch aria-label="Show tool results" bind:checked={showResults} />
|
<Switch aria-label="Show tool results" bind:checked={showResults} />
|
||||||
show tool results
|
show tool results
|
||||||
</span>
|
</span>
|
||||||
@@ -169,7 +188,9 @@
|
|||||||
/>
|
/>
|
||||||
{:else if block.type === "tool_use"}
|
{:else if block.type === "tool_use"}
|
||||||
<p class="flex items-baseline gap-2 px-1 text-xs">
|
<p class="flex items-baseline gap-2 px-1 text-xs">
|
||||||
<span class="font-medium">{toolLabel(block.name ?? "?")}</span>
|
<span class="font-medium"
|
||||||
|
>{toolLabel(block.name ?? "?")}</span
|
||||||
|
>
|
||||||
<span class="truncate text-muted-foreground">
|
<span class="truncate text-muted-foreground">
|
||||||
{clip(summarizeInput(block.name ?? "", block.input), 160)}
|
{clip(summarizeInput(block.name ?? "", block.input), 160)}
|
||||||
</span>
|
</span>
|
||||||
@@ -197,4 +218,5 @@
|
|||||||
{#each tail as turn (turn.id)}
|
{#each tail as turn (turn.id)}
|
||||||
<TurnCard {now} {turn} />
|
<TurnCard {now} {turn} />
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user