fix(electrobun): remove requestAnimationFrame scroll effect (was contributing to effect cycle)

This commit is contained in:
Hibryda 2026-03-23 22:04:33 +01:00
parent 085b88107f
commit 02560e341d

View file

@ -1,8 +1,7 @@
<script lang="ts">
import { tick } from 'svelte';
import ChatInput from './ChatInput.svelte';
import type { AgentMessage, AgentStatus } from './agent-store.svelte.ts';
import { t } from './i18n.svelte.ts';
import ChatInput from "./ChatInput.svelte";
import type { AgentMessage, AgentStatus } from "./agent-store.svelte.ts";
import { t } from "./i18n.svelte.ts";
interface Props {
messages: AgentMessage[];
@ -23,15 +22,15 @@
status,
costUsd,
tokens,
model = 'claude-opus-4-5',
provider = 'claude',
model = "claude-opus-4-5",
provider = "claude",
contextPct = 0,
onSend,
onStop,
}: Props = $props();
let scrollEl: HTMLDivElement;
let promptText = $state('');
let promptText = $state("");
let expandedTools = $state<Set<string>>(new Set());
// Drag-resize state
@ -49,7 +48,7 @@
function handleSend() {
const text = promptText.trim();
if (!text) return;
promptText = '';
promptText = "";
onSend?.(text);
}
@ -59,21 +58,25 @@
expandedTools = next;
}
function fmtTokens(n: number) { return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); }
function fmtCost(n: number) { return `$${n.toFixed(3)}`; }
function fmtTokens(n: number) {
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
}
function fmtCost(n: number) {
return `$${n.toFixed(3)}`;
}
function dotClass(s: AgentStatus) {
if (s === 'running') return 'dot-progress';
if (s === 'error') return 'dot-error';
if (s === 'done') return 'dot-success';
return 'dot-idle';
if (s === "running") return "dot-progress";
if (s === "error") return "dot-error";
if (s === "done") return "dot-success";
return "dot-idle";
}
function statusLabel(s: AgentStatus) {
if (s === 'running') return t('agent.status.running');
if (s === 'error') return t('agent.status.error');
if (s === 'done') return t('agent.status.done');
return t('agent.status.idle');
if (s === "running") return t("agent.status.running");
if (s === "error") return t("agent.status.error");
if (s === "done") return t("agent.status.done");
return t("agent.status.idle");
}
function onResizeMouseDown(e: MouseEvent) {
@ -85,16 +88,16 @@
if (!agentPaneEl) return;
const newH = Math.max(120, startH + (ev.clientY - startY));
agentPaneEl.style.flexBasis = `${newH}px`;
agentPaneEl.style.flexGrow = '0';
agentPaneEl.style.flexShrink = '0';
agentPaneEl.style.flexGrow = "0";
agentPaneEl.style.flexShrink = "0";
}
function onUp() {
isDragging = false;
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
}
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
}
</script>
@ -107,8 +110,13 @@
<span class="strip-tokens">{fmtTokens(tokens)} tok</span>
<span class="strip-sep" aria-hidden="true"></span>
<span class="strip-cost">{fmtCost(costUsd)}</span>
{#if status === 'running' && onStop}
<button class="strip-stop-btn" onclick={onStop} title={t('agent.prompt.stop')} aria-label={t('agent.prompt.stop')}>
{#if status === "running" && onStop}
<button
class="strip-stop-btn"
onclick={onStop}
title={t("agent.prompt.stop")}
aria-label={t("agent.prompt.stop")}
>
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<rect x="3" y="3" width="10" height="10" rx="1" />
</svg>
@ -125,41 +133,37 @@
{@const isLast = idx === messages.length - 1}
<!-- timeline rows have no gap; user bubbles get margin -->
<div class="msg-row msg-animated">
{#if msg.role === 'user'}
{#if msg.role === "user"}
<div class="user-bubble">{msg.content}</div>
{:else if msg.role === 'assistant'}
{:else if msg.role === "assistant"}
<div class="timeline-row">
{#if !isFirst}<div class="timeline-line-up"></div>{/if}
<div class="timeline-diamond dot-success"></div>
{#if !isLast}<div class="timeline-line-down"></div>{/if}
<div class="timeline-content">{msg.content}</div>
</div>
{:else if msg.role === 'thinking'}
{:else if msg.role === "thinking"}
<div class="timeline-row">
{#if !isFirst}<div class="timeline-line-up"></div>{/if}
<div class="timeline-diamond dot-thinking"></div>
{#if !isLast}<div class="timeline-line-down"></div>{/if}
<div class="thinking-content">{msg.content}</div>
</div>
{:else if msg.role === 'system'}
{:else if msg.role === "system"}
<div class="timeline-row">
{#if !isFirst}<div class="timeline-line-up"></div>{/if}
<div class="timeline-diamond dot-system"></div>
{#if !isLast}<div class="timeline-line-down"></div>{/if}
<div class="system-content">{msg.content}</div>
</div>
{:else if msg.role === 'tool-call'}
{:else if msg.role === "tool-call"}
<div class="timeline-row">
{#if !isFirst}<div class="timeline-line-up"></div>{/if}
<div class="timeline-diamond dot-progress"></div>
{#if !isLast}<div class="timeline-line-down"></div>{/if}
<div class="tool-box">
<div class="tool-header">
<span class="tool-name">{msg.toolName ?? 'Tool'}</span>
<span class="tool-name">{msg.toolName ?? "Tool"}</span>
{#if msg.toolPath}
<span class="tool-path">{msg.toolPath}</span>
{/if}
@ -171,17 +175,21 @@
</div>
{#if !expandedTools.has(msg.id)}
<div class="tool-fade-overlay">
<button class="show-more-btn" onclick={() => toggleTool(msg.id)}>Show more</button>
<button
class="show-more-btn"
onclick={() => toggleTool(msg.id)}>Show more</button
>
</div>
{/if}
</div>
{#if expandedTools.has(msg.id)}
<button class="collapse-btn" onclick={() => toggleTool(msg.id)}>Show less</button>
<button class="collapse-btn" onclick={() => toggleTool(msg.id)}
>Show less</button
>
{/if}
</div>
</div>
{:else if msg.role === 'tool-result'}
{:else if msg.role === "tool-result"}
<div class="timeline-row">
{#if !isFirst}<div class="timeline-line-up"></div>{/if}
<div class="timeline-diamond dot-success"></div>
@ -189,7 +197,9 @@
<div class="tool-box tool-result-box">
<div class="tool-grid">
<span class="tool-col-label">result</span>
<span class="tool-col-content tool-result-content">{msg.content}</span>
<span class="tool-col-content tool-result-content"
>{msg.content}</span
>
</div>
</div>
</div>
@ -208,7 +218,7 @@
{model}
{provider}
{contextPct}
placeholder={t('agent.prompt.placeholder')}
placeholder={t("agent.prompt.placeholder")}
onSend={handleSend}
onInput={(v) => (promptText = v)}
/>
@ -246,23 +256,49 @@
flex-shrink: 0;
}
.dot-success { background: var(--ctp-green); }
.dot-progress { background: var(--ctp-peach); }
.dot-error { background: var(--ctp-red); }
.dot-idle { background: var(--ctp-overlay1); }
.dot-thinking { background: var(--ctp-mauve); }
.dot-system { background: var(--ctp-overlay0); }
.dot-success {
background: var(--ctp-green);
}
.dot-progress {
background: var(--ctp-peach);
}
.dot-error {
background: var(--ctp-red);
}
.dot-idle {
background: var(--ctp-overlay1);
}
.dot-thinking {
background: var(--ctp-mauve);
}
.dot-system {
background: var(--ctp-overlay0);
}
.strip-label { color: var(--ctp-subtext1); font-weight: 500; }
.strip-model { color: var(--ctp-overlay1); margin-left: 0.25rem; }
.strip-spacer { flex: 1; }
.strip-tokens { color: var(--ctp-overlay1); }
.strip-label {
color: var(--ctp-subtext1);
font-weight: 500;
}
.strip-model {
color: var(--ctp-overlay1);
margin-left: 0.25rem;
}
.strip-spacer {
flex: 1;
}
.strip-tokens {
color: var(--ctp-overlay1);
}
.strip-sep {
width: 1px; height: 0.75rem;
width: 1px;
height: 0.75rem;
background: var(--ctp-surface1);
margin: 0 0.125rem;
}
.strip-cost { color: var(--ctp-text); font-weight: 500; }
.strip-cost {
color: var(--ctp-text);
font-weight: 500;
}
.strip-stop-btn {
display: flex;
@ -277,7 +313,9 @@
border-radius: 0.2rem;
color: var(--ctp-red);
cursor: pointer;
transition: background 0.12s, color 0.12s;
transition:
background 0.12s,
color 0.12s;
flex-shrink: 0;
}
@ -312,9 +350,16 @@
gap: 0; /* No gap — timeline lines must connect between rows */
}
.messages-scroll::-webkit-scrollbar { width: 0.25rem; }
.messages-scroll::-webkit-scrollbar-track { background: transparent; }
.messages-scroll::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
.messages-scroll::-webkit-scrollbar {
width: 0.25rem;
}
.messages-scroll::-webkit-scrollbar-track {
background: transparent;
}
.messages-scroll::-webkit-scrollbar-thumb {
background: var(--ctp-surface1);
border-radius: 0.25rem;
}
/* ── Gradient fade ────────────────────────────────────────── */
.scroll-fade {
@ -338,14 +383,25 @@
}
/* ── Message row ──────────────────────────────────────────── */
.msg-row { display: flex; flex-direction: column; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
.msg-row {
display: flex;
flex-direction: column;
}
.msg-animated { animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.msg-animated {
animation: fadeIn 0.3s ease-in-out;
}
/* ── User bubble (left-aligned inline block) ─────────────── */
.user-bubble {
@ -478,7 +534,9 @@
line-height: 1.4;
}
.tool-result-content { color: var(--ctp-teal); }
.tool-result-content {
color: var(--ctp-teal);
}
/* ── Thinking content ──────────────────────────────────────── */
.thinking-content {
@ -528,7 +586,9 @@
}
.show-more-btn:hover,
.collapse-btn:hover { color: var(--ctp-lavender); }
.collapse-btn:hover {
color: var(--ctp-lavender);
}
.collapse-btn {
display: block;
@ -545,5 +605,7 @@
}
.resize-handle:hover,
.resize-handle.dragging { background: var(--ctp-surface1); }
.resize-handle.dragging {
background: var(--ctp-surface1);
}
</style>