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">
|
||||
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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue