797 lines
21 KiB
Svelte
797 lines
21 KiB
Svelte
<script lang="ts">
|
|
import AgentPane from './AgentPane.svelte';
|
|
import TerminalTabs from './TerminalTabs.svelte';
|
|
import FileBrowser from './FileBrowser.svelte';
|
|
import MemoryTab from './MemoryTab.svelte';
|
|
import CommsTab from './CommsTab.svelte';
|
|
import TaskBoardTab from './TaskBoardTab.svelte';
|
|
import {
|
|
startAgent, stopAgent, sendPrompt, getSession, hasSession,
|
|
loadLastSession,
|
|
type AgentStatus, type AgentMessage,
|
|
} from './agent-store.svelte.ts';
|
|
|
|
type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memory' | 'comms' | 'tasks';
|
|
|
|
interface Props {
|
|
id: string;
|
|
name: string;
|
|
cwd: string;
|
|
accent: string;
|
|
provider?: string;
|
|
profile?: string;
|
|
model?: string;
|
|
contextPct?: number;
|
|
burnRate?: number;
|
|
blinkVisible?: boolean;
|
|
/** Worktree branch name — set when this is a clone card. */
|
|
worktreeBranch?: string;
|
|
/** ID of parent project — set when this is a clone card. */
|
|
cloneOf?: string;
|
|
/** Max clones reached for this project. */
|
|
clonesAtMax?: boolean;
|
|
/** Callback when user requests cloning (receives projectId and branch name). */
|
|
onClone?: (projectId: string, branch: string) => void;
|
|
/** Group ID for btmsg/bttask context. */
|
|
groupId?: string;
|
|
}
|
|
|
|
let {
|
|
id,
|
|
name,
|
|
cwd,
|
|
accent,
|
|
provider = 'claude',
|
|
profile,
|
|
model = 'claude-opus-4-5',
|
|
contextPct = 0,
|
|
burnRate = 0,
|
|
blinkVisible = true,
|
|
worktreeBranch,
|
|
cloneOf,
|
|
clonesAtMax = false,
|
|
onClone,
|
|
groupId = 'dev',
|
|
}: Props = $props();
|
|
|
|
// ── Agent session (reactive from store) ──────────────────────────
|
|
let session = $derived(getSession(id));
|
|
let agentStatus: AgentStatus = $derived(session?.status ?? 'idle');
|
|
let agentMessages: AgentMessage[] = $derived(session?.messages ?? []);
|
|
let agentCost = $derived(session?.costUsd ?? 0);
|
|
let agentTokens = $derived((session?.inputTokens ?? 0) + (session?.outputTokens ?? 0));
|
|
let agentModel = $derived(session?.model ?? model);
|
|
|
|
// ── Demo messages (fallback when no real session) ────────────────
|
|
const demoMessages: AgentMessage[] = [];
|
|
let displayMessages = $derived(agentMessages.length > 0 ? agentMessages : demoMessages);
|
|
|
|
// ── Clone dialog state ──────────────────────────────────────────
|
|
let showCloneDialog = $state(false);
|
|
let cloneBranchName = $state('');
|
|
let cloneError = $state('');
|
|
|
|
const BRANCH_RE = /^[a-zA-Z0-9/_.-]+$/;
|
|
|
|
function openCloneDialog() {
|
|
cloneBranchName = '';
|
|
cloneError = '';
|
|
showCloneDialog = true;
|
|
}
|
|
|
|
function submitClone() {
|
|
if (!BRANCH_RE.test(cloneBranchName)) {
|
|
cloneError = 'Use only letters, numbers, /, _, -, .';
|
|
return;
|
|
}
|
|
onClone?.(id, cloneBranchName);
|
|
showCloneDialog = false;
|
|
}
|
|
|
|
let activeTab = $state<ProjectTab>('model');
|
|
// 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', 'comms', 'tasks'];
|
|
|
|
// ── Load last session on mount ──────────────────────────────────────
|
|
$effect(() => {
|
|
loadLastSession(id);
|
|
});
|
|
|
|
function setTab(tab: ProjectTab) {
|
|
activeTab = tab;
|
|
activatedTabs = new Set([...activatedTabs, tab]);
|
|
}
|
|
|
|
function handleSend(text: string) {
|
|
if (hasSession(id)) {
|
|
// Session exists — send follow-up prompt
|
|
sendPrompt(id, text).catch((err) => {
|
|
console.error('[agent.prompt] error:', err);
|
|
});
|
|
} else {
|
|
// No session — start a new agent
|
|
startAgent(id, provider, text, { cwd, model }).catch((err) => {
|
|
console.error('[agent.start] error:', err);
|
|
});
|
|
}
|
|
}
|
|
|
|
function handleStop() {
|
|
stopAgent(id).catch((err) => {
|
|
console.error('[agent.stop] error:', err);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<article
|
|
class="project-card"
|
|
class:is-clone={!!cloneOf}
|
|
style="--accent: {accent}"
|
|
aria-label="Project: {name}{cloneOf ? ' (worktree clone)' : ''}"
|
|
>
|
|
<!-- Header -->
|
|
<header class="project-header">
|
|
<div class="status-dot-wrap" aria-label="Status: {agentStatus}">
|
|
<div
|
|
class="status-dot {agentStatus}"
|
|
class:blink-off={agentStatus === 'running' && !blinkVisible}
|
|
role="img"
|
|
aria-label={agentStatus}
|
|
></div>
|
|
</div>
|
|
|
|
<span class="project-name" title={name}>{name}</span>
|
|
<span class="project-cwd" title={cwd}>{cwd}</span>
|
|
|
|
{#if worktreeBranch}
|
|
<span class="wt-badge" title="Worktree branch: {worktreeBranch}">
|
|
WT · {worktreeBranch}
|
|
</span>
|
|
{/if}
|
|
|
|
<span class="provider-badge" title="Provider: {provider}">{provider}</span>
|
|
|
|
{#if profile}
|
|
<span class="profile-badge" title="Profile: {profile}">{profile}</span>
|
|
{/if}
|
|
|
|
{#if contextPct > 50}
|
|
<span
|
|
class="ctx-badge"
|
|
class:ctx-warn={contextPct >= 75}
|
|
class:ctx-danger={contextPct >= 90}
|
|
title="Context window {contextPct}% used"
|
|
>{contextPct}%</span>
|
|
{/if}
|
|
|
|
{#if burnRate > 0}
|
|
<span class="burn-badge" title="Burn rate">${burnRate.toFixed(2)}/hr</span>
|
|
{/if}
|
|
|
|
<!-- Clone button (only on non-clone cards) -->
|
|
{#if !cloneOf && onClone}
|
|
<button
|
|
class="clone-btn"
|
|
onclick={openCloneDialog}
|
|
disabled={clonesAtMax}
|
|
title={clonesAtMax ? 'Maximum 3 clones reached' : 'Clone into git worktree'}
|
|
aria-label="Clone project into worktree"
|
|
>
|
|
<!-- Fork / branch SVG icon -->
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<line x1="6" y1="3" x2="6" y2="15"/>
|
|
<circle cx="18" cy="6" r="3"/>
|
|
<circle cx="6" cy="18" r="3"/>
|
|
<path d="M18 9a9 9 0 0 1-9 9"/>
|
|
</svg>
|
|
</button>
|
|
{/if}
|
|
</header>
|
|
|
|
<!-- Clone dialog (inline, shown above tab bar) -->
|
|
{#if showCloneDialog}
|
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
<div
|
|
class="clone-dialog"
|
|
role="dialog"
|
|
aria-label="Create worktree clone"
|
|
onkeydown={(e) => { if (e.key === 'Escape') showCloneDialog = false; }}
|
|
>
|
|
<span class="clone-dialog-label">Branch name</span>
|
|
<input
|
|
class="clone-dialog-input"
|
|
type="text"
|
|
placeholder="feature/my-branch"
|
|
bind:value={cloneBranchName}
|
|
onkeydown={(e) => { if (e.key === 'Enter') submitClone(); }}
|
|
autofocus
|
|
aria-label="New branch name for worktree"
|
|
/>
|
|
{#if cloneError}
|
|
<span class="clone-dialog-error">{cloneError}</span>
|
|
{/if}
|
|
<div class="clone-dialog-actions">
|
|
<button class="clone-dialog-cancel" onclick={() => showCloneDialog = false}>Cancel</button>
|
|
<button class="clone-dialog-submit" onclick={submitClone}>Create</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Project tab bar -->
|
|
<div class="tab-bar" role="tablist" aria-label="{name} tabs">
|
|
{#each ALL_TABS as tab}
|
|
<button
|
|
class="tab-btn"
|
|
class:active={activeTab === tab}
|
|
role="tab"
|
|
aria-selected={activeTab === tab}
|
|
aria-controls="tabpanel-{id}-{tab}"
|
|
onclick={() => setTab(tab)}
|
|
>
|
|
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Tab content: display:flex/none to keep agent session mounted -->
|
|
<div class="tab-content">
|
|
<!-- Model tab: agent pane + terminal tabs — always mounted -->
|
|
<div
|
|
id="tabpanel-{id}-model"
|
|
class="tab-pane"
|
|
style:display={activeTab === 'model' ? 'flex' : 'none'}
|
|
role="tabpanel"
|
|
aria-label="Model"
|
|
>
|
|
<AgentPane
|
|
messages={displayMessages}
|
|
status={agentStatus}
|
|
costUsd={agentCost}
|
|
tokens={agentTokens}
|
|
model={agentModel}
|
|
{provider}
|
|
{profile}
|
|
{contextPct}
|
|
{burnRate}
|
|
onSend={handleSend}
|
|
onStop={handleStop}
|
|
/>
|
|
<TerminalTabs projectId={id} {accent} />
|
|
</div>
|
|
|
|
<!-- Docs tab -->
|
|
{#if activatedTabs.has('docs')}
|
|
<div
|
|
id="tabpanel-{id}-docs"
|
|
class="tab-pane"
|
|
style:display={activeTab === 'docs' ? 'flex' : 'none'}
|
|
role="tabpanel"
|
|
aria-label="Docs"
|
|
>
|
|
<div class="placeholder-pane">No markdown files open</div>
|
|
</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">{agentTokens.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">{agentModel}</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 displayMessages.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
|
|
id="tabpanel-{id}-files"
|
|
class="tab-pane"
|
|
style:display={activeTab === 'files' ? 'flex' : 'none'}
|
|
role="tabpanel"
|
|
aria-label="Files"
|
|
>
|
|
<FileBrowser {cwd} />
|
|
</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}
|
|
|
|
<!-- Comms tab (inter-agent messaging) -->
|
|
{#if activatedTabs.has('comms')}
|
|
<div
|
|
id="tabpanel-{id}-comms"
|
|
class="tab-pane"
|
|
style:display={activeTab === 'comms' ? 'flex' : 'none'}
|
|
role="tabpanel"
|
|
aria-label="Comms"
|
|
>
|
|
<CommsTab {groupId} agentId={id} />
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Tasks tab (kanban board) -->
|
|
{#if activatedTabs.has('tasks')}
|
|
<div
|
|
id="tabpanel-{id}-tasks"
|
|
class="tab-pane"
|
|
style:display={activeTab === 'tasks' ? 'flex' : 'none'}
|
|
role="tabpanel"
|
|
aria-label="Tasks"
|
|
>
|
|
<TaskBoardTab {groupId} agentId={id} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</article>
|
|
|
|
<style>
|
|
.project-card {
|
|
background: var(--ctp-base);
|
|
border: 1px solid var(--ctp-surface0);
|
|
border-radius: 0.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
min-height: 0;
|
|
position: relative;
|
|
}
|
|
|
|
/* Accent stripe on left edge */
|
|
.project-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background: var(--accent, var(--ctp-mauve));
|
|
border-radius: 0.5rem 0 0 0.5rem;
|
|
}
|
|
|
|
/* Worktree clone: accent top border instead of left stripe */
|
|
.project-card.is-clone {
|
|
border-top: 2px solid var(--accent, var(--ctp-mauve));
|
|
}
|
|
|
|
.project-card.is-clone::before {
|
|
display: none;
|
|
}
|
|
|
|
/* Header */
|
|
.project-header {
|
|
height: 2.5rem;
|
|
background: var(--ctp-mantle);
|
|
border-bottom: 1px solid var(--ctp-surface0);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
padding: 0 0.625rem 0 0.875rem;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.status-dot-wrap {
|
|
flex-shrink: 0;
|
|
width: 0.625rem;
|
|
height: 0.625rem;
|
|
position: relative;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
background: var(--ctp-overlay0);
|
|
}
|
|
|
|
.status-dot.running { background: var(--ctp-green); }
|
|
.status-dot.idle { background: var(--ctp-overlay1); }
|
|
.status-dot.done { background: var(--ctp-green); }
|
|
.status-dot.error { background: var(--ctp-red); }
|
|
.status-dot.stalled { background: var(--ctp-peach); }
|
|
.status-dot.blink-off { opacity: 0.3; }
|
|
|
|
.project-name {
|
|
font-weight: 600;
|
|
color: var(--ctp-text);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
flex-shrink: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.project-cwd {
|
|
font-size: 0.6875rem;
|
|
color: var(--ctp-subtext0);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
direction: rtl;
|
|
max-width: 8rem;
|
|
flex-shrink: 2;
|
|
}
|
|
|
|
/* Badges */
|
|
.provider-badge,
|
|
.profile-badge,
|
|
.ctx-badge,
|
|
.burn-badge {
|
|
flex-shrink: 0;
|
|
padding: 0.1rem 0.375rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.6rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.provider-badge {
|
|
background: color-mix(in srgb, var(--accent, var(--ctp-mauve)) 15%, transparent);
|
|
color: var(--accent, var(--ctp-mauve));
|
|
}
|
|
|
|
.profile-badge {
|
|
background: color-mix(in srgb, var(--ctp-blue) 12%, transparent);
|
|
color: var(--ctp-blue);
|
|
}
|
|
|
|
.ctx-badge {
|
|
background: color-mix(in srgb, var(--ctp-yellow) 15%, transparent);
|
|
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); }
|
|
|
|
.burn-badge {
|
|
background: color-mix(in srgb, var(--ctp-peach) 10%, transparent);
|
|
color: var(--ctp-peach);
|
|
}
|
|
|
|
/* Worktree badge */
|
|
.wt-badge {
|
|
flex-shrink: 0;
|
|
padding: 0.1rem 0.375rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.6rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
white-space: nowrap;
|
|
background: color-mix(in srgb, var(--accent, var(--ctp-mauve)) 20%, transparent);
|
|
color: var(--accent, var(--ctp-mauve));
|
|
border: 1px solid color-mix(in srgb, var(--accent, var(--ctp-mauve)) 40%, transparent);
|
|
}
|
|
|
|
/* Clone button */
|
|
.clone-btn {
|
|
flex-shrink: 0;
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--ctp-overlay1);
|
|
cursor: pointer;
|
|
border-radius: 0.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
transition: background 0.12s, color 0.12s;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.clone-btn:hover:not(:disabled) {
|
|
background: var(--ctp-surface0);
|
|
color: var(--ctp-text);
|
|
}
|
|
|
|
.clone-btn:disabled {
|
|
opacity: 0.35;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.clone-btn svg {
|
|
width: 0.875rem;
|
|
height: 0.875rem;
|
|
}
|
|
|
|
/* Inline clone dialog */
|
|
.clone-dialog {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.375rem 0.625rem;
|
|
background: var(--ctp-mantle);
|
|
border-bottom: 1px solid var(--ctp-surface0);
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.clone-dialog-label {
|
|
font-size: 0.75rem;
|
|
color: var(--ctp-subtext0);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.clone-dialog-input {
|
|
flex: 1;
|
|
min-width: 8rem;
|
|
height: 1.625rem;
|
|
background: var(--ctp-surface0);
|
|
border: 1px solid var(--ctp-surface1);
|
|
border-radius: 0.25rem;
|
|
color: var(--ctp-text);
|
|
font-family: var(--term-font-family);
|
|
font-size: 0.75rem;
|
|
padding: 0 0.375rem;
|
|
outline: none;
|
|
}
|
|
|
|
.clone-dialog-input:focus { border-color: var(--ctp-mauve); }
|
|
|
|
.clone-dialog-error {
|
|
width: 100%;
|
|
font-size: 0.6875rem;
|
|
color: var(--ctp-red);
|
|
}
|
|
|
|
.clone-dialog-actions {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.clone-dialog-cancel,
|
|
.clone-dialog-submit {
|
|
padding: 0.125rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-family: var(--ui-font-family);
|
|
cursor: pointer;
|
|
transition: background 0.1s, border-color 0.1s;
|
|
}
|
|
|
|
.clone-dialog-cancel {
|
|
background: transparent;
|
|
border: 1px solid var(--ctp-surface1);
|
|
color: var(--ctp-subtext0);
|
|
}
|
|
|
|
.clone-dialog-cancel:hover {
|
|
background: var(--ctp-surface0);
|
|
color: var(--ctp-text);
|
|
}
|
|
|
|
.clone-dialog-submit {
|
|
background: color-mix(in srgb, var(--ctp-mauve) 20%, transparent);
|
|
border: 1px solid var(--ctp-mauve);
|
|
color: var(--ctp-mauve);
|
|
}
|
|
|
|
.clone-dialog-submit:hover {
|
|
background: color-mix(in srgb, var(--ctp-mauve) 35%, transparent);
|
|
}
|
|
|
|
/* Tab bar */
|
|
.tab-bar {
|
|
height: 2rem;
|
|
background: var(--ctp-mantle);
|
|
border-bottom: 1px solid var(--ctp-surface0);
|
|
display: flex;
|
|
align-items: stretch;
|
|
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.625rem;
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
color: var(--ctp-subtext0);
|
|
font-family: var(--ui-font-family);
|
|
font-size: 0.8125rem;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: color 0.12s, border-color 0.12s;
|
|
margin-bottom: -1px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tab-btn:hover { color: var(--ctp-text); }
|
|
.tab-btn.active {
|
|
color: var(--ctp-text);
|
|
border-bottom-color: var(--accent, var(--ctp-mauve));
|
|
}
|
|
|
|
/* Tab content */
|
|
.tab-content {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.tab-pane {
|
|
flex: 1;
|
|
min-height: 0;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.placeholder-pane {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--ctp-overlay0);
|
|
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>
|