feat(electrobun): fixes + 7 new features (terminal input, file browser, memory, toasts)
Fixes: - Terminal accepts keyboard input (echo mode with prompt) - Terminal drawer collapses properly (display:none preserves xterm state) Features: - 6 project tabs: Model | Docs | Context | Files | SSH | Memory - FileBrowser.svelte: recursive tree with expand/collapse + file preview - MemoryTab.svelte: memory cards with trust badges (human/agent/auto) - Subagent tree in AgentPane (demo: search-agent, test-runner) - Drag resize handle between agent pane and terminal - Theme dropdown in Settings (4 Catppuccin flavors) - ToastContainer.svelte: auto-dismiss notifications
This commit is contained in:
parent
b11a856b72
commit
4ae558af17
14 changed files with 1168 additions and 196 deletions
|
|
@ -1,10 +1,12 @@
|
|||
<script lang="ts">
|
||||
import AgentPane from './AgentPane.svelte';
|
||||
import TerminalTabs from './TerminalTabs.svelte';
|
||||
import FileBrowser from './FileBrowser.svelte';
|
||||
import MemoryTab from './MemoryTab.svelte';
|
||||
|
||||
type AgentStatus = 'running' | 'idle' | 'stalled';
|
||||
type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result';
|
||||
type ProjectTab = 'model' | 'docs' | 'files';
|
||||
type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memory';
|
||||
|
||||
interface AgentMessage {
|
||||
id: number;
|
||||
|
|
@ -47,26 +49,22 @@
|
|||
}: Props = $props();
|
||||
|
||||
let activeTab = $state<ProjectTab>('model');
|
||||
// Capture snapshot of initial prop value — messages is stable mount-time demo data
|
||||
// svelte-ignore state_referenced_locally
|
||||
const seedMessages = initialMessages.slice();
|
||||
let messages = $state(seedMessages);
|
||||
// Track which project tabs have been activated (PERSISTED-LAZY pattern)
|
||||
let activatedTabs = $state<Set<ProjectTab>>(new Set(['model']));
|
||||
|
||||
const ALL_TABS: ProjectTab[] = ['model', 'docs', 'context', 'files', 'ssh', 'memory'];
|
||||
|
||||
function setTab(tab: ProjectTab) {
|
||||
activeTab = tab;
|
||||
activatedTabs = new Set([...activatedTabs, tab]);
|
||||
}
|
||||
|
||||
function handleSend(text: string) {
|
||||
const newMsg: AgentMessage = {
|
||||
id: messages.length + 1,
|
||||
role: 'user',
|
||||
content: text,
|
||||
};
|
||||
const newMsg: AgentMessage = { id: messages.length + 1, role: 'user', content: text };
|
||||
messages = [...messages, newMsg];
|
||||
// Simulate assistant echo (demo only)
|
||||
setTimeout(() => {
|
||||
messages = [...messages, {
|
||||
id: messages.length + 1,
|
||||
|
|
@ -96,15 +94,12 @@
|
|||
<span class="project-name" title={name}>{name}</span>
|
||||
<span class="project-cwd" title={cwd}>{cwd}</span>
|
||||
|
||||
<!-- Provider badge -->
|
||||
<span class="provider-badge" title="Provider: {provider}">{provider}</span>
|
||||
|
||||
<!-- Profile (if set) -->
|
||||
{#if profile}
|
||||
<span class="profile-badge" title="Profile: {profile}">{profile}</span>
|
||||
{/if}
|
||||
|
||||
<!-- Context pressure badge -->
|
||||
{#if contextPct > 50}
|
||||
<span
|
||||
class="ctx-badge"
|
||||
|
|
@ -114,7 +109,6 @@
|
|||
>{contextPct}%</span>
|
||||
{/if}
|
||||
|
||||
<!-- Burn rate -->
|
||||
{#if burnRate > 0}
|
||||
<span class="burn-badge" title="Burn rate">${burnRate.toFixed(2)}/hr</span>
|
||||
{/if}
|
||||
|
|
@ -122,7 +116,7 @@
|
|||
|
||||
<!-- Project tab bar -->
|
||||
<div class="tab-bar" role="tablist" aria-label="{name} tabs">
|
||||
{#each (['model', 'docs', 'files'] as ProjectTab[]) as tab}
|
||||
{#each ALL_TABS as tab}
|
||||
<button
|
||||
class="tab-btn"
|
||||
class:active={activeTab === tab}
|
||||
|
|
@ -138,7 +132,7 @@
|
|||
|
||||
<!-- Tab content: display:flex/none to keep agent session mounted -->
|
||||
<div class="tab-content">
|
||||
<!-- Model tab: agent pane + terminal tabs -->
|
||||
<!-- Model tab: agent pane + terminal tabs — always mounted -->
|
||||
<div
|
||||
id="tabpanel-{id}-model"
|
||||
class="tab-pane"
|
||||
|
|
@ -174,6 +168,49 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Context tab -->
|
||||
{#if activatedTabs.has('context')}
|
||||
<div
|
||||
id="tabpanel-{id}-context"
|
||||
class="tab-pane"
|
||||
style:display={activeTab === 'context' ? 'flex' : 'none'}
|
||||
role="tabpanel"
|
||||
aria-label="Context"
|
||||
>
|
||||
<div class="context-pane">
|
||||
<div class="ctx-stats-row">
|
||||
<div class="ctx-stat">
|
||||
<span class="ctx-stat-label">Tokens used</span>
|
||||
<span class="ctx-stat-value">{tokens.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="ctx-stat">
|
||||
<span class="ctx-stat-label">Context %</span>
|
||||
<span class="ctx-stat-value">{contextPct}%</span>
|
||||
</div>
|
||||
<div class="ctx-stat">
|
||||
<span class="ctx-stat-label">Model</span>
|
||||
<span class="ctx-stat-value">{model}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctx-meter-wrap" title="{contextPct}% context used">
|
||||
<div class="ctx-meter-bar" style:width="{contextPct}%"
|
||||
class:meter-warn={contextPct >= 75}
|
||||
class:meter-danger={contextPct >= 90}
|
||||
></div>
|
||||
</div>
|
||||
<div class="ctx-turn-list">
|
||||
<div class="ctx-section-label">Turn breakdown</div>
|
||||
{#each messages.slice(0, 5) as msg}
|
||||
<div class="ctx-turn-row">
|
||||
<span class="ctx-turn-role ctx-role-{msg.role}">{msg.role}</span>
|
||||
<span class="ctx-turn-preview">{msg.content.slice(0, 60)}{msg.content.length > 60 ? '…' : ''}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Files tab -->
|
||||
{#if activatedTabs.has('files')}
|
||||
<div
|
||||
|
|
@ -183,7 +220,33 @@
|
|||
role="tabpanel"
|
||||
aria-label="Files"
|
||||
>
|
||||
<div class="placeholder-pane">File browser — coming soon</div>
|
||||
<FileBrowser />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- SSH tab -->
|
||||
{#if activatedTabs.has('ssh')}
|
||||
<div
|
||||
id="tabpanel-{id}-ssh"
|
||||
class="tab-pane"
|
||||
style:display={activeTab === 'ssh' ? 'flex' : 'none'}
|
||||
role="tabpanel"
|
||||
aria-label="SSH"
|
||||
>
|
||||
<div class="placeholder-pane">No SSH connections configured</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Memory tab -->
|
||||
{#if activatedTabs.has('memory')}
|
||||
<div
|
||||
id="tabpanel-{id}-memory"
|
||||
class="tab-pane"
|
||||
style:display={activeTab === 'memory' ? 'flex' : 'none'}
|
||||
role="tabpanel"
|
||||
aria-label="Memory"
|
||||
>
|
||||
<MemoryTab />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -296,8 +359,8 @@
|
|||
color: var(--ctp-yellow);
|
||||
}
|
||||
|
||||
.ctx-badge.ctx-warn { color: var(--ctp-peach); background: color-mix(in srgb, var(--ctp-peach) 15%, transparent); }
|
||||
.ctx-badge.ctx-danger { color: var(--ctp-red); background: color-mix(in srgb, var(--ctp-red) 15%, transparent); }
|
||||
.ctx-badge.ctx-warn { color: var(--ctp-peach); background: color-mix(in srgb, var(--ctp-peach) 15%, transparent); }
|
||||
.ctx-badge.ctx-danger { color: var(--ctp-red); background: color-mix(in srgb, var(--ctp-red) 15%, transparent); }
|
||||
|
||||
.burn-badge {
|
||||
background: color-mix(in srgb, var(--ctp-peach) 10%, transparent);
|
||||
|
|
@ -314,10 +377,14 @@
|
|||
flex-shrink: 0;
|
||||
padding: 0 0.25rem;
|
||||
gap: 0.125rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.tab-bar::-webkit-scrollbar { display: none; }
|
||||
|
||||
.tab-btn {
|
||||
padding: 0 0.75rem;
|
||||
padding: 0 0.625rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
|
@ -328,6 +395,7 @@
|
|||
white-space: nowrap;
|
||||
transition: color 0.12s, border-color 0.12s;
|
||||
margin-bottom: -1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn:hover { color: var(--ctp-text); }
|
||||
|
|
@ -361,4 +429,101 @@
|
|||
font-size: 0.8125rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Context tab */
|
||||
.context-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 0.625rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ctx-stats-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ctx-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.ctx-stat-label {
|
||||
font-size: 0.625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--ctp-overlay0);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctx-stat-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-text);
|
||||
font-family: var(--term-font-family);
|
||||
}
|
||||
|
||||
.ctx-meter-wrap {
|
||||
height: 0.375rem;
|
||||
background: var(--ctp-surface0);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ctx-meter-bar {
|
||||
height: 100%;
|
||||
background: var(--ctp-teal);
|
||||
border-radius: 0.25rem;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.ctx-meter-bar.meter-warn { background: var(--ctp-peach); }
|
||||
.ctx-meter-bar.meter-danger { background: var(--ctp-red); }
|
||||
|
||||
.ctx-section-label {
|
||||
font-size: 0.625rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--ctp-overlay0);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ctx-turn-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.ctx-turn-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ctx-turn-role {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
min-width: 4.5rem;
|
||||
}
|
||||
|
||||
.ctx-role-user { color: var(--ctp-blue); }
|
||||
.ctx-role-assistant { color: var(--ctp-mauve); }
|
||||
.ctx-role-tool-call { color: var(--ctp-peach); }
|
||||
.ctx-role-tool-result { color: var(--ctp-teal); }
|
||||
|
||||
.ctx-turn-preview {
|
||||
color: var(--ctp-subtext0);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue