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