fix(electrobun): remove requestAnimationFrame scroll effect (was contributing to effect cycle)
This commit is contained in:
parent
085b88107f
commit
02560e341d
1 changed files with 129 additions and 67 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue