mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 17:21:34 +00:00
refactor(ui): move sidebar session data into controller (#112853)
This commit is contained in:
committed by
GitHub
parent
4c5e8f3b85
commit
efc19faca4
@@ -370,7 +370,7 @@ export abstract class AppSidebarMenusElement extends AppSidebarSessionGroupsElem
|
||||
themeMode: this.themeMode,
|
||||
agentUnreadCount: (agentId) => this.agentUnreadCount(agentId),
|
||||
agentApprovalCount: (agentId) =>
|
||||
this.approvalBadgeSnapshot().agentCounts.get(normalizeAgentId(agentId)) ?? 0,
|
||||
this.sessionData.approvalBadgeSnapshot().agentCounts.get(normalizeAgentId(agentId)) ?? 0,
|
||||
onFilterChange: (next) => {
|
||||
this.agentMenuFilter = next;
|
||||
},
|
||||
@@ -428,7 +428,7 @@ export abstract class AppSidebarMenusElement extends AppSidebarSessionGroupsElem
|
||||
.anchor=${menu}
|
||||
.trigger=${this.sessionMenuTrigger}
|
||||
.disabled=${!this.connected}
|
||||
.forkDisabled=${this.sessionsLoading || session.modelSelectionLocked}
|
||||
.forkDisabled=${this.sessionData.sessionsLoading || session.modelSelectionLocked}
|
||||
.archiveAllowed=${archiveAllowed}
|
||||
.cloudWorkerStopAllowed=${Boolean(
|
||||
!batchRows &&
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
import { state } from "lit/decorators.js";
|
||||
import type {
|
||||
SessionCatalog,
|
||||
SessionsCatalogListResult,
|
||||
} from "../../../packages/gateway-protocol/src/index.ts";
|
||||
import type { GatewayBrowserClient } from "../api/gateway.ts";
|
||||
import {
|
||||
CATALOG_SESSION_CONTINUED_EVENT,
|
||||
type CatalogSessionContinuedDetail,
|
||||
} from "../lib/sessions/catalog-key.ts";
|
||||
import { AppSidebarBase } from "./app-sidebar-base.ts";
|
||||
import {
|
||||
refreshSessionCatalogsLive,
|
||||
SESSION_CATALOG_CHANGED_REFRESH_MS,
|
||||
SessionCatalogLiveState,
|
||||
sessionCatalogListClient,
|
||||
} from "./app-sidebar-session-catalog-live.ts";
|
||||
import {
|
||||
mergeSessionCatalogPage,
|
||||
sessionCatalogRequestError,
|
||||
} from "./app-sidebar-session-catalog-state.ts";
|
||||
import { bindAdoptedCatalogSession } from "./app-sidebar-session-catalogs.ts";
|
||||
import { sessionCatalogHostKey } from "./app-sidebar-session-types.ts";
|
||||
|
||||
/** Gateway-backed external session-catalog synchronization. */
|
||||
export abstract class AppSidebarSessionCatalogDataElement extends AppSidebarBase {
|
||||
@state() protected sessionCatalogs: SessionCatalog[] = [];
|
||||
@state() protected loadingMoreSessionCatalogIds: ReadonlySet<string> = new Set();
|
||||
|
||||
private readonly sessionCatalogLive = new SessionCatalogLiveState();
|
||||
private sessionCatalogAgentId: string | null = null;
|
||||
private sessionCatalogGeneration = 0;
|
||||
private sessionCatalogRevision = 0;
|
||||
private readonly sessionCatalogPageDepths = new Map<string, number>();
|
||||
private readonly sessionCatalogRevisions = new Map<string, number>();
|
||||
|
||||
protected abstract expandedAgentId(): string;
|
||||
protected abstract sessionCatalogGatewayClient(): GatewayBrowserClient | null;
|
||||
|
||||
protected connectSessionCatalogListeners() {
|
||||
// The chat pane announces catalog adoptions so the catalog row binds to
|
||||
// the new session key before the next catalog poll.
|
||||
document.addEventListener(
|
||||
CATALOG_SESSION_CONTINUED_EVENT,
|
||||
this.handleCatalogSessionContinued as EventListener,
|
||||
);
|
||||
document.addEventListener("visibilitychange", this.handleSessionCatalogPageActivation);
|
||||
globalThis.addEventListener("focus", this.handleSessionCatalogPageActivation);
|
||||
}
|
||||
|
||||
protected disconnectSessionCatalogListeners() {
|
||||
document.removeEventListener(
|
||||
CATALOG_SESSION_CONTINUED_EVENT,
|
||||
this.handleCatalogSessionContinued as EventListener,
|
||||
);
|
||||
document.removeEventListener("visibilitychange", this.handleSessionCatalogPageActivation);
|
||||
globalThis.removeEventListener("focus", this.handleSessionCatalogPageActivation);
|
||||
}
|
||||
|
||||
protected retireSessionCatalogData() {
|
||||
this.sessionCatalogGeneration += 1;
|
||||
this.sessionCatalogLive.clear();
|
||||
}
|
||||
|
||||
protected resetSessionCatalogConnection() {
|
||||
this.sessionCatalogGeneration += 1;
|
||||
this.sessionCatalogRevision += 1;
|
||||
this.sessionCatalogLive.resetConnection();
|
||||
this.sessionCatalogs = [];
|
||||
this.loadingMoreSessionCatalogIds = new Set();
|
||||
this.sessionCatalogPageDepths.clear();
|
||||
this.sessionCatalogRevisions.clear();
|
||||
}
|
||||
|
||||
protected updateSessionCatalogData() {
|
||||
if (this.context) {
|
||||
this.synchronizeSessionCatalogAgent(this.expandedAgentId());
|
||||
}
|
||||
if (
|
||||
!this.visibleSessionCatalogClient() ||
|
||||
this.sessionCatalogLive.timer ||
|
||||
this.sessionCatalogLive.requestGeneration === this.sessionCatalogGeneration
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void this.refreshSessionCatalogs();
|
||||
}
|
||||
|
||||
protected handleSessionCatalogHostEvent(payload: unknown) {
|
||||
this.applySessionCatalogHostEvent(payload);
|
||||
}
|
||||
|
||||
protected handleSessionCatalogPresence(payload: unknown) {
|
||||
if (this.sessionCatalogLive.observePresence(payload)) {
|
||||
this.requestSessionCatalogRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
private visibleSessionCatalogClient(): GatewayBrowserClient | null {
|
||||
if (document.visibilityState === "hidden") {
|
||||
return null;
|
||||
}
|
||||
return sessionCatalogListClient(this.context?.gateway.snapshot, this.connected);
|
||||
}
|
||||
|
||||
private synchronizeSessionCatalogAgent(agentId: string) {
|
||||
if (agentId === this.sessionCatalogAgentId) {
|
||||
return;
|
||||
}
|
||||
this.sessionCatalogAgentId = agentId;
|
||||
this.sessionCatalogGeneration += 1;
|
||||
this.sessionCatalogRevision += 1;
|
||||
this.sessionCatalogLive.clear();
|
||||
this.loadingMoreSessionCatalogIds = new Set();
|
||||
if (this.sessionCatalogs.some((catalog) => catalog.capabilities.createSession)) {
|
||||
this.sessionCatalogs = this.sessionCatalogs.map((catalog) => {
|
||||
const { createSession: _createSession, ...capabilities } = catalog.capabilities;
|
||||
return { ...catalog, capabilities };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private readonly handleCatalogSessionContinued = (
|
||||
event: CustomEvent<CatalogSessionContinuedDetail>,
|
||||
) => {
|
||||
const detail = event.detail;
|
||||
if (!detail?.sessionKey) {
|
||||
return;
|
||||
}
|
||||
this.sessionCatalogs = bindAdoptedCatalogSession(this.sessionCatalogs, detail);
|
||||
// Invalidate in-flight polls and load-more merges so a pre-adoption
|
||||
// snapshot cannot clobber the patched rows; the 30s poll reconfirms.
|
||||
this.sessionCatalogRevision += 1;
|
||||
this.sessionCatalogRevisions.set(
|
||||
detail.catalogId,
|
||||
(this.sessionCatalogRevisions.get(detail.catalogId) ?? 0) + 1,
|
||||
);
|
||||
};
|
||||
|
||||
private readonly handleSessionCatalogPageActivation = () => {
|
||||
if (document.visibilityState === "hidden") {
|
||||
this.sessionCatalogLive.cancelScheduledRefreshes();
|
||||
return;
|
||||
}
|
||||
this.sessionCatalogLive.scheduleActivation(() => this.requestSessionCatalogRefresh());
|
||||
};
|
||||
|
||||
private requestSessionCatalogRefresh() {
|
||||
const snapshot = this.context?.gateway.snapshot;
|
||||
this.sessionCatalogLive.requestRefresh({
|
||||
visible: document.visibilityState !== "hidden",
|
||||
connected: this.isConnected && Boolean(sessionCatalogListClient(snapshot, this.connected)),
|
||||
generation: this.sessionCatalogGeneration,
|
||||
refresh: () => void this.refreshSessionCatalogs(),
|
||||
});
|
||||
}
|
||||
|
||||
private applySessionCatalogHostEvent(payload: unknown) {
|
||||
const update = this.sessionCatalogLive.applyHost({
|
||||
payload,
|
||||
agentId: this.sessionCatalogAgentId ?? "",
|
||||
catalogs: this.sessionCatalogs,
|
||||
pageDepths: this.sessionCatalogPageDepths,
|
||||
});
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
this.sessionCatalogs = update.catalogs;
|
||||
this.sessionCatalogRevision += this.sessionCatalogLive.refetching ? 1 : 0;
|
||||
const catalogRevision = this.sessionCatalogRevisions.get(update.catalogId) ?? 0;
|
||||
this.sessionCatalogRevisions.set(update.catalogId, catalogRevision + 1);
|
||||
if (this.sessionCatalogLive.requestGeneration !== this.sessionCatalogGeneration) {
|
||||
this.sessionCatalogLive.schedule(
|
||||
SESSION_CATALOG_CHANGED_REFRESH_MS,
|
||||
this.isConnected,
|
||||
() => void this.refreshSessionCatalogs(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshSessionCatalogs() {
|
||||
// Hidden pages resume through the coalesced activation handler. Starting
|
||||
// here without a timer makes catalog state updates poll at request latency.
|
||||
const client = this.visibleSessionCatalogClient();
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
const generation = this.sessionCatalogGeneration;
|
||||
const revision = this.sessionCatalogRevision;
|
||||
const agentId = this.sessionCatalogAgentId ?? this.expandedAgentId();
|
||||
await refreshSessionCatalogsLive({
|
||||
live: this.sessionCatalogLive,
|
||||
client,
|
||||
agentId,
|
||||
generation,
|
||||
revision,
|
||||
currentGeneration: () => this.sessionCatalogGeneration,
|
||||
currentRevision: () => this.sessionCatalogRevision,
|
||||
currentClient: () => this.sessionCatalogGatewayClient(),
|
||||
catalogs: () => this.sessionCatalogs,
|
||||
pageDepths: this.sessionCatalogPageDepths,
|
||||
connected: () => this.isConnected,
|
||||
applyFinal: (catalogs, revisedCatalogIds) => {
|
||||
this.sessionCatalogs = catalogs;
|
||||
for (const catalogId of revisedCatalogIds) {
|
||||
this.sessionCatalogRevisions.set(
|
||||
catalogId,
|
||||
(this.sessionCatalogRevisions.get(catalogId) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
this.sessionCatalogRevision += 1;
|
||||
},
|
||||
refresh: () => void this.refreshSessionCatalogs(),
|
||||
});
|
||||
}
|
||||
|
||||
async loadMoreSessionCatalog(catalogId: string) {
|
||||
if (this.loadingMoreSessionCatalogIds.has(catalogId)) {
|
||||
return;
|
||||
}
|
||||
const catalog = this.sessionCatalogs.find((candidate) => candidate.id === catalogId);
|
||||
const cursors = Object.fromEntries(
|
||||
(catalog?.hosts ?? []).flatMap((host) =>
|
||||
host.nextCursor ? [[host.hostId, host.nextCursor] as const] : [],
|
||||
),
|
||||
);
|
||||
if (!catalog || Object.keys(cursors).length === 0) {
|
||||
return;
|
||||
}
|
||||
const client = this.context?.gateway.snapshot.client;
|
||||
if (!client || !this.connected) {
|
||||
return;
|
||||
}
|
||||
const generation = this.sessionCatalogGeneration;
|
||||
const agentId = this.sessionCatalogAgentId ?? this.expandedAgentId();
|
||||
const revision = this.sessionCatalogRevisions.get(catalogId) ?? 0;
|
||||
this.loadingMoreSessionCatalogIds = new Set([...this.loadingMoreSessionCatalogIds, catalogId]);
|
||||
try {
|
||||
const result = await client.request<SessionsCatalogListResult>("sessions.catalog.list", {
|
||||
agentId,
|
||||
catalogId,
|
||||
cursors,
|
||||
});
|
||||
if (
|
||||
generation !== this.sessionCatalogGeneration ||
|
||||
revision !== (this.sessionCatalogRevisions.get(catalogId) ?? 0) ||
|
||||
client !== this.sessionCatalogGatewayClient()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const page = result.catalogs.find((candidate) => candidate.id === catalogId);
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
const current = this.sessionCatalogs.find((candidate) => candidate.id === catalogId);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
const merged = mergeSessionCatalogPage({ current, page, cursors });
|
||||
for (const hostId of merged.advancedHostIds) {
|
||||
const key = sessionCatalogHostKey(catalogId, hostId);
|
||||
this.sessionCatalogPageDepths.set(key, (this.sessionCatalogPageDepths.get(key) ?? 0) + 1);
|
||||
}
|
||||
this.sessionCatalogs = this.sessionCatalogs.map((candidate) =>
|
||||
candidate.id === catalogId ? merged.catalog : candidate,
|
||||
);
|
||||
this.sessionCatalogRevisions.set(catalogId, revision + 1);
|
||||
this.sessionCatalogRevision += 1;
|
||||
} catch (error) {
|
||||
if (
|
||||
generation !== this.sessionCatalogGeneration ||
|
||||
revision !== (this.sessionCatalogRevisions.get(catalogId) ?? 0) ||
|
||||
client !== this.sessionCatalogGatewayClient()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Preserve rows and cursors: retrying Load More requests this page again.
|
||||
this.sessionCatalogs = this.sessionCatalogs.map((candidate) =>
|
||||
candidate.id === catalogId
|
||||
? { ...candidate, error: sessionCatalogRequestError(error) }
|
||||
: candidate,
|
||||
);
|
||||
this.sessionCatalogRevisions.set(catalogId, revision + 1);
|
||||
this.sessionCatalogRevision += 1;
|
||||
} finally {
|
||||
if (generation === this.sessionCatalogGeneration) {
|
||||
const loading = new Set(this.loadingMoreSessionCatalogIds);
|
||||
loading.delete(catalogId);
|
||||
this.loadingMoreSessionCatalogIds = loading;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { normalizeOptionalString } from "../lib/string-coerce.ts";
|
||||
import { AppSidebarSessionMutationsElement } from "./app-sidebar-session-mutations.ts";
|
||||
import {
|
||||
loadStoredCollapsedSessionSections,
|
||||
SIDEBAR_SESSION_PAGE_SIZE,
|
||||
storeSidebarSessionStatusFilter,
|
||||
storeCollapsedSessionSections,
|
||||
storeSidebarSessionsGrouping,
|
||||
@@ -223,12 +222,12 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
}
|
||||
try {
|
||||
await scope.sessions.groupsPut([...groups, name]);
|
||||
return this.isSessionMutationScopeCurrent(scope) ? "completed" : "stale";
|
||||
return this.sessionData.isSessionMutationScopeCurrent(scope) ? "completed" : "stale";
|
||||
} catch (error) {
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return "stale";
|
||||
}
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
return "failed";
|
||||
}
|
||||
}
|
||||
@@ -246,7 +245,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
@@ -256,7 +255,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
}
|
||||
if (sessions.length > 0) {
|
||||
await this.patchSessions(sessions, { category: name }, scope);
|
||||
} else if (this.isSessionMutationScopeCurrent(scope)) {
|
||||
} else if (this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
// Header-created groups start empty; re-render so the section shows up.
|
||||
this.requestUpdate();
|
||||
}
|
||||
@@ -268,7 +267,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
if (!next || next === group) {
|
||||
return;
|
||||
}
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
@@ -277,7 +276,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
void (async () => {
|
||||
try {
|
||||
const outcome = await scope.sessions.groupsRename(group, next);
|
||||
if (outcome !== "completed" || !this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (outcome !== "completed" || !this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
const from = `category:${group}`;
|
||||
@@ -289,7 +288,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
}
|
||||
this.requestUpdate();
|
||||
} catch (error) {
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -298,14 +297,14 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
if (!window.confirm(t("sessionsView.deleteGroupConfirm", { group }))) {
|
||||
return;
|
||||
}
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const outcome = await scope.sessions.groupsDelete(group);
|
||||
if (outcome !== "completed" || !this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (outcome !== "completed" || !this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
const collapsed = new Set(this.collapsedSessionSections);
|
||||
@@ -313,7 +312,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
this.saveCollapsedSessionSections(collapsed);
|
||||
this.requestUpdate();
|
||||
} catch (error) {
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -339,18 +338,18 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
|
||||
private reorderSessionGroup(source: string, target: string, position: "before" | "after") {
|
||||
const groups = reorderSessionCustomGroups(this.knownSessionGroups(), source, target, position);
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
await scope.sessions.groupsPut(groups);
|
||||
if (this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
} catch (error) {
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -360,7 +359,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
category: string | null,
|
||||
patch: { pinned?: boolean } = {},
|
||||
) {
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
@@ -422,7 +421,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
if (active) {
|
||||
return active;
|
||||
}
|
||||
for (const rows of Object.values(this.sessionRowsByAgent)) {
|
||||
for (const rows of Object.values(this.sessionData.sessionRowsByAgent)) {
|
||||
const row = rows.find((candidate) => candidate.key === sessionKey);
|
||||
if (row) {
|
||||
return navigationState.toSidebarSession(row);
|
||||
@@ -494,21 +493,12 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu
|
||||
}
|
||||
this.sessionsStatusFilter = statusFilter;
|
||||
this.clearSessionSelection();
|
||||
this.visibleSessionLimit = SIDEBAR_SESSION_PAGE_SIZE;
|
||||
this.childSessionRowsByParent = {};
|
||||
this.loadedChildSessionKeys = new Set();
|
||||
this.failedChildSessionKeys = new Set();
|
||||
this.loadingChildSessionKeys = new Set();
|
||||
this.sessionRowsByAgent = {};
|
||||
if (statusFilter === "active" && this.context) {
|
||||
this.sessionsResult = this.context.sessions.state.result;
|
||||
this.sessionsAgentId = this.context.sessions.state.agentId;
|
||||
}
|
||||
this.sessionData.resetForStatusFilter(statusFilter);
|
||||
try {
|
||||
storeSidebarSessionStatusFilter(statusFilter);
|
||||
} catch {
|
||||
// Keep the in-memory preference when storage is unavailable.
|
||||
}
|
||||
void this.refreshSidebarSessions();
|
||||
void this.sessionData.refreshSidebarSessions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ function renderSessionCatalogs(params: {
|
||||
}),
|
||||
onToggleSection: (sectionId) => host.toggleSection(sectionId),
|
||||
onToggleProjectGrouping: () => host.toggleCatalogProjectGrouping(),
|
||||
onLoadMore: (catalogId) => void host.loadMoreSessionCatalog(catalogId),
|
||||
onLoadMore: (catalogId) => void host.sessionData.loadMoreSessionCatalog(catalogId),
|
||||
onOpenNewSession: host.onOpenNewSession,
|
||||
onNavigate: host.onNavigate,
|
||||
catalogOpenTarget: snapshot.catalogOpenTarget,
|
||||
@@ -388,14 +388,14 @@ export function renderSessionList(params: {
|
||||
@dragleave=${(event: DragEvent) => host.handleSessionListDragLeave(event)}
|
||||
@drop=${(event: DragEvent) => host.handleSessionListDrop(event)}
|
||||
>
|
||||
${host.sessionMutationError
|
||||
${host.sessionData.sessionMutationError
|
||||
? html`
|
||||
<div
|
||||
class="sidebar-session-error callout danger callout--dismissible"
|
||||
role="alert"
|
||||
data-sidebar-session-error
|
||||
>
|
||||
<span class="callout__content">${host.sessionMutationError}</span>
|
||||
<span class="callout__content">${host.sessionData.sessionMutationError}</span>
|
||||
<openclaw-tooltip .content=${t("chat.actions.dismissError")}>
|
||||
<button
|
||||
class="callout__dismiss"
|
||||
|
||||
@@ -74,11 +74,11 @@ export abstract class AppSidebarSessionListElement
|
||||
}
|
||||
|
||||
setVisibleSessionLimit(limit: number): void {
|
||||
this.visibleSessionLimit = limit;
|
||||
this.sessionData.setVisibleSessionLimit(limit);
|
||||
}
|
||||
|
||||
dismissSessionMutationError(): void {
|
||||
this.sessionMutationError = null;
|
||||
this.sessionData.dismissSessionMutationError();
|
||||
}
|
||||
|
||||
toggleCatalogProjectGrouping(): void {
|
||||
@@ -108,8 +108,8 @@ export abstract class AppSidebarSessionListElement
|
||||
const visibleSessions = this.selectedAgentSessionRows(navigationState);
|
||||
const expandedAgentId = this.expandedAgentId();
|
||||
const liveRows = [
|
||||
...(this.sessionsResult?.sessions ?? []),
|
||||
...Object.values(this.sessionRowsByAgent).flat(),
|
||||
...(this.sessionData.sessionsResult?.sessions ?? []),
|
||||
...Object.values(this.sessionData.sessionRowsByAgent).flat(),
|
||||
];
|
||||
const sidebarRowsByKey = new Map<string, SidebarRecentSession>();
|
||||
for (const row of liveRows) {
|
||||
@@ -136,11 +136,11 @@ export abstract class AppSidebarSessionListElement
|
||||
},
|
||||
}),
|
||||
catalogs: {
|
||||
catalogs: this.sessionCatalogs,
|
||||
catalogs: this.sessionData.sessionCatalogs,
|
||||
basePath: this.basePath,
|
||||
routeSessionKey: this.activeRouteId === "chat" ? this.getRouteSessionKey() : "",
|
||||
newSessionAgentId: expandedAgentId,
|
||||
loadingMoreCatalogIds: this.loadingMoreSessionCatalogIds,
|
||||
loadingMoreCatalogIds: this.sessionData.loadingMoreSessionCatalogIds,
|
||||
projectGrouping: this.catalogProjectGrouping,
|
||||
liveRows,
|
||||
sidebarRowsByKey,
|
||||
|
||||
@@ -19,7 +19,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
protected readonly patchSession = async (
|
||||
session: SidebarRecentSession,
|
||||
patch: SidebarSessionPatch,
|
||||
scope: SidebarSessionMutationScope | null = this.beginSessionMutation(),
|
||||
scope: SidebarSessionMutationScope | null = this.sessionData.beginSessionMutation(),
|
||||
): Promise<SidebarSessionMutationResult> => {
|
||||
if (!scope) {
|
||||
return "stale";
|
||||
@@ -27,12 +27,12 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
const agentId = parseAgentSessionKey(session.key)?.agentId ?? scope.selectedAgentId;
|
||||
try {
|
||||
const patched = await scope.sessions.patch(session.key, patch, { agentId });
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return "stale";
|
||||
}
|
||||
if (!patched) {
|
||||
if (scope.sessions.state.error) {
|
||||
this.publishSessionMutationError(scope, scope.sessions.state.error);
|
||||
this.sessionData.publishSessionMutationError(scope, scope.sessions.state.error);
|
||||
}
|
||||
return "failed";
|
||||
}
|
||||
@@ -44,8 +44,8 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
this.pruneSidebarSessionEntry(session.key);
|
||||
}
|
||||
if (this.sidebarSessionStatusFilter() !== "active") {
|
||||
await this.refreshSidebarSessions(agentId);
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
await this.sessionData.refreshSidebarSessions(agentId);
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return "stale";
|
||||
}
|
||||
}
|
||||
@@ -63,10 +63,10 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
);
|
||||
return "completed";
|
||||
} catch (error) {
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return "stale";
|
||||
}
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
return "failed";
|
||||
}
|
||||
};
|
||||
@@ -74,7 +74,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
protected async patchSessions(
|
||||
rows: readonly SidebarRecentSession[],
|
||||
patch: SidebarSessionPatch,
|
||||
scope: SidebarSessionMutationScope | null = this.beginSessionMutation(),
|
||||
scope: SidebarSessionMutationScope | null = this.sessionData.beginSessionMutation(),
|
||||
): Promise<SidebarSessionMutationResult> {
|
||||
if (!scope) {
|
||||
return "stale";
|
||||
@@ -95,12 +95,12 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
}
|
||||
|
||||
protected async archiveSessionWithUndo(session: SidebarRecentSession) {
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
const result = await this.patchSession(session, { archived: true }, scope);
|
||||
if (result !== "completed" || !this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (result !== "completed" || !this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
showToast({
|
||||
@@ -113,7 +113,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
}
|
||||
|
||||
private async archiveSessionsWithUndo(rows: readonly SidebarRecentSession[]) {
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
@@ -127,7 +127,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
archived.push({ session, pinned: session.pinned });
|
||||
}
|
||||
}
|
||||
if (archived.length === 0 || !this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (archived.length === 0 || !this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
showToast({
|
||||
@@ -144,7 +144,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
archived: readonly { session: SidebarRecentSession; pinned: boolean }[],
|
||||
scope: SidebarSessionMutationScope,
|
||||
) {
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
let restoredActiveKey: string | null = null;
|
||||
@@ -161,7 +161,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
restoredActiveKey = session.key;
|
||||
}
|
||||
}
|
||||
if (restoredActiveKey && this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (restoredActiveKey && this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
this.replaceCurrentSession(restoredActiveKey);
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
if (!window.confirm(t("sessionsView.deleteSessionsConfirm", { count: String(rows.length) }))) {
|
||||
return;
|
||||
}
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
@@ -187,12 +187,12 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
...(row.archived === true ? { archivedOnly: true } : {}),
|
||||
})),
|
||||
);
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
if (this.sidebarSessionStatusFilter() !== "active") {
|
||||
await this.refreshSidebarSessions(scope.selectedAgentId);
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
await this.sessionData.refreshSidebarSessions(scope.selectedAgentId);
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
branches: result.preservedWorktrees.map((worktree) => worktree.branch).join(", "),
|
||||
}),
|
||||
);
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -220,10 +220,10 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
this.publishSessionMutationError(scope, result.errors.join("; "));
|
||||
this.sessionData.publishSessionMutationError(scope, result.errors.join("; "));
|
||||
}
|
||||
} catch (error) {
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
}
|
||||
|
||||
protected async forkSession(session: SidebarRecentSession) {
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
@@ -272,19 +272,19 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
fork: true,
|
||||
agentId,
|
||||
});
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
if (key) {
|
||||
this.selectSession(key);
|
||||
} else {
|
||||
this.publishSessionMutationError(
|
||||
this.sessionData.publishSessionMutationError(
|
||||
scope,
|
||||
scope.sessions.state.error ?? t("newSession.createFailed"),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
@@ -307,12 +307,12 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
{ key: session.key, agentId },
|
||||
{ timeoutMs: 10 * 60_000 },
|
||||
);
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
await scope.sessions.refreshReplacement(agentId);
|
||||
} catch (error) {
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
if (!window.confirm(t("sessionsView.deleteSessionConfirm", { session: session.label }))) {
|
||||
return;
|
||||
}
|
||||
const scope = this.beginSessionMutation();
|
||||
const scope = this.sessionData.beginSessionMutation();
|
||||
if (!scope) {
|
||||
return;
|
||||
}
|
||||
@@ -331,12 +331,12 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
deleteTranscript: true,
|
||||
...(session.archived === true ? { archivedOnly: true } : {}),
|
||||
});
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
if (this.sidebarSessionStatusFilter() !== "active") {
|
||||
await this.refreshSidebarSessions(agentId);
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
await this.sessionData.refreshSidebarSessions(agentId);
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -348,7 +348,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
t("sessionsView.deletePreservedWorktreeConfirm", { branch: preserved.branch }),
|
||||
)
|
||||
) {
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -357,9 +357,9 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
force: true,
|
||||
});
|
||||
} catch (error) {
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
}
|
||||
if (!this.isSessionMutationScopeCurrent(scope)) {
|
||||
if (!this.sessionData.isSessionMutationScopeCurrent(scope)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -377,7 +377,7 @@ export abstract class AppSidebarSessionMutationsElement extends AppSidebarSessio
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
this.publishSessionMutationError(scope, error);
|
||||
this.sessionData.publishSessionMutationError(scope, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
import type { SessionObserverDigest } from "../../../packages/gateway-protocol/src/schema/sessions.js";
|
||||
import { SubscriptionsController } from "../lit/subscriptions-controller.ts";
|
||||
@@ -85,8 +86,8 @@ export abstract class AppSidebarSessionNarrationElement extends AppSidebarMenusE
|
||||
});
|
||||
}
|
||||
|
||||
override updated() {
|
||||
super.updated();
|
||||
override updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (!this.narration) {
|
||||
if (this.sidebarLiveActivity) {
|
||||
this.ensureNarrationController();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../api/types.ts";
|
||||
import { SIDEBAR_NAV_ROUTES, serializeSidebarEntry } from "../app-navigation.ts";
|
||||
@@ -75,8 +76,8 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
return this.context;
|
||||
}
|
||||
|
||||
override updated() {
|
||||
super.updated();
|
||||
override updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
const activeRouteKey = this.activeRouteId === "chat" ? this.getRouteSessionKey() : "";
|
||||
if (activeRouteKey !== this.collapsedActiveRouteKey) {
|
||||
this.collapsedActiveRouteKey = activeRouteKey;
|
||||
@@ -85,7 +86,7 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
}
|
||||
}
|
||||
if (this.activeRouteId === "chat") {
|
||||
void this.loadActiveSessionLineage(activeRouteKey);
|
||||
void this.sessionData.loadActiveSessionLineage(activeRouteKey);
|
||||
}
|
||||
const pending = [...this.visibleSessionRowsInOrder()];
|
||||
while (pending.length > 0) {
|
||||
@@ -97,11 +98,11 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
if (
|
||||
session.childSessionKeys.length > 0 &&
|
||||
this.isSessionChildrenExpanded(session) &&
|
||||
!this.loadedChildSessionKeys.has(session.key) &&
|
||||
!this.failedChildSessionKeys.has(session.key) &&
|
||||
!this.loadingChildSessionKeys.has(session.key)
|
||||
!this.sessionData.loadedChildSessionKeys.has(session.key) &&
|
||||
!this.sessionData.failedChildSessionKeys.has(session.key) &&
|
||||
!this.sessionData.loadingChildSessionKeys.has(session.key)
|
||||
) {
|
||||
void this.loadChildSessions(session.key);
|
||||
void this.sessionData.loadChildSessions(session.key);
|
||||
}
|
||||
}
|
||||
// The main session hides behind the identity card, so nothing in the list
|
||||
@@ -110,11 +111,11 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
if (
|
||||
mainRow &&
|
||||
(mainRow.childSessions?.length ?? 0) > 0 &&
|
||||
!this.loadedChildSessionKeys.has(mainRow.key) &&
|
||||
!this.failedChildSessionKeys.has(mainRow.key) &&
|
||||
!this.loadingChildSessionKeys.has(mainRow.key)
|
||||
!this.sessionData.loadedChildSessionKeys.has(mainRow.key) &&
|
||||
!this.sessionData.failedChildSessionKeys.has(mainRow.key) &&
|
||||
!this.sessionData.loadingChildSessionKeys.has(mainRow.key)
|
||||
) {
|
||||
void this.loadChildSessions(mainRow.key);
|
||||
void this.sessionData.loadChildSessions(mainRow.key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +135,8 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
const context = this.context;
|
||||
const routeSessionKey = this.getRouteSessionKey();
|
||||
const navigation = resolveSessionNavigation({
|
||||
result: this.sessionsResult,
|
||||
resultAgentId: this.sessionsAgentId,
|
||||
result: this.sessionData.sessionsResult,
|
||||
resultAgentId: this.sessionData.sessionsAgentId,
|
||||
sessionKey: routeSessionKey,
|
||||
assistantAgentId:
|
||||
context?.agentSelection.state.selectedId ?? context?.gateway.snapshot.assistantAgentId,
|
||||
@@ -211,7 +212,7 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
childSessionKeys: row.archived === true ? [] : (row.childSessions ?? []),
|
||||
children: [],
|
||||
isChild,
|
||||
loadingChildren: this.loadingChildSessionKeys.has(row.key),
|
||||
loadingChildren: this.sessionData.loadingChildSessionKeys.has(row.key),
|
||||
containsActiveDescendant: false,
|
||||
runningChildCount: 0,
|
||||
failedChildCount: 0,
|
||||
@@ -226,11 +227,11 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
};
|
||||
}
|
||||
|
||||
protected selectedAgentIdForSessions(): string {
|
||||
selectedAgentIdForSessions(): string {
|
||||
return this.getSessionNavigationState().selectedAgentId;
|
||||
}
|
||||
|
||||
protected sidebarSessionStatusFilter(): SidebarSessionStatusFilter {
|
||||
sidebarSessionStatusFilter(): SidebarSessionStatusFilter {
|
||||
return this.sessionsStatusFilter;
|
||||
}
|
||||
|
||||
@@ -270,7 +271,7 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
const expandedRows = sections.flatMap((section) =>
|
||||
this.isSessionSectionCollapsed(section.id) ? [] : section.rows,
|
||||
);
|
||||
const visibleRows = limitSidebarSessionRows(expandedRows, this.visibleSessionLimit);
|
||||
const visibleRows = limitSidebarSessionRows(expandedRows, this.sessionData.visibleSessionLimit);
|
||||
const keep = new Set(visibleRows.map((row) => row.key));
|
||||
// totalRowCount is the pre-pagination size: headers and empty-zone
|
||||
// checks must not mistake a page-filtered section for an empty one.
|
||||
@@ -439,12 +440,12 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
}
|
||||
this.clearSessionSelection();
|
||||
this.expandedChildSessionKeys = new Set();
|
||||
this.visibleSessionLimit = SIDEBAR_SESSION_PAGE_SIZE;
|
||||
this.sessionData.setVisibleSessionLimit(SIDEBAR_SESSION_PAGE_SIZE);
|
||||
context.agentSelection.set(nextAgentId);
|
||||
void this.refreshSidebarSessions(nextAgentId);
|
||||
void this.sessionData.refreshSidebarSessions(nextAgentId);
|
||||
};
|
||||
|
||||
protected expandedAgentId(): string {
|
||||
expandedAgentId(): string {
|
||||
const selected = normalizeOptionalString(this.context?.agentSelection.state.selectedId);
|
||||
return selected
|
||||
? normalizeAgentId(selected)
|
||||
@@ -462,9 +463,9 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
private latestAgentSessionRow(agentId: string): SessionsListResult["sessions"][number] | null {
|
||||
const normalized = normalizeAgentId(agentId);
|
||||
const rows =
|
||||
normalized === normalizeAgentId(this.sessionsAgentId ?? "")
|
||||
? (this.sessionsResult?.sessions ?? [])
|
||||
: (this.sessionRowsByAgent[normalized] ?? []);
|
||||
normalized === normalizeAgentId(this.sessionData.sessionsAgentId ?? "")
|
||||
? (this.sessionData.sessionsResult?.sessions ?? [])
|
||||
: (this.sessionData.sessionRowsByAgent[normalized] ?? []);
|
||||
// Unprefixed keys belong to the system default agent. Keeping them for
|
||||
// another agent would resume the wrong conversation with the raw key.
|
||||
const visible = filterVisibleSessionRows(rows, {
|
||||
@@ -533,7 +534,7 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
protected knownSessionGroups(): string[] {
|
||||
const catalog = this.context?.sessions.state.groups ?? [];
|
||||
const catalogSet = new Set(catalog);
|
||||
const discovered = (this.sessionsResult?.sessions ?? [])
|
||||
const discovered = (this.sessionData.sessionsResult?.sessions ?? [])
|
||||
.map((row) => normalizeOptionalString(row.category))
|
||||
.filter((name): name is string => typeof name === "string" && !catalogSet.has(name))
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
@@ -544,14 +545,14 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
protected selectedAgentSessionRows(
|
||||
navigationState: ReturnType<AppSidebarSessionNavigationElement["getSessionNavigationState"]>,
|
||||
): SidebarRecentSession[] {
|
||||
const adopted = adoptedCatalogSessionKeys(this.sessionCatalogs);
|
||||
const adopted = adoptedCatalogSessionKeys(this.sessionData.sessionCatalogs);
|
||||
const selected = this.expandedAgentId();
|
||||
const loadedAgentId = normalizeAgentId(this.sessionsAgentId ?? "");
|
||||
const loadedAgentId = normalizeAgentId(this.sessionData.sessionsAgentId ?? "");
|
||||
const routeAgentId = normalizeAgentId(navigationState.selectedAgentId);
|
||||
const rows =
|
||||
selected === loadedAgentId
|
||||
? (this.sessionsResult?.sessions ?? [])
|
||||
: (this.sessionRowsByAgent[selected] ?? []);
|
||||
? (this.sessionData.sessionsResult?.sessions ?? [])
|
||||
: (this.sessionData.sessionRowsByAgent[selected] ?? []);
|
||||
const rowsByKey = new Map(rows.map((row) => [row.key, row]));
|
||||
const rootRows =
|
||||
selected === routeAgentId && selected === loadedAgentId
|
||||
@@ -582,7 +583,7 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const lineageRoot = this.activeSessionLineageRoot;
|
||||
const lineageRoot = this.sessionData.activeSessionLineageRoot;
|
||||
const lineageAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(lineageRoot?.key ?? "")?.agentId ?? "",
|
||||
);
|
||||
@@ -604,18 +605,19 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
// the same visibility rules and sort order as ordinary roots so archived
|
||||
// or cron children cannot sneak in and pagination stays deterministic.
|
||||
const scopedRootKeys = new Set(scopedRootRows.map((row) => row.key));
|
||||
const promotedRows = [...rows, ...Object.values(this.childSessionRowsByParent).flat()].filter(
|
||||
(row) => {
|
||||
const parentKey = row.spawnedBy ?? row.parentSessionKey;
|
||||
return (
|
||||
parentKey != null &&
|
||||
mainSessionKeys.has(parentKey) &&
|
||||
!scopedRootKeys.has(row.key) &&
|
||||
!row.archived &&
|
||||
(this.sessionsShowCron || !isCronSessionKey(row.key))
|
||||
);
|
||||
},
|
||||
);
|
||||
const promotedRows = [
|
||||
...rows,
|
||||
...Object.values(this.sessionData.childSessionRowsByParent).flat(),
|
||||
].filter((row) => {
|
||||
const parentKey = row.spawnedBy ?? row.parentSessionKey;
|
||||
return (
|
||||
parentKey != null &&
|
||||
mainSessionKeys.has(parentKey) &&
|
||||
!scopedRootKeys.has(row.key) &&
|
||||
!row.archived &&
|
||||
(this.sessionsShowCron || !isCronSessionKey(row.key))
|
||||
);
|
||||
});
|
||||
for (const row of promotedRows) {
|
||||
if (!scopedRootKeys.has(row.key)) {
|
||||
scopedRootKeys.add(row.key);
|
||||
@@ -632,13 +634,15 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
const projected = projectSessionTree({
|
||||
roots: orderedRootRows.filter((row) => !adopted.has(row.key)),
|
||||
agentRows: rows,
|
||||
childRowsByParent: this.childSessionRowsByParent,
|
||||
loadingChildKeys: this.loadingChildSessionKeys,
|
||||
childRowsByParent: this.sessionData.childSessionRowsByParent,
|
||||
loadingChildKeys: this.sessionData.loadingChildSessionKeys,
|
||||
knownSessionAttention: this.attention.knownSessionAttention(),
|
||||
toSidebarSession: navigationState.toSidebarSession,
|
||||
});
|
||||
const creatorFacet =
|
||||
rows === this.sessionsResult?.sessions ? this.sessionsResult.creators : undefined;
|
||||
rows === this.sessionData.sessionsResult?.sessions
|
||||
? this.sessionData.sessionsResult.creators
|
||||
: undefined;
|
||||
return this.applySessionCreatorFilter(projected, rows, creatorFacet);
|
||||
}
|
||||
|
||||
@@ -664,9 +668,9 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
const normalized = normalizeAgentId(agentId ?? this.expandedAgentId());
|
||||
const mainKey = this.selectedAgentMainSessionKey(normalized);
|
||||
const rows =
|
||||
normalized === normalizeAgentId(this.sessionsAgentId ?? "")
|
||||
? (this.sessionsResult?.sessions ?? [])
|
||||
: (this.sessionRowsByAgent[normalized] ?? []);
|
||||
normalized === normalizeAgentId(this.sessionData.sessionsAgentId ?? "")
|
||||
? (this.sessionData.sessionsResult?.sessions ?? [])
|
||||
: (this.sessionData.sessionRowsByAgent[normalized] ?? []);
|
||||
return rows.find((row) => areUiSessionKeysEquivalent(row.key, mainKey)) ?? null;
|
||||
}
|
||||
|
||||
@@ -697,23 +701,11 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
if (session.containsActiveDescendant) {
|
||||
collapsedActive.add(session.key);
|
||||
}
|
||||
if (this.childSessionRowsByParent[session.key]?.length === 0) {
|
||||
const childRows = { ...this.childSessionRowsByParent };
|
||||
delete childRows[session.key];
|
||||
this.childSessionRowsByParent = childRows;
|
||||
const loadedKeys = new Set(this.loadedChildSessionKeys);
|
||||
loadedKeys.delete(session.key);
|
||||
this.loadedChildSessionKeys = loadedKeys;
|
||||
}
|
||||
this.sessionData.discardEmptyChildSessionSnapshot(session.key);
|
||||
} else {
|
||||
next.add(session.key);
|
||||
collapsedActive.delete(session.key);
|
||||
if (this.failedChildSessionKeys.has(session.key)) {
|
||||
const failedKeys = new Set(this.failedChildSessionKeys);
|
||||
failedKeys.delete(session.key);
|
||||
this.failedChildSessionKeys = failedKeys;
|
||||
}
|
||||
void this.loadChildSessions(session.key);
|
||||
this.sessionData.retryChildSessions(session.key);
|
||||
}
|
||||
this.expandedChildSessionKeys = next;
|
||||
this.collapsedActiveChildSessionKeys = collapsedActive;
|
||||
@@ -725,7 +717,7 @@ export abstract class AppSidebarSessionNavigationElement extends AppSidebarSessi
|
||||
}
|
||||
|
||||
protected agentUnreadCount(agentId: string): number {
|
||||
const rows = this.sessionRowsByAgent[normalizeAgentId(agentId)] ?? [];
|
||||
const rows = this.sessionData.sessionRowsByAgent[normalizeAgentId(agentId)] ?? [];
|
||||
return rows.filter((row) => row.unread === true && row.archived !== true).length;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
import { AppSidebarSessionProjectionElement } from "./app-sidebar-session-projection.ts";
|
||||
import type { SidebarRecentSession } from "./app-sidebar-session-types.ts";
|
||||
@@ -16,10 +17,10 @@ export abstract class AppSidebarSessionOwnershipElement extends AppSidebarSessio
|
||||
protected sessionCreatorFilterActive = false;
|
||||
sessionOwnershipVisible = false;
|
||||
|
||||
override updated() {
|
||||
super.updated();
|
||||
override updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
const selectedId = this.sessionCreatorFilterId;
|
||||
const creators = this.sessionsResult?.creators;
|
||||
const creators = this.sessionData.sessionsResult?.creators;
|
||||
if (
|
||||
selectedId &&
|
||||
creators &&
|
||||
@@ -44,7 +45,7 @@ export abstract class AppSidebarSessionOwnershipElement extends AppSidebarSessio
|
||||
pending.push(...row.children);
|
||||
}
|
||||
}
|
||||
const completeFacet = creatorFacet ?? this.sessionsResult?.creators;
|
||||
const completeFacet = creatorFacet ?? this.sessionData.sessionsResult?.creators;
|
||||
this.sessionCreatorOptions = listSessionCreators([
|
||||
...(completeFacet ?? []).map((creator) => ({
|
||||
createdActor: { type: "human" as const, ...creator },
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { state } from "lit/decorators.js";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../api/types.ts";
|
||||
import { compareSessionRowsByUpdatedAt } from "../lib/sessions/index.ts";
|
||||
import { AppSidebarBase } from "./app-sidebar-base.ts";
|
||||
import { adoptedCatalogSessionKeys } from "./app-sidebar-session-catalogs.ts";
|
||||
import { AppSidebarSessionDataElement } from "./app-sidebar-session-data.ts";
|
||||
import { SessionPullRequestIndicatorsController } from "./app-sidebar-session-pr-indicators.ts";
|
||||
import type { SidebarRecentSession, SidebarSessionSortMode } from "./app-sidebar-session-types.ts";
|
||||
import type {
|
||||
SidebarRecentSession,
|
||||
SidebarSessionSortMode,
|
||||
SidebarSessionStatusFilter,
|
||||
} from "./app-sidebar-session-types.ts";
|
||||
import { SessionDataController } from "./session-data-controller.ts";
|
||||
|
||||
/** Shared ordering and PR-state projection used by sidebar navigation. */
|
||||
export abstract class AppSidebarSessionProjectionElement extends AppSidebarSessionDataElement {
|
||||
export abstract class AppSidebarSessionProjectionElement extends AppSidebarBase {
|
||||
@state() protected sessionSortMode: SidebarSessionSortMode = "created";
|
||||
|
||||
readonly sessionData = new SessionDataController(this);
|
||||
private readonly sessionPullRequestIndicators = new SessionPullRequestIndicatorsController(this, {
|
||||
getConnected: () => this.connected,
|
||||
getRows: () => this.visibleSessionPullRequestRows(),
|
||||
@@ -17,6 +23,10 @@ export abstract class AppSidebarSessionProjectionElement extends AppSidebarSessi
|
||||
getSnapshot: () => this.context?.gateway.snapshot,
|
||||
});
|
||||
|
||||
get sessionDataContext() {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
protected readonly compareSidebarSessionRows = (
|
||||
a: SessionsListResult["sessions"][number],
|
||||
b: SessionsListResult["sessions"][number],
|
||||
@@ -25,22 +35,22 @@ export abstract class AppSidebarSessionProjectionElement extends AppSidebarSessi
|
||||
return compareSessionRowsByUpdatedAt(a, b);
|
||||
}
|
||||
return (
|
||||
(this.sessionCreatedOrder.get(a.key) ?? Number.MAX_SAFE_INTEGER) -
|
||||
(this.sessionCreatedOrder.get(b.key) ?? Number.MAX_SAFE_INTEGER)
|
||||
(this.sessionData.sessionCreatedOrder.get(a.key) ?? Number.MAX_SAFE_INTEGER) -
|
||||
(this.sessionData.sessionCreatedOrder.get(b.key) ?? Number.MAX_SAFE_INTEGER)
|
||||
);
|
||||
};
|
||||
|
||||
protected promoteCreatedSession(sessionKey: string) {
|
||||
const currentOrder = this.sessionCreatedOrder.get(sessionKey);
|
||||
promoteCreatedSession(sessionKey: string) {
|
||||
const currentOrder = this.sessionData.sessionCreatedOrder.get(sessionKey);
|
||||
if (currentOrder === 0) {
|
||||
return;
|
||||
}
|
||||
for (const [key, order] of this.sessionCreatedOrder) {
|
||||
for (const [key, order] of this.sessionData.sessionCreatedOrder) {
|
||||
if (key !== sessionKey && (currentOrder === undefined || order < currentOrder)) {
|
||||
this.sessionCreatedOrder.set(key, order + 1);
|
||||
this.sessionData.sessionCreatedOrder.set(key, order + 1);
|
||||
}
|
||||
}
|
||||
this.sessionCreatedOrder.set(sessionKey, 0);
|
||||
this.sessionData.sessionCreatedOrder.set(sessionKey, 0);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -50,14 +60,14 @@ export abstract class AppSidebarSessionProjectionElement extends AppSidebarSessi
|
||||
|
||||
private visibleSessionPullRequestRows(): SidebarRecentSession[] {
|
||||
const rows = this.visibleSessionRowsInOrder();
|
||||
const adopted = adoptedCatalogSessionKeys(this.sessionCatalogs);
|
||||
const adopted = adoptedCatalogSessionKeys(this.sessionData.sessionCatalogs);
|
||||
if (adopted.size === 0) {
|
||||
return rows;
|
||||
}
|
||||
const byKey = new Map(rows.map((row) => [row.key, row]));
|
||||
const liveRows = [
|
||||
...(this.sessionsResult?.sessions ?? []),
|
||||
...Object.values(this.sessionRowsByAgent).flat(),
|
||||
...(this.sessionData.sessionsResult?.sessions ?? []),
|
||||
...Object.values(this.sessionData.sessionRowsByAgent).flat(),
|
||||
];
|
||||
for (const row of liveRows) {
|
||||
if (adopted.has(row.key) && !byKey.has(row.key)) {
|
||||
@@ -69,4 +79,8 @@ export abstract class AppSidebarSessionProjectionElement extends AppSidebarSessi
|
||||
|
||||
protected abstract visibleSessionRowsInOrder(): SidebarRecentSession[];
|
||||
protected abstract projectSidebarSession(row: GatewaySessionRow): SidebarRecentSession;
|
||||
abstract dismissTransientMenus(): boolean;
|
||||
abstract expandedAgentId(): string;
|
||||
abstract selectedAgentIdForSessions(): string;
|
||||
abstract sidebarSessionStatusFilter(): SidebarSessionStatusFilter;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import { html, nothing, type TemplateResult } from "lit";
|
||||
import { keyed } from "lit/directives/keyed.js";
|
||||
import type { SessionObserverDigest } from "../../../packages/gateway-protocol/src/schema/sessions.js";
|
||||
import type { NavigationRouteId } from "../app-navigation.ts";
|
||||
import type { ApprovalBadgeSnapshot } from "../app/approval-presentation.ts";
|
||||
import { sessionHasPendingApproval } from "../app/approval-presentation.ts";
|
||||
import type { ApplicationNavigationOptions } from "../app/context.ts";
|
||||
import type { PresencePayload } from "../app/user-profile.ts";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { sessionHasBoard } from "../lib/board/provider.ts";
|
||||
import { formatDurationCompact } from "../lib/format.ts";
|
||||
@@ -25,6 +23,7 @@ import {
|
||||
type SidebarSessionStatusFilter,
|
||||
} from "./app-sidebar-session-types.ts";
|
||||
import { icons } from "./icons.ts";
|
||||
import type { SessionDataController } from "./session-data-controller.ts";
|
||||
import { renderSessionLeadingState } from "./session-leading-indicator.ts";
|
||||
import type { SessionPullRequestIndicatorState } from "./session-menu-work.ts";
|
||||
import { renderSessionOwnerChip } from "./session-owner-chip.ts";
|
||||
@@ -44,8 +43,14 @@ export interface SessionListHost {
|
||||
readonly selectedSessionKeys: ReadonlySet<string>;
|
||||
readonly draggingSessionKey: string | null;
|
||||
readonly connected: boolean;
|
||||
readonly presencePayload: PresencePayload | undefined;
|
||||
readonly presenceInstanceId?: string;
|
||||
readonly sessionData: Pick<
|
||||
SessionDataController,
|
||||
| "approvalBadgeSnapshot"
|
||||
| "loadMoreSessionCatalog"
|
||||
| "presenceInstanceId"
|
||||
| "presencePayload"
|
||||
| "sessionMutationError"
|
||||
>;
|
||||
readonly fullyShownChildSessionKeys: ReadonlySet<string>;
|
||||
readonly sessionsGrouping: SidebarSessionsGrouping;
|
||||
readonly collapsedSessionSections: ReadonlySet<string>;
|
||||
@@ -57,7 +62,6 @@ export interface SessionListHost {
|
||||
readonly sessionGroupMenu: { readonly group: string } | null;
|
||||
readonly sessionsStatusFilter: SidebarSessionStatusFilter;
|
||||
readonly sessionListRemovalDrop: boolean;
|
||||
readonly sessionMutationError: string | null;
|
||||
readonly sessionOwnershipVisible: boolean;
|
||||
readonly onOpenNewSession?: (agentId: string, target?: NewSessionTarget) => void;
|
||||
readonly onNavigate?: (
|
||||
@@ -69,7 +73,6 @@ export interface SessionListHost {
|
||||
sessionKey: string,
|
||||
worktreeId: string,
|
||||
): SessionPullRequestIndicatorState;
|
||||
approvalBadgeSnapshot(): ApprovalBadgeSnapshot;
|
||||
isSessionChildrenExpanded(session: SidebarRecentSession): boolean;
|
||||
startSessionDrag(session: SidebarRecentSession): void;
|
||||
finishSessionDrag(): void;
|
||||
@@ -99,7 +102,6 @@ export interface SessionListHost {
|
||||
handleSessionListDrop(event: DragEvent): void;
|
||||
dismissSessionMutationError(): void;
|
||||
toggleCatalogProjectGrouping(): void;
|
||||
loadMoreSessionCatalog(catalogId: string): Promise<void>;
|
||||
openCatalogMenu(
|
||||
request: CatalogSessionMenuRequest,
|
||||
x: number,
|
||||
@@ -233,8 +235,8 @@ export function renderRecentSession(params: {
|
||||
>`
|
||||
: nothing}
|
||||
<openclaw-viewer-facepile
|
||||
.presencePayload=${host.presencePayload}
|
||||
.selfInstanceId=${host.presenceInstanceId}
|
||||
.presencePayload=${host.sessionData.presencePayload}
|
||||
.selfInstanceId=${host.sessionData.presenceInstanceId}
|
||||
.sessionKey=${session.key}
|
||||
.maxVisible=${3}
|
||||
variant="session"
|
||||
@@ -242,7 +244,10 @@ export function renderRecentSession(params: {
|
||||
${renderSessionRowBadges({
|
||||
...session,
|
||||
pullRequest: session.pullRequest ?? display?.pullRequest,
|
||||
hasApproval: sessionHasPendingApproval(host.approvalBadgeSnapshot(), session.key),
|
||||
hasApproval: sessionHasPendingApproval(
|
||||
host.sessionData.approvalBadgeSnapshot(),
|
||||
session.key,
|
||||
),
|
||||
})}
|
||||
${pinnedState}
|
||||
</a>
|
||||
|
||||
@@ -140,6 +140,22 @@ export type SidebarSessionGroupMenuState = {
|
||||
export type SidebarSessionSortMode = "created" | "updated";
|
||||
export type SidebarSessionStatusFilter = "active" | "archived" | "all";
|
||||
export type SidebarSessionsScrollState = "none" | "top" | "middle" | "bottom";
|
||||
|
||||
export function resolveSidebarSessionsScrollState(
|
||||
element: HTMLElement,
|
||||
): SidebarSessionsScrollState {
|
||||
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
||||
if (maxScrollTop <= 1) {
|
||||
return "none";
|
||||
}
|
||||
if (element.scrollTop <= 1) {
|
||||
return "top";
|
||||
}
|
||||
if (element.scrollTop >= maxScrollTop - 1) {
|
||||
return "bottom";
|
||||
}
|
||||
return "middle";
|
||||
}
|
||||
export type SidebarSessionGroupDropTarget = {
|
||||
group: string;
|
||||
position: "before" | "after";
|
||||
|
||||
@@ -127,7 +127,8 @@ class AppSidebar extends AppSidebarSessionListElement {
|
||||
return agentId !== cardAgentId && this.agentUnreadCount(agentId) > 0;
|
||||
});
|
||||
const cardName = cardAgent ? normalizeAgentLabel(cardAgent) : cardAgentId;
|
||||
const approvalCount = this.approvalBadgeSnapshot().agentCounts.get(cardAgentId) ?? 0;
|
||||
const approvalCount =
|
||||
this.sessionData.approvalBadgeSnapshot().agentCounts.get(cardAgentId) ?? 0;
|
||||
const cardAvatarText =
|
||||
(cardAgent ? resolveAgentTextAvatar(cardAgent) : null) ??
|
||||
(cardName || cardAgentId).slice(0, 1).toUpperCase();
|
||||
@@ -184,7 +185,10 @@ class AppSidebar extends AppSidebarSessionListElement {
|
||||
const agentId = this.activeChipAgent().activeId;
|
||||
const mainKey = this.selectedAgentMainSessionKey(agentId);
|
||||
const mainRow = this.mainSessionRow(agentId);
|
||||
const approvalNeeded = sessionHasPendingApproval(this.approvalBadgeSnapshot(), mainKey);
|
||||
const approvalNeeded = sessionHasPendingApproval(
|
||||
this.sessionData.approvalBadgeSnapshot(),
|
||||
mainKey,
|
||||
);
|
||||
const outboxCount = this.outboxCountForSessionKey(mainKey);
|
||||
const active =
|
||||
this.activeRouteId === "chat" &&
|
||||
@@ -275,8 +279,8 @@ class AppSidebar extends AppSidebarSessionListElement {
|
||||
const selfUser = this.connected
|
||||
? resolveCurrentSelfUser({
|
||||
snapshotUser: this.context?.gateway.snapshot.selfUser,
|
||||
presenceEntries: readPresenceEntries(this.presencePayload),
|
||||
presenceInstanceId: this.presenceInstanceId,
|
||||
presenceEntries: readPresenceEntries(this.sessionData.presencePayload),
|
||||
presenceInstanceId: this.sessionData.presenceInstanceId,
|
||||
})
|
||||
: null;
|
||||
const selfLabel = selfUser?.name ?? selfUser?.email ?? selfUser?.id;
|
||||
@@ -308,8 +312,8 @@ class AppSidebar extends AppSidebarSessionListElement {
|
||||
</openclaw-tooltip>`
|
||||
: nothing}
|
||||
<openclaw-viewer-facepile
|
||||
.presencePayload=${this.presencePayload}
|
||||
.selfInstanceId=${this.presenceInstanceId}
|
||||
.presencePayload=${this.sessionData.presencePayload}
|
||||
.selfInstanceId=${this.sessionData.presenceInstanceId}
|
||||
.buildInfo=${CONTROL_UI_BUILD_INFO}
|
||||
.gatewayVersion=${this.gatewayVersion}
|
||||
.maxVisible=${5}
|
||||
@@ -439,9 +443,10 @@ class AppSidebar extends AppSidebarSessionListElement {
|
||||
<div class="sidebar-shell" @mousedown=${beginNativeWindowDragFromTopInset}>
|
||||
${this.renderBrand()}
|
||||
<div
|
||||
class="sidebar-shell__body sidebar-shell__body--scroll-${this.sessionsScrollState}"
|
||||
class="sidebar-shell__body sidebar-shell__body--scroll-${this.sessionData
|
||||
.sessionsScrollState}"
|
||||
@scroll=${(event: Event) =>
|
||||
this.updateSessionsScrollState(event.currentTarget as HTMLElement)}
|
||||
this.sessionData.updateSessionsScrollState(event.currentTarget as HTMLElement)}
|
||||
>
|
||||
<nav class="sidebar-nav" @contextmenu=${this.openCustomizeMenuFromContext}>
|
||||
${this.renderPagesHead()}
|
||||
@@ -478,8 +483,11 @@ class AppSidebar extends AppSidebarSessionListElement {
|
||||
></openclaw-sidebar-update-card>
|
||||
<openclaw-lobster-pet
|
||||
.seed=${lobsterPetSeed(this.sessionKey)}
|
||||
.mode=${resolveLobsterPetMode(!this.offline, this.sessionsResult?.sessions)}
|
||||
.runOutcome=${resolveLobsterRunOutcome(this.sessionsResult?.sessions)}
|
||||
.mode=${resolveLobsterPetMode(
|
||||
!this.offline,
|
||||
this.sessionData.sessionsResult?.sessions,
|
||||
)}
|
||||
.runOutcome=${resolveLobsterRunOutcome(this.sessionData.sessionsResult?.sessions)}
|
||||
.visitsEnabled=${this.lobsterPetVisits}
|
||||
.soundsEnabled=${this.lobsterPetSounds}
|
||||
.gatewayVersion=${this.gatewayVersion}
|
||||
|
||||
243
ui/src/components/session-data-controller-catalog.ts
Normal file
243
ui/src/components/session-data-controller-catalog.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import type {
|
||||
SessionCatalog,
|
||||
SessionsCatalogListResult,
|
||||
} from "../../../packages/gateway-protocol/src/index.ts";
|
||||
import type { GatewayBrowserClient } from "../api/gateway.ts";
|
||||
import type { RouteId } from "../app-route-paths.ts";
|
||||
import type { ApplicationContext } from "../app/context.ts";
|
||||
import {
|
||||
refreshSessionCatalogsLive,
|
||||
SESSION_CATALOG_CHANGED_REFRESH_MS,
|
||||
SessionCatalogLiveState,
|
||||
sessionCatalogListClient,
|
||||
} from "./app-sidebar-session-catalog-live.ts";
|
||||
import {
|
||||
mergeSessionCatalogPage,
|
||||
sessionCatalogRequestError,
|
||||
} from "./app-sidebar-session-catalog-state.ts";
|
||||
import { sessionCatalogHostKey } from "./app-sidebar-session-types.ts";
|
||||
import type { SidebarSessionStatusFilter } from "./app-sidebar-session-types.ts";
|
||||
|
||||
export interface SessionDataControllerHost extends ReactiveControllerHost {
|
||||
readonly isConnected: boolean;
|
||||
readonly connected: boolean;
|
||||
readonly sessionDataContext: ApplicationContext<RouteId> | undefined;
|
||||
dismissTransientMenus(): boolean;
|
||||
expandedAgentId(): string;
|
||||
promoteCreatedSession(sessionKey: string): void;
|
||||
selectedAgentIdForSessions(): string;
|
||||
sidebarSessionStatusFilter(): SidebarSessionStatusFilter;
|
||||
querySelector(selectors: string): Element | null;
|
||||
}
|
||||
|
||||
export interface SessionCatalogDataOwner {
|
||||
readonly context: ApplicationContext<RouteId> | undefined;
|
||||
readonly isSessionDataHostConnected: boolean;
|
||||
readonly sessionDataHostConnected: boolean;
|
||||
sessionCatalogs: SessionCatalog[];
|
||||
loadingMoreSessionCatalogIds: ReadonlySet<string>;
|
||||
readonly sessionCatalogLive: SessionCatalogLiveState;
|
||||
sessionCatalogAgentId: string | null;
|
||||
sessionCatalogGeneration: number;
|
||||
sessionCatalogRevision: number;
|
||||
readonly sessionCatalogPageDepths: Map<string, number>;
|
||||
readonly sessionCatalogRevisions: Map<string, number>;
|
||||
expandedAgentId(): string;
|
||||
sessionCatalogGatewayClient(): GatewayBrowserClient | null;
|
||||
requestSessionDataUpdate(): void;
|
||||
refreshSessionCatalogs(): Promise<void>;
|
||||
}
|
||||
|
||||
export function visibleSessionCatalogClient(
|
||||
owner: SessionCatalogDataOwner,
|
||||
): GatewayBrowserClient | null {
|
||||
if (document.visibilityState === "hidden") {
|
||||
return null;
|
||||
}
|
||||
return sessionCatalogListClient(owner.context?.gateway.snapshot, owner.sessionDataHostConnected);
|
||||
}
|
||||
|
||||
export function synchronizeSessionCatalogAgent(
|
||||
owner: SessionCatalogDataOwner,
|
||||
agentId: string,
|
||||
): void {
|
||||
if (agentId === owner.sessionCatalogAgentId) {
|
||||
return;
|
||||
}
|
||||
owner.sessionCatalogAgentId = agentId;
|
||||
owner.sessionCatalogGeneration += 1;
|
||||
owner.sessionCatalogRevision += 1;
|
||||
owner.sessionCatalogLive.clear();
|
||||
owner.loadingMoreSessionCatalogIds = new Set();
|
||||
if (owner.sessionCatalogs.some((catalog) => catalog.capabilities.createSession)) {
|
||||
owner.sessionCatalogs = owner.sessionCatalogs.map((catalog) => {
|
||||
const { createSession: _createSession, ...capabilities } = catalog.capabilities;
|
||||
return { ...catalog, capabilities };
|
||||
});
|
||||
}
|
||||
owner.requestSessionDataUpdate();
|
||||
}
|
||||
|
||||
export function requestSessionCatalogRefresh(owner: SessionCatalogDataOwner): void {
|
||||
const snapshot = owner.context?.gateway.snapshot;
|
||||
owner.sessionCatalogLive.requestRefresh({
|
||||
visible: document.visibilityState !== "hidden",
|
||||
connected:
|
||||
owner.isSessionDataHostConnected &&
|
||||
Boolean(sessionCatalogListClient(snapshot, owner.sessionDataHostConnected)),
|
||||
generation: owner.sessionCatalogGeneration,
|
||||
refresh: () => void owner.refreshSessionCatalogs(),
|
||||
});
|
||||
}
|
||||
|
||||
export function applySessionCatalogHostEvent(
|
||||
owner: SessionCatalogDataOwner,
|
||||
payload: unknown,
|
||||
): void {
|
||||
const update = owner.sessionCatalogLive.applyHost({
|
||||
payload,
|
||||
agentId: owner.sessionCatalogAgentId ?? "",
|
||||
catalogs: owner.sessionCatalogs,
|
||||
pageDepths: owner.sessionCatalogPageDepths,
|
||||
});
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
owner.sessionCatalogs = update.catalogs;
|
||||
owner.requestSessionDataUpdate();
|
||||
owner.sessionCatalogRevision += owner.sessionCatalogLive.refetching ? 1 : 0;
|
||||
const catalogRevision = owner.sessionCatalogRevisions.get(update.catalogId) ?? 0;
|
||||
owner.sessionCatalogRevisions.set(update.catalogId, catalogRevision + 1);
|
||||
if (owner.sessionCatalogLive.requestGeneration !== owner.sessionCatalogGeneration) {
|
||||
owner.sessionCatalogLive.schedule(
|
||||
SESSION_CATALOG_CHANGED_REFRESH_MS,
|
||||
owner.isSessionDataHostConnected,
|
||||
() => void owner.refreshSessionCatalogs(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshSessionCatalogs(owner: SessionCatalogDataOwner): Promise<void> {
|
||||
// Hidden pages resume through the coalesced activation handler. Starting
|
||||
// here without a timer makes catalog state updates poll at request latency.
|
||||
const client = visibleSessionCatalogClient(owner);
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
const generation = owner.sessionCatalogGeneration;
|
||||
const revision = owner.sessionCatalogRevision;
|
||||
const agentId = owner.sessionCatalogAgentId ?? owner.expandedAgentId();
|
||||
await refreshSessionCatalogsLive({
|
||||
live: owner.sessionCatalogLive,
|
||||
client,
|
||||
agentId,
|
||||
generation,
|
||||
revision,
|
||||
currentGeneration: () => owner.sessionCatalogGeneration,
|
||||
currentRevision: () => owner.sessionCatalogRevision,
|
||||
currentClient: () => owner.sessionCatalogGatewayClient(),
|
||||
catalogs: () => owner.sessionCatalogs,
|
||||
pageDepths: owner.sessionCatalogPageDepths,
|
||||
connected: () => owner.isSessionDataHostConnected,
|
||||
applyFinal: (catalogs, revisedCatalogIds) => {
|
||||
owner.sessionCatalogs = catalogs;
|
||||
owner.requestSessionDataUpdate();
|
||||
for (const catalogId of revisedCatalogIds) {
|
||||
owner.sessionCatalogRevisions.set(
|
||||
catalogId,
|
||||
(owner.sessionCatalogRevisions.get(catalogId) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
owner.sessionCatalogRevision += 1;
|
||||
},
|
||||
refresh: () => void owner.refreshSessionCatalogs(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadMoreSessionCatalog(
|
||||
owner: SessionCatalogDataOwner,
|
||||
catalogId: string,
|
||||
): Promise<void> {
|
||||
if (owner.loadingMoreSessionCatalogIds.has(catalogId)) {
|
||||
return;
|
||||
}
|
||||
const catalog = owner.sessionCatalogs.find((candidate) => candidate.id === catalogId);
|
||||
const cursors = Object.fromEntries(
|
||||
(catalog?.hosts ?? []).flatMap((host) =>
|
||||
host.nextCursor ? [[host.hostId, host.nextCursor] as const] : [],
|
||||
),
|
||||
);
|
||||
if (!catalog || Object.keys(cursors).length === 0) {
|
||||
return;
|
||||
}
|
||||
const client = owner.context?.gateway.snapshot.client;
|
||||
if (!client || !owner.sessionDataHostConnected) {
|
||||
return;
|
||||
}
|
||||
const generation = owner.sessionCatalogGeneration;
|
||||
const agentId = owner.sessionCatalogAgentId ?? owner.expandedAgentId();
|
||||
const revision = owner.sessionCatalogRevisions.get(catalogId) ?? 0;
|
||||
owner.loadingMoreSessionCatalogIds = new Set([...owner.loadingMoreSessionCatalogIds, catalogId]);
|
||||
owner.requestSessionDataUpdate();
|
||||
try {
|
||||
const result = await client.request<SessionsCatalogListResult>("sessions.catalog.list", {
|
||||
agentId,
|
||||
catalogId,
|
||||
cursors,
|
||||
});
|
||||
if (!isCurrentSessionCatalogRequest(owner, catalogId, client, generation, revision)) {
|
||||
return;
|
||||
}
|
||||
const page = result.catalogs.find((candidate) => candidate.id === catalogId);
|
||||
const current = owner.sessionCatalogs.find((candidate) => candidate.id === catalogId);
|
||||
if (!page || !current) {
|
||||
return;
|
||||
}
|
||||
const merged = mergeSessionCatalogPage({ current, page, cursors });
|
||||
for (const hostId of merged.advancedHostIds) {
|
||||
const key = sessionCatalogHostKey(catalogId, hostId);
|
||||
owner.sessionCatalogPageDepths.set(key, (owner.sessionCatalogPageDepths.get(key) ?? 0) + 1);
|
||||
}
|
||||
owner.sessionCatalogs = owner.sessionCatalogs.map((candidate) =>
|
||||
candidate.id === catalogId ? merged.catalog : candidate,
|
||||
);
|
||||
owner.requestSessionDataUpdate();
|
||||
owner.sessionCatalogRevisions.set(catalogId, revision + 1);
|
||||
owner.sessionCatalogRevision += 1;
|
||||
} catch (error) {
|
||||
if (!isCurrentSessionCatalogRequest(owner, catalogId, client, generation, revision)) {
|
||||
return;
|
||||
}
|
||||
// Preserve rows and cursors: retrying Load More requests this page again.
|
||||
owner.sessionCatalogs = owner.sessionCatalogs.map((candidate) =>
|
||||
candidate.id === catalogId
|
||||
? { ...candidate, error: sessionCatalogRequestError(error) }
|
||||
: candidate,
|
||||
);
|
||||
owner.requestSessionDataUpdate();
|
||||
owner.sessionCatalogRevisions.set(catalogId, revision + 1);
|
||||
owner.sessionCatalogRevision += 1;
|
||||
} finally {
|
||||
if (generation === owner.sessionCatalogGeneration) {
|
||||
const loading = new Set(owner.loadingMoreSessionCatalogIds);
|
||||
loading.delete(catalogId);
|
||||
owner.loadingMoreSessionCatalogIds = loading;
|
||||
owner.requestSessionDataUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isCurrentSessionCatalogRequest(
|
||||
owner: SessionCatalogDataOwner,
|
||||
catalogId: string,
|
||||
client: GatewayBrowserClient,
|
||||
generation: number,
|
||||
revision: number,
|
||||
): boolean {
|
||||
return (
|
||||
generation === owner.sessionCatalogGeneration &&
|
||||
revision === (owner.sessionCatalogRevisions.get(catalogId) ?? 0) &&
|
||||
client === owner.sessionCatalogGatewayClient()
|
||||
);
|
||||
}
|
||||
import type { ReactiveControllerHost } from "lit";
|
||||
@@ -1,4 +1,5 @@
|
||||
import { state } from "lit/decorators.js";
|
||||
import type { ReactiveController } from "lit";
|
||||
import type { SessionCatalog } from "../../../packages/gateway-protocol/src/index.ts";
|
||||
import type { GatewayBrowserClient } from "../api/gateway.ts";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../api/types.ts";
|
||||
import type { RouteId } from "../app-route-paths.ts";
|
||||
@@ -8,6 +9,10 @@ import {
|
||||
} from "../app/approval-presentation.ts";
|
||||
import type { ApplicationContext } from "../app/context.ts";
|
||||
import { readPresenceEntries, type PresencePayload } from "../app/user-profile.ts";
|
||||
import {
|
||||
CATALOG_SESSION_CONTINUED_EVENT,
|
||||
type CatalogSessionContinuedDetail,
|
||||
} from "../lib/sessions/catalog-key.ts";
|
||||
import type { SessionCapability } from "../lib/sessions/index.ts";
|
||||
import { normalizeAgentId } from "../lib/sessions/session-key.ts";
|
||||
import { SubscriptionsController } from "../lit/subscriptions-controller.ts";
|
||||
@@ -17,35 +22,56 @@ import {
|
||||
fetchSessionLineage,
|
||||
mergeChildSessionRows,
|
||||
} from "./app-sidebar-child-session-data.ts";
|
||||
import { AppSidebarSessionCatalogDataElement } from "./app-sidebar-session-catalog-data.ts";
|
||||
import { SessionCatalogLiveState } from "./app-sidebar-session-catalog-live.ts";
|
||||
import { bindAdoptedCatalogSession } from "./app-sidebar-session-catalogs.ts";
|
||||
import {
|
||||
SIDEBAR_AGENT_SESSION_LIST_LIMIT,
|
||||
SIDEBAR_SESSION_PAGE_SIZE,
|
||||
resolveSidebarSessionsScrollState,
|
||||
type SidebarSessionMutationScope,
|
||||
type SidebarSessionStatusFilter,
|
||||
type SidebarSessionsScrollState,
|
||||
} from "./app-sidebar-session-types.ts";
|
||||
/** Gateway-backed session and external-catalog synchronization. */
|
||||
export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCatalogDataElement {
|
||||
@state() protected visibleSessionLimit = SIDEBAR_SESSION_PAGE_SIZE;
|
||||
@state() protected sessionsResult: SessionsListResult | null = null;
|
||||
@state() protected sessionsAgentId: string | null = null;
|
||||
@state() protected sessionsLoading = false;
|
||||
@state() protected childSessionRowsByParent: Readonly<
|
||||
Record<string, readonly GatewaySessionRow[]>
|
||||
> = {};
|
||||
@state() protected loadedChildSessionKeys: ReadonlySet<string> = new Set();
|
||||
@state() protected failedChildSessionKeys: ReadonlySet<string> = new Set();
|
||||
@state() protected loadingChildSessionKeys: ReadonlySet<string> = new Set();
|
||||
@state() protected activeSessionLineageRoot: GatewaySessionRow | null = null;
|
||||
@state() protected sessionsScrollState: SidebarSessionsScrollState = "none";
|
||||
@state() sessionMutationError: string | null = null;
|
||||
@state() presencePayload: PresencePayload | undefined;
|
||||
@state() presenceInstanceId?: string;
|
||||
import {
|
||||
applySessionCatalogHostEvent as applySessionCatalogHostEventToData,
|
||||
loadMoreSessionCatalog as loadMoreSessionCatalogData,
|
||||
refreshSessionCatalogs as refreshSessionCatalogData,
|
||||
requestSessionCatalogRefresh as requestSessionCatalogDataRefresh,
|
||||
synchronizeSessionCatalogAgent,
|
||||
type SessionCatalogDataOwner,
|
||||
type SessionDataControllerHost,
|
||||
visibleSessionCatalogClient,
|
||||
} from "./session-data-controller-catalog.ts";
|
||||
|
||||
protected sessionRowsByAgent: Record<string, SessionsListResult["sessions"]> = {};
|
||||
protected sessionCreatedOrder = new Map<string, number>();
|
||||
private readonly subscriptions = new SubscriptionsController(this);
|
||||
/** Gateway-backed session-list and external-catalog data ownership. */
|
||||
export class SessionDataController implements ReactiveController, SessionCatalogDataOwner {
|
||||
sessionCatalogs: SessionCatalog[] = [];
|
||||
loadingMoreSessionCatalogIds: ReadonlySet<string> = new Set();
|
||||
visibleSessionLimit = SIDEBAR_SESSION_PAGE_SIZE;
|
||||
sessionsResult: SessionsListResult | null = null;
|
||||
sessionsAgentId: string | null = null;
|
||||
sessionsLoading = false;
|
||||
childSessionRowsByParent: Readonly<Record<string, readonly GatewaySessionRow[]>> = {};
|
||||
loadedChildSessionKeys: ReadonlySet<string> = new Set();
|
||||
failedChildSessionKeys: ReadonlySet<string> = new Set();
|
||||
loadingChildSessionKeys: ReadonlySet<string> = new Set();
|
||||
activeSessionLineageRoot: GatewaySessionRow | null = null;
|
||||
sessionsScrollState: SidebarSessionsScrollState = "none";
|
||||
sessionMutationError: string | null = null;
|
||||
presencePayload: PresencePayload | undefined;
|
||||
presenceInstanceId?: string;
|
||||
|
||||
// These caches were not Lit state on the element and stay non-reactive here.
|
||||
sessionRowsByAgent: Record<string, SessionsListResult["sessions"]> = {};
|
||||
sessionCreatedOrder = new Map<string, number>();
|
||||
|
||||
private readonly subscriptions: SubscriptionsController;
|
||||
readonly sessionCatalogLive = new SessionCatalogLiveState();
|
||||
sessionCatalogAgentId: string | null = null;
|
||||
sessionCatalogGeneration = 0;
|
||||
sessionCatalogRevision = 0;
|
||||
readonly sessionCatalogPageDepths = new Map<string, number>();
|
||||
readonly sessionCatalogRevisions = new Map<string, number>();
|
||||
private sessionsSource: SessionCapability | null = null;
|
||||
private childSessionGeneration = 0;
|
||||
private sidebarListRequestToken: symbol | null = null;
|
||||
@@ -67,13 +93,18 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
[];
|
||||
private approvalBadges: ApprovalBadgeSnapshot = deriveApprovalBadgeSnapshot([]);
|
||||
|
||||
abstract dismissTransientMenus(): boolean;
|
||||
protected abstract promoteCreatedSession(sessionKey: string): void;
|
||||
protected abstract selectedAgentIdForSessions(): string;
|
||||
protected abstract sidebarSessionStatusFilter(): SidebarSessionStatusFilter;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(private readonly host: SessionDataControllerHost) {
|
||||
host.addController(this);
|
||||
// The element used to enter subscriptions before connecting catalog listeners,
|
||||
// then tear subscriptions down after all session cleanup. Keep that ordering.
|
||||
this.subscriptions = new SubscriptionsController({
|
||||
addController: () => undefined,
|
||||
removeController: () => undefined,
|
||||
requestUpdate: () => host.requestUpdate(),
|
||||
get updateComplete() {
|
||||
return host.updateComplete;
|
||||
},
|
||||
});
|
||||
this.subscriptions
|
||||
.watch(
|
||||
() => this.context?.gateway,
|
||||
@@ -87,7 +118,7 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
)
|
||||
.effect(
|
||||
() => this.context?.sessions,
|
||||
(sessions) => sessions.subscribeCreated((key) => this.promoteCreatedSession(key)),
|
||||
(sessions) => sessions.subscribeCreated((key) => host.promoteCreatedSession(key)),
|
||||
)
|
||||
.effect(
|
||||
() => this.context?.gateway,
|
||||
@@ -100,6 +131,7 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
if (event.event === "presence") {
|
||||
const presence = readPresenceEntries(event.payload);
|
||||
this.presencePayload = presence ? { presence } : undefined;
|
||||
this.notify();
|
||||
this.handleSessionCatalogPresence(event.payload);
|
||||
}
|
||||
}),
|
||||
@@ -118,27 +150,45 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
);
|
||||
}
|
||||
|
||||
approvalBadgeSnapshot(): ApprovalBadgeSnapshot {
|
||||
const queue = this.context?.overlays?.snapshot.approvalQueue ?? [];
|
||||
if (queue !== this.approvalBadgeQueue) {
|
||||
this.approvalBadgeQueue = queue;
|
||||
this.approvalBadges = deriveApprovalBadgeSnapshot(queue);
|
||||
}
|
||||
return this.approvalBadges;
|
||||
get context(): ApplicationContext<RouteId> | undefined {
|
||||
return this.host.sessionDataContext;
|
||||
}
|
||||
|
||||
protected sessionCatalogGatewayClient(): GatewayBrowserClient | null {
|
||||
return this.gatewayClient;
|
||||
get isSessionDataHostConnected(): boolean {
|
||||
return this.host.isConnected;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
get sessionDataHostConnected(): boolean {
|
||||
return this.host.connected;
|
||||
}
|
||||
|
||||
expandedAgentId(): string {
|
||||
return this.host.expandedAgentId();
|
||||
}
|
||||
|
||||
requestSessionDataUpdate(): void {
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
|
||||
private readonly notify = () => this.requestSessionDataUpdate();
|
||||
|
||||
hostConnected(): void {
|
||||
this.subscriptions.hostConnected();
|
||||
this.connectSessionCatalogListeners();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
hostUpdate(): void {
|
||||
this.subscriptions.hostUpdate();
|
||||
}
|
||||
|
||||
hostUpdated(): void {
|
||||
this.syncSessionsScrollObserver();
|
||||
this.updateSessionCatalogData();
|
||||
}
|
||||
|
||||
hostDisconnected(): void {
|
||||
this.disconnectSessionCatalogListeners();
|
||||
this.dismissTransientMenus();
|
||||
this.host.dismissTransientMenus();
|
||||
this.invalidateSessionMutations();
|
||||
this.gatewaySource = null;
|
||||
this.gatewayClient = null;
|
||||
@@ -155,16 +205,118 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
globalThis.clearTimeout(this.activeSessionLineageRetryTimer);
|
||||
this.activeSessionLineageRetryTimer = null;
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
this.subscriptions.hostDisconnected();
|
||||
}
|
||||
|
||||
override updated() {
|
||||
this.syncSessionsScrollObserver();
|
||||
this.updateSessionCatalogData();
|
||||
approvalBadgeSnapshot(): ApprovalBadgeSnapshot {
|
||||
const queue = this.context?.overlays?.snapshot.approvalQueue ?? [];
|
||||
if (queue !== this.approvalBadgeQueue) {
|
||||
this.approvalBadgeQueue = queue;
|
||||
this.approvalBadges = deriveApprovalBadgeSnapshot(queue);
|
||||
}
|
||||
return this.approvalBadges;
|
||||
}
|
||||
|
||||
private syncSessionsScrollObserver() {
|
||||
const element = this.querySelector<HTMLElement>(".sidebar-shell__body");
|
||||
sessionCatalogGatewayClient(): GatewayBrowserClient | null {
|
||||
return this.gatewayClient;
|
||||
}
|
||||
|
||||
connectSessionCatalogListeners(): void {
|
||||
// The chat pane announces catalog adoptions so the catalog row binds to
|
||||
// the new session key before the next catalog poll.
|
||||
document.addEventListener(
|
||||
CATALOG_SESSION_CONTINUED_EVENT,
|
||||
this.handleCatalogSessionContinued as EventListener,
|
||||
);
|
||||
document.addEventListener("visibilitychange", this.handleSessionCatalogPageActivation);
|
||||
globalThis.addEventListener("focus", this.handleSessionCatalogPageActivation);
|
||||
}
|
||||
|
||||
disconnectSessionCatalogListeners(): void {
|
||||
document.removeEventListener(
|
||||
CATALOG_SESSION_CONTINUED_EVENT,
|
||||
this.handleCatalogSessionContinued as EventListener,
|
||||
);
|
||||
document.removeEventListener("visibilitychange", this.handleSessionCatalogPageActivation);
|
||||
globalThis.removeEventListener("focus", this.handleSessionCatalogPageActivation);
|
||||
}
|
||||
|
||||
retireSessionCatalogData(): void {
|
||||
this.sessionCatalogGeneration += 1;
|
||||
this.sessionCatalogLive.clear();
|
||||
}
|
||||
|
||||
resetSessionCatalogConnection(): void {
|
||||
this.sessionCatalogGeneration += 1;
|
||||
this.sessionCatalogRevision += 1;
|
||||
this.sessionCatalogLive.resetConnection();
|
||||
this.sessionCatalogs = [];
|
||||
this.loadingMoreSessionCatalogIds = new Set();
|
||||
this.sessionCatalogPageDepths.clear();
|
||||
this.sessionCatalogRevisions.clear();
|
||||
this.notify();
|
||||
}
|
||||
|
||||
updateSessionCatalogData(): void {
|
||||
if (this.context) {
|
||||
synchronizeSessionCatalogAgent(this, this.host.expandedAgentId());
|
||||
}
|
||||
if (
|
||||
!visibleSessionCatalogClient(this) ||
|
||||
this.sessionCatalogLive.timer ||
|
||||
this.sessionCatalogLive.requestGeneration === this.sessionCatalogGeneration
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void this.refreshSessionCatalogs();
|
||||
}
|
||||
|
||||
handleSessionCatalogHostEvent(payload: unknown): void {
|
||||
applySessionCatalogHostEventToData(this, payload);
|
||||
}
|
||||
|
||||
handleSessionCatalogPresence(payload: unknown): void {
|
||||
if (this.sessionCatalogLive.observePresence(payload)) {
|
||||
requestSessionCatalogDataRefresh(this);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly handleCatalogSessionContinued = (
|
||||
event: CustomEvent<CatalogSessionContinuedDetail>,
|
||||
) => {
|
||||
const detail = event.detail;
|
||||
if (!detail?.sessionKey) {
|
||||
return;
|
||||
}
|
||||
this.sessionCatalogs = bindAdoptedCatalogSession(this.sessionCatalogs, detail);
|
||||
this.notify();
|
||||
// Invalidate in-flight polls and load-more merges so a pre-adoption
|
||||
// snapshot cannot clobber the patched rows; the 30s poll reconfirms.
|
||||
this.sessionCatalogRevision += 1;
|
||||
this.sessionCatalogRevisions.set(
|
||||
detail.catalogId,
|
||||
(this.sessionCatalogRevisions.get(detail.catalogId) ?? 0) + 1,
|
||||
);
|
||||
};
|
||||
|
||||
private readonly handleSessionCatalogPageActivation = () => {
|
||||
if (document.visibilityState === "hidden") {
|
||||
this.sessionCatalogLive.cancelScheduledRefreshes();
|
||||
return;
|
||||
}
|
||||
this.sessionCatalogLive.scheduleActivation(() => requestSessionCatalogDataRefresh(this));
|
||||
};
|
||||
|
||||
refreshSessionCatalogs(): Promise<void> {
|
||||
return refreshSessionCatalogData(this);
|
||||
}
|
||||
|
||||
loadMoreSessionCatalog(catalogId: string): Promise<void> {
|
||||
return loadMoreSessionCatalogData(this, catalogId);
|
||||
}
|
||||
|
||||
private syncSessionsScrollObserver(): void {
|
||||
const element = this.host.querySelector(".sidebar-shell__body") as HTMLElement | null;
|
||||
if (element !== this.sessionsScrollElement) {
|
||||
this.sessionsScrollResizeObserver?.disconnect();
|
||||
this.sessionsScrollElement = element;
|
||||
@@ -182,7 +334,7 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
}
|
||||
|
||||
// One rAF-coalesced scroll read rides paint layout instead of flushing every update.
|
||||
private scheduleSessionsScrollStateSync() {
|
||||
private scheduleSessionsScrollStateSync(): void {
|
||||
if (this.sessionsScrollStateFrame !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -195,20 +347,11 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
});
|
||||
}
|
||||
|
||||
protected updateSessionsScrollState(element: HTMLElement) {
|
||||
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
||||
let nextState: SidebarSessionsScrollState = "none";
|
||||
if (maxScrollTop > 1) {
|
||||
if (element.scrollTop <= 1) {
|
||||
nextState = "top";
|
||||
} else if (element.scrollTop >= maxScrollTop - 1) {
|
||||
nextState = "bottom";
|
||||
} else {
|
||||
nextState = "middle";
|
||||
}
|
||||
}
|
||||
updateSessionsScrollState(element: HTMLElement): void {
|
||||
const nextState = resolveSidebarSessionsScrollState(element);
|
||||
if (nextState !== this.sessionsScrollState) {
|
||||
this.sessionsScrollState = nextState;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,9 +373,10 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
globalThis.clearTimeout(this.activeSessionLineageRetryTimer);
|
||||
this.activeSessionLineageRetryTimer = null;
|
||||
}
|
||||
this.notify();
|
||||
}
|
||||
const snapshot = sessions.state;
|
||||
if (this.sidebarSessionStatusFilter() !== "active") {
|
||||
if (this.host.sidebarSessionStatusFilter() !== "active") {
|
||||
return;
|
||||
}
|
||||
const gateway = this.context?.gateway;
|
||||
@@ -266,9 +410,10 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
}
|
||||
}
|
||||
this.sessionsLoading = snapshot.loading;
|
||||
this.notify();
|
||||
};
|
||||
|
||||
private synchronizeSessions(sessions: SessionCapability) {
|
||||
private synchronizeSessions(sessions: SessionCapability): void {
|
||||
if (sessions !== this.sessionsSource) {
|
||||
this.invalidateSessionMutations();
|
||||
this.clearSessionCache();
|
||||
@@ -278,13 +423,13 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
if (this.context?.gateway.snapshot.connected) {
|
||||
// Group catalog hydration is idempotent per connection.
|
||||
void sessions.groupsLoad();
|
||||
if (this.sidebarSessionStatusFilter() !== "active") {
|
||||
if (this.host.sidebarSessionStatusFilter() !== "active") {
|
||||
void this.refreshSidebarSessions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronizeGateway(gateway: ApplicationContext<RouteId>["gateway"]) {
|
||||
private synchronizeGateway(gateway: ApplicationContext<RouteId>["gateway"]): void {
|
||||
const client = gateway.snapshot.client;
|
||||
const connected = gateway.snapshot.connected;
|
||||
const clientChanged = client !== this.gatewayClient;
|
||||
@@ -305,17 +450,18 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
const presence = readPresenceEntries(gateway.snapshot.hello?.snapshot);
|
||||
this.presencePayload = presence ? { presence } : undefined;
|
||||
}
|
||||
this.notify();
|
||||
if (!sourceOrClientChanged) {
|
||||
return;
|
||||
}
|
||||
this.clearSessionCache();
|
||||
this.resetSessionCatalogConnection();
|
||||
if (connected && this.sessionsSource && this.sidebarSessionStatusFilter() !== "active") {
|
||||
if (connected && this.sessionsSource && this.host.sidebarSessionStatusFilter() !== "active") {
|
||||
void this.refreshSidebarSessions();
|
||||
}
|
||||
}
|
||||
|
||||
private clearSessionCache() {
|
||||
private clearSessionCache(): void {
|
||||
this.sidebarListRequestToken = null;
|
||||
this.childSessionGeneration += 1;
|
||||
this.childSessionCanonicalListRevision = null;
|
||||
@@ -337,14 +483,15 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
}
|
||||
this.sessionCreatedOrder.clear();
|
||||
this.visibleSessionLimit = SIDEBAR_SESSION_PAGE_SIZE;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
protected async refreshSidebarSessions(agentId = this.expandedAgentId()): Promise<void> {
|
||||
async refreshSidebarSessions(agentId = this.host.expandedAgentId()): Promise<void> {
|
||||
const sessions = this.context?.sessions;
|
||||
if (!sessions) {
|
||||
return;
|
||||
}
|
||||
const archivedFilter = this.sidebarSessionStatusFilter();
|
||||
const archivedFilter = this.host.sidebarSessionStatusFilter();
|
||||
const options = {
|
||||
agentId,
|
||||
archivedFilter,
|
||||
@@ -364,12 +511,13 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
const token = Symbol(agentId);
|
||||
this.sidebarListRequestToken = token;
|
||||
this.sessionsLoading = true;
|
||||
this.notify();
|
||||
try {
|
||||
const result = await sessions.list(options);
|
||||
if (
|
||||
token !== this.sidebarListRequestToken ||
|
||||
sessions !== this.context?.sessions ||
|
||||
archivedFilter !== this.sidebarSessionStatusFilter()
|
||||
archivedFilter !== this.host.sidebarSessionStatusFilter()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -383,18 +531,21 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
}
|
||||
}
|
||||
}
|
||||
this.notify();
|
||||
} catch (error) {
|
||||
if (token === this.sidebarListRequestToken) {
|
||||
this.sessionMutationError = String(error);
|
||||
this.notify();
|
||||
}
|
||||
} finally {
|
||||
if (token === this.sidebarListRequestToken) {
|
||||
this.sessionsLoading = false;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadChildSessions(parentKey: string): Promise<void> {
|
||||
async loadChildSessions(parentKey: string): Promise<void> {
|
||||
if (
|
||||
!parentKey ||
|
||||
this.loadedChildSessionKeys.has(parentKey) ||
|
||||
@@ -409,14 +560,11 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
}
|
||||
const generation = this.childSessionGeneration;
|
||||
this.loadingChildSessionKeys = new Set([...this.loadingChildSessionKeys, parentKey]);
|
||||
this.notify();
|
||||
try {
|
||||
const isCurrent = () =>
|
||||
generation === this.childSessionGeneration && sessions === this.context?.sessions;
|
||||
const rows = await fetchChildSessionRows({
|
||||
sessions,
|
||||
parentKey,
|
||||
isCurrent,
|
||||
});
|
||||
const rows = await fetchChildSessionRows({ sessions, parentKey, isCurrent });
|
||||
if (!rows || !isCurrent()) {
|
||||
return;
|
||||
}
|
||||
@@ -432,6 +580,7 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
failedKeys.delete(parentKey);
|
||||
this.failedChildSessionKeys = failedKeys;
|
||||
}
|
||||
this.notify();
|
||||
} catch {
|
||||
if (generation !== this.childSessionGeneration || sessions !== this.context?.sessions) {
|
||||
return;
|
||||
@@ -443,16 +592,18 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
[parentKey]: this.childSessionRowsByParent[parentKey] ?? [],
|
||||
};
|
||||
this.failedChildSessionKeys = new Set([...this.failedChildSessionKeys, parentKey]);
|
||||
this.notify();
|
||||
} finally {
|
||||
if (generation === this.childSessionGeneration && sessions === this.context?.sessions) {
|
||||
const next = new Set(this.loadingChildSessionKeys);
|
||||
next.delete(parentKey);
|
||||
this.loadingChildSessionKeys = next;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadActiveSessionLineage(sessionKey: string): Promise<void> {
|
||||
async loadActiveSessionLineage(sessionKey: string): Promise<void> {
|
||||
const normalizedKey = sessionKey.trim();
|
||||
if (normalizedKey !== this.activeSessionLineageRouteKey) {
|
||||
this.activeSessionLineageRouteKey = normalizedKey;
|
||||
@@ -463,6 +614,7 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
globalThis.clearTimeout(this.activeSessionLineageRetryTimer);
|
||||
this.activeSessionLineageRetryTimer = null;
|
||||
}
|
||||
this.notify();
|
||||
}
|
||||
const gateway = this.context?.gateway;
|
||||
const client = gateway?.snapshot.client;
|
||||
@@ -503,12 +655,13 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
lineage.rowsByParent,
|
||||
);
|
||||
this.activeSessionLineageRoot = lineage.topmostRow;
|
||||
this.notify();
|
||||
this.activeSessionLineageRequestToken = null;
|
||||
if (lineage.lookupFailed) {
|
||||
this.activeSessionLineageRetryTimer = globalThis.setTimeout(() => {
|
||||
this.activeSessionLineageRetryTimer = null;
|
||||
if (this.activeSessionLineageRouteKey === normalizedKey) {
|
||||
this.requestUpdate();
|
||||
this.notify();
|
||||
}
|
||||
}, 5_000);
|
||||
return;
|
||||
@@ -516,14 +669,61 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
this.activeSessionLineageLoaded = true;
|
||||
}
|
||||
|
||||
private invalidateSessionMutations() {
|
||||
this.sessionMutationEpoch += 1;
|
||||
this.sessionMutationError = null;
|
||||
setVisibleSessionLimit(limit: number): void {
|
||||
this.visibleSessionLimit = limit;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
protected beginSessionMutation(): SidebarSessionMutationScope | null {
|
||||
dismissSessionMutationError(): void {
|
||||
this.sessionMutationError = null;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
resetForStatusFilter(statusFilter: SidebarSessionStatusFilter): void {
|
||||
this.visibleSessionLimit = SIDEBAR_SESSION_PAGE_SIZE;
|
||||
this.childSessionRowsByParent = {};
|
||||
this.loadedChildSessionKeys = new Set();
|
||||
this.failedChildSessionKeys = new Set();
|
||||
this.loadingChildSessionKeys = new Set();
|
||||
this.sessionRowsByAgent = {};
|
||||
if (statusFilter === "active" && this.context) {
|
||||
this.sessionsResult = this.context.sessions.state.result;
|
||||
this.sessionsAgentId = this.context.sessions.state.agentId;
|
||||
}
|
||||
this.notify();
|
||||
}
|
||||
|
||||
discardEmptyChildSessionSnapshot(sessionKey: string): void {
|
||||
if (this.childSessionRowsByParent[sessionKey]?.length === 0) {
|
||||
const childRows = { ...this.childSessionRowsByParent };
|
||||
delete childRows[sessionKey];
|
||||
this.childSessionRowsByParent = childRows;
|
||||
const loadedKeys = new Set(this.loadedChildSessionKeys);
|
||||
loadedKeys.delete(sessionKey);
|
||||
this.loadedChildSessionKeys = loadedKeys;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
retryChildSessions(sessionKey: string): void {
|
||||
if (this.failedChildSessionKeys.has(sessionKey)) {
|
||||
const failedKeys = new Set(this.failedChildSessionKeys);
|
||||
failedKeys.delete(sessionKey);
|
||||
this.failedChildSessionKeys = failedKeys;
|
||||
this.notify();
|
||||
}
|
||||
void this.loadChildSessions(sessionKey);
|
||||
}
|
||||
|
||||
private invalidateSessionMutations(): void {
|
||||
this.sessionMutationEpoch += 1;
|
||||
this.sessionMutationError = null;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
beginSessionMutation(): SidebarSessionMutationScope | null {
|
||||
const context = this.context;
|
||||
if (!context || !this.connected) {
|
||||
if (!context || !this.host.connected) {
|
||||
return null;
|
||||
}
|
||||
const gateway = context.gateway;
|
||||
@@ -532,21 +732,22 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
return null;
|
||||
}
|
||||
this.sessionMutationError = null;
|
||||
this.notify();
|
||||
return {
|
||||
epoch: this.sessionMutationEpoch,
|
||||
context,
|
||||
gateway,
|
||||
sessions: context.sessions,
|
||||
client,
|
||||
selectedAgentId: this.selectedAgentIdForSessions(),
|
||||
selectedAgentId: this.host.selectedAgentIdForSessions(),
|
||||
};
|
||||
}
|
||||
|
||||
protected isSessionMutationScopeCurrent(scope: SidebarSessionMutationScope): boolean {
|
||||
isSessionMutationScopeCurrent(scope: SidebarSessionMutationScope): boolean {
|
||||
const context = this.context;
|
||||
const gateway = context?.gateway;
|
||||
return (
|
||||
this.connected &&
|
||||
this.host.connected &&
|
||||
this.sessionMutationEpoch === scope.epoch &&
|
||||
context === scope.context &&
|
||||
gateway === scope.gateway &&
|
||||
@@ -556,9 +757,10 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarSessionCata
|
||||
);
|
||||
}
|
||||
|
||||
protected publishSessionMutationError(scope: SidebarSessionMutationScope, error: unknown) {
|
||||
publishSessionMutationError(scope: SidebarSessionMutationScope, error: unknown): void {
|
||||
if (this.isSessionMutationScopeCurrent(scope)) {
|
||||
this.sessionMutationError = String(error);
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
import type {
|
||||
SessionCatalog,
|
||||
SessionCatalogPullRequestSummary,
|
||||
SessionsCatalogListResult,
|
||||
} from "../../../packages/gateway-protocol/src/index.ts";
|
||||
@@ -19,6 +18,7 @@ import type {
|
||||
SidebarWorkboardBoard,
|
||||
SidebarWorkboardRenderers,
|
||||
} from "../components/app-sidebar-workboard.ts";
|
||||
import type { SessionDataController } from "../components/session-data-controller.ts";
|
||||
import type { SessionCapability } from "../lib/sessions/index.ts";
|
||||
import { createApplicationContextProvider } from "./application-context.ts";
|
||||
import { createStorageMock } from "./storage.ts";
|
||||
@@ -31,44 +31,69 @@ export type SessionGroupMutationResult = Awaited<ReturnType<SessionCapability["g
|
||||
type SessionDeleteResult = Awaited<ReturnType<SessionCapability["delete"]>>;
|
||||
type SessionState = SessionCapability["state"];
|
||||
|
||||
export type SidebarLifecycleState = HTMLElement & {
|
||||
activeRouteId?: string;
|
||||
activeWorkboardBoardId: string;
|
||||
enabledRouteIds?: readonly NavigationRouteId[];
|
||||
connected: boolean;
|
||||
offline: boolean;
|
||||
outboxCountForSession: (sessionKey: string) => number;
|
||||
queuedOutboxCount: number;
|
||||
lastError: string | null;
|
||||
terminalAvailable: boolean;
|
||||
catalogOpenTarget: "viewer" | "terminal";
|
||||
canPairDevice: boolean;
|
||||
sidebarEntries: readonly string[];
|
||||
workboardBoards: readonly SidebarWorkboardBoard[];
|
||||
workboardBoardsReady: boolean;
|
||||
workboardRenderers?: SidebarWorkboardRenderers;
|
||||
sidebarLiveActivity: boolean;
|
||||
onUpdateSidebarEntries?: (entries: string[]) => void;
|
||||
pinnedAgentIds: readonly string[];
|
||||
sessionKey: string;
|
||||
onNavigate: (
|
||||
routeId: string,
|
||||
options?: { pathname?: string; search?: string; hash?: string },
|
||||
) => void;
|
||||
sessionCatalogs: SessionCatalog[];
|
||||
sessionRowsByAgent: Record<string, SessionsListResult["sessions"]>;
|
||||
sessionCreatedOrder: Map<string, number>;
|
||||
sessionsAgentId: string | null;
|
||||
sessionsResult: SessionsListResult | null;
|
||||
requestUpdate: () => void;
|
||||
updateComplete: Promise<boolean>;
|
||||
updateAvailable: { currentVersion: string; latestVersion: string; channel: string } | null;
|
||||
updateRunning: boolean;
|
||||
onUpdate: () => void;
|
||||
onRetryConnect?: () => void;
|
||||
onOpenNewSession?: (agentId: string, target?: { catalogId: string }) => void;
|
||||
variant: "panel" | "drawer";
|
||||
};
|
||||
type SidebarTestSessionDataKey =
|
||||
| "sessionCatalogs"
|
||||
| "sessionCreatedOrder"
|
||||
| "sessionRowsByAgent"
|
||||
| "sessionsAgentId"
|
||||
| "sessionsResult";
|
||||
|
||||
export type SidebarLifecycleState = HTMLElement &
|
||||
Pick<SessionDataController, SidebarTestSessionDataKey> & {
|
||||
activeRouteId?: string;
|
||||
activeWorkboardBoardId: string;
|
||||
enabledRouteIds?: readonly NavigationRouteId[];
|
||||
connected: boolean;
|
||||
offline: boolean;
|
||||
outboxCountForSession: (sessionKey: string) => number;
|
||||
queuedOutboxCount: number;
|
||||
lastError: string | null;
|
||||
terminalAvailable: boolean;
|
||||
catalogOpenTarget: "viewer" | "terminal";
|
||||
canPairDevice: boolean;
|
||||
sidebarEntries: readonly string[];
|
||||
workboardBoards: readonly SidebarWorkboardBoard[];
|
||||
workboardBoardsReady: boolean;
|
||||
workboardRenderers?: SidebarWorkboardRenderers;
|
||||
sidebarLiveActivity: boolean;
|
||||
onUpdateSidebarEntries?: (entries: string[]) => void;
|
||||
pinnedAgentIds: readonly string[];
|
||||
sessionKey: string;
|
||||
onNavigate: (
|
||||
routeId: string,
|
||||
options?: { pathname?: string; search?: string; hash?: string },
|
||||
) => void;
|
||||
readonly sessionData: SessionDataController;
|
||||
requestUpdate: () => void;
|
||||
updateComplete: Promise<boolean>;
|
||||
updateAvailable: { currentVersion: string; latestVersion: string; channel: string } | null;
|
||||
updateRunning: boolean;
|
||||
onUpdate: () => void;
|
||||
onRetryConnect?: () => void;
|
||||
onOpenNewSession?: (agentId: string, target?: { catalogId: string }) => void;
|
||||
variant: "panel" | "drawer";
|
||||
};
|
||||
|
||||
const sidebarTestSessionDataKeys: readonly SidebarTestSessionDataKey[] = [
|
||||
"sessionCatalogs",
|
||||
"sessionCreatedOrder",
|
||||
"sessionRowsByAgent",
|
||||
"sessionsAgentId",
|
||||
"sessionsResult",
|
||||
];
|
||||
|
||||
function exposeSessionDataForSidebarTests(sidebar: SidebarLifecycleState): void {
|
||||
for (const key of sidebarTestSessionDataKeys) {
|
||||
Object.defineProperty(sidebar, key, {
|
||||
configurable: true,
|
||||
get: () => sidebar.sessionData[key],
|
||||
set: (value: SessionDataController[SidebarTestSessionDataKey]) => {
|
||||
Reflect.set(sidebar.sessionData, key, value);
|
||||
sidebar.sessionData.requestSessionDataUpdate();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type LobsterPetElement = HTMLElement & {
|
||||
runOutcome: "ok" | "error" | "aborted";
|
||||
@@ -329,6 +354,7 @@ export async function mountSidebar(
|
||||
const sidebar = document.createElement(
|
||||
"openclaw-app-sidebar",
|
||||
) as unknown as SidebarLifecycleState;
|
||||
exposeSessionDataForSidebarTests(sidebar);
|
||||
sidebar.variant = variant;
|
||||
provider.append(sidebar);
|
||||
document.body.append(provider);
|
||||
|
||||
Reference in New Issue
Block a user