mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-10-19 15:33:54 +00:00
Persist sidebar project, status, and activity filters
The sidebar saved View and Sort by, but kept Project, Status, and Last activity only in memory. Reloading reset those three controls to their defaults. Store the filters in the existing Sidebar Folders configuration and reuse its save/load API. Preserve the empty No project value, apply safe defaults to missing or malformed settings, and keep the menu open when changing filters. Changing view or sort retains the selected filters. Update defaults, documentation, and frontend regression coverage for saving, reloading, fallback values, and menu behavior. Validation: frontend checks and seven focused Python tests passed. Live native dropdown checks confirmed named-project and No-project selections survive reloads, including after changing view and sort. Original runtime settings were restored afterward.
This commit is contained in:
parent
f4700b4256
commit
50832e0229
6 changed files with 64 additions and 28 deletions
|
|
@ -29,7 +29,7 @@
|
|||
- Keep parallel children nested and selectable. Move visible descendants and background workers with their root; retain parent IDs, history, and selection. Reject running families and scheduler-owned contexts before any mutation.
|
||||
- Use existing project activation/deactivation helpers, persistence, and state invalidation. Restore project/profile state if a family move fails.
|
||||
- Scheduler tasks retain their configured project; dragging may reorder them within their folder or within the shared pinned section.
|
||||
- Closed project folders mount their rows only when expanded. Expansion is browser-local, while view, sort, and order survive server/browser reloads.
|
||||
- Closed project folders mount their rows only when expanded. Expansion is browser-local, while view, sort, filters, and order survive server/browser reloads. The empty project-filter key means **No project** and must not be replaced with the all-projects default.
|
||||
- Folder rows use compact spacing tokens; thread lists have no vertical guide border and reuse the smaller core thread dots. Dim empty-folder labels and give their rows the same height as a chat row, preserving space before the next folder.
|
||||
- Extend list rows `--spacing-xs` beyond the header controls, with the same inset after their last action button. Keep header positions fixed so folder, chat, and worker action columns align in both views.
|
||||
- Folder actions reuse the standard chat action buttons with a subtle border at rest. Show them on pointer hover, keyboard focus, or while the folder menu is open; touch devices also show them for expanded folders.
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
## Work Guidance
|
||||
|
||||
- Reuse project creation/edit/file dialogs, notifications, native drag events, and Material `x-icon` elements.
|
||||
- View and sort preferences are saved through the plugin configuration API from the sidebar menu. This plugin has no separate configuration page.
|
||||
- View, sort, and Project/Status/Last activity filters share the plugin configuration API and save automatically from the sidebar menu. Missing or invalid filters fall back to all projects/statuses/times. This plugin has no separate configuration page.
|
||||
- Project moves and ordering use drag and drop, without move actions in thread menus. Folder-row creation uses the same `chat_add_on` icon as the New chat menu action.
|
||||
- Header, folder, chat, worker, and task action buttons share hover/focus treatment. The two Chats header icons use `1.125rem` inside the existing button frames, preserving action-column alignment with the smaller row icons. Header borders appear only on hover; row buttons retain their resting borders. Use `--spacing-xs` between buttons and at row ends. Keep these sidebar-scoped overrides in the plugin's header extension and retain row visibility behavior and confirmation states.
|
||||
- The options menu contains only View (Folders / Flat mode), Sort by, Project, Status, and Last activity. Keep native dropdowns consistent with Settings: muted borders, the small radius token, and an opaque menu-colored surface so native option lists remain readable. Use `--spacing-xs` gaps between fields and `--spacing-sm` outer padding. Show focus outlines for keyboard navigation, without a lingering pointer-click ring. Folder expansion belongs to individual folder rows.
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ Organize chats and tasks into project folders. Folder view is on by default; use
|
|||
- Scheduler tasks have their own pinned section and project folders. Their configured project stays unchanged when reordered.
|
||||
- Chats and tasks default to **Created time**, newest first, so new messages do not change their position. **Last active**, name, and manual order are also available. Filter by project, **Working** / **Idle** status, or recent activity. Idle includes waiting or completed chats, paused chats, and disabled tasks. A working parallel child keeps its family under Working.
|
||||
|
||||
View and sort settings, project pins, and manual order are saved for the instance. Folder expansion is saved in the browser; filters apply to the current page. Disabling this plugin restores the standard sidebar.
|
||||
View, sort, Project, Status, and Last activity choices save automatically for the instance and survive reloads, along with project pins and manual order. Folder expansion is saved in the browser. Disabling this plugin restores the standard sidebar.
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
folder_view: true
|
||||
sort_by: created
|
||||
project_filter: '*'
|
||||
status_filter: all
|
||||
activity_filter: all
|
||||
|
|
|
|||
|
|
@ -32,21 +32,24 @@
|
|||
</select>
|
||||
</label>
|
||||
<label class="sidebar-option-field"><span>Project</span>
|
||||
<select x-model="$store.sidebarFolders.projectFilter">
|
||||
<select :value="$store.sidebarFolders.config.project_filter" :disabled="$store.sidebarFolders.busy"
|
||||
@change="$store.sidebarFolders.setConfig({ project_filter: $event.target.value })">
|
||||
<option value="*">All projects</option><option value="">No project</option>
|
||||
<template x-for="project in $store.projects.projectList" :key="project.name">
|
||||
<option :value="project.name" x-text="project.title || project.name"></option>
|
||||
<option :value="project.name" :selected="project.name === $store.sidebarFolders.config.project_filter" x-text="project.title || project.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</label>
|
||||
<label class="sidebar-option-field"><span>Status</span>
|
||||
<select x-model="$store.sidebarFolders.statusFilter">
|
||||
<select :value="$store.sidebarFolders.config.status_filter" :disabled="$store.sidebarFolders.busy"
|
||||
@change="$store.sidebarFolders.setConfig({ status_filter: $event.target.value })">
|
||||
<option value="all">All</option><option value="running">Working</option>
|
||||
<option value="idle">Idle</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="sidebar-option-field"><span>Last activity</span>
|
||||
<select x-model="$store.sidebarFolders.activityFilter">
|
||||
<select :value="$store.sidebarFolders.config.activity_filter" :disabled="$store.sidebarFolders.busy"
|
||||
@change="$store.sidebarFolders.setConfig({ activity_filter: $event.target.value })">
|
||||
<option value="all">Any time</option><option value="1">24 hours</option><option value="3">3 days</option>
|
||||
<option value="7">7 days</option><option value="30">30 days</option>
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -63,26 +63,26 @@ await store.loadConfig();
|
|||
assert.equal(store.config.sort_by, "created", "missing sort preferences fall back to creation order");
|
||||
assert.equal(store.rootChat("child").id, "a");
|
||||
assert.deepEqual(store.family(contexts[0], "chat").map((row) => row.id), ["a", "child"]);
|
||||
store.statusFilter = "running";
|
||||
store.config.status_filter = "running";
|
||||
assert.deepEqual(chats.topLevelContexts().map((row) => row.id), ["a"], "working children keep their parent visible");
|
||||
assert.deepEqual(store.groups("task"), [], "parallel children never become scheduler tasks");
|
||||
store.statusFilter = "idle";
|
||||
store.config.status_filter = "idle";
|
||||
assert.deepEqual(chats.topLevelContexts().map((row) => row.id), ["b", "c"], "a working descendant keeps its entire family out of Idle");
|
||||
contexts[3].paused = true;
|
||||
tasks.tasks[0].state = "disabled";
|
||||
assert.deepEqual(chats.topLevelContexts().map((row) => row.id), ["b", "c", "a"], "paused and inactive chats are Idle");
|
||||
assert.equal(store.groups("task")[0].rows[0].id, "task", "disabled tasks use the same Idle filter");
|
||||
store.statusFilter = "running";
|
||||
store.config.status_filter = "running";
|
||||
assert.deepEqual(chats.topLevelContexts(), [], "a paused worker is not Working even while its execution task is alive");
|
||||
assert.deepEqual(store.groups("task"), [], "disabled tasks are not Working");
|
||||
tasks.tasks[0].state = "idle";
|
||||
tasks.tasks[0].running = true;
|
||||
assert.equal(store.groups("task")[0].rows[0].id, "task", "running tasks use the same Working filter");
|
||||
store.statusFilter = "idle";
|
||||
store.config.status_filter = "idle";
|
||||
assert.deepEqual(store.groups("task"), [], "working tasks do not also appear under Idle");
|
||||
tasks.tasks[0].running = false;
|
||||
assert.equal(store.groups("task")[0].rows[0].id, "task", "waiting tasks also count as Idle");
|
||||
store.statusFilter = "all";
|
||||
store.config.status_filter = "all";
|
||||
store.config.sort_by = "name";
|
||||
assert.deepEqual(store.groups("chat")[0].rows.map((row) => row.id), ["b", "a"]);
|
||||
pins.chat.add("a");
|
||||
|
|
@ -321,10 +321,10 @@ await store.drop(event, "task", null, tasks.tasks[2]);
|
|||
assert.deepEqual(store.pinnedRows("task").map((row) => row.id), ["task3", "task"]);
|
||||
assert.equal(tasks.tasks[0].project.name, "alpha", "global task pin ordering retains configured projects");
|
||||
assert.equal(store.groups("task").flatMap((group) => group.rows).some((row) => pins.task.has(row.id)), false);
|
||||
store.projectFilter = "beta";
|
||||
store.config.project_filter = "beta";
|
||||
assert.deepEqual(store.pinnedRows("chat").map((row) => row.id), ["b"], "global pins still respect project filters");
|
||||
pins.chat.clear();
|
||||
store.projectFilter = "beta";
|
||||
store.config.project_filter = "beta";
|
||||
assert.deepEqual(store.groups("chat").map((g) => g.id), ["beta"]);
|
||||
assert.ok(apiCalls.some(([url]) => url.endsWith("/layout")));
|
||||
const sidebar = globalThis.__foldersTest.sidebar;
|
||||
|
|
@ -380,4 +380,34 @@ store.menuPointer(leave);
|
|||
store.dismissMenus();
|
||||
await settleMenu();
|
||||
assert.equal(store.menu, "", "component cleanup cancels pending dismissal");
|
||||
console.log("Folder view grouping, sorting, pins, movement, selection, drop, and menu-scope checks passed.");
|
||||
let savedConfig = {};
|
||||
globalThis.__folderApi = async (_url, data) => {
|
||||
if (data.action === "save_config") savedConfig = JSON.parse(JSON.stringify(data.settings));
|
||||
return { ok: true, data: JSON.parse(JSON.stringify(savedConfig)) };
|
||||
};
|
||||
for (const filters of [
|
||||
{ project_filter: "alpha", status_filter: "idle", activity_filter: "7" },
|
||||
{ project_filter: "", status_filter: "running", activity_filter: "30" },
|
||||
]) {
|
||||
store.menu = "options";
|
||||
await store.setConfig(filters);
|
||||
assert.equal(store.menu, "options", "saving a filter keeps the other filters accessible");
|
||||
await store.setConfig({ folder_view: false, sort_by: "recent" });
|
||||
assert.equal(store.menu, "", "view/sort changes retain their existing dismissal behavior");
|
||||
store.config = {};
|
||||
await store.loadConfig();
|
||||
for (const [key, value] of Object.entries(filters)) assert.equal(store.config[key], value, `${key} survives saving view/sort and reloading`);
|
||||
assert.equal(store.config.folder_view, false);
|
||||
assert.equal(store.config.sort_by, "recent");
|
||||
}
|
||||
savedConfig = { project_filter: null, status_filter: "invalid", activity_filter: "-1" };
|
||||
await store.loadConfig();
|
||||
assert.equal(store.config.project_filter, "*");
|
||||
assert.equal(store.config.status_filter, "all");
|
||||
assert.equal(store.config.activity_filter, "all");
|
||||
savedConfig = {};
|
||||
await store.loadConfig();
|
||||
assert.equal(store.config.project_filter, "*");
|
||||
assert.equal(store.config.status_filter, "all");
|
||||
assert.equal(store.config.activity_filter, "all");
|
||||
console.log("Folder view grouping, sorting, pins, movement, selection, drop, menu-scope, and preference checks passed.");
|
||||
|
|
|
|||
|
|
@ -16,13 +16,10 @@ function timestamp(value) {
|
|||
}
|
||||
|
||||
export const store = createStore("sidebarFolders", {
|
||||
config: { folder_view: true, sort_by: "created" },
|
||||
config: { folder_view: true, sort_by: "created", project_filter: "*", status_filter: "all", activity_filter: "all" },
|
||||
order: { project: [], chat: [], task: [] },
|
||||
collapsed: {},
|
||||
lastSelection: {},
|
||||
projectFilter: "*",
|
||||
statusFilter: "all",
|
||||
activityFilter: "all",
|
||||
menu: "",
|
||||
menuProject: "",
|
||||
menuStyle: {},
|
||||
|
|
@ -62,6 +59,9 @@ export const store = createStore("sidebarFolders", {
|
|||
this.config = {
|
||||
folder_view: result.data?.folder_view !== false,
|
||||
sort_by: SORTS.includes(result.data?.sort_by) ? result.data.sort_by : "created",
|
||||
project_filter: typeof result.data?.project_filter === "string" ? result.data.project_filter : "*",
|
||||
status_filter: ["all", "running", "idle"].includes(result.data?.status_filter) ? result.data.status_filter : "all",
|
||||
activity_filter: ["all", "1", "3", "7", "30"].includes(result.data?.activity_filter) ? result.data.activity_filter : "all",
|
||||
};
|
||||
} catch (error) {
|
||||
this.report(error);
|
||||
|
|
@ -87,7 +87,7 @@ export const store = createStore("sidebarFolders", {
|
|||
});
|
||||
if (!result.ok) throw new Error(result.error || "Could not save folder settings");
|
||||
this.config = config;
|
||||
this.closeMenu();
|
||||
if ("folder_view" in change || "sort_by" in change) this.closeMenu();
|
||||
} catch (error) {
|
||||
this.report(error);
|
||||
} finally {
|
||||
|
|
@ -127,15 +127,15 @@ export const store = createStore("sidebarFolders", {
|
|||
},
|
||||
|
||||
matches(row, kind) {
|
||||
if (this.projectFilter !== "*" && (row.project?.name || "") !== this.projectFilter) return false;
|
||||
if (this.statusFilter === "all" && this.activityFilter === "all") return true;
|
||||
if (this.config.project_filter !== "*" && (row.project?.name || "") !== this.config.project_filter) return false;
|
||||
if (this.config.status_filter === "all" && this.config.activity_filter === "all") return true;
|
||||
const family = this.family(row, kind);
|
||||
const working = family.some((item) => item.running && !item.paused && item.state !== "disabled");
|
||||
if (this.statusFilter === "running" && !working) return false;
|
||||
if (this.statusFilter === "idle" && working) return false;
|
||||
if (this.config.status_filter === "running" && !working) return false;
|
||||
if (this.config.status_filter === "idle" && working) return false;
|
||||
return family.some((item) => {
|
||||
if (this.activityFilter !== "all") {
|
||||
const cutoff = Date.now() - Number(this.activityFilter) * 86400000;
|
||||
if (this.config.activity_filter !== "all") {
|
||||
const cutoff = Date.now() - Number(this.config.activity_filter) * 86400000;
|
||||
if (timestamp(item.last_message || item.created_at) < cutoff) return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -188,8 +188,8 @@ export const store = createStore("sidebarFolders", {
|
|||
}
|
||||
const rank = new Map(this.order.project.map((id, index) => [id, index]));
|
||||
return [...groups.values()].filter((group) => {
|
||||
if (this.projectFilter !== "*" && group.id !== this.projectFilter) return false;
|
||||
return group.rows.length || (kind === "chat" && this.statusFilter === "all" && this.activityFilter === "all");
|
||||
if (this.config.project_filter !== "*" && group.id !== this.config.project_filter) return false;
|
||||
return group.rows.length || (kind === "chat" && this.config.status_filter === "all" && this.config.activity_filter === "all");
|
||||
}).sort((a, b) => {
|
||||
const aPinned = pins.isProjectPinned(a.id);
|
||||
const bPinned = pins.isProjectPinned(b.id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue