<script lang="ts">
import { onMount } from "svelte";
import type { Session } from "../../api/types.js";
import {
resumeSession,
openSession,
getSessionDirectory,
listOpeners,
downloadExport,
getCompactText,
type Opener,
} from "../../api/client.js";
import { copyToClipboard, copyProducedText } from "../../utils/clipboard.js";
import { agentColor } from "../../utils/agents.js";
import { formatTokenCount } from "../../utils/format.js";
import { sessions } from "../../stores/sessions.svelte.js";
import { router } from "../../stores/router.svelte.js";
import {
supportsResume,
buildResumeCommand,
} from "../../utils/resume.js";
import { inSessionSearch } from "../../stores/inSessionSearch.svelte.js";
import { messages as messagesStore } from "../../stores/messages.svelte.js";
import { ui, ALL_BLOCK_TYPES, type BlockType } from "../../stores/ui.svelte.js";
import { notifications } from "../../stores/notifications.svelte.js";
import { statusBar } from "../../stores/status-bar.svelte.js";
import AppIcon from "./AppIcon.svelte";
interface Props {
session: Session | undefined;
onBack: () => void;
}
let { session, onBack }: Props = $props();
let copiedSessionId = $state("");
let renaming = $state(false);
let renameValue = $state("");
let renameStartedWith = "";
let renameInput = $state<HTMLInputElement | null>(null);
let showOpenMenu = $state(false);
let openers: Opener[] = $state([]);
let openFeedback = $state("");
let feedbackTimer: ReturnType<typeof setTimeout> | undefined;
let sessionDir = $state<string | null>(null);
let showActionsMenu = $state(false);
let menuBtnRef: HTMLButtonElement | undefined = $state(undefined);
let menuDropRef: HTMLDivElement | undefined = $state(undefined);
let copiedLinkId = $state("");
let copiedLinkTimer: ReturnType<typeof setTimeout> | undefined;
let compactTextCache = $state<string | null>(null);
let compactTextLoading = $state(false);
const BLOCK_LABELS: Record<BlockType, string> = {
user: "User messages",
assistant: "Assistant text",
thinking: "Thinking blocks",
tool: "Tool calls",
code: "Code blocks",
};
const BLOCK_COLORS: Record<BlockType, string> = {
user: "var(--vscode-focusBorder)",
assistant: "var(--vscode-textPreformat-foreground)",
thinking: "var(--vscode-textPreformat-foreground)",
tool: "var(--vscode-editorWarning-foreground)",
code: "var(--vscode-disabledForeground)",
};
async function handleExport() {
if (!session) return;
showActionsMenu = false;
try {
await downloadExport(session.id);
} catch (e) {
notifications.upsert(
"session:export",
"error",
"Export failed",
e instanceof Error ? e.message : String(e),
"session",
);
}
}
async function handleCopyText() {
const msgs = messagesStore.messages;
showActionsMenu = false;
if (msgs.length === 0) {
statusBar.showToast("No messages to copy", "error");
return;
}
const text = msgs
.map((m) => {
const role = m.role === "user" ? "User" : m.role === "assistant" ? "Assistant" : m.role;
return `[${role}]\n${m.content}`;
})
.join("\n\n---\n\n");
try {
await copyToClipboard(text);
statusBar.showToast("Session text copied", "success");
} catch (e) {
statusBar.showToast(`Copy failed: ${e instanceof Error ? e.message : String(e)}`, "error");
}
}
async function handleCopyCompactText() {
if (!session) return;
showActionsMenu = false;
const sessionId = session.id;
try {
await copyProducedText(async () => {
if (compactTextCache !== null) return compactTextCache;
compactTextLoading = true;
try {
const text = await getCompactText(sessionId);
compactTextCache = text;
return text;
} finally {
compactTextLoading = false;
}
});
statusBar.showToast("Compact text copied", "success");
} catch (e) {
compactTextLoading = false;
statusBar.showToast(`Copy compact failed: ${e instanceof Error ? e.message : String(e)}`, "error");
}
}
async function handleCopyCompactTail() {
if (!session) return;
showActionsMenu = false;
const sessionId = session.id;
try {
await copyProducedText(async () => {
if (compactTextCache !== null) return compactTextCache.split("\n").slice(-1000).join("\n");
compactTextLoading = true;
try {
const text = await getCompactText(sessionId);
compactTextCache = text;
return text.split("\n").slice(-1000).join("\n");
} finally {
compactTextLoading = false;
}
});
statusBar.showToast("Last 1000 lines of compact text copied", "success");
} catch (e) {
compactTextLoading = false;
statusBar.showToast(`Copy failed: ${e instanceof Error ? e.message : String(e)}`, "error");
}
}
$effect(() => {
if (showActionsMenu && session) {
compactTextCache = null;
getCompactText(session.id)
.then((text) => { compactTextCache = text; })
.catch(() => {});
} else {
compactTextCache = null;
}
});
$effect(() => {
if (!showActionsMenu) return;
function onClickOutside(e: MouseEvent) {
const target = e.target as Node;
if (menuBtnRef?.contains(target) || menuDropRef?.contains(target)) return;
showActionsMenu = false;
}
document.addEventListener("click", onClickOutside, true);
return () => document.removeEventListener("click", onClickOutside, true);
});
onMount(() => {
listOpeners()
.then((res) => { openers = res.openers; })
.catch(() => {});
});
$effect(() => {
sessionDir = null;
if (!session) return;
const id = session.id;
getSessionDirectory(id)
.then(({ path }) => {
if (session?.id === id) sessionDir = path || null;
})
.catch(() => {});
});
let sessionContextTokens = $derived(session?.peak_context_tokens ?? 0);
let mainModel = $derived(
messagesStore.sessionId === session?.id
? messagesStore.mainModel
: "",
);
function sessionDisplayId(id: string): string {
const idx = id.indexOf(":");
return idx >= 0 ? id.slice(idx + 1) : id;
}
async function copySessionId(rawId: string, sessionId: string) {
showActionsMenu = false;
try {
await copyToClipboard(rawId);
copiedSessionId = sessionId;
setTimeout(() => {
if (copiedSessionId === sessionId) copiedSessionId = "";
}, 1500);
statusBar.showToast("Session ID copied", "success");
} catch (e) {
statusBar.showToast(`Copy failed: ${e instanceof Error ? e.message : String(e)}`, "error");
}
}
async function copySessionLink() {
if (!session) return;
showActionsMenu = false;
const id = session.id;
const href = router.href({ level: "session", session: id });
const url = window.location.origin + href;
try {
await copyToClipboard(url);
copiedLinkId = id;
clearTimeout(copiedLinkTimer);
copiedLinkTimer = setTimeout(() => {
if (copiedLinkId === id) copiedLinkId = "";
}, 1500);
statusBar.showToast("Session link copied", "success");
} catch (e) {
statusBar.showToast(`Copy failed: ${e instanceof Error ? e.message : String(e)}`, "error");
}
}
function startRename() {
if (!session) return;
renameValue = session.display_name ?? session.first_message ?? "";
renameStartedWith = renameValue;
renaming = true;
showActionsMenu = false;
requestAnimationFrame(() => renameInput?.select());
}
async function submitRename() {
if (!renaming || !session) return;
renaming = false;
if (renameValue.trim() === renameStartedWith.trim()) return;
const name = renameValue.trim() || null;
try {
await sessions.renameSession(session.id, name);
} catch (e) {
notifications.upsert(
"session:rename",
"error",
"Rename failed",
e instanceof Error ? e.message : String(e),
"session",
);
}
}
function cancelRename() {
renaming = false;
}
async function handleDelete() {
if (!session) return;
showActionsMenu = false;
try {
await sessions.deleteSession(session.id);
} catch (e) {
notifications.upsert(
"session:delete",
"error",
"Delete failed",
e instanceof Error ? e.message : String(e),
"session",
);
}
}
function showFeedback(msg: string) {
openFeedback = msg;
clearTimeout(feedbackTimer);
feedbackTimer = setTimeout(() => { openFeedback = ""; }, 2000);
}
async function handleResumeIn(opener: Opener) {
if (!session) return;
showOpenMenu = false;
try {
const resp = await resumeSession(session.id, {
opener_id: opener.id,
});
if (resp.launched) {
showFeedback(`Resumed in ${resp.terminal ?? opener.name}`);
return;
}
if (resp.command) {
try {
await copyToClipboard(resp.command);
showFeedback("Command copied!");
} catch {
showFeedback("Copy failed");
}
return;
}
} catch {
}
const cmd = buildResumeCommand(session.agent, session.id);
if (cmd) {
try {
await copyToClipboard(cmd);
showFeedback("Command copied!");
} catch {
showFeedback("Copy failed");
}
} else {
showFeedback("Not supported");
}
}
function closeAllMenus() {
showActionsMenu = false;
showOpenMenu = false;
}
async function handleCopyResumeCommand() {
if (!session) return;
closeAllMenus();
const cmd = buildResumeCommand(session.agent, session.id);
if (cmd) {
try {
await copyToClipboard(cmd);
showFeedback("Command copied!");
} catch {
showFeedback("Copy failed");
}
} else {
showFeedback("Not supported");
}
}
async function handleCopyFilePath() {
closeAllMenus();
if (!sessionDir) {
showFeedback("No path available");
return;
}
try {
await copyToClipboard(sessionDir);
showFeedback("Path copied!");
} catch {
showFeedback("Copy failed");
}
}
async function handleOpenIn(opener: Opener) {
if (!session) return;
showOpenMenu = false;
try {
await openSession(session.id, opener.id);
showFeedback(`Opened in ${opener.name}`);
} catch (e) {
showFeedback("Failed to open");
notifications.upsert(
"session:open",
"error",
"Open failed",
e instanceof Error ? e.message : String(e),
"session",
);
}
}
async function handleResumeDefault() {
if (!session) return;
showOpenMenu = false;
try {
const resp = await resumeSession(session.id, {});
if (resp.launched) {
showFeedback(`Resumed in ${resp.terminal ?? "terminal"}`);
return;
}
if (resp.command) {
try {
await copyToClipboard(resp.command);
showFeedback("Command copied!");
} catch {
showFeedback("Copy failed");
}
return;
}
} catch {
}
const cmd = buildResumeCommand(session.agent, session.id);
if (cmd) {
try {
await copyToClipboard(cmd);
showFeedback("Command copied!");
} catch {
showFeedback("Copy failed");
}
} else {
showFeedback("Not supported");
}
}
const canResume = $derived(
session ? supportsResume(session.agent) : false,
);
const terminalOpeners = $derived(
openers.filter((o) => o.kind === "terminal"),
);
const editorOpeners = $derived(
openers.filter((o) => o.kind === "editor"),
);
const fileOpeners = $derived(
openers.filter((o) => o.kind === "files"),
);
const showDropdown = $derived(
canResume ||
editorOpeners.length > 0 ||
fileOpeners.length > 0 ||
sessionDir !== null,
);
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
if (renaming) {
cancelRename();
} else if (showActionsMenu) {
showActionsMenu = false;
} else if (showOpenMenu) {
showOpenMenu = false;
e.preventDefault();
}
return;
}
if (showOpenMenu) {
const num = parseInt(e.key);
if (num >= 1 && num <= 9) {
const idx = num - 1;
if (idx < terminalOpeners.length) {
e.preventDefault();
handleResumeIn(terminalOpeners[idx]!);
}
}
}
}
function handleClickOutside(e: MouseEvent) {
const target = e.target as Node;
if (!(target as HTMLElement).closest?.(".open-group")) {
showOpenMenu = false;
}
if (!(target as HTMLElement).closest?.(".more-group")) {
showActionsMenu = false;
}
}
</script>
<svelte:document
onkeydown={handleKeydown}
onclick={handleClickOutside}
/>
<div class="session-breadcrumb">
<button class="breadcrumb-link" onclick={onBack}>
Sessions
</button>
<span class="breadcrumb-sep">/</span>
{#if renaming}
<input
class="rename-input"
type="text"
bind:value={renameValue}
bind:this={renameInput}
onkeydown={(e) => {
if (e.key === "Enter") submitRename();
if (e.key === "Escape") cancelRename();
}}
onblur={submitRename}
/>
{:else}
<span class="breadcrumb-current">
{session?.display_name || session?.project || ""}
</span>
{/if}
{#if session}
<span class="breadcrumb-meta">
<span
class="agent-badge"
style:background={agentColor(session.agent)}
>{session.agent}</span>
{#if showDropdown}
<span class="open-group">
<button
class="resume-btn"
class:has-feedback={openFeedback !== ""}
onclick={(e) => { e.stopPropagation(); showOpenMenu = !showOpenMenu; }}
title={canResume ? "Resume session in terminal" : "Session actions"}
aria-label={canResume ? "Resume session" : "Session actions"}
>
{#if openFeedback}
<AppIcon name="check" size={11} />
{openFeedback}
{:else}
{canResume ? "Resume" : "Open"}
<AppIcon name="chevron-down" size={10} />
{/if}
</button>
{#if showOpenMenu}
<div class="open-menu">
{#if canResume}
{#each terminalOpeners as opener, i (opener.id)}
<button
class="open-menu-item"
onclick={() => handleResumeIn(opener)}
>
<span class="open-menu-num">{i + 1}</span>
<span class="open-menu-name">{opener.name}</span>
</button>
{/each}
<button class="open-menu-item" onclick={handleResumeDefault}>
<span class="open-menu-num">
<AppIcon name="square-terminal" size={10} />
</span>
<span class="open-menu-name">Default terminal</span>
</button>
<div class="open-menu-divider"></div>
<button class="open-menu-item" onclick={handleCopyResumeCommand}>
<span class="open-menu-num">
<AppIcon name="copy" size={10} />
</span>
<span class="open-menu-name">Copy command</span>
</button>
{/if}
<button class="open-menu-item" onclick={handleCopyFilePath}>
<span class="open-menu-num">
<AppIcon name="folder-open" size={10} />
</span>
<span class="open-menu-name">Copy directory path</span>
</button>
{#if editorOpeners.length > 0 || fileOpeners.length > 0}
<div class="open-menu-divider"></div>
<div class="open-menu-section">Open in</div>
{#each editorOpeners as opener (opener.id)}
<button
class="open-menu-item"
onclick={() => handleOpenIn(opener)}
>
<span class="open-menu-num">
<AppIcon name="code" size={10} />
</span>
<span class="open-menu-name">{opener.name}</span>
</button>
{/each}
{#each fileOpeners as opener (opener.id)}
<button
class="open-menu-item"
onclick={() => handleOpenIn(opener)}
>
<span class="open-menu-num">
<AppIcon name="folder" size={10} />
</span>
<span class="open-menu-name">{opener.name}</span>
</button>
{/each}
{/if}
</div>
{/if}
</span>
{/if}
<span class="more-group">
<button
class="header-btn"
bind:this={menuBtnRef}
onclick={() => (showActionsMenu = !showActionsMenu)}
title="Session actions"
aria-label="Session actions"
>
<AppIcon name="ellipsis" size={16} />
</button>
{#if showActionsMenu}
<div class="actions-dropdown" bind:this={menuDropRef}>
<div class="menu-section">
<div class="mode-strip">
<button
class="mode-pill"
class:active={ui.transcriptMode === "normal"}
onclick={() => ui.setTranscriptMode("normal")}
>Normal</button>
<button
class="mode-pill"
class:active={ui.transcriptMode === "focused"}
onclick={() => ui.setTranscriptMode("focused")}
>Focused</button>
</div>
</div>
<div class="menu-divider"></div>
<button class="menu-item" onclick={() => { ui.cycleLayout(); }}>
<AppIcon name={ui.messageLayout === "compact" ? "panel-top" : "layout-list"} size={13} />
<span>Layout: {ui.messageLayout}</span>
</button>
<button class="menu-item" onclick={() => ui.toggleSort()}>
<AppIcon name={ui.sortNewestFirst ? "chevron-down" : "chevron-up"} size={13} />
<span>Sort: {ui.sortNewestFirst ? "newest first" : "oldest first"}</span>
</button>
<div class="menu-divider"></div>
<div class="menu-section-label">Block visibility</div>
{#each ALL_BLOCK_TYPES as bt}
{@const visible = ui.isBlockVisible(bt)}
<button
class="menu-item"
class:dimmed={!visible}
onclick={() => ui.toggleBlock(bt)}
>
<span class="block-dot" style:background={visible ? BLOCK_COLORS[bt] : "var(--vscode-widget-border)"}></span>
<span class="flex-1">{BLOCK_LABELS[bt]}</span>
{#if visible}
<AppIcon name="check" size={11} />
{/if}
</button>
{/each}
{#if ui.hasBlockFilters}
<button class="menu-item submenu-action" onclick={() => ui.showAllBlocks()}>
Show all
</button>
{/if}
<div class="menu-divider"></div>
<button class="menu-item" onclick={() => copySessionId(sessionDisplayId(session.id), session.id)}>
<AppIcon name="copy" size={13} />
<span>{copiedSessionId === session.id ? "Copied!" : "Copy ID"}</span>
</button>
<button class="menu-item" onclick={copySessionLink}>
<AppIcon name="link" size={13} />
<span>{copiedLinkId === session?.id ? "Copied!" : "Copy link"}</span>
</button>
<button class="menu-item" onclick={handleCopyText}>
<AppIcon name="file-text" size={13} />
<span>Copy text</span>
</button>
<button class="menu-item" onclick={handleCopyCompactText} disabled={compactTextLoading}>
<AppIcon name="minimize" size={13} />
<span>{compactTextLoading ? "Loading…" : "Copy compact"}</span>
</button>
<button class="menu-item" onclick={handleCopyCompactTail} disabled={compactTextLoading}>
<AppIcon name="minimize" size={13} />
<span>{compactTextLoading ? "Loading…" : "Copy compact (last 1000 lines)"}</span>
</button>
{#if canResume}
<button class="menu-item" onclick={handleCopyResumeCommand}>
<AppIcon name="square-terminal" size={13} />
<span>Copy command</span>
</button>
{/if}
{#if sessionDir}
<button class="menu-item" onclick={handleCopyFilePath}>
<AppIcon name="folder-open" size={13} />
<span>Copy path</span>
</button>
{/if}
<div class="menu-divider"></div>
<button class="menu-item" onclick={handleExport}>
<AppIcon name="upload" size={13} />
<span>Export session</span>
</button>
{#if mainModel}
<span class="menu-info">Model: {mainModel}</span>
{/if}
{#if sessionContextTokens + (session?.total_output_tokens ?? 0) > 0}
<span class="menu-info">Tokens: {formatTokenCount(sessionContextTokens)} ctx / {formatTokenCount(session?.total_output_tokens ?? 0)} out</span>
{/if}
{#if session.started_at}
<span class="menu-info">{new Date(session.started_at).toLocaleDateString(undefined, { month: "short", day: "numeric" })} {new Date(session.started_at).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}</span>
{/if}
<div class="menu-divider"></div>
<button class="menu-item" onclick={startRename}>
<AppIcon name="pencil" size={13} />
<span>Rename</span>
</button>
<button class="menu-item danger" onclick={handleDelete}>
<AppIcon name="trash" size={13} />
<span>Delete</span>
</button>
</div>
{/if}
</span>
<button
class="header-btn find-btn"
class:find-btn--active={inSessionSearch.isOpen}
title="Find in session (/)"
onclick={() => inSessionSearch.toggle()}
aria-label="Find in session"
>
<AppIcon name="search" size={14} />
</button>
</span>
{/if}
</div>
<style>
.session-breadcrumb {
display: flex;
align-items: center;
gap: var(--wb-spacing-6);
height: var(--wb-control-size-32);
padding: 0 var(--wb-spacing-14);
border-bottom: 1px solid var(--vscode-widget-border);
flex-shrink: 0;
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
}
.breadcrumb-link {
color: var(--vscode-disabledForeground);
font-size: var(--wb-font-size-11);
font-weight: 500;
cursor: pointer;
transition: color 0.12s;
}
.breadcrumb-link:hover {
color: var(--vscode-focusBorder);
}
.breadcrumb-sep {
opacity: 0.3;
font-size: var(--wb-font-size-10);
}
.breadcrumb-current {
color: var(--vscode-editor-foreground);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.rename-input {
flex: 1;
min-width: 0;
font-size: var(--wb-font-size-11);
font-weight: 500;
color: var(--vscode-editor-foreground);
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-focusBorder);
border-radius: var(--wb-corner-radius-4);
padding: var(--wb-spacing-2) var(--wb-spacing-6);
outline: none;
font-family: inherit;
}
.breadcrumb-meta {
display: flex;
align-items: center;
gap: var(--wb-spacing-6);
margin-left: auto;
flex-shrink: 0;
}
.agent-badge {
font-size: var(--wb-font-size-9);
font-weight: 600;
padding: var(--wb-spacing-1) var(--wb-spacing-6);
border-radius: var(--wb-corner-radius-8);
text-transform: uppercase;
letter-spacing: 0.03em;
color: white;
flex-shrink: 0;
background: var(--vscode-disabledForeground);
}
.open-group {
position: relative;
display: flex;
align-items: center;
flex-shrink: 0;
}
.resume-btn {
display: flex;
align-items: center;
gap: var(--wb-spacing-4);
font-size: var(--wb-font-size-10);
font-weight: 500;
color: var(--vscode-disabledForeground);
padding: var(--wb-spacing-1) var(--wb-spacing-8);
border-radius: var(--wb-corner-radius-4);
background: var(--vscode-input-background);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
transition: color 0.15s, background 0.15s;
}
.resume-btn:hover {
color: var(--vscode-foreground);
background: var(--vscode-list-hoverBackground);
}
.resume-btn.has-feedback {
color: var(--vscode-testing-iconPassed, #2ea043);
}
.open-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: var(--wb-spacing-4);
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wb-corner-radius-8);
padding: var(--wb-spacing-4);
min-width: 200px;
z-index: 200;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.open-menu-item {
display: flex;
align-items: center;
gap: var(--wb-spacing-10);
width: 100%;
padding: var(--wb-spacing-6) var(--wb-spacing-10);
font-size: var(--wb-font-size-13);
color: var(--vscode-editor-foreground);
border-radius: var(--wb-corner-radius-5);
cursor: pointer;
transition: background 0.1s;
}
.open-menu-item:hover {
background: var(--vscode-list-hoverBackground);
}
.open-menu-num {
display: flex;
align-items: center;
justify-content: center;
width: var(--wb-control-size-18);
font-size: var(--wb-font-size-11);
font-weight: 500;
color: var(--vscode-disabledForeground);
flex-shrink: 0;
}
.open-menu-name {
flex: 1;
font-weight: 500;
}
.open-menu-divider {
height: 1px;
background: var(--vscode-widget-border);
margin: var(--wb-spacing-4) 0;
}
.open-menu-section {
padding: var(--wb-spacing-4) var(--wb-spacing-10) var(--wb-spacing-2);
font-size: var(--wb-font-size-10);
font-weight: 600;
color: var(--vscode-disabledForeground);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.more-group {
position: relative;
display: flex;
align-items: center;
flex-shrink: 0;
}
.header-btn {
display: flex;
align-items: center;
justify-content: center;
width: var(--wb-control-size-26);
height: var(--wb-control-size-22);
border: none;
border-radius: var(--wsb-radius-sm, var(--wb-corner-radius-4));
background: transparent;
color: var(--vscode-disabledForeground);
cursor: pointer;
transition: background 0.12s, color 0.12s;
flex-shrink: 0;
}
.header-btn:hover {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-foreground);
}
.find-btn--active {
color: var(--vscode-focusBorder);
background: color-mix(in srgb, var(--vscode-focusBorder) 12%, transparent);
}
.actions-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: var(--wb-spacing-4);
min-width: 220px;
max-height: 480px;
overflow-y: auto;
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-md, var(--wb-corner-radius-6));
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
padding: var(--wb-spacing-4) 0;
z-index: 200;
animation: dropdown-in 0.12s ease-out;
transform-origin: top right;
}
@keyframes dropdown-in {
from {
opacity: 0;
transform: scale(0.95) translateY(-2px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.menu-section {
padding: var(--wb-spacing-4) var(--wb-spacing-8);
}
.menu-section-label {
padding: var(--wb-spacing-4) var(--wb-spacing-12) var(--wb-spacing-2);
font-size: var(--wb-font-size-9);
font-weight: 600;
color: var(--vscode-disabledForeground);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.mode-strip {
display: flex;
align-items: center;
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-sm, var(--wb-corner-radius-4));
overflow: hidden;
}
.mode-pill {
padding: var(--wb-spacing-2) var(--wb-spacing-10);
font-size: var(--wb-font-size-10);
font-weight: 500;
color: var(--vscode-disabledForeground);
background: transparent;
cursor: pointer;
white-space: nowrap;
transition: background 0.1s, color 0.1s;
border: none;
}
.mode-pill:hover {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-foreground);
}
.mode-pill.active {
background: color-mix(in srgb, var(--vscode-focusBorder) 12%, transparent);
color: var(--vscode-focusBorder);
font-weight: 600;
}
.menu-divider {
height: 1px;
background: var(--vscode-widget-border);
margin: var(--wb-spacing-4) 0;
}
.menu-item {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
width: 100%;
padding: var(--wb-spacing-5) var(--wb-spacing-12);
font-size: var(--wb-font-size-12);
color: var(--vscode-editor-foreground);
text-align: left;
background: none;
border: none;
cursor: pointer;
font-family: var(--wsb-font-sans);
transition: background 0.08s;
}
.menu-item:hover {
background: var(--vscode-list-hoverBackground);
}
.menu-item.dimmed {
opacity: 0.5;
}
.menu-item.danger {
color: var(--vscode-errorForeground, #e55);
}
.menu-item.danger:hover {
background: color-mix(in srgb, var(--vscode-errorForeground, #e55) 10%, transparent);
}
.menu-item.submenu-action {
padding: var(--wb-spacing-3) var(--wb-spacing-12);
font-size: var(--wb-font-size-10);
color: var(--vscode-disabledForeground);
text-align: center;
}
.menu-item.submenu-action:hover {
color: var(--vscode-editor-foreground);
}
.block-dot {
width: var(--wb-control-size-6);
height: var(--wb-control-size-6);
border-radius: 50%;
flex-shrink: 0;
transition: background 0.1s;
}
.menu-info {
display: block;
padding: var(--wb-spacing-2) var(--wb-spacing-12);
font-size: var(--wb-font-size-10);
color: var(--vscode-disabledForeground);
font-variant-numeric: tabular-nums;
}
.flex-1 {
flex: 1;
}
@media (max-width: 767px) {
.breadcrumb-meta {
gap: var(--wb-spacing-4);
}
}
</style>