From 811035ba4f9ebb9f1543451bf64630aeb9b5fcd2 Mon Sep 17 00:00:00 2001 From: h Date: Sat, 29 Aug 2026 19:26:45 +0200 Subject: [PATCH] fix(ui): chat keeps the bottom pinned while async markdown lands --- ui/src/lib/panel/chat-view.svelte | 134 +++++++++++++++++------------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/ui/src/lib/panel/chat-view.svelte b/ui/src/lib/panel/chat-view.svelte index cc0252e..50e33a2 100644 --- a/ui/src/lib/panel/chat-view.svelte +++ b/ui/src/lib/panel/chat-view.svelte @@ -35,6 +35,7 @@ let showResults = $state(false); let loadedAt = $state(new Date(0).toISOString()); let scroller = $state(null); + let body = $state(null); let now = $state(Date.now()); 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(() => { const timer = setInterval(() => { if (model.running) { @@ -124,77 +140,83 @@
{ pinned = nearBottom(); }} bind:this={scroller} > - - - show tool results - - {#if failure} - - {:else if messages === null} -
- - - -
- {:else if messages.length === 0 && tail.length === 0} - - {:else} - {#each messages as message, index (index)} - {@const parts = blocks(message)} - {@const isResultOnly = parts.every((b) => b.type === "tool_result")} - {#if !(isResultOnly && !showResults)} -
+ + + show tool results + + {#if failure} + + {:else if messages === null} +
+ + + +
+ {:else if messages.length === 0 && tail.length === 0} + + {:else} + {#each messages as message, index (index)} + {@const parts = blocks(message)} + {@const isResultOnly = parts.every((b) => b.type === "tool_result")} + {#if !(isResultOnly && !showResults)} +
- {#each parts as block, blockIndex (blockIndex)} - {#if block.type === "text" && block.text} - + {#each parts as block, blockIndex (blockIndex)} + {#if block.type === "text" && block.text} + - {:else if block.type === "tool_use"} -

- {toolLabel(block.name ?? "?")} - - {clip(summarizeInput(block.name ?? "", block.input), 160)} - -

- {:else if block.type === "tool_result" && showResults} -
+              {:else if block.type === "tool_use"}
+                

+ {toolLabel(block.name ?? "?")} + + {clip(summarizeInput(block.name ?? "", block.input), 160)} + +

+ {:else if block.type === "tool_result" && showResults} +
{clip(resultText(block), RESULT_CLIP)}
- {:else if block.type === "thinking"} -

thinking

- {/if} - {/each} -
- {/if} + >{clip(resultText(block), RESULT_CLIP)} + {:else if block.type === "thinking"} +

thinking

+ {/if} + {/each} +
+ {/if} + {/each} + {/if} + {#if model.question} + + {/if} + {#each tail as turn (turn.id)} + {/each} - {/if} - {#if model.question} - - {/if} - {#each tail as turn (turn.id)} - - {/each} +