BTerminal/v2/src/lib/components/Workspace/TerminalTabs.svelte

249 lines
5.5 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 type { ProjectConfig } from '../../types/groups';
import {
getTerminalTabs,
addTerminalTab,
removeTerminalTab,
type TerminalTab,
} from '../../stores/workspace.svelte';
import TerminalPane from '../Terminal/TerminalPane.svelte';
import AgentPreviewPane from '../Terminal/AgentPreviewPane.svelte';
interface Props {
project: ProjectConfig;
agentSessionId?: string | null;
}
let { project, agentSessionId }: Props = $props();
let tabs = $derived(getTerminalTabs(project.id));
let activeTabId = $state<string | null>(null);
// Auto-select first tab
$effect(() => {
if (tabs.length > 0 && (!activeTabId || !tabs.find(t => t.id === activeTabId))) {
activeTabId = tabs[0].id;
}
if (tabs.length === 0) {
activeTabId = null;
}
});
function addShellTab() {
const id = crypto.randomUUID();
const num = tabs.filter(t => t.type === 'shell').length + 1;
addTerminalTab(project.id, {
id,
title: `Shell ${num}`,
type: 'shell',
});
activeTabId = id;
}
function addAgentPreviewTab() {
if (!agentSessionId) return;
// Don't create duplicate — check if one already exists for this session
const existing = tabs.find(
t => t.type === 'agent-preview' && t.agentSessionId === agentSessionId,
);
if (existing) {
activeTabId = existing.id;
return;
}
const id = crypto.randomUUID();
addTerminalTab(project.id, {
id,
title: 'Agent Preview',
type: 'agent-preview',
agentSessionId,
});
activeTabId = id;
}
function closeTab(tabId: string) {
removeTerminalTab(project.id, tabId);
}
function handleTabExit(tabId: string) {
closeTab(tabId);
}
</script>
<div class="terminal-tabs">
<div class="tab-bar">
{#each tabs as tab (tab.id)}
<div
class="tab"
class:active={activeTabId === tab.id}
role="tab"
tabindex="0"
onclick={() => (activeTabId = tab.id)}
onkeydown={e => e.key === 'Enter' && (activeTabId = tab.id)}
>
<span class="tab-title">{tab.title}</span>
<button
class="tab-close"
onclick={(e) => { e.stopPropagation(); closeTab(tab.id); }}
title="Close"
>×</button>
</div>
{/each}
<button class="tab-add" onclick={addShellTab} title="New shell">+</button>
{#if agentSessionId}
<button
class="tab-add tab-agent-preview"
onclick={addAgentPreviewTab}
title="Watch agent activity"
>👁</button>
{/if}
</div>
<div class="tab-content">
{#each tabs as tab (tab.id)}
<div class="tab-pane" class:visible={activeTabId === tab.id}>
{#if activeTabId === tab.id}
{#if tab.type === 'agent-preview' && tab.agentSessionId}
<AgentPreviewPane sessionId={tab.agentSessionId} />
{:else}
<TerminalPane
cwd={project.cwd}
shell={tab.type === 'ssh' ? '/usr/bin/ssh' : undefined}
onExit={() => handleTabExit(tab.id)}
/>
{/if}
{/if}
</div>
{/each}
{#if tabs.length === 0}
<div class="empty-terminals">
<button class="add-first" onclick={addShellTab}>
+ Open terminal
</button>
</div>
{/if}
</div>
</div>
<style>
.terminal-tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.tab-bar {
display: flex;
align-items: center;
gap: 1px;
padding: 0 0.25rem;
background: var(--ctp-mantle);
border-bottom: 1px solid var(--ctp-surface0);
overflow-x: auto;
flex-shrink: 0;
}
.tab {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: transparent;
border: none;
color: var(--ctp-overlay1);
font-size: 0.72rem;
cursor: pointer;
border-radius: 0.1875rem 0.1875rem 0 0;
white-space: nowrap;
transition: color 0.1s, background 0.1s;
}
.tab:hover {
color: var(--ctp-text);
background: var(--ctp-surface0);
}
.tab.active {
color: var(--ctp-text);
background: var(--ctp-base);
border-bottom: 1px solid var(--ctp-blue);
}
.tab-title {
max-width: 6.25rem;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-close {
background: transparent;
border: none;
color: var(--ctp-overlay0);
font-size: 0.8rem;
cursor: pointer;
padding: 0 0.125rem;
line-height: 1;
}
.tab-close:hover {
color: var(--ctp-red);
}
.tab-add {
background: transparent;
border: none;
color: var(--ctp-overlay0);
font-size: 0.85rem;
cursor: pointer;
padding: 0.125rem 0.375rem;
border-radius: 0.1875rem;
}
.tab-add:hover {
color: var(--ctp-text);
background: var(--ctp-surface0);
}
.tab-agent-preview {
font-size: 0.7rem;
}
.tab-content {
flex: 1;
position: relative;
overflow: hidden;
}
.tab-pane {
position: absolute;
inset: 0;
display: none;
}
.tab-pane.visible {
display: block;
}
.empty-terminals {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.add-first {
padding: 0.375rem 1rem;
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem;
color: var(--ctp-subtext0);
font-size: 0.8rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.add-first:hover {
background: var(--ctp-surface1);
color: var(--ctp-text);
}
</style>