feat(electrobun): redesign AgentPane to match Claude Code VSCode extension
Based on official Claude Code v2.1.79 webview CSS analysis: - Timeline pattern: 7px dots + 1px vertical line, 30px content indent - User messages: left-aligned inline blocks (not right-aligned bubbles) - Tool calls: flat bordered grid boxes with 60px mask-fade clipping - Floating input: absolute bottom:16px, crust bg, 8px radius, shadow - ChatInput.svelte extracted: auto-resize textarea, peach send button (26×26, 5px radius), focus ring with color-mix peach 12% - Footer strip: model name + attach button + divider + send button - Status strip: compact top bar with dot + status + model + cost - 150px gradient fade at message area bottom - fadeIn 0.3s animation on new messages
This commit is contained in:
parent
0225fdf3c9
commit
8248d465df
7 changed files with 583 additions and 424 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Svelte App</title>
|
<title>Svelte App</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DQr-K0KR.js"></script>
|
<script type="module" crossorigin src="/assets/index-8IlHjlDZ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BtkSUnsK.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-yFiSNunC.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
|
import ChatInput from './ChatInput.svelte';
|
||||||
|
|
||||||
type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result';
|
type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result';
|
||||||
|
|
||||||
|
|
@ -7,12 +8,8 @@
|
||||||
id: number;
|
id: number;
|
||||||
role: MsgRole;
|
role: MsgRole;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
toolName?: string;
|
||||||
|
toolPath?: string;
|
||||||
interface SubAgent {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
status: 'running' | 'done' | 'error';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -34,23 +31,17 @@
|
||||||
costUsd,
|
costUsd,
|
||||||
tokens,
|
tokens,
|
||||||
model = 'claude-opus-4-5',
|
model = 'claude-opus-4-5',
|
||||||
provider: _provider = 'claude',
|
|
||||||
profile: _profile,
|
|
||||||
contextPct = 0,
|
|
||||||
burnRate = 0,
|
|
||||||
onSend,
|
onSend,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Demo subagents — in production these come from agent messages
|
|
||||||
const SUBAGENTS: SubAgent[] = [
|
|
||||||
{ id: 'sa1', name: 'search-agent', status: 'done' },
|
|
||||||
{ id: 'sa2', name: 'test-runner', status: 'running' },
|
|
||||||
];
|
|
||||||
|
|
||||||
let scrollEl: HTMLDivElement;
|
let scrollEl: HTMLDivElement;
|
||||||
let promptText = $state('');
|
let promptText = $state('');
|
||||||
|
let expandedTools = $state<Set<number>>(new Set());
|
||||||
|
|
||||||
|
// Drag-resize state
|
||||||
|
let agentPaneEl: HTMLDivElement;
|
||||||
|
let isDragging = $state(false);
|
||||||
|
|
||||||
// Auto-scroll to bottom on new messages
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
void messages.length;
|
void messages.length;
|
||||||
tick().then(() => {
|
tick().then(() => {
|
||||||
|
|
@ -65,354 +56,390 @@
|
||||||
onSend?.(text);
|
onSend?.(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
function toggleTool(id: number) {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
const next = new Set(expandedTools);
|
||||||
e.preventDefault();
|
next.has(id) ? next.delete(id) : next.add(id);
|
||||||
handleSend();
|
expandedTools = next;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtTokens(n: number): string {
|
function fmtTokens(n: number) { return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); }
|
||||||
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
function fmtCost(n: number) { return `$${n.toFixed(3)}`; }
|
||||||
}
|
|
||||||
|
|
||||||
function fmtCost(n: number): string {
|
function dotClass(s: string) {
|
||||||
return `$${n.toFixed(3)}`;
|
if (s === 'running') return 'dot-progress';
|
||||||
|
if (s === 'stalled') return 'dot-error';
|
||||||
|
return 'dot-success';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Drag-resize between messages and terminal ──────────────
|
|
||||||
// The "terminal" section is a sibling rendered by ProjectCard.
|
|
||||||
// We expose a flex-basis on .agent-pane via CSS variable and update it on drag.
|
|
||||||
let agentPaneEl: HTMLDivElement;
|
|
||||||
let isDragging = $state(false);
|
|
||||||
|
|
||||||
function onResizeMouseDown(e: MouseEvent) {
|
function onResizeMouseDown(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
|
|
||||||
const startY = e.clientY;
|
const startY = e.clientY;
|
||||||
const startH = agentPaneEl?.getBoundingClientRect().height ?? 300;
|
const startH = agentPaneEl?.getBoundingClientRect().height ?? 300;
|
||||||
|
|
||||||
function onMove(ev: MouseEvent) {
|
function onMove(ev: MouseEvent) {
|
||||||
if (!agentPaneEl) return;
|
if (!agentPaneEl) return;
|
||||||
const delta = ev.clientY - startY;
|
const newH = Math.max(120, startH + (ev.clientY - startY));
|
||||||
const newH = Math.max(120, startH + delta);
|
|
||||||
// Apply as explicit height on the agent-pane element
|
|
||||||
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>
|
||||||
|
|
||||||
<div class="agent-pane" bind:this={agentPaneEl}>
|
<!-- Status strip (top) -->
|
||||||
<!-- Messages -->
|
<div class="status-strip">
|
||||||
<div class="agent-messages" bind:this={scrollEl}>
|
<span class="strip-dot {dotClass(status)}"></span>
|
||||||
{#each messages as msg (msg.id)}
|
<span class="strip-label">{status === 'running' ? 'Running' : status === 'stalled' ? 'Stalled' : 'Done'}</span>
|
||||||
{#if msg.role === 'tool-call' || msg.role === 'tool-result'}
|
<span class="strip-model">{model}</span>
|
||||||
<details class="tool-group" open={msg.role === 'tool-call'}>
|
|
||||||
<summary class="tool-summary">
|
|
||||||
<span class="tool-icon" aria-hidden="true">{msg.role === 'tool-call' ? '⚙' : '↩'}</span>
|
|
||||||
<span class="tool-label">{msg.role === 'tool-call' ? 'Tool call' : 'Tool result'}</span>
|
|
||||||
</summary>
|
|
||||||
<div
|
|
||||||
class="msg-body"
|
|
||||||
class:tool-call={msg.role === 'tool-call'}
|
|
||||||
class:tool-result={msg.role === 'tool-result'}
|
|
||||||
>{msg.content}</div>
|
|
||||||
</details>
|
|
||||||
{:else}
|
|
||||||
<div class="msg">
|
|
||||||
<span class="msg-role {msg.role === 'assistant' ? 'assistant' : 'user'}">{msg.role}</span>
|
|
||||||
<div class="msg-body">{msg.content}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status strip -->
|
|
||||||
<div class="agent-status-strip">
|
|
||||||
<span
|
|
||||||
class="status-badge"
|
|
||||||
class:badge-running={status === 'running'}
|
|
||||||
class:badge-idle={status === 'idle'}
|
|
||||||
class:badge-stalled={status === 'stalled'}
|
|
||||||
>{status}</span>
|
|
||||||
<span class="strip-model" title={model}>{model}</span>
|
|
||||||
<span class="strip-spacer"></span>
|
<span class="strip-spacer"></span>
|
||||||
{#if contextPct > 0}
|
<span class="strip-tokens">{fmtTokens(tokens)} tok</span>
|
||||||
<span
|
<span class="strip-sep" aria-hidden="true"></span>
|
||||||
class="strip-ctx"
|
|
||||||
class:ctx-warn={contextPct >= 75}
|
|
||||||
class:ctx-danger={contextPct >= 90}
|
|
||||||
title="Context window {contextPct}% used"
|
|
||||||
>{contextPct}%</span>
|
|
||||||
{/if}
|
|
||||||
{#if burnRate > 0}
|
|
||||||
<span class="strip-burn" title="Burn rate">${burnRate.toFixed(2)}/hr</span>
|
|
||||||
{/if}
|
|
||||||
<span class="strip-tokens" title="Tokens used">{fmtTokens(tokens)}</span>
|
|
||||||
<span class="strip-cost">{fmtCost(costUsd)}</span>
|
<span class="strip-cost">{fmtCost(costUsd)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Subagents tree -->
|
<!-- Main pane with floating input -->
|
||||||
{#if SUBAGENTS.length > 0}
|
<div class="agent-pane" bind:this={agentPaneEl}>
|
||||||
<div class="subagents-section" aria-label="Subagents">
|
<!-- Scroll area -->
|
||||||
<div class="subagents-label">Subagents</div>
|
<div class="messages-scroll" bind:this={scrollEl}>
|
||||||
<ul class="subagents-list">
|
{#each messages as msg (msg.id)}
|
||||||
{#each SUBAGENTS as sa (sa.id)}
|
<div class="msg-row msg-animated">
|
||||||
<li class="subagent-row">
|
{#if msg.role === 'user'}
|
||||||
<span class="subagent-indent">└</span>
|
<!-- Left-aligned inline block -->
|
||||||
<span
|
<div class="user-bubble">{msg.content}</div>
|
||||||
class="subagent-dot dot-{sa.status}"
|
|
||||||
aria-label={sa.status}
|
{:else if msg.role === 'assistant'}
|
||||||
></span>
|
<!-- Timeline pattern -->
|
||||||
<span class="subagent-name">{sa.name}</span>
|
<div class="timeline-row">
|
||||||
<span class="subagent-status">{sa.status}</span>
|
<div class="timeline-line"></div>
|
||||||
</li>
|
<div class="timeline-dot dot-success"></div>
|
||||||
{/each}
|
<div class="timeline-content">{msg.content}</div>
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
|
{:else if msg.role === 'tool-call'}
|
||||||
|
<!-- Flat bordered tool box -->
|
||||||
|
<div class="timeline-row">
|
||||||
|
<div class="timeline-line"></div>
|
||||||
|
<div class="timeline-dot dot-progress"></div>
|
||||||
|
<div class="tool-box">
|
||||||
|
<div class="tool-header">
|
||||||
|
<span class="tool-name">{msg.toolName ?? 'Tool'}</span>
|
||||||
|
{#if msg.toolPath}
|
||||||
|
<span class="tool-path">{msg.toolPath}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="tool-body" class:expanded={expandedTools.has(msg.id)}>
|
||||||
|
<div class="tool-grid">
|
||||||
|
<span class="tool-col-label">input</span>
|
||||||
|
<span class="tool-col-content">{msg.content}</span>
|
||||||
|
</div>
|
||||||
|
{#if !expandedTools.has(msg.id)}
|
||||||
|
<div class="tool-fade-overlay">
|
||||||
|
<button class="show-more-btn" onclick={() => toggleTool(msg.id)}>Show more</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
<!-- Prompt input -->
|
{#if expandedTools.has(msg.id)}
|
||||||
<div class="agent-prompt">
|
<button class="collapse-btn" onclick={() => toggleTool(msg.id)}>Show less</button>
|
||||||
<textarea
|
{/if}
|
||||||
class="prompt-input"
|
|
||||||
placeholder="Ask Claude anything..."
|
|
||||||
rows="2"
|
|
||||||
bind:value={promptText}
|
|
||||||
onkeydown={handleKeydown}
|
|
||||||
aria-label="Message input"
|
|
||||||
></textarea>
|
|
||||||
<button
|
|
||||||
class="prompt-send"
|
|
||||||
onclick={handleSend}
|
|
||||||
disabled={!promptText.trim()}
|
|
||||||
aria-label="Send message"
|
|
||||||
>Send</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Drag-resize handle — sits between agent pane and terminal section -->
|
{:else if msg.role === 'tool-result'}
|
||||||
|
<div class="timeline-row">
|
||||||
|
<div class="timeline-line"></div>
|
||||||
|
<div class="timeline-dot dot-success"></div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gradient fade -->
|
||||||
|
<div class="scroll-fade" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<!-- Floating input -->
|
||||||
|
<div class="floating-input">
|
||||||
|
<ChatInput
|
||||||
|
value={promptText}
|
||||||
|
{model}
|
||||||
|
onSend={handleSend}
|
||||||
|
onInput={(v) => (promptText = v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drag-resize handle -->
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="resize-handle"
|
class="resize-handle"
|
||||||
class:dragging={isDragging}
|
class:dragging={isDragging}
|
||||||
role="separator"
|
role="separator"
|
||||||
aria-label="Drag to resize agent pane"
|
aria-label="Drag to resize"
|
||||||
onmousedown={onResizeMouseDown}
|
onmousedown={onResizeMouseDown}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* ── Status strip (top) ──────────────────────────────────── */
|
||||||
|
.status-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: var(--ctp-mantle);
|
||||||
|
border-bottom: 0.5px solid var(--ctp-surface1);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--ctp-subtext0);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-success { background: var(--ctp-green); }
|
||||||
|
.dot-progress { background: var(--ctp-peach); }
|
||||||
|
.dot-error { background: var(--ctp-red); }
|
||||||
|
|
||||||
|
.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;
|
||||||
|
background: var(--ctp-surface1);
|
||||||
|
margin: 0 0.125rem;
|
||||||
|
}
|
||||||
|
.strip-cost { color: var(--ctp-text); font-weight: 500; }
|
||||||
|
|
||||||
|
/* ── Main pane ────────────────────────────────────────────── */
|
||||||
.agent-pane {
|
.agent-pane {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background: var(--ctp-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Messages scroll area */
|
/* ── Messages scroll area ─────────────────────────────────── */
|
||||||
.agent-messages {
|
.messages-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
padding: 1.25rem 1.25rem 2.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.375rem;
|
gap: 0.625rem;
|
||||||
padding: 0.5rem 0.625rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-messages::-webkit-scrollbar { width: 0.375rem; }
|
.messages-scroll::-webkit-scrollbar { width: 0.25rem; }
|
||||||
.agent-messages::-webkit-scrollbar-track { background: transparent; }
|
.messages-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||||
.agent-messages::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
|
.messages-scroll::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
|
||||||
|
|
||||||
/* Regular messages */
|
/* ── Gradient fade ────────────────────────────────────────── */
|
||||||
.msg {
|
.scroll-fade {
|
||||||
display: flex;
|
position: absolute;
|
||||||
flex-direction: column;
|
bottom: 0;
|
||||||
gap: 0.125rem;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 150px;
|
||||||
|
background: linear-gradient(to bottom, transparent, var(--ctp-base));
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-role {
|
/* ── Floating input ───────────────────────────────────────── */
|
||||||
font-size: 0.6875rem;
|
.floating-input {
|
||||||
font-weight: 600;
|
position: absolute;
|
||||||
text-transform: uppercase;
|
bottom: 16px;
|
||||||
letter-spacing: 0.04em;
|
left: 16px;
|
||||||
color: var(--ctp-overlay1);
|
right: 16px;
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-role.user { color: var(--ctp-blue); }
|
/* ── Message row ──────────────────────────────────────────── */
|
||||||
.msg-role.assistant { color: var(--ctp-mauve); }
|
.msg-row { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
.msg-body {
|
@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 {
|
||||||
|
display: inline-block;
|
||||||
|
align-self: flex-start;
|
||||||
background: var(--ctp-surface0);
|
background: var(--ctp-surface0);
|
||||||
border-radius: 0.3125rem;
|
border: 1px solid var(--ctp-surface1);
|
||||||
padding: 0.375rem 0.5rem;
|
border-radius: 6px;
|
||||||
|
padding: 0.25rem 0.375rem;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: var(--ctp-text);
|
color: var(--ctp-text);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
max-width: 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-body.tool-call {
|
/* ── Timeline pattern ────────────────────────────────────── */
|
||||||
background: color-mix(in srgb, var(--ctp-peach) 8%, var(--ctp-surface0));
|
.timeline-row {
|
||||||
border-left: 2px solid var(--ctp-peach);
|
position: relative;
|
||||||
font-family: var(--term-font-family);
|
padding-left: 1.875rem; /* 30px */
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-body.tool-result {
|
.timeline-line {
|
||||||
background: color-mix(in srgb, var(--ctp-teal) 6%, var(--ctp-surface0));
|
position: absolute;
|
||||||
border-left: 2px solid var(--ctp-teal);
|
left: 12px;
|
||||||
font-family: var(--term-font-family);
|
top: 0;
|
||||||
font-size: 0.75rem;
|
bottom: 0;
|
||||||
color: var(--ctp-subtext1);
|
width: 1px;
|
||||||
|
background: var(--ctp-surface0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tool call collapsible */
|
.timeline-dot {
|
||||||
.tool-group { border-radius: 0.3125rem; overflow: hidden; }
|
position: absolute;
|
||||||
|
left: 9px;
|
||||||
.tool-summary {
|
top: 15px;
|
||||||
display: flex;
|
width: 7px;
|
||||||
align-items: center;
|
height: 7px;
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
background: color-mix(in srgb, var(--ctp-peach) 8%, var(--ctp-surface0));
|
|
||||||
border-left: 2px solid var(--ctp-peach);
|
|
||||||
cursor: pointer;
|
|
||||||
list-style: none;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--ctp-subtext1);
|
|
||||||
border-radius: 0.3125rem 0.3125rem 0 0;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-summary::-webkit-details-marker { display: none; }
|
|
||||||
.tool-summary:hover { color: var(--ctp-text); }
|
|
||||||
.tool-icon { font-size: 0.6875rem; color: var(--ctp-peach); }
|
|
||||||
.tool-label { font-weight: 500; font-family: var(--term-font-family); }
|
|
||||||
|
|
||||||
.tool-group[open] .tool-summary { border-radius: 0.3125rem 0.3125rem 0 0; }
|
|
||||||
.tool-group .msg-body { border-radius: 0 0 0.3125rem 0.3125rem; border-left: 2px solid var(--ctp-peach); border-top: none; }
|
|
||||||
.tool-group .msg-body.tool-result { border-left-color: var(--ctp-teal); }
|
|
||||||
|
|
||||||
/* Status strip */
|
|
||||||
.agent-status-strip {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.25rem 0.625rem;
|
|
||||||
background: var(--ctp-mantle);
|
|
||||||
border-top: 1px solid var(--ctp-surface0);
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: var(--ctp-subtext0);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
padding: 0.125rem 0.4rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
font-size: 0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-running { background: color-mix(in srgb, var(--ctp-green) 20%, transparent); color: var(--ctp-green); }
|
|
||||||
.badge-idle { background: color-mix(in srgb, var(--ctp-overlay0) 20%, transparent); color: var(--ctp-overlay1); }
|
|
||||||
.badge-stalled { background: color-mix(in srgb, var(--ctp-peach) 20%, transparent); color: var(--ctp-peach); }
|
|
||||||
|
|
||||||
.strip-model { color: var(--ctp-overlay1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 8rem; }
|
|
||||||
.strip-spacer { flex: 1; }
|
|
||||||
|
|
||||||
.strip-ctx {
|
|
||||||
padding: 0.1rem 0.35rem;
|
|
||||||
border-radius: 0.2rem;
|
|
||||||
background: color-mix(in srgb, var(--ctp-yellow) 15%, transparent);
|
|
||||||
color: var(--ctp-yellow);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strip-ctx.ctx-warn { background: color-mix(in srgb, var(--ctp-peach) 15%, transparent); color: var(--ctp-peach); }
|
|
||||||
.strip-ctx.ctx-danger { background: color-mix(in srgb, var(--ctp-red) 15%, transparent); color: var(--ctp-red); }
|
|
||||||
.strip-burn { color: var(--ctp-peach); }
|
|
||||||
.strip-tokens { color: var(--ctp-subtext1); }
|
|
||||||
.strip-cost { color: var(--ctp-text); font-weight: 500; }
|
|
||||||
|
|
||||||
/* Subagents section */
|
|
||||||
.subagents-section {
|
|
||||||
border-top: 1px solid var(--ctp-surface0);
|
|
||||||
background: var(--ctp-mantle);
|
|
||||||
padding: 0.3rem 0.625rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subagents-label {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
color: var(--ctp-overlay0);
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subagents-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subagent-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--ctp-subtext1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subagent-indent {
|
|
||||||
color: var(--ctp-overlay0);
|
|
||||||
font-family: var(--term-font-family);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subagent-dot {
|
|
||||||
width: 0.4rem;
|
|
||||||
height: 0.4rem;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot-running { background: var(--ctp-green); }
|
.timeline-content {
|
||||||
.dot-done { background: var(--ctp-overlay1); }
|
font-size: 0.8125rem;
|
||||||
.dot-error { background: var(--ctp-red); }
|
line-height: 1.6;
|
||||||
|
color: var(--ctp-text);
|
||||||
.subagent-name { flex: 1; font-family: var(--term-font-family); }
|
padding: 0.5rem 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
.subagent-status {
|
word-break: break-word;
|
||||||
font-size: 0.625rem;
|
|
||||||
color: var(--ctp-overlay0);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Drag-resize handle */
|
/* ── Tool box ────────────────────────────────────────────── */
|
||||||
|
.tool-box {
|
||||||
|
border: 0.5px solid var(--ctp-surface1);
|
||||||
|
background: var(--ctp-mantle);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-result-box {
|
||||||
|
border-color: color-mix(in srgb, var(--ctp-teal) 30%, var(--ctp-surface1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
border-bottom: 0.5px solid var(--ctp-surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--ctp-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-path {
|
||||||
|
font-family: var(--term-font-family);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--ctp-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-body {
|
||||||
|
position: relative;
|
||||||
|
max-height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-body.expanded {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4rem 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-col-label {
|
||||||
|
font-family: var(--term-font-family);
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--ctp-subtext0);
|
||||||
|
opacity: 0.5;
|
||||||
|
padding-top: 0.05em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-col-content {
|
||||||
|
font-family: var(--term-font-family);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--ctp-subtext1);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-result-content { color: var(--ctp-teal); }
|
||||||
|
|
||||||
|
.tool-fade-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2.5rem;
|
||||||
|
background: linear-gradient(to bottom, transparent, var(--ctp-mantle));
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more-btn,
|
||||||
|
.collapse-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--ctp-blue);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
font-family: var(--ui-font-family);
|
||||||
|
transition: color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more-btn:hover,
|
||||||
|
.collapse-btn:hover { color: var(--ctp-lavender); }
|
||||||
|
|
||||||
|
.collapse-btn {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Drag-resize handle ──────────────────────────────────── */
|
||||||
.resize-handle {
|
.resize-handle {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -422,53 +449,5 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle:hover,
|
.resize-handle:hover,
|
||||||
.resize-handle.dragging {
|
.resize-handle.dragging { background: var(--ctp-surface1); }
|
||||||
background: var(--ctp-surface1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prompt area */
|
|
||||||
.agent-prompt {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.375rem 0.5rem;
|
|
||||||
background: var(--ctp-mantle);
|
|
||||||
border-top: 1px solid var(--ctp-surface0);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-input {
|
|
||||||
flex: 1;
|
|
||||||
background: var(--ctp-surface0);
|
|
||||||
border: 1px solid var(--ctp-surface1);
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
color: var(--ctp-text);
|
|
||||||
font-family: var(--ui-font-family);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
padding: 0.375rem 0.5rem;
|
|
||||||
resize: none;
|
|
||||||
line-height: 1.4;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.12s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-input:focus { border-color: var(--accent, var(--ctp-mauve)); }
|
|
||||||
.prompt-input::placeholder { color: var(--ctp-overlay0); }
|
|
||||||
|
|
||||||
.prompt-send {
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
background: var(--accent, var(--ctp-mauve));
|
|
||||||
color: var(--ctp-base);
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
font-family: var(--ui-font-family);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: opacity 0.12s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-send:hover:not(:disabled) { opacity: 0.85; }
|
|
||||||
.prompt-send:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
180
ui-electrobun/src/mainview/ChatInput.svelte
Normal file
180
ui-electrobun/src/mainview/ChatInput.svelte
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
model?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onSend?: () => void;
|
||||||
|
onInput?: (v: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { value, model = 'claude-opus-4-5', disabled = false, onSend, onInput }: Props = $props();
|
||||||
|
|
||||||
|
let textareaEl: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
function autoResize() {
|
||||||
|
if (!textareaEl) return;
|
||||||
|
textareaEl.style.height = 'auto';
|
||||||
|
textareaEl.style.height = Math.min(textareaEl.scrollHeight, 200) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (value.trim()) onSend?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(e: Event) {
|
||||||
|
onInput?.((e.target as HTMLTextAreaElement).value);
|
||||||
|
autoResize();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chat-input-outer">
|
||||||
|
<textarea
|
||||||
|
bind:this={textareaEl}
|
||||||
|
class="chat-textarea"
|
||||||
|
placeholder="Ask Claude anything..."
|
||||||
|
rows="1"
|
||||||
|
{value}
|
||||||
|
oninput={handleInput}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
aria-label="Message input"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="footer-strip">
|
||||||
|
<!-- Left: attach + model -->
|
||||||
|
<button class="footer-btn attach-btn" aria-label="Attach file" title="Attach">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
|
<path d="M3 8l5-5a3.5 3.5 0 015 5l-6 6a2 2 0 01-3-3l5-5a.5.5 0 01.7.7L5 11.3a1 1 0 001.4 1.4l6-6a2.5 2.5 0 00-3.5-3.5L3.7 8.7" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="model-label">{model}</span>
|
||||||
|
|
||||||
|
<!-- Right: divider + send -->
|
||||||
|
<span class="footer-spacer"></span>
|
||||||
|
<span class="footer-divider" aria-hidden="true"></span>
|
||||||
|
<button
|
||||||
|
class="send-btn"
|
||||||
|
onclick={onSend}
|
||||||
|
disabled={disabled || !value.trim()}
|
||||||
|
aria-label="Send message"
|
||||||
|
title="Send (Enter)"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||||
|
<path d="M10 15V5M10 5L6 9M10 5l4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chat-input-outer {
|
||||||
|
background: var(--ctp-crust);
|
||||||
|
border: 1px solid var(--ctp-surface1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-textarea {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
color: var(--ctp-text);
|
||||||
|
font-family: var(--ui-font-family);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
max-height: 12.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
transition: box-shadow 0.12s, border-color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-textarea:focus {
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ctp-peach) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-textarea::placeholder {
|
||||||
|
color: var(--ctp-subtext0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-textarea::-webkit-scrollbar { width: 0.25rem; }
|
||||||
|
.chat-textarea::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
|
||||||
|
|
||||||
|
/* Footer strip */
|
||||||
|
.footer-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.125rem;
|
||||||
|
padding: 0.3125rem;
|
||||||
|
border-top: 0.5px solid var(--ctp-surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--ctp-overlay1);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.12s, background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn:hover {
|
||||||
|
color: var(--ctp-text);
|
||||||
|
background: var(--ctp-surface0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-label {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--ctp-overlay1);
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-spacer { flex: 1; }
|
||||||
|
|
||||||
|
.footer-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 1rem;
|
||||||
|
background: var(--ctp-surface1);
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
background: var(--ctp-peach);
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #f5efe6;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: filter 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue