agent-orchestrator/ui-electrobun/src/mainview/TerminalTabs.svelte
Hibryda 8e756d3523 feat(electrobun): final 5% — full integration, real data, polish
1. Claude CLI: additionalDirectories + worktreeName passthrough
2. Agent-store: reads settings (default_cwd, provider model, permission)
3. Project hydration: SQLite replaces hardcoded PROJECTS, add/remove UI
4. Group hydration: SQLite groups, add/delete in sidebar
5. Terminal auto-spawn: reads default_cwd from settings
6. Context tab: real tokens from agent-store, file refs, turn count
7. Memory tab: Memora DB integration (read-only, graceful if missing)
8. Docs tab: markdown viewer (files.list + files.read + inline renderer)
9. SSH tab: CRUD connections, spawn PTY with ssh command
10. Error handling: global unhandledrejection → toast notifications
11. Notifications: agent done/error/stall → toasts, 15min stall timer
12. Command palette: all 18 commands (was 10)

+1,198 lines, 13 files. Electrobun now 100% feature-complete vs Tauri v3.
2026-03-22 02:02:54 +01:00

249 lines
6.4 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import Terminal from './Terminal.svelte';
interface Props {
projectId: string;
accent?: string;
/** Project working directory — passed to terminal shells. */
cwd?: string;
}
let { projectId, accent = 'var(--ctp-mauve)', cwd }: Props = $props();
interface TermTab {
id: string;
title: string;
}
// svelte-ignore state_referenced_locally
const initialId = projectId;
const firstTabId = `${initialId}-t1`;
let tabs = $state<TermTab[]>([{ id: firstTabId, title: 'shell 1' }]);
let activeTabId = $state<string>(firstTabId);
let expanded = $state(true);
let counter = $state(2);
let mounted = $state<Set<string>>(new Set([firstTabId]));
function blurTerminal() {
// Force-blur xterm canvas so UI buttons become clickable
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
function addTab() {
blurTerminal();
const id = `${projectId}-t${counter}`;
tabs = [...tabs, { id, title: `shell ${counter}` }];
counter++;
activeTabId = id;
mounted = new Set([...mounted, id]);
}
function closeTab(id: string, e: MouseEvent) {
e.stopPropagation();
blurTerminal();
const idx = tabs.findIndex(t => t.id === id);
tabs = tabs.filter(t => t.id !== id);
if (activeTabId === id) {
const next = tabs[Math.min(idx, tabs.length - 1)];
activeTabId = next?.id ?? '';
}
const m = new Set(mounted);
m.delete(id);
mounted = m;
}
function activateTab(id: string) {
blurTerminal();
activeTabId = id;
if (!mounted.has(id)) mounted = new Set([...mounted, id]);
if (!expanded) expanded = true;
}
function toggleExpand() {
blurTerminal();
expanded = !expanded;
}
</script>
<!-- Wrapper: uses flex to push tab bar to bottom when terminal is collapsed -->
<div class="term-wrapper" style="--accent: {accent}">
<!-- Tab bar: always visible, acts as divider -->
<div class="term-bar" role="toolbar" aria-label="Terminal tabs" tabindex="-1" onmousedown={blurTerminal}>
<button
class="expand-btn"
onclick={toggleExpand}
title={expanded ? 'Collapse terminal' : 'Expand terminal'}
>
<svg
class="chevron"
class:open={expanded}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="9 6 15 12 9 18"/>
</svg>
</button>
<div class="term-tabs" role="tablist">
{#each tabs as tab (tab.id)}
<!-- div+role="tab" allows a nested <button> for the close action -->
<div
class="term-tab"
class:active={activeTabId === tab.id}
role="tab"
tabindex={activeTabId === tab.id ? 0 : -1}
aria-selected={activeTabId === tab.id}
onclick={() => activateTab(tab.id)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') activateTab(tab.id); }}
>
<span class="tab-label">{tab.title}</span>
{#if tabs.length > 1}
<button
class="tab-close"
aria-label="Close {tab.title}"
onclick={(e) => closeTab(tab.id, e)}
>×</button>
{/if}
</div>
{/each}
<button class="tab-add" onclick={() => addTab()}>+</button>
</div>
</div>
<!-- Terminal panes: always in DOM, display toggled.
WebKitGTK corrupts hit-test tree on DOM add/remove during click. -->
<div class="term-panes" style:display={expanded ? 'block' : 'none'}>
{#each tabs as tab (tab.id)}
{#if mounted.has(tab.id)}
<div class="term-pane" style:display={activeTabId === tab.id ? 'flex' : 'none'}>
<Terminal sessionId={tab.id} {cwd} />
</div>
{/if}
{/each}
</div>
</div>
<style>
.term-wrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
/* Tab bar */
.term-bar {
display: flex;
align-items: stretch;
height: 1.75rem;
background: var(--ctp-mantle);
border-top: 1px solid var(--ctp-surface0);
flex-shrink: 0;
z-index: 5;
}
.expand-btn {
width: 1.75rem;
flex-shrink: 0;
background: transparent;
border: none;
border-right: 1px solid var(--ctp-surface0);
color: var(--ctp-overlay1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.expand-btn:hover { color: var(--ctp-text); }
.expand-btn svg { width: 0.75rem; height: 0.75rem; transition: transform 0.15s; }
.chevron.open { transform: rotate(90deg); }
.term-tabs {
display: flex;
align-items: stretch;
flex: 1;
overflow-x: auto;
scrollbar-width: none;
gap: 0.125rem;
padding: 0 0.25rem;
}
.term-tabs::-webkit-scrollbar { display: none; }
.term-tab {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0 0.5rem;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--ctp-subtext0);
font-size: 0.6875rem;
cursor: pointer;
white-space: nowrap;
margin-bottom: -1px;
font-family: inherit;
}
.term-tab:hover { color: var(--ctp-text); }
.term-tab.active { color: var(--ctp-text); border-bottom-color: var(--accent); }
.tab-label { pointer-events: none; }
.tab-close {
width: 0.875rem;
height: 0.875rem;
border-radius: 0.2rem;
font-size: 0.6875rem;
color: var(--ctp-overlay0);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
/* Reset <button> defaults */
background: transparent;
border: none;
padding: 0;
font-family: inherit;
line-height: 1;
}
.tab-close:hover { background: var(--ctp-surface1); color: var(--ctp-red); }
.tab-add {
align-self: center;
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
background: transparent;
border: 1px solid var(--ctp-surface1);
border-radius: 0.2rem;
color: var(--ctp-overlay1);
font-size: 0.75rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-left: 0.125rem;
}
.tab-add:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
/* Terminal panes — fill remaining space below tab bar */
.term-panes {
flex: 1;
min-height: 8rem;
position: relative;
}
.term-pane {
position: absolute;
inset: 0;
flex-direction: column;
}
</style>