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 name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Svelte App</title>
|
||||
<script type="module" crossorigin src="/assets/index-DQr-K0KR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BtkSUnsK.css">
|
||||
<script type="module" crossorigin src="/assets/index-8IlHjlDZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-yFiSNunC.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import ChatInput from './ChatInput.svelte';
|
||||
|
||||
type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result';
|
||||
|
||||
|
|
@ -7,12 +8,8 @@
|
|||
id: number;
|
||||
role: MsgRole;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface SubAgent {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'running' | 'done' | 'error';
|
||||
toolName?: string;
|
||||
toolPath?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
@ -34,23 +31,17 @@
|
|||
costUsd,
|
||||
tokens,
|
||||
model = 'claude-opus-4-5',
|
||||
provider: _provider = 'claude',
|
||||
profile: _profile,
|
||||
contextPct = 0,
|
||||
burnRate = 0,
|
||||
onSend,
|
||||
}: 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 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(() => {
|
||||
void messages.length;
|
||||
tick().then(() => {
|
||||
|
|
@ -65,354 +56,390 @@
|
|||
onSend?.(text);
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
function toggleTool(id: number) {
|
||||
const next = new Set(expandedTools);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
expandedTools = next;
|
||||
}
|
||||
|
||||
function fmtTokens(n: number): string {
|
||||
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
||||
}
|
||||
function fmtTokens(n: number) { return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); }
|
||||
function fmtCost(n: number) { return `$${n.toFixed(3)}`; }
|
||||
|
||||
function fmtCost(n: number): string {
|
||||
return `$${n.toFixed(3)}`;
|
||||
function dotClass(s: string) {
|
||||
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) {
|
||||
e.preventDefault();
|
||||
isDragging = true;
|
||||
|
||||
const startY = e.clientY;
|
||||
const startH = agentPaneEl?.getBoundingClientRect().height ?? 300;
|
||||
|
||||
function onMove(ev: MouseEvent) {
|
||||
if (!agentPaneEl) return;
|
||||
const delta = ev.clientY - startY;
|
||||
const newH = Math.max(120, startH + delta);
|
||||
// Apply as explicit height on the agent-pane element
|
||||
const newH = Math.max(120, startH + (ev.clientY - startY));
|
||||
agentPaneEl.style.flexBasis = `${newH}px`;
|
||||
agentPaneEl.style.flexGrow = '0';
|
||||
agentPaneEl.style.flexShrink = '0';
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
isDragging = false;
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="agent-pane" bind:this={agentPaneEl}>
|
||||
<!-- Messages -->
|
||||
<div class="agent-messages" bind:this={scrollEl}>
|
||||
{#each messages as msg (msg.id)}
|
||||
{#if msg.role === 'tool-call' || msg.role === 'tool-result'}
|
||||
<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>
|
||||
<!-- Status strip (top) -->
|
||||
<div class="status-strip">
|
||||
<span class="strip-dot {dotClass(status)}"></span>
|
||||
<span class="strip-label">{status === 'running' ? 'Running' : status === 'stalled' ? 'Stalled' : 'Done'}</span>
|
||||
<span class="strip-model">{model}</span>
|
||||
<span class="strip-spacer"></span>
|
||||
{#if contextPct > 0}
|
||||
<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-tokens">{fmtTokens(tokens)} tok</span>
|
||||
<span class="strip-sep" aria-hidden="true"></span>
|
||||
<span class="strip-cost">{fmtCost(costUsd)}</span>
|
||||
</div>
|
||||
|
||||
<!-- Subagents tree -->
|
||||
{#if SUBAGENTS.length > 0}
|
||||
<div class="subagents-section" aria-label="Subagents">
|
||||
<div class="subagents-label">Subagents</div>
|
||||
<ul class="subagents-list">
|
||||
{#each SUBAGENTS as sa (sa.id)}
|
||||
<li class="subagent-row">
|
||||
<span class="subagent-indent">└</span>
|
||||
<span
|
||||
class="subagent-dot dot-{sa.status}"
|
||||
aria-label={sa.status}
|
||||
></span>
|
||||
<span class="subagent-name">{sa.name}</span>
|
||||
<span class="subagent-status">{sa.status}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<!-- Main pane with floating input -->
|
||||
<div class="agent-pane" bind:this={agentPaneEl}>
|
||||
<!-- Scroll area -->
|
||||
<div class="messages-scroll" bind:this={scrollEl}>
|
||||
{#each messages as msg (msg.id)}
|
||||
<div class="msg-row msg-animated">
|
||||
{#if msg.role === 'user'}
|
||||
<!-- Left-aligned inline block -->
|
||||
<div class="user-bubble">{msg.content}</div>
|
||||
|
||||
{:else if msg.role === 'assistant'}
|
||||
<!-- Timeline pattern -->
|
||||
<div class="timeline-row">
|
||||
<div class="timeline-line"></div>
|
||||
<div class="timeline-dot dot-success"></div>
|
||||
<div class="timeline-content">{msg.content}</div>
|
||||
</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>
|
||||
{/if}
|
||||
|
||||
<!-- Prompt input -->
|
||||
<div class="agent-prompt">
|
||||
<textarea
|
||||
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>
|
||||
{#if expandedTools.has(msg.id)}
|
||||
<button class="collapse-btn" onclick={() => toggleTool(msg.id)}>Show less</button>
|
||||
{/if}
|
||||
</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 -->
|
||||
<div
|
||||
class="resize-handle"
|
||||
class:dragging={isDragging}
|
||||
role="separator"
|
||||
aria-label="Drag to resize agent pane"
|
||||
aria-label="Drag to resize"
|
||||
onmousedown={onResizeMouseDown}
|
||||
></div>
|
||||
|
||||
<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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--ctp-base);
|
||||
}
|
||||
|
||||
/* Messages scroll area */
|
||||
.agent-messages {
|
||||
/* ── Messages scroll area ─────────────────────────────────── */
|
||||
.messages-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 1.25rem 2.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.agent-messages::-webkit-scrollbar { width: 0.375rem; }
|
||||
.agent-messages::-webkit-scrollbar-track { background: transparent; }
|
||||
.agent-messages::-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; }
|
||||
|
||||
/* Regular messages */
|
||||
.msg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
/* ── Gradient fade ────────────────────────────────────────── */
|
||||
.scroll-fade {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 150px;
|
||||
background: linear-gradient(to bottom, transparent, var(--ctp-base));
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.msg-role {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--ctp-overlay1);
|
||||
/* ── Floating input ───────────────────────────────────────── */
|
||||
.floating-input {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.msg-role.user { color: var(--ctp-blue); }
|
||||
.msg-role.assistant { color: var(--ctp-mauve); }
|
||||
/* ── Message row ──────────────────────────────────────────── */
|
||||
.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);
|
||||
border-radius: 0.3125rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: var(--ctp-text);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.msg-body.tool-call {
|
||||
background: color-mix(in srgb, var(--ctp-peach) 8%, var(--ctp-surface0));
|
||||
border-left: 2px solid var(--ctp-peach);
|
||||
font-family: var(--term-font-family);
|
||||
font-size: 0.75rem;
|
||||
/* ── Timeline pattern ────────────────────────────────────── */
|
||||
.timeline-row {
|
||||
position: relative;
|
||||
padding-left: 1.875rem; /* 30px */
|
||||
}
|
||||
|
||||
.msg-body.tool-result {
|
||||
background: color-mix(in srgb, var(--ctp-teal) 6%, var(--ctp-surface0));
|
||||
border-left: 2px solid var(--ctp-teal);
|
||||
font-family: var(--term-font-family);
|
||||
font-size: 0.75rem;
|
||||
color: var(--ctp-subtext1);
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--ctp-surface0);
|
||||
}
|
||||
|
||||
/* Tool call collapsible */
|
||||
.tool-group { border-radius: 0.3125rem; overflow: hidden; }
|
||||
|
||||
.tool-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: 9px;
|
||||
top: 15px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot-running { background: var(--ctp-green); }
|
||||
.dot-done { background: var(--ctp-overlay1); }
|
||||
.dot-error { background: var(--ctp-red); }
|
||||
|
||||
.subagent-name { flex: 1; font-family: var(--term-font-family); }
|
||||
|
||||
.subagent-status {
|
||||
font-size: 0.625rem;
|
||||
color: var(--ctp-overlay0);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
.timeline-content {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
color: var(--ctp-text);
|
||||
padding: 0.5rem 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
height: 4px;
|
||||
background: transparent;
|
||||
|
|
@ -422,53 +449,5 @@
|
|||
}
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle.dragging {
|
||||
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; }
|
||||
.resize-handle.dragging { background: var(--ctp-surface1); }
|
||||
</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