Archived
fix: keep archived parent sessions visible
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Keep archived parent sessions visible in the current session tree while they still have unarchived children.
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SessionInfo } from "../api";
|
||||
import { sessionRowsForCurrentTree } from "./SessionList";
|
||||
|
||||
describe("sessionRowsForCurrentTree", () => {
|
||||
it("keeps archived ancestors visible while they have unarchived descendants", () => {
|
||||
const parent = { ...session("parent"), archived: true, archivedAt: "2026-06-09T00:00:00.000Z" };
|
||||
const child = session("child", { parentSessionPath: parent.path });
|
||||
|
||||
expect(rowSummaries(sessionRowsForCurrentTree([parent, child]))).toEqual([
|
||||
{ id: "parent", depth: 0, hasMissingParent: false },
|
||||
{ id: "child", depth: 1, hasMissingParent: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("hides archived parents from the current tree once children are detached", () => {
|
||||
const parent = { ...session("parent"), archived: true, archivedAt: "2026-06-09T00:00:00.000Z" };
|
||||
const detachedChild = session("child");
|
||||
|
||||
expect(rowSummaries(sessionRowsForCurrentTree([parent, detachedChild]))).toEqual([
|
||||
{ id: "child", depth: 0, hasMissingParent: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("still marks unavailable parents when the parent record is missing", () => {
|
||||
const child = session("child", { parentSessionPath: "/sessions/missing.jsonl" });
|
||||
|
||||
expect(rowSummaries(sessionRowsForCurrentTree([child]))).toEqual([
|
||||
{ id: "child", depth: 0, hasMissingParent: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function rowSummaries(rows: ReturnType<typeof sessionRowsForCurrentTree>) {
|
||||
return rows.map((row) => ({ id: row.session.id, depth: row.depth, hasMissingParent: row.hasMissingParent }));
|
||||
}
|
||||
|
||||
function session(id: string, overrides: Partial<SessionInfo> = {}): SessionInfo {
|
||||
return {
|
||||
id,
|
||||
path: `/sessions/${id}.jsonl`,
|
||||
cwd: "/workspace",
|
||||
created: "2026-06-09T00:00:00.000Z",
|
||||
modified: "2026-06-09T00:00:00.000Z",
|
||||
messageCount: 1,
|
||||
firstMessage: id,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -14,7 +14,7 @@ function sessionLabel(session: SessionInfo): string {
|
||||
return session.firstMessage !== "" ? session.firstMessage : session.id.slice(0, 8);
|
||||
}
|
||||
|
||||
interface SessionRow {
|
||||
export interface SessionRow {
|
||||
session: SessionInfo;
|
||||
depth: number;
|
||||
hasMissingParent: boolean;
|
||||
@@ -92,15 +92,17 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
||||
}
|
||||
|
||||
override render() {
|
||||
const currentRows = sessionRowsForCurrentSessions(this.sessions);
|
||||
const archivedRows = sessionRows(this.sessions.filter((session) => session.archived === true));
|
||||
const currentRows = sessionRowsForCurrentTree(this.sessions);
|
||||
const currentRowIds = new Set(currentRows.map((row) => row.session.id));
|
||||
const currentSelectableSessions = currentRows.map((row) => row.session).filter((session) => sessionSelectionScope(session) === "current");
|
||||
const archivedRows = sessionRows(this.sessions.filter((session) => session.archived === true && !currentRowIds.has(session.id)));
|
||||
const descendantCounts = unarchivedDescendantCounts(this.sessions);
|
||||
return html`
|
||||
<section>
|
||||
${this.renderHeading(currentRows.length + archivedRows.length, currentRows.map((row) => row.session))}
|
||||
${this.renderHeading(currentRows.length + archivedRows.length, currentSelectableSessions)}
|
||||
${this.collapsed ? null : html`
|
||||
<div class="list-body">
|
||||
${this.renderCurrentSelectionToolbar(currentRows.map((row) => row.session))}
|
||||
${this.renderCurrentSelectionToolbar(currentSelectableSessions)}
|
||||
${currentRows.map((row) => this.renderSession(row, descendantCounts.get(row.session.id) ?? 0, "current"))}
|
||||
${archivedRows.length > 0 ? html`
|
||||
${this.renderArchivedHeading(archivedRows.map((row) => row.session))}
|
||||
@@ -193,18 +195,20 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
||||
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 canBulkSelect = sessionSelectionScope(session) === scope;
|
||||
const selectionActive = this.selectionScopes.has(scope);
|
||||
const showsCheckbox = selectionActive && canBulkSelect;
|
||||
const bulkSelected = showsCheckbox && this.selectedSessionIds.has(session.id);
|
||||
return html`
|
||||
<div
|
||||
class="action-row ${this.selected?.id === session.id ? "selected" : ""} ${bulkSelected ? "bulk-selected" : ""} ${session.archived === true ? "archived" : ""} ${showsCheckbox ? "selecting" : ""}"
|
||||
class="action-row ${this.selected?.id === session.id ? "selected" : ""} ${bulkSelected ? "bulk-selected" : ""} ${session.archived === true ? "archived" : ""} ${selectionActive ? "selecting" : ""}"
|
||||
style=${`--depth:${String(cappedDepth)}`}
|
||||
tabindex="0"
|
||||
title=${session.path}
|
||||
@click=${(event: MouseEvent) => { activateSelectableRow(event, () => { this.activateSessionRow(session, scope); }); }}
|
||||
@keydown=${(event: KeyboardEvent) => { this.handleSessionKeydown(event, session, scope); }}
|
||||
>
|
||||
<div class="action-main ${showsCheckbox ? "selecting" : ""}">
|
||||
<div class="action-main ${selectionActive ? "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)}
|
||||
@@ -242,7 +246,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
||||
}
|
||||
|
||||
private activateSessionRow(session: SessionInfo, scope: SessionSelectionScope): void {
|
||||
if (this.selectionScopes.has(scope)) {
|
||||
if (this.selectionScopes.has(scope) && sessionSelectionScope(session) === scope) {
|
||||
this.toggleSelected(session.id);
|
||||
return;
|
||||
}
|
||||
@@ -411,8 +415,23 @@ function unarchivedDescendantCounts(sessions: SessionInfo[]): Map<string, number
|
||||
return new Map(sessions.map((session) => [session.id, countFor(session, new Set())]));
|
||||
}
|
||||
|
||||
function sessionRowsForCurrentSessions(sessions: SessionInfo[]): SessionRow[] {
|
||||
return sessionRows(sessions.filter((session) => session.archived !== true));
|
||||
export function sessionRowsForCurrentTree(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 seenPaths = new Set<string>([session.path]);
|
||||
while (parentPath !== undefined && !seenPaths.has(parentPath)) {
|
||||
seenPaths.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 sessionRows(sessions: SessionInfo[]): SessionRow[] {
|
||||
|
||||
Reference in New Issue
Block a user