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.
249 lines
6.4 KiB
Svelte
249 lines
6.4 KiB
Svelte
<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>
|