<script lang="ts">
import { untrack } from "svelte";
import { onDestroy } from "svelte";
import { sessions } from "../../stores/sessions.svelte.js";
import { starred } from "../../stores/starred.svelte.js";
import { sync } from "../../stores/sync.svelte.js";
import { workbench } from "../../stores/workbench.svelte.js";
import { commands } from "../../stores/commands.svelte.js";
import type { Session } from "../../api/types.js";
import { currentWorktree } from "../../api/scope.svelte.js";
import { worktreePlace } from "../../utils/places.js";
import AppIcon from "../layout/AppIcon.svelte";
import ProjectTypeahead from "../layout/ProjectTypeahead.svelte";
import SessionItem from "./SessionItem.svelte";
import { formatNumber } from "../../utils/format.js";
import {
agentColor,
agentLabel,
} from "../../utils/agents.js";
import {
type GroupMode,
type DisplayItemHeights,
OVERSCAN,
DEFAULT_DISPLAY_ITEM_HEIGHTS,
STORAGE_KEY_GROUP,
getInitialGroupMode,
buildGroupSections,
buildDisplayItems,
computeTotalSize,
findStart,
isSubagentDescendant,
selectPrimaryId,
} from "./session-list-utils.js";
let containerRef: HTMLDivElement | undefined = $state(undefined);
let scrollTop = $state(0);
let viewportHeight = $state(0);
let scrollRaf: number | null = $state(null);
let showFilterDropdown = $state(false);
let filterBtnRef: HTMLButtonElement | undefined =
$state(undefined);
let dropdownRef: HTMLDivElement | undefined =
$state(undefined);
let agentSearch = $state("");
let displayItemHeights: DisplayItemHeights = $state({
...DEFAULT_DISPLAY_ITEM_HEIGHTS,
});
function resolvedLength(name: string, fallback: number): number {
if (!containerRef) return fallback;
const value = Number.parseFloat(
getComputedStyle(containerRef).getPropertyValue(name),
);
return Number.isFinite(value) ? value : fallback;
}
function syncDisplayItemHeights() {
displayItemHeights = {
item: resolvedLength(
"--wb-session-row-height",
DEFAULT_DISPLAY_ITEM_HEIGHTS.item,
),
child: resolvedLength(
"--wb-session-child-row-height",
DEFAULT_DISPLAY_ITEM_HEIGHTS.child,
),
group: resolvedLength(
"--wb-session-group-row-height",
DEFAULT_DISPLAY_ITEM_HEIGHTS.group,
),
header: resolvedLength(
"--wb-session-group-row-height",
DEFAULT_DISPLAY_ITEM_HEIGHTS.header,
),
};
}
// Agents sorted by frequency, filtered by search.
let sortedAgents = $derived.by(() => {
const agents = [...sessions.agents].sort(
(a, b) => b.session_count - a.session_count,
);
if (!agentSearch) return agents;
const q = agentSearch.toLowerCase();
return agents.filter((a) =>
agentLabel(a.name).toLowerCase().includes(q),
);
});
let machineSearch = $state("");
let sortedMachines = $derived.by(() => {
const machines = [...sessions.machines].sort();
if (!machineSearch) return machines;
const q = machineSearch.toLowerCase();
return machines.filter((m) => m.toLowerCase().includes(q));
});
// Ensure agents and machines are loaded when dropdown opens.
// Opening the dropdown is the trigger; what the loads then read and write is
// not. Tracked, this is the shape that loops the page.
$effect(() => {
if (!showFilterDropdown) return;
untrack(() => {
sessions.loadAgents();
sessions.loadMachines();
agentSearch = "";
machineSearch = "";
});
});
let groupMode: GroupMode = $state(getInitialGroupMode());
let manualExpanded: Set<string> = $state(new Set());
// Start all collapsed when grouping is first enabled.
let collapseAll = $state(getInitialGroupMode() !== "none");
// Track which continuation chains are expanded.
let expandedGroups: Set<string> = $state(new Set());
$effect(() => {
if (typeof localStorage !== "undefined") {
localStorage.setItem(STORAGE_KEY_GROUP, groupMode);
}
});
let hasFilters = $derived(
sessions.hasActiveFilters || starred.filterOnly,
);
let isRecentlyActiveOn = $derived(
sessions.filters.recentlyActive,
);
let isPlacedOnlyOn = $derived(
sessions.filters.placedOnly,
);
/**
* Where this worktree is, by prj's names. A worktree outside `$PRJ` has no
* place, so "this worktree" and "this project" have nothing to show.
*/
let here = $derived.by(() => {
const id = currentWorktree();
return id ? worktreePlace(id) : null;
});
/** The project the project scope shows: the one chosen, or this one. */
let shownProject = $derived(
sessions.filters.scope === "project"
? sessions.filters.project || here?.project || ""
: "",
);
let isIncludeOneShotOn = $derived(
sessions.filters.includeOneShot,
);
let groups = $derived.by(() => {
const all = sessions.groupedSessions;
if (!starred.filterOnly) return all;
return all
.map((g) => {
const filtered = g.sessions.filter((s) =>
starred.isStarred(s.id),
);
// Recompute primarySessionId so it points to a
// session that survived the filter, using the same
// recency rule as buildSessionGroups.
const primaryStillPresent = filtered.some(
(s) => s.id === g.primarySessionId,
);
return {
...g,
sessions: filtered,
// Preserve full session list so ancestry helpers
// can still walk the parent chain correctly.
allSessions: g.sessions,
primarySessionId: primaryStillPresent
? g.primarySessionId
: selectPrimaryId(filtered, g.key),
};
})
.filter((g) => g.sessions.length > 0);
});
// Build grouped structure when groupMode is not "none".
let groupSections = $derived.by(() =>
buildGroupSections(groups, groupMode),
);
// Derive effective collapsed set synchronously so the first
// render is already collapsed (no flicker).
let collapsed = $derived.by(() => {
if (groupMode === "none") return new Set<string>();
if (collapseAll) {
return new Set(groupSections.map((s) => s.label));
}
// Invert: all labels minus the manually expanded ones.
const all = new Set(groupSections.map((s) => s.label));
for (const a of manualExpanded) all.delete(a);
return all;
});
// Build flat display items for virtual scrolling.
let displayItems = $derived.by(() =>
buildDisplayItems(
groups,
groupSections,
groupMode,
collapsed,
expandedGroups,
displayItemHeights,
),
);
// When include_children is enabled the API total includes
// child/subagent sessions. The header should show the count of
// root-level groups the user actually sees in the sidebar.
let totalCount = $derived(
starred.filterOnly
? groups.reduce((n, g) => n + g.sessions.length, 0)
: groups.length,
);
let totalSize = $derived(computeTotalSize(displayItems));
let visibleItems = $derived.by(() => {
if (displayItems.length === 0) return [];
const start = findStart(
displayItems,
scrollTop,
displayItemHeights.item,
);
const end =
scrollTop +
viewportHeight +
OVERSCAN * displayItemHeights.item;
const result: typeof displayItems = [];
for (let i = start; i < displayItems.length; i++) {
const item = displayItems[i]!;
if (item.top > end) break;
result.push(item);
}
return result;
});
function setGroupMode(mode: GroupMode) {
groupMode = mode;
collapseAll = mode !== "none";
manualExpanded = new Set();
}
function toggleGroupByAgent() {
setGroupMode(groupMode === "agent" ? "none" : "agent");
}
function toggleGroupByProject() {
setGroupMode(groupMode === "project" ? "none" : "project");
}
function toggleGroup(label: string) {
if (collapseAll) {
collapseAll = false;
manualExpanded = new Set([label]);
} else {
const next = new Set(manualExpanded);
if (next.has(label)) {
next.delete(label);
} else {
next.add(label);
}
manualExpanded = next;
}
}
function toggleChainExpand(groupKey: string) {
const next = new Set(expandedGroups);
if (next.has(groupKey)) {
next.delete(groupKey);
// When collapsing a parent, also remove sub-group keys.
if (!groupKey.includes(":")) {
next.delete(`subagent:${groupKey}`);
next.delete(`team:${groupKey}`);
}
} else {
next.add(groupKey);
// When expanding a parent, auto-expand sub-groups.
if (!groupKey.includes(":")) {
next.add(`subagent:${groupKey}`);
next.add(`team:${groupKey}`);
}
}
expandedGroups = next;
}
$effect(() => {
if (!containerRef) return;
syncDisplayItemHeights();
viewportHeight = containerRef.clientHeight;
const ro = new ResizeObserver(() => {
if (!containerRef) return;
syncDisplayItemHeights();
viewportHeight = containerRef.clientHeight;
});
ro.observe(containerRef);
const tokenObserver = new MutationObserver(syncDisplayItemHeights);
tokenObserver.observe(document.documentElement, { attributes: true });
return () => {
ro.disconnect();
tokenObserver.disconnect();
};
});
// Clamp stale scrollTop when count shrinks.
$effect(() => {
if (!containerRef) return;
const maxTop = Math.max(
0,
totalSize - containerRef.clientHeight,
);
if (scrollTop > maxTop) {
scrollTop = maxTop;
containerRef.scrollTop = maxTop;
}
});
// Close filter dropdown on outside click.
$effect(() => {
if (!showFilterDropdown) return;
function onClickOutside(e: MouseEvent) {
const target = e.target as Node;
if (
filterBtnRef?.contains(target) ||
dropdownRef?.contains(target)
)
return;
showFilterDropdown = false;
}
document.addEventListener("click", onClickOutside, true);
return () =>
document.removeEventListener(
"click",
onClickOutside,
true,
);
});
function handleScroll() {
if (!containerRef) return;
if (scrollRaf !== null) return;
scrollRaf = requestAnimationFrame(() => {
scrollRaf = null;
if (!containerRef) return;
scrollTop = containerRef.scrollTop;
});
}
// Scroll to the active session when it changes (e.g. from
// the command palette). Expands collapsed agent groups and
// scrolls the item into view. Only fires on selection
// changes, not on displayItems rebuilds, so collapsing a
// group containing the active session stays collapsed.
let prevRevealedId: string | null = null;
$effect(() => {
const activeId = sessions.activeSessionId;
if (!activeId) {
prevRevealedId = null;
return;
}
if (activeId === prevRevealedId) return;
if (!containerRef) return;
// Read displayItems inside the effect so Svelte tracks
// it — needed to re-run after a group expansion.
const items = displayItems;
// Try to find the exact child row first (when expanded).
let item = items.find(
(it) =>
it.type === "session" &&
it.isChild &&
it.session?.id === activeId,
);
// Fall back to the parent row only if the active session
// IS the primary (visible as the root row). If it's a
// child hidden in a collapsed subgroup, fall through to
// the auto-expand path below instead.
if (!item) {
item = items.find(
(it) =>
it.type === "session" &&
!it.isChild &&
it.group?.primarySessionId === activeId,
);
}
if (!item) {
// Session may be hidden in a collapsed group section.
// Expand it — the effect will re-run when displayItems
// updates, and prevRevealedId is still unset so the
// second pass will proceed to scroll.
if (groupMode !== "none") {
for (const section of groupSections) {
const owns = section.groups.some((g) =>
g.sessions.some((s) => s.id === activeId),
);
if (owns && collapsed.has(section.label)) {
toggleGroup(section.label);
return;
}
}
}
// Session may be inside a collapsed continuation chain.
// Auto-expand the parent group and relevant sub-groups.
for (const g of groups) {
const match = g.sessions.find((s) => s.id === activeId);
if (!match) continue;
if (match.id === g.primarySessionId) break; // already primary
const next = new Set(expandedGroups);
if (!next.has(g.key)) next.add(g.key);
// Auto-expand the correct sub-group.
next.add(`subagent:${g.key}`);
next.add(`team:${g.key}`);
expandedGroups = next;
return;
}
return;
}
// Item found — mark as revealed so subsequent
// displayItems rebuilds don't re-trigger.
prevRevealedId = activeId;
const itemBottom = item.top + item.height;
const viewTop = containerRef.scrollTop;
const viewBottom = viewTop + containerRef.clientHeight;
if (item.top >= viewTop && itemBottom <= viewBottom) return;
containerRef.scrollTop = Math.max(
0,
item.top - containerRef.clientHeight / 2 + item.height / 2,
);
});
/**
* The session rows the list draws, in its order, each with the sessions it
* stands for: a collapsed chain's row stands for every session in it, as its
* highlight does.
*/
function sessionRows(): { session: Session; covers: string[] }[] {
const rows: { session: Session; covers: string[] }[] = [];
for (const item of displayItems) {
if (item.type !== "session") continue;
if (item.isChild && item.session) {
rows.push({ session: item.session, covers: [item.session.id] });
} else if (item.group) {
const group = item.group;
const primary =
group.sessions.find((s) => s.id === group.primarySessionId) ?? group.sessions[0];
if (!primary) continue;
rows.push({
session: primary,
covers: expandedGroups.has(group.key) ? [primary.id] : group.sessions.map((s) => s.id),
});
}
}
return rows;
}
/** `]` and `[`: open the next or previous session row, as a click on it would. */
function step(delta: number) {
const rows = sessionRows();
if (rows.length === 0) return;
const active = sessions.activeSessionId;
const at = active === null ? -1 : rows.findIndex((row) => row.covers.includes(active));
const next = at === -1 ? (delta > 0 ? 0 : rows.length - 1) : at + delta;
const target = rows[next];
if (!target) return;
sessions.selectSession(target.session.id);
workbench.openSession(target.session, true);
}
$effect(() => {
const offNext = commands.register("wsb.sessions.next", () => step(1));
const offPrevious = commands.register("wsb.sessions.previous", () => step(-1));
return () => {
offNext();
offPrevious();
};
});
onDestroy(() => {
if (scrollRaf !== null) {
cancelAnimationFrame(scrollRaf);
scrollRaf = null;
}
});
</script>
<div class="session-list-header">
{#if here}
<div class="segmented session-scope" role="group" aria-label="Which sessions">
<button
aria-pressed={sessions.filters.scope === "worktree"}
onclick={() => sessions.setScope("worktree")}
title="Sessions that ran in {here.project}/{here.worktree}"
>
This worktree
</button>
<button
aria-pressed={sessions.filters.scope === "project" && shownProject === here.project}
onclick={() => sessions.setScope("project")}
title="Sessions that ran in any worktree of {here.project}"
>
This project
</button>
<button
aria-pressed={sessions.filters.scope === "all"}
onclick={() => sessions.setScope("all")}
title="Every session on this machine"
>
All
</button>
</div>
{/if}
<div class="session-list-top">
<ProjectTypeahead
projects={sessions.projects}
value={shownProject}
onselect={(v) => sessions.chooseProject(v)}
/>
<div class="header-actions">
<button
class="filter-btn"
bind:this={filterBtnRef}
onclick={() =>
(showFilterDropdown = !showFilterDropdown)}
>
<AppIcon name="filter" size={14} />
{#if hasFilters || groupMode !== "none"}
<span class="filter-indicator"></span>
{/if}
</button>
{#if showFilterDropdown}
<div class="filter-dropdown" bind:this={dropdownRef}>
<div class="filter-section">
<div class="filter-section-label">Display</div>
<button
class="filter-toggle"
class:active={groupMode === "agent"}
onclick={toggleGroupByAgent}
>
<span
class="toggle-check"
class:on={groupMode === "agent"}
></span>
Group by agent
</button>
<button
class="filter-toggle"
class:active={groupMode === "project"}
onclick={toggleGroupByProject}
>
<span
class="toggle-check"
class:on={groupMode === "project"}
></span>
Group by project
</button>
</div>
<div class="filter-section">
<div class="filter-section-label">Starred</div>
<button
class="filter-toggle"
class:active={starred.filterOnly}
onclick={() => (starred.filterOnly = !starred.filterOnly)}
>
<span
class="toggle-check"
class:on={starred.filterOnly}
></span>
Starred only
{#if starred.count > 0}
<span class="starred-count">{starred.count}</span>
{/if}
</button>
</div>
<div class="filter-section">
<div class="filter-section-label">Activity</div>
<button
class="filter-toggle"
class:active={isRecentlyActiveOn}
onclick={() =>
sessions.setRecentlyActiveFilter(
!isRecentlyActiveOn,
)}
>
<span
class="toggle-check"
class:on={isRecentlyActiveOn}
></span>
Recently Active
</button>
</div>
<div class="filter-section">
<div class="filter-section-label">
Session Type
</div>
<button
class="filter-toggle"
class:active={isIncludeOneShotOn}
onclick={() =>
sessions.setIncludeOneShotFilter(
!isIncludeOneShotOn,
)}
>
<span
class="toggle-check"
class:on={isIncludeOneShotOn}
></span>
Include single-turn
</button>
</div>
<div class="filter-section">
<div class="filter-section-label">Project</div>
<button
class="filter-toggle"
class:active={isPlacedOnlyOn}
onclick={() =>
sessions.setPlacedOnlyFilter(
!isPlacedOnlyOn,
)}
>
<span
class="toggle-check"
class:on={isPlacedOnlyOn}
></span>
Only in projects
</button>
</div>
<div class="filter-section">
<div class="filter-section-label">Agent</div>
{#if sessions.agents.length > 5}
<input
class="agent-search"
type="text"
placeholder="Search agents..."
bind:value={agentSearch}
/>
{/if}
<div class="agent-select-list">
{#each sortedAgents as agent (agent.name)}
{@const selected =
sessions.isAgentSelected(agent.name)}
<button
class="agent-select-row"
class:selected
style:--agent-color={agentColor(agent.name)}
onclick={() =>
sessions.toggleAgentFilter(agent.name)}
>
<span
class="agent-check"
class:on={selected}
>
{#if selected}
<AppIcon name="check" size={8} />
{/if}
</span>
<span
class="agent-dot-mini"
style:background={agentColor(agent.name)}
></span>
<span class="agent-select-name">
{agentLabel(agent.name)}
</span>
<span class="agent-select-count">
{agent.session_count}
</span>
</button>
{:else}
<span class="agent-select-empty">
{agentSearch ? "No match" : "No agents"}
</span>
{/each}
</div>
</div>
{#if sessions.machines.length > 0}
<div class="filter-section">
<div class="filter-section-label">Machine</div>
{#if sessions.machines.length > 5}
<input
class="agent-search"
type="text"
placeholder="Search machines..."
bind:value={machineSearch}
/>
{/if}
<div class="agent-select-list">
{#each sortedMachines as machine (machine)}
{@const selected =
sessions.isMachineSelected(machine)}
<button
class="agent-select-row"
class:selected
style:--agent-color={"var(--vscode-focusBorder)"}
onclick={() =>
sessions.toggleMachineFilter(machine)}
>
<span
class="agent-check"
class:on={selected}
>
{#if selected}
<AppIcon name="check" size={8} />
{/if}
</span>
<span class="agent-select-name">
{machine}
</span>
</button>
{:else}
<span class="agent-select-empty">
{machineSearch ? "No match" : "No machines"}
</span>
{/each}
</div>
</div>
{/if}
<div class="filter-section">
<div class="filter-section-label">Min Prompts</div>
<div class="pill-buttons">
{#each [2, 3, 5, 10] as n}
<button
class="pill-btn"
class:active={sessions.filters.minUserMessages === n}
onclick={() =>
sessions.setMinUserMessagesFilter(n)}
>
{n}
</button>
{/each}
</div>
</div>
{#if hasFilters || groupMode !== "none"}
<button
class="clear-filters-btn"
onclick={() => {
if (groupMode !== "none") setGroupMode("none");
if (sessions.hasActiveFilters && starred.filterOnly) {
starred.filterOnly = false;
sessions.clearSessionFilters();
} else if (sessions.hasActiveFilters) {
sessions.clearSessionFilters();
} else if (starred.filterOnly) {
starred.filterOnly = false;
}
}}
>
Clear filters
</button>
{/if}
</div>
{/if}
</div>
</div>
<div class="session-list-meta">
<span class="session-count">
{formatNumber(totalCount)} sessions
</span>
{#if sessions.loading}
<span class="loading-indicator">loading</span>
{/if}
</div>
</div>
<div
class="session-list-scroll"
bind:this={containerRef}
onscroll={handleScroll}
>
{#if sessions.loading && displayItems.length === 0}
<div class="skel-list">
{#each { length: 12 } as _, i}
<div class="skel-session-item">
<div class="skel" style="width: {50 + ((i * 29) % 40)}%; height: var(--wb-control-size-10);"></div>
<div class="skel" style="width: {30 + ((i * 17) % 50)}%; height: var(--wb-control-size-8); margin-top: var(--wb-spacing-4);"></div>
</div>
{/each}
</div>
{/if}
<div
style="height: {totalSize}px; width: 100%; position: relative;"
>
{#each visibleItems as item (item.id)}
<div
style="position: absolute; top: 0; left: 0; width: 100%; height: {item.height}px; transform: translateY({item.top}px);"
>
{#if item.type === "header"}
<button
class="group-header"
onclick={() => toggleGroup(item.label)}
>
<AppIcon
class={`chevron${!collapsed.has(item.label) ? " expanded" : ""}`}
name="chevron-right"
size={10}
/>
{#if groupMode === "agent"}
<span
class="group-dot"
style:background={agentColor(item.label)}
></span>
{:else}
<AppIcon class="project-icon" name="folder" size={11} />
{/if}
<span class="group-name">{item.label}</span>
<span class="group-count">{item.count}</span>
</button>
{:else if item.type === "subagent-group" && item.group}
{@const subKey = `subagent:${item.group.key}`}
{@const subExpanded = expandedGroups.has(subKey)}
<button
class="sub-group-header"
style:padding-left={`var(--wb-tree-indent-${item.depth ?? 1})`}
onclick={() => toggleChainExpand(subKey)}
>
<AppIcon class={`sub-group-arrow${subExpanded ? " expanded" : ""}`} name="chevron-right" size={10} />
<AppIcon class="sub-group-icon" name="user" size={10} />
<span class="sub-group-label">Subagents</span>
<span class="sub-group-count">({item.count})</span>
</button>
{:else if item.type === "team-group" && item.group}
{@const teamKey = `team:${item.group.key}`}
{@const teamExpanded = expandedGroups.has(teamKey)}
<button
class="sub-group-header"
style:padding-left={`var(--wb-tree-indent-${item.depth ?? 1})`}
onclick={() => toggleChainExpand(teamKey)}
>
<AppIcon class={`sub-group-arrow${teamExpanded ? " expanded" : ""}`} name="chevron-right" size={10} />
<AppIcon class="sub-group-icon" name="users" size={12} />
<span class="sub-group-label">Team</span>
<span class="sub-group-count">({item.count})</span>
</button>
{:else if item.isChild && item.session}
<SessionItem
session={item.session}
continuationCount={1}
hideAgent={groupMode === "agent"}
hideProject={groupMode === "project"}
compact
depth={item.depth ?? 1}
isLastChild={item.isLastChild ?? false}
/>
{:else if item.group}
{@const primary = item.group.sessions.find(
(s) => s.id === item.group!.primarySessionId,
) ?? item.group.sessions[0]}
{@const children = item.group.sessions.filter((s) => s.id !== item.group!.primarySessionId)}
{@const groupHasSubagents = children.some((s) => isSubagentDescendant(s, item.group!.sessions))}
{@const groupHasTeammates = children.some((s) => s.first_message?.includes("<teammate-message") ?? false)}
{#if primary}
<SessionItem
session={primary}
continuationCount={item.group.sessions.length}
groupSessionIds={item.group.sessions.length > 1
? item.group.sessions.map((s) => s.id)
: undefined}
hideAgent={groupMode === "agent"}
hideProject={groupMode === "project"}
expanded={expandedGroups.has(item.group.key)}
onToggleExpand={item.group.sessions.length > 1
? () => toggleChainExpand(item.group!.key)
: undefined}
depth={0}
hasSubagents={groupHasSubagents}
hasTeammates={groupHasTeammates}
/>
{/if}
{/if}
</div>
{/each}
</div>
</div>
<style>
.session-list-header {
display: flex;
flex-direction: column;
gap: var(--wb-spacing-8);
padding: var(--wb-spacing-10) var(--wb-spacing-14) var(--wb-spacing-8);
font-size: var(--wb-font-size-10);
color: var(--vscode-disabledForeground);
border-bottom: 1px solid var(--vscode-widget-border);
flex-shrink: 0;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.session-scope {
display: flex;
width: 100%;
margin-bottom: var(--wb-spacing-6);
}
.session-scope > button {
flex: 1;
}
.session-list-top {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
width: 100%;
}
.session-list-top :global(.typeahead) {
flex: 1;
min-width: 0;
max-width: none;
}
.session-list-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--wb-spacing-8);
width: 100%;
}
.session-count {
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
position: relative;
}
.loading-indicator {
color: var(--vscode-testing-iconPassed);
}
.filter-btn {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: var(--wb-control-size-24);
height: var(--wb-control-size-24);
border-radius: var(--wb-corner-radius-4);
color: var(--vscode-disabledForeground);
transition: color 0.1s, background 0.1s;
}
.filter-btn:hover {
color: var(--vscode-editor-foreground);
background: var(--vscode-list-hoverBackground);
}
.filter-indicator {
position: absolute;
top: var(--wb-spacing-2);
right: var(--wb-spacing-2);
width: var(--wb-control-size-6);
height: var(--wb-control-size-6);
border-radius: 50%;
background: var(--vscode-testing-iconPassed);
}
.filter-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: var(--wb-spacing-4);
width: 220px;
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-md);
box-shadow: var(--wb-shadow-lg);
padding: var(--wb-spacing-8);
z-index: 100;
text-transform: none;
letter-spacing: normal;
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);
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.filter-section {
padding: var(--wb-spacing-4) 0;
}
.filter-section + .filter-section {
border-top: 1px solid var(--vscode-widget-border);
margin-top: var(--wb-spacing-4);
padding-top: var(--wb-spacing-8);
}
.filter-section-label {
font-size: var(--wb-font-size-9);
font-weight: 600;
color: var(--vscode-disabledForeground);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: var(--wb-spacing-6);
}
.filter-toggle {
display: flex;
align-items: center;
gap: var(--wb-spacing-6);
width: 100%;
padding: var(--wb-spacing-4) var(--wb-spacing-8);
font-size: var(--wb-font-size-11);
color: var(--vscode-foreground);
text-align: left;
border-radius: var(--wb-corner-radius-4);
transition: background 0.1s, color 0.1s;
}
.filter-toggle:hover {
background: var(--vscode-list-hoverBackground);
}
.filter-toggle.active {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-testing-iconPassed);
font-weight: 500;
}
.toggle-check {
width: var(--wb-control-size-10);
height: var(--wb-control-size-10);
border-radius: var(--wb-corner-radius-2);
border: 1.5px solid var(--vscode-panel-border);
flex-shrink: 0;
transition: background 0.1s, border-color 0.1s;
}
.toggle-check.on {
background: var(--vscode-testing-iconPassed);
border-color: var(--vscode-testing-iconPassed);
}
.agent-search {
width: 100%;
height: var(--wb-control-size-24);
padding: 0 var(--wb-spacing-8);
margin-bottom: var(--wb-spacing-4);
font-size: var(--wb-font-size-10);
color: var(--vscode-editor-foreground);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-widget-border);
border-radius: var(--wb-corner-radius-4);
outline: none;
transition: border-color 0.1s;
}
.agent-search::placeholder {
color: var(--vscode-disabledForeground);
}
.agent-search:focus {
border-color: var(--vscode-focusBorder);
}
.agent-select-list {
display: flex;
flex-direction: column;
max-height: 180px;
overflow-y: auto;
overflow-x: hidden;
gap: var(--wb-spacing-1);
}
.agent-select-row {
display: flex;
align-items: center;
gap: var(--wb-spacing-6);
width: 100%;
padding: var(--wb-spacing-3) var(--wb-spacing-8);
font-size: var(--wb-font-size-11);
color: var(--vscode-foreground);
text-align: left;
border-radius: var(--wb-corner-radius-3);
transition: background 0.08s, color 0.08s;
flex-shrink: 0;
}
.agent-select-row:hover {
background: var(--vscode-list-hoverBackground);
}
.agent-select-row.selected {
color: var(--agent-color, var(--vscode-focusBorder));
font-weight: 500;
background: color-mix(
in srgb,
var(--agent-color, var(--vscode-focusBorder)) 10%,
transparent
);
}
.agent-check {
width: var(--wb-control-size-10);
height: var(--wb-control-size-10);
border-radius: var(--wb-corner-radius-2);
border: 1.5px solid var(--vscode-panel-border);
flex-shrink: 0;
transition: background 0.1s, border-color 0.1s;
color: white;
display: flex;
align-items: center;
justify-content: center;
}
.agent-check.on {
background: var(--agent-color, var(--vscode-focusBorder));
border-color: var(--agent-color, var(--vscode-focusBorder));
}
.agent-dot-mini {
width: var(--wb-control-size-5);
height: var(--wb-control-size-5);
border-radius: 50%;
flex-shrink: 0;
}
.agent-select-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-select-count {
flex-shrink: 0;
font-size: var(--wb-font-size-9);
font-weight: 500;
color: var(--vscode-disabledForeground);
min-width: var(--wb-control-size-14);
text-align: right;
font-variant-numeric: tabular-nums;
}
.agent-select-empty {
display: block;
padding: var(--wb-spacing-6) var(--wb-spacing-8);
font-size: var(--wb-font-size-10);
color: var(--vscode-disabledForeground);
text-align: center;
}
.pill-buttons {
display: flex;
flex-wrap: wrap;
gap: var(--wb-spacing-4);
}
.pill-btn {
display: flex;
align-items: center;
gap: var(--wb-spacing-4);
padding: var(--wb-spacing-2) var(--wb-spacing-8);
font-size: var(--wb-font-size-10);
color: var(--vscode-foreground);
border: 1px solid var(--vscode-widget-border);
border-radius: var(--wb-corner-radius-4);
transition:
background 0.1s,
border-color 0.1s,
color 0.1s;
}
.pill-btn:hover {
background: var(--vscode-list-hoverBackground);
border-color: var(--vscode-panel-border);
}
.pill-btn.active {
background: var(--vscode-list-hoverBackground);
border-color: var(--vscode-testing-iconPassed);
color: var(--vscode-testing-iconPassed);
font-weight: 500;
}
.clear-filters-btn {
display: block;
width: 100%;
padding: var(--wb-spacing-4) var(--wb-spacing-8);
margin-top: var(--wb-spacing-8);
font-size: var(--wb-font-size-10);
color: var(--vscode-disabledForeground);
text-align: center;
border-top: 1px solid var(--vscode-widget-border);
padding-top: var(--wb-spacing-8);
transition: color 0.1s;
}
.starred-count {
margin-left: auto;
font-size: var(--wb-font-size-9);
font-weight: 600;
color: var(--vscode-editorWarning-foreground);
min-width: var(--wb-control-size-14);
text-align: center;
}
.clear-filters-btn:hover {
color: var(--vscode-editor-foreground);
}
.session-list-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
/* Group headers (agent and project) */
.group-header {
display: flex;
align-items: center;
gap: var(--wb-spacing-6);
width: 100%;
height: var(--wb-control-size-28);
padding: 0 var(--wb-spacing-10);
font-size: var(--wb-font-size-10);
font-weight: 600;
color: var(--vscode-disabledForeground);
text-transform: none;
letter-spacing: 0.02em;
background: var(--vscode-input-background);
border-bottom: 1px solid var(--vscode-widget-border);
cursor: pointer;
transition: color 0.1s, background 0.1s;
user-select: none;
}
.group-header:hover {
color: var(--vscode-foreground);
background: var(--vscode-list-hoverBackground);
}
:global(.chevron) {
flex-shrink: 0;
transition: transform 0.15s ease;
}
:global(.chevron.expanded) {
transform: rotate(90deg);
}
.group-dot {
width: var(--wb-control-size-6);
height: var(--wb-control-size-6);
border-radius: 50%;
flex-shrink: 0;
}
:global(.project-icon) {
flex-shrink: 0;
color: var(--vscode-disabledForeground);
}
.group-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.group-count {
flex-shrink: 0;
font-size: var(--wb-font-size-9);
font-weight: 500;
color: var(--vscode-disabledForeground);
background: var(--vscode-sideBar-background);
padding: 0 var(--wb-spacing-5);
border-radius: var(--wb-corner-radius-8);
line-height: var(--wb-line-height-16);
}
/* Sub-group headers (Subagents, Team) at depth 1 */
.sub-group-header {
display: flex;
align-items: center;
gap: var(--wb-spacing-5);
width: 100%;
height: var(--wb-control-size-28);
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
cursor: pointer;
user-select: none;
background: transparent;
border: none;
transition: background 0.1s;
}
.sub-group-header:hover {
background: var(--vscode-list-hoverBackground);
}
:global(.sub-group-arrow) {
flex-shrink: 0;
transition: transform 150ms ease;
color: var(--vscode-disabledForeground);
opacity: 0.5;
}
:global(.sub-group-arrow.expanded) {
transform: rotate(90deg);
}
:global(.sub-group-icon) {
flex-shrink: 0;
color: var(--vscode-disabledForeground);
opacity: 0.6;
}
.sub-group-label {
font-weight: 600;
font-size: var(--wb-font-size-10);
letter-spacing: 0.02em;
text-transform: uppercase;
}
.sub-group-count {
font-size: var(--wb-font-size-9);
color: var(--vscode-disabledForeground);
font-weight: 500;
}
.skel-list {
display: flex;
flex-direction: column;
padding: var(--wb-spacing-4) 0;
}
.skel-session-item {
padding: var(--wb-spacing-8) var(--wb-spacing-14);
}
</style>