|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
import { LitElement, html, type PropertyValues } from "lit";
|
|
|
|
|
import { LitElement, css, html, type PropertyValues } from "lit";
|
|
|
|
|
import { customElement, property, state } from "lit/decorators.js";
|
|
|
|
|
import type { SessionActivity, SessionInfo, SessionStatus } from "../api";
|
|
|
|
|
import { isCachedNewSessionInfo } from "../cachedNewSessions";
|
|
|
|
@@ -20,6 +20,8 @@ interface SessionRow {
|
|
|
|
|
hasMissingParent: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SessionSelectionScope = "current" | "archived";
|
|
|
|
|
|
|
|
|
|
@customElement("session-list")
|
|
|
|
|
export class SessionList extends LitElement implements KeyboardNavigableSection {
|
|
|
|
|
@property({ attribute: false }) sessions: SessionInfo[] = [];
|
|
|
|
@@ -36,18 +38,25 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|
|
|
|
@property({ attribute: false }) onFocusPreviousSection?: () => void | Promise<void>;
|
|
|
|
|
@property({ attribute: false }) onFocusNextSection?: () => void | Promise<void>;
|
|
|
|
|
@property({ attribute: false }) onCancelKeyboardNavigation?: () => void | Promise<void>;
|
|
|
|
|
@property({ attribute: false }) onArchive?: (session: SessionInfo) => void;
|
|
|
|
|
@property({ attribute: false }) onArchiveWithDescendants?: (session: SessionInfo) => void;
|
|
|
|
|
@property({ attribute: false }) onArchiveMany?: (sessions: SessionInfo[]) => void | Promise<void>;
|
|
|
|
|
@property({ attribute: false }) onRestore?: (session: SessionInfo) => void;
|
|
|
|
|
@property({ attribute: false }) onDelete?: (session: SessionInfo) => void;
|
|
|
|
|
@property({ attribute: false }) onDeleteArchived?: (session: SessionInfo) => void | Promise<void>;
|
|
|
|
|
@property({ attribute: false }) onDeleteArchivedMany?: (sessions: SessionInfo[]) => void | Promise<void>;
|
|
|
|
|
@property({ attribute: false }) onDetachParent?: (session: SessionInfo) => void;
|
|
|
|
|
|
|
|
|
|
@state() private openMenuSessionId: string | undefined;
|
|
|
|
|
@state() private menuStyle = "";
|
|
|
|
|
@state() private archivedExpanded = false;
|
|
|
|
|
@state() private selectionScopes: ReadonlySet<SessionSelectionScope> = new Set();
|
|
|
|
|
@state() private selectedSessionIds: ReadonlySet<string> = new Set();
|
|
|
|
|
|
|
|
|
|
private readonly onDocumentClick = (event: MouseEvent) => {
|
|
|
|
|
if (event.composedPath().includes(this)) return;
|
|
|
|
|
this.openMenuSessionId = undefined;
|
|
|
|
|
};
|
|
|
|
|
@property({ attribute: false }) onArchive?: (session: SessionInfo) => void;
|
|
|
|
|
@property({ attribute: false }) onArchiveWithDescendants?: (session: SessionInfo) => void;
|
|
|
|
|
@property({ attribute: false }) onRestore?: (session: SessionInfo) => void;
|
|
|
|
|
@property({ attribute: false }) onDelete?: (session: SessionInfo) => void;
|
|
|
|
|
@property({ attribute: false }) onDetachParent?: (session: SessionInfo) => void;
|
|
|
|
|
|
|
|
|
|
override connectedCallback(): void {
|
|
|
|
|
super.connectedCallback();
|
|
|
|
@@ -60,9 +69,12 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override updated(changed: PropertyValues<this>): void {
|
|
|
|
|
if (changed.has("sessions") && this.openMenuSessionId !== undefined && !this.sessions.some((session) => session.id === this.openMenuSessionId)) this.openMenuSessionId = undefined;
|
|
|
|
|
if (changed.has("sessions")) {
|
|
|
|
|
if (this.openMenuSessionId !== undefined && !this.sessions.some((session) => session.id === this.openMenuSessionId)) this.openMenuSessionId = undefined;
|
|
|
|
|
if (!this.sessions.some((session) => session.archived === true)) this.archivedExpanded = false;
|
|
|
|
|
this.pruneSelectedSessionIds();
|
|
|
|
|
}
|
|
|
|
|
if (changed.has("collapsed") && this.collapsed) this.openMenuSessionId = undefined;
|
|
|
|
|
if (changed.has("sessions") && !this.sessions.some((session) => session.archived === true)) this.archivedExpanded = false;
|
|
|
|
|
const previousSelected = changed.get("selected");
|
|
|
|
|
if (changed.has("selected") && this.selected?.archived === true && (previousSelected?.id !== this.selected.id || previousSelected.archived !== true) && !this.archivedExpanded) {
|
|
|
|
|
this.archivedExpanded = true;
|
|
|
|
@@ -78,19 +90,22 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override render() {
|
|
|
|
|
const activeRows = sessionRowsForActiveTree(this.sessions);
|
|
|
|
|
const activeIds = new Set(activeRows.map((row) => row.session.id));
|
|
|
|
|
const archivedRows = sessionRows(this.sessions.filter((session) => session.archived === true && !activeIds.has(session.id)));
|
|
|
|
|
const currentRows = sessionRowsForCurrentSessions(this.sessions);
|
|
|
|
|
const archivedRows = sessionRows(this.sessions.filter((session) => session.archived === true));
|
|
|
|
|
const descendantCounts = unarchivedDescendantCounts(this.sessions);
|
|
|
|
|
return html`
|
|
|
|
|
<section>
|
|
|
|
|
${this.renderHeading(activeRows.length + archivedRows.length)}
|
|
|
|
|
${this.renderHeading(currentRows.length + archivedRows.length, currentRows.map((row) => row.session))}
|
|
|
|
|
${this.collapsed ? null : html`
|
|
|
|
|
<div class="list-body">
|
|
|
|
|
${activeRows.map((row) => this.renderSession(row, descendantCounts.get(row.session.id) ?? 0))}
|
|
|
|
|
${this.renderCurrentSelectionToolbar(currentRows.map((row) => row.session))}
|
|
|
|
|
${currentRows.map((row) => this.renderSession(row, descendantCounts.get(row.session.id) ?? 0, "current"))}
|
|
|
|
|
${archivedRows.length > 0 ? html`
|
|
|
|
|
<h2 class="subheading"><button class="section-toggle" aria-expanded=${String(this.archivedExpanded)} @click=${() => { this.toggleArchived(); }}><span>${this.archivedExpanded ? "▾" : "▸"} Archived</span><small>${archivedRows.length}</small></button></h2>
|
|
|
|
|
${this.archivedExpanded ? archivedRows.map((row) => this.renderSession(row, descendantCounts.get(row.session.id) ?? 0)) : null}
|
|
|
|
|
${this.renderArchivedHeading(archivedRows.map((row) => row.session))}
|
|
|
|
|
${this.archivedExpanded ? html`
|
|
|
|
|
${this.renderArchivedSelectionToolbar(archivedRows.map((row) => row.session))}
|
|
|
|
|
${archivedRows.map((row) => this.renderSession(row, descendantCounts.get(row.session.id) ?? 0, "archived"))}
|
|
|
|
|
` : null}
|
|
|
|
|
` : null}
|
|
|
|
|
</div>
|
|
|
|
|
`}
|
|
|
|
@@ -98,31 +113,96 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderHeading(sessionCount: number) {
|
|
|
|
|
if (!this.collapsible) return html`<h2>Sessions <button ?disabled=${!this.canStart} @click=${() => this.onStart?.()}>+</button></h2>`;
|
|
|
|
|
private renderHeading(sessionCount: number, currentSessions: SessionInfo[]) {
|
|
|
|
|
if (!this.collapsible) {
|
|
|
|
|
return html`
|
|
|
|
|
<h2>
|
|
|
|
|
Sessions
|
|
|
|
|
${this.renderCurrentSelectionButton(currentSessions)}
|
|
|
|
|
<button ?disabled=${!this.canStart} @click=${() => this.onStart?.()}>+</button>
|
|
|
|
|
</h2>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
const selectedSummary = this.selected === undefined ? "No session selected" : sessionLabel(this.selected);
|
|
|
|
|
const selectedTitle = this.selected?.path ?? selectedSummary;
|
|
|
|
|
return html`
|
|
|
|
|
<h2>
|
|
|
|
|
<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Sessions</span>${this.collapsed ? html`<small class="section-selected" title=${selectedTitle}>${selectedSummary}</small>` : null}</span><small class="section-count">${sessionCount}</small></button>
|
|
|
|
|
<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Sessions</span>${this.collapsed ? html`<small class="section-selected" title=${selectedTitle}>${selectedSummary}</small>` : null}</span></button>
|
|
|
|
|
${this.renderCurrentSelectionButton(currentSessions)}
|
|
|
|
|
<small class="section-count">${sessionCount}</small>
|
|
|
|
|
<button ?disabled=${!this.canStart} @click=${(event: MouseEvent) => { event.stopPropagation(); this.onStart?.(); }}>+</button>
|
|
|
|
|
</h2>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderSession(row: SessionRow, descendantCount: number) {
|
|
|
|
|
private renderCurrentSelectionButton(currentSessions: SessionInfo[]) {
|
|
|
|
|
if (this.collapsed || currentSessions.length === 0) return null;
|
|
|
|
|
const active = this.selectionScopes.has("current");
|
|
|
|
|
return html`<button class="bulk-select-entry ${active ? "selected" : ""}" title=${active ? "Close current session selection" : "Select current sessions"} aria-label=${active ? "Close current session selection" : "Select current sessions"} aria-expanded=${String(active)} aria-pressed=${String(active)} @click=${(event: MouseEvent) => { event.stopPropagation(); this.toggleSelection("current", currentSessions); }}>☑</button>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderArchivedHeading(archivedSessions: SessionInfo[]) {
|
|
|
|
|
const active = this.selectionScopes.has("archived");
|
|
|
|
|
return html`
|
|
|
|
|
<h2 class="subheading">
|
|
|
|
|
<button class="section-toggle" aria-expanded=${String(this.archivedExpanded)} @click=${() => { this.toggleArchived(); }}><span>${this.archivedExpanded ? "▾" : "▸"} Archived</span></button>
|
|
|
|
|
${this.archivedExpanded ? html`<button class="bulk-select-entry ${active ? "selected" : ""}" title=${active ? "Close archived session selection" : "Select archived sessions"} aria-label=${active ? "Close archived session selection" : "Select archived sessions"} aria-expanded=${String(active)} aria-pressed=${String(active)} @click=${() => { this.toggleSelection("archived", archivedSessions); }}>☑</button>` : null}
|
|
|
|
|
<small class="section-count">${archivedSessions.length}</small>
|
|
|
|
|
</h2>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderCurrentSelectionToolbar(visibleSessions: SessionInfo[]) {
|
|
|
|
|
if (visibleSessions.length === 0 || !this.selectionScopes.has("current")) return null;
|
|
|
|
|
|
|
|
|
|
const selectedSessions = this.selectedSessions("current");
|
|
|
|
|
const archivableSessions = selectedSessions.filter((session) => !isCachedNewSessionInfo(session));
|
|
|
|
|
const allVisibleSelected = visibleSessions.length > 0 && visibleSessions.every((session) => this.selectedSessionIds.has(session.id));
|
|
|
|
|
const visibleSelectedCount = visibleSessions.filter((session) => this.selectedSessionIds.has(session.id)).length;
|
|
|
|
|
return html`
|
|
|
|
|
<div class="bulk-row selecting">
|
|
|
|
|
<button ?disabled=${visibleSessions.length === 0} @click=${() => { this.toggleVisibleSelection(visibleSessions, !allVisibleSelected); }}>${allVisibleSelected ? "Clear visible" : "Select visible"}</button>
|
|
|
|
|
<small>${selectedSessions.length} selected${visibleSelectedCount !== selectedSessions.length ? html` · ${visibleSelectedCount} visible` : null}</small>
|
|
|
|
|
<button ?disabled=${archivableSessions.length === 0} @click=${() => { this.archiveSelectedCurrent(); }}>Archive selected</button>
|
|
|
|
|
<button @click=${() => { this.clearSelection("current"); }}>Clear</button>
|
|
|
|
|
<button @click=${() => { this.closeSelection("current"); }}>Done</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderArchivedSelectionToolbar(visibleSessions: SessionInfo[]) {
|
|
|
|
|
if (visibleSessions.length === 0 || !this.selectionScopes.has("archived")) return null;
|
|
|
|
|
|
|
|
|
|
const selectedSessions = this.selectedSessions("archived");
|
|
|
|
|
const allVisibleSelected = visibleSessions.length > 0 && visibleSessions.every((session) => this.selectedSessionIds.has(session.id));
|
|
|
|
|
const visibleSelectedCount = visibleSessions.filter((session) => this.selectedSessionIds.has(session.id)).length;
|
|
|
|
|
return html`
|
|
|
|
|
<div class="bulk-row selecting">
|
|
|
|
|
<button ?disabled=${visibleSessions.length === 0} @click=${() => { this.toggleVisibleSelection(visibleSessions, !allVisibleSelected); }}>${allVisibleSelected ? "Clear visible" : "Select visible"}</button>
|
|
|
|
|
<small>${selectedSessions.length} selected${visibleSelectedCount !== selectedSessions.length ? html` · ${visibleSelectedCount} visible` : null}</small>
|
|
|
|
|
<button class="danger" ?disabled=${selectedSessions.length === 0} @click=${() => { this.confirmDeleteSelectedArchived(); }}>Delete selected</button>
|
|
|
|
|
<button @click=${() => { this.clearSelection("archived"); }}>Clear</button>
|
|
|
|
|
<button @click=${() => { this.closeSelection("archived"); }}>Done</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderSession(row: SessionRow, descendantCount: number, scope: SessionSelectionScope) {
|
|
|
|
|
const { session } = row;
|
|
|
|
|
const cappedDepth = Math.min(row.depth, 2);
|
|
|
|
|
const showsCheckbox = this.selectionScopes.has(scope);
|
|
|
|
|
const bulkSelected = showsCheckbox && this.selectedSessionIds.has(session.id);
|
|
|
|
|
return html`
|
|
|
|
|
<div
|
|
|
|
|
class="action-row ${this.selected?.id === session.id ? "selected" : ""} ${session.archived === true ? "archived" : ""}"
|
|
|
|
|
class="action-row ${this.selected?.id === session.id ? "selected" : ""} ${bulkSelected ? "bulk-selected" : ""} ${session.archived === true ? "archived" : ""} ${showsCheckbox ? "selecting" : ""}"
|
|
|
|
|
style=${`--depth:${String(cappedDepth)}`}
|
|
|
|
|
tabindex="0"
|
|
|
|
|
title=${session.path}
|
|
|
|
|
@click=${(event: MouseEvent) => { activateSelectableRow(event, () => this.onSelect?.(session)); }}
|
|
|
|
|
@keydown=${(event: KeyboardEvent) => { this.handleSessionKeydown(event, session); }}
|
|
|
|
|
@click=${(event: MouseEvent) => { activateSelectableRow(event, () => { this.activateSessionRow(session, scope); }); }}
|
|
|
|
|
@keydown=${(event: KeyboardEvent) => { this.handleSessionKeydown(event, session, scope); }}
|
|
|
|
|
>
|
|
|
|
|
<div class="action-main">
|
|
|
|
|
<div class="action-main ${showsCheckbox ? "selecting" : ""}">
|
|
|
|
|
${showsCheckbox ? html`<input class="session-checkbox" type="checkbox" aria-label=${`Select ${sessionLabel(session)}`} .checked=${bulkSelected} @click=${(event: MouseEvent) => { event.stopPropagation(); }} @change=${() => { this.toggleSelected(session.id); }}>` : null}
|
|
|
|
|
<span class="action-name">${row.depth > 0 ? html`<span class="tree-marker">↳</span>` : null}${sessionLabel(session)}${row.depth > 2 ? html` <span class="badge">depth ${row.depth}</span>` : null}${row.hasMissingParent ? html` <span class="badge">parent unavailable</span>` : null}</span><small>${this.renderSessionMetaPrefix(session)}${String(session.messageCount)} messages</small>
|
|
|
|
|
${this.renderActivity(session)}
|
|
|
|
|
</div>
|
|
|
|
@@ -130,12 +210,15 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|
|
|
|
<button class="action-menu-toggle" title="Session actions" @click=${(event: MouseEvent) => { event.stopPropagation(); this.toggleMenu(session.id, event.currentTarget); }}>⋯</button>
|
|
|
|
|
${this.openMenuSessionId === session.id ? html`
|
|
|
|
|
<div class="action-menu-panel" style=${this.menuStyle}>
|
|
|
|
|
${session.parentSessionPath !== undefined ? html`<button title="Detach from parent" @click=${() => { this.openMenuSessionId = undefined; this.onDetachParent?.(session); }}>Detach from parent</button>` : null}
|
|
|
|
|
${isCachedNewSessionInfo(session)
|
|
|
|
|
? html`<button title="Delete browser-cached new session" @click=${() => { this.openMenuSessionId = undefined; this.onDelete?.(session); }}>Delete</button>`
|
|
|
|
|
: session.archived === true
|
|
|
|
|
? html`<button title="Restore session" @click=${() => { this.openMenuSessionId = undefined; this.onRestore?.(session); }}>Restore</button>`
|
|
|
|
|
? html`
|
|
|
|
|
<button title="Restore session" @click=${() => { this.openMenuSessionId = undefined; this.onRestore?.(session); }}>Restore</button>
|
|
|
|
|
<button class="danger" title="Permanently delete archived session" @click=${() => { this.openMenuSessionId = undefined; this.confirmDeleteArchived(session); }}>Delete archived session</button>
|
|
|
|
|
`
|
|
|
|
|
: html`
|
|
|
|
|
${session.parentSessionPath !== undefined ? html`<button title="Detach from parent" @click=${() => { this.openMenuSessionId = undefined; this.onDetachParent?.(session); }}>Detach from parent</button>` : null}
|
|
|
|
|
<button title="Archive session" @click=${() => { this.openMenuSessionId = undefined; this.onArchive?.(session); }}>Archive</button>
|
|
|
|
|
${descendantCount > 0 ? html`<button title="Archive this session and its descendants" @click=${() => { this.openMenuSessionId = undefined; this.confirmArchiveWithDescendants(session, descendantCount); }}>Archive with descendants (${descendantCount})</button>` : null}
|
|
|
|
|
`}
|
|
|
|
@@ -146,20 +229,99 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleSessionKeydown(event: KeyboardEvent, session: SessionInfo): void {
|
|
|
|
|
private handleSessionKeydown(event: KeyboardEvent, session: SessionInfo, scope: SessionSelectionScope): void {
|
|
|
|
|
handleSelectableRowKeyboard(event, {
|
|
|
|
|
activate: () => this.onSelect?.(session),
|
|
|
|
|
activate: () => { this.activateSessionRow(session, scope); },
|
|
|
|
|
previousSection: this.onFocusPreviousSection === undefined ? undefined : () => { void this.onFocusPreviousSection?.(); },
|
|
|
|
|
nextSection: this.onFocusNextSection === undefined ? undefined : () => { void this.onFocusNextSection?.(); },
|
|
|
|
|
cancel: this.onCancelKeyboardNavigation === undefined ? undefined : () => { void this.onCancelKeyboardNavigation?.(); },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private activateSessionRow(session: SessionInfo, scope: SessionSelectionScope): void {
|
|
|
|
|
if (this.selectionScopes.has(scope)) {
|
|
|
|
|
this.toggleSelected(session.id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.onSelect?.(session);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private confirmArchiveWithDescendants(session: SessionInfo, descendantCount: number): void {
|
|
|
|
|
const noun = descendantCount === 1 ? "descendant session" : "descendant sessions";
|
|
|
|
|
if (confirm(`Archive “${sessionLabel(session)}” and ${String(descendantCount)} ${noun}?`)) this.onArchiveWithDescendants?.(session);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private confirmDeleteArchived(session: SessionInfo): void {
|
|
|
|
|
if (confirm(`Permanently delete archived session “${sessionLabel(session)}”? This cannot be undone.`)) void this.onDeleteArchived?.(session);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private confirmDeleteSelectedArchived(): void {
|
|
|
|
|
const archived = this.selectedSessions("archived");
|
|
|
|
|
if (archived.length === 0) return;
|
|
|
|
|
const noun = archived.length === 1 ? "archived session" : "archived sessions";
|
|
|
|
|
if (!confirm(`Permanently delete ${String(archived.length)} selected ${noun}? This cannot be undone.`)) return;
|
|
|
|
|
this.selectedSessionIds = removeSessionIds(this.selectedSessionIds, archived.map((session) => session.id));
|
|
|
|
|
void this.onDeleteArchivedMany?.(archived);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private archiveSelectedCurrent(): void {
|
|
|
|
|
const sessions = this.selectedSessions("current").filter((session) => !isCachedNewSessionInfo(session));
|
|
|
|
|
this.selectedSessionIds = removeSessionIds(this.selectedSessionIds, sessions.map((session) => session.id));
|
|
|
|
|
void this.onArchiveMany?.(sessions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toggleSelection(scope: SessionSelectionScope, visibleSessions: SessionInfo[]): void {
|
|
|
|
|
if (this.selectionScopes.has(scope)) {
|
|
|
|
|
this.closeSelection(scope);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.startSelection(scope, visibleSessions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startSelection(scope: SessionSelectionScope, visibleSessions: SessionInfo[]): void {
|
|
|
|
|
this.selectionScopes = new Set([...this.selectionScopes, scope]);
|
|
|
|
|
const onlyVisibleSession = visibleSessions.length === 1 ? visibleSessions[0] : undefined;
|
|
|
|
|
if (onlyVisibleSession !== undefined) this.selectedSessionIds = new Set([...this.selectedSessionIds, onlyVisibleSession.id]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private closeSelection(scope: SessionSelectionScope): void {
|
|
|
|
|
this.selectionScopes = new Set([...this.selectionScopes].filter((candidate) => candidate !== scope));
|
|
|
|
|
this.clearSelection(scope);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private clearSelection(scope: SessionSelectionScope): void {
|
|
|
|
|
const sessionIds = this.sessions.filter((session) => sessionSelectionScope(session) === scope).map((session) => session.id);
|
|
|
|
|
this.selectedSessionIds = removeSessionIds(this.selectedSessionIds, sessionIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toggleSelected(sessionId: string): void {
|
|
|
|
|
const next = new Set(this.selectedSessionIds);
|
|
|
|
|
if (next.has(sessionId)) next.delete(sessionId);
|
|
|
|
|
else next.add(sessionId);
|
|
|
|
|
this.selectedSessionIds = next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toggleVisibleSelection(sessions: SessionInfo[], selected: boolean): void {
|
|
|
|
|
const next = new Set(this.selectedSessionIds);
|
|
|
|
|
for (const session of sessions) {
|
|
|
|
|
if (selected) next.add(session.id);
|
|
|
|
|
else next.delete(session.id);
|
|
|
|
|
}
|
|
|
|
|
this.selectedSessionIds = next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private selectedSessions(scope: SessionSelectionScope): SessionInfo[] {
|
|
|
|
|
return this.sessions.filter((session) => this.selectedSessionIds.has(session.id) && sessionSelectionScope(session) === scope);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private pruneSelectedSessionIds(): void {
|
|
|
|
|
const existing = new Set(this.sessions.map((session) => session.id));
|
|
|
|
|
const next = new Set([...this.selectedSessionIds].filter((sessionId) => existing.has(sessionId)));
|
|
|
|
|
if (next.size !== this.selectedSessionIds.size) this.selectedSessionIds = next;
|
|
|
|
|
if (this.selectionScopes.has("archived") && !this.sessions.some((session) => session.archived === true)) this.closeSelection("archived");
|
|
|
|
|
if (this.selectionScopes.has("current") && !this.sessions.some((session) => session.archived !== true)) this.closeSelection("current");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toggleMenu(sessionId: string, target: EventTarget | null) {
|
|
|
|
|
if (this.openMenuSessionId === sessionId) {
|
|
|
|
|
this.openMenuSessionId = undefined;
|
|
|
|
@@ -173,6 +335,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|
|
|
|
this.archivedExpanded = !this.archivedExpanded;
|
|
|
|
|
if (!this.archivedExpanded) {
|
|
|
|
|
this.openMenuSessionId = undefined;
|
|
|
|
|
if (this.selectionScopes.has("archived")) this.closeSelection("archived");
|
|
|
|
|
this.onArchivedCollapsed?.();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -192,7 +355,29 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|
|
|
|
return renderActionActivityIndicator(isSessionActive(this.statuses[session.id], this.activities[session.id]) ? "session" : undefined, "Session active");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static override styles = listStyles;
|
|
|
|
|
static override styles = [listStyles, css`
|
|
|
|
|
h2 { min-height: 30px; }
|
|
|
|
|
h2 > .section-count { flex: 0 0 auto; display: inline; color: var(--pi-muted); font-size: inherit; }
|
|
|
|
|
.bulk-select-entry { box-sizing: border-box; flex: 0 0 auto; display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; font-size: 13px; line-height: 1; text-transform: none; }
|
|
|
|
|
.bulk-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 6px; }
|
|
|
|
|
.bulk-row button { padding: 5px 7px; font-size: 12px; }
|
|
|
|
|
.bulk-row small { display: inline; min-width: 0; color: var(--pi-muted); }
|
|
|
|
|
.bulk-row.selecting { padding: 6px; border: 1px solid var(--pi-border-muted); border-radius: 8px; background: color-mix(in srgb, var(--pi-surface) 65%, transparent); }
|
|
|
|
|
button.danger, .action-menu-panel button.danger { color: var(--pi-danger); }
|
|
|
|
|
button.danger:hover, .action-menu-panel button.danger:hover { background: color-mix(in srgb, var(--pi-danger) 14%, transparent); }
|
|
|
|
|
.action-row.bulk-selected .action-main { border-color: var(--pi-accent); box-shadow: inset 3px 0 0 var(--pi-accent); }
|
|
|
|
|
.action-main.selecting { padding-left: calc(32px + var(--depth, 0) * 16px); }
|
|
|
|
|
.session-checkbox { position: absolute; top: 9px; left: calc(8px + var(--depth, 0) * 16px); z-index: 2; margin: 0; }
|
|
|
|
|
`];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sessionSelectionScope(session: SessionInfo): SessionSelectionScope {
|
|
|
|
|
return session.archived === true ? "archived" : "current";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeSessionIds(sessionIds: ReadonlySet<string>, removedIds: readonly string[]): ReadonlySet<string> {
|
|
|
|
|
const removed = new Set(removedIds);
|
|
|
|
|
return new Set([...sessionIds].filter((sessionId) => !removed.has(sessionId)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function unarchivedDescendantCounts(sessions: SessionInfo[]): Map<string, number> {
|
|
|
|
@@ -220,23 +405,8 @@ function unarchivedDescendantCounts(sessions: SessionInfo[]): Map<string, number
|
|
|
|
|
return new Map(sessions.map((session) => [session.id, countFor(session, new Set())]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sessionRowsForActiveTree(sessions: SessionInfo[]): SessionRow[] {
|
|
|
|
|
const byPath = new Map(sessions.map((session) => [session.path, session]));
|
|
|
|
|
const visible = new Set<string>();
|
|
|
|
|
for (const session of sessions) {
|
|
|
|
|
if (session.archived === true) continue;
|
|
|
|
|
visible.add(session.id);
|
|
|
|
|
let parentPath = session.parentSessionPath;
|
|
|
|
|
const seen = new Set<string>([session.path]);
|
|
|
|
|
while (parentPath !== undefined && !seen.has(parentPath)) {
|
|
|
|
|
seen.add(parentPath);
|
|
|
|
|
const parent = byPath.get(parentPath);
|
|
|
|
|
if (parent === undefined) break;
|
|
|
|
|
visible.add(parent.id);
|
|
|
|
|
parentPath = parent.parentSessionPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return sessionRows(sessions.filter((session) => visible.has(session.id)));
|
|
|
|
|
function sessionRowsForCurrentSessions(sessions: SessionInfo[]): SessionRow[] {
|
|
|
|
|
return sessionRows(sessions.filter((session) => session.archived !== true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sessionRows(sessions: SessionInfo[]): SessionRow[] {
|
|
|
|
|