Simplify project filter to single selection and persist sidebar order

This commit is contained in:
Mohamed Boudra
2026-03-01 11:26:56 +07:00
parent 3a6a8cfa38
commit 2566171b4c
4 changed files with 386 additions and 306 deletions
@@ -6,13 +6,14 @@ import {
} from "@/runtime/host-runtime";
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
import {
deriveSidebarStateBucket,
isSidebarActiveAgent,
} from "@/utils/sidebar-agent-state";
import type { ProjectPlacementPayload } from "@server/shared/messages";
import { resolveProjectPlacement } from "@/utils/project-placement";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
const SIDEBAR_DONE_FILL_TARGET = 50;
const EMPTY_ORDER: string[] = [];
export interface SidebarProjectFilterOption {
projectKey: string;
@@ -38,25 +39,23 @@ export interface SidebarAgentsListResult {
refreshAll: () => void;
}
function compareByLastActivityDesc(
function compareByCreatedAtDesc(
left: SidebarAgentListEntry,
right: SidebarAgentListEntry
): number {
return right.agent.lastActivityAt.getTime() - left.agent.lastActivityAt.getTime();
}
const createdDelta = right.agent.createdAt.getTime() - left.agent.createdAt.getTime();
if (createdDelta !== 0) {
return createdDelta;
}
function compareByTitleAsc(
left: SidebarAgentListEntry,
right: SidebarAgentListEntry
): number {
const leftTitle = (left.agent.title?.trim() || "New agent").toLocaleLowerCase();
const rightTitle = (right.agent.title?.trim() || "New agent").toLocaleLowerCase();
const titleCmp = leftTitle.localeCompare(rightTitle, undefined, {
const titleDelta = leftTitle.localeCompare(rightTitle, undefined, {
numeric: true,
sensitivity: "base",
});
if (titleCmp !== 0) {
return titleCmp;
if (titleDelta !== 0) {
return titleDelta;
}
return left.agent.id.localeCompare(right.agent.id, undefined, {
@@ -65,50 +64,66 @@ function compareByTitleAsc(
});
}
function applySidebarDefaultOrdering(
entries: SidebarAgentListEntry[]
): { entries: SidebarAgentListEntry[]; hasMore: boolean } {
const needsInput: SidebarAgentListEntry[] = [];
const failed: SidebarAgentListEntry[] = [];
const running: SidebarAgentListEntry[] = [];
const attention: SidebarAgentListEntry[] = [];
const done: SidebarAgentListEntry[] = [];
function toSidebarAgentKey(entry: SidebarAgentListEntry): string {
return `${entry.agent.serverId}:${entry.agent.id}`;
}
for (const entry of entries) {
const bucket = deriveSidebarStateBucket({
function applySidebarUserOrdering(input: {
entries: SidebarAgentListEntry[];
order: string[];
}): { entries: SidebarAgentListEntry[]; hasMore: boolean } {
const entryKeySet = new Set(input.entries.map((entry) => toSidebarAgentKey(entry)));
const prunedOrder = input.order.filter((key) => entryKeySet.has(key));
const knownOrderSet = new Set(prunedOrder);
const newEntries = input.entries
.filter((entry) => !knownOrderSet.has(toSidebarAgentKey(entry)))
.sort(compareByCreatedAtDesc);
const effectiveOrder = [
...newEntries.map((entry) => toSidebarAgentKey(entry)),
...prunedOrder,
];
const orderIndexByKey = new Map<string, number>();
for (let index = 0; index < effectiveOrder.length; index += 1) {
orderIndexByKey.set(effectiveOrder[index] ?? "", index);
}
const sorted = [...input.entries].sort((left, right) => {
const leftOrder = orderIndexByKey.get(toSidebarAgentKey(left));
const rightOrder = orderIndexByKey.get(toSidebarAgentKey(right));
if (leftOrder === undefined && rightOrder === undefined) {
return compareByCreatedAtDesc(left, right);
}
if (leftOrder === undefined) {
return -1;
}
if (rightOrder === undefined) {
return 1;
}
return leftOrder - rightOrder;
});
const active: SidebarAgentListEntry[] = [];
const done: SidebarAgentListEntry[] = [];
for (const entry of sorted) {
const isActive = isSidebarActiveAgent({
status: entry.agent.status,
pendingPermissionCount: entry.agent.pendingPermissionCount,
requiresAttention: entry.agent.requiresAttention,
attentionReason: entry.agent.attentionReason,
});
if (bucket === "needs_input") {
needsInput.push(entry);
continue;
}
if (bucket === "failed") {
failed.push(entry);
continue;
}
if (bucket === "running") {
running.push(entry);
continue;
}
if (bucket === "attention") {
attention.push(entry);
if (isActive) {
active.push(entry);
continue;
}
done.push(entry);
}
needsInput.sort(compareByLastActivityDesc);
failed.sort(compareByLastActivityDesc);
running.sort(compareByTitleAsc);
attention.sort(compareByLastActivityDesc);
done.sort(compareByLastActivityDesc);
const active = [...needsInput, ...failed, ...running, ...attention];
if (active.length >= SIDEBAR_DONE_FILL_TARGET) {
return { entries: active, hasMore: done.length > 0 };
return {
entries: active,
hasMore: done.length > 0,
};
}
const remainingDoneSlots = SIDEBAR_DONE_FILL_TARGET - active.length;
@@ -147,7 +162,7 @@ function toAggregatedAgent(params: {
export function useSidebarAgentsList(options?: {
serverId?: string | null;
selectedProjectFilterKeys?: string[];
selectedProjectFilterKey?: string | null;
}): SidebarAgentsListResult {
const { daemons } = useDaemonRegistry();
const runtime = getHostRuntimeStore();
@@ -171,14 +186,16 @@ export function useSidebarAgentsList(options?: {
return daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
}, [daemonLabelSignature, serverId]);
const selectedProjectFilterKeys = useMemo(
() =>
new Set(
(options?.selectedProjectFilterKeys ?? [])
.map((item) => item.trim())
.filter((item) => item.length > 0)
),
[options?.selectedProjectFilterKeys]
const selectedProjectFilterKey = useMemo(() => {
const value = options?.selectedProjectFilterKey;
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}, [options?.selectedProjectFilterKey]);
const persistedOrder = useSidebarOrderStore((state) =>
serverId ? state.orderByServerId[serverId] ?? EMPTY_ORDER : EMPTY_ORDER
);
const isActive = Boolean(serverId);
@@ -213,7 +230,12 @@ export function useSidebarAgentsList(options?: {
const [connectionStatus = "idle", directoryStatus = "idle"] =
runtimeStatusSignature.split(":", 2);
const { entries, projectFilterOptions, hasAnyData, hasMoreEntries } = useMemo(() => {
const {
entries,
projectFilterOptions,
hasAnyData,
hasMoreEntries,
} = useMemo(() => {
if (!isActive || !serverId || !liveAgents) {
return {
entries: [] as SidebarAgentListEntry[],
@@ -282,14 +304,15 @@ export function useSidebarAgentsList(options?: {
pushEntry({ agent, project });
}
const filteredEntries =
selectedProjectFilterKeys.size > 0
? mergedEntries.filter((entry) =>
selectedProjectFilterKeys.has(entry.project.projectKey)
)
: mergedEntries;
const ordered = applySidebarDefaultOrdering(filteredEntries);
const filteredEntries = selectedProjectFilterKey
? mergedEntries.filter(
(entry) => entry.project.projectKey === selectedProjectFilterKey
)
: mergedEntries;
const ordered = applySidebarUserOrdering({
entries: filteredEntries,
order: persistedOrder,
});
const options = Array.from(byProject.values()).sort((left, right) => {
if (left.activeCount !== right.activeCount) {
return right.activeCount - left.activeCount;
@@ -297,14 +320,21 @@ export function useSidebarAgentsList(options?: {
return left.projectName.localeCompare(right.projectName);
});
const result = {
return {
entries: ordered.entries,
projectFilterOptions: options,
hasAnyData: ordered.entries.length > 0,
hasAnyData: mergedEntries.length > 0,
hasMoreEntries: ordered.hasMore,
};
return result;
}, [agentLastActivity, isActive, liveAgents, selectedProjectFilterKeys, serverId, serverLabel]);
}, [
agentLastActivity,
isActive,
liveAgents,
persistedOrder,
selectedProjectFilterKey,
serverId,
serverLabel,
]);
const refreshAll = useCallback(() => {
if (!isActive || !serverId || connectionStatus !== "online") {