refactor(app): unified panel state machine for mobile sidebars

Replace separate sidebar stores with a unified panel store that uses a
discriminated union state machine on mobile. This makes it impossible
for both the agent list and file explorer sidebars to be open at the
same time on mobile.

Mobile state machine (mobileView):
- 'agent': Main agent view (no overlay panel)
- 'agent-list': Agent list sidebar (left overlay)
- 'file-explorer': File explorer sidebar (right overlay)

Desktop retains independent boolean toggles since sidebars sit
alongside content rather than overlaying it.

Key changes:
- Created stores/panel-store.ts with unified state
- Deleted stores/sidebar-store.ts and explorer-sidebar-store.ts
- Updated all components to use new panel store
- Animation contexts derive isOpen from unified state
This commit is contained in:
Mohamed Boudra
2026-01-16 16:19:49 +07:00
parent b8f056060f
commit ed50c168ad
58 changed files with 1579 additions and 893 deletions
+36 -9
View File
@@ -1,4 +1,4 @@
import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
import { readdir, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
import { join } from "node:path";
import { randomBytes } from "node:crypto";
import type {
@@ -13,6 +13,22 @@ function generateId(): string {
return randomBytes(4).toString("hex");
}
function sortByPriorityThenCreated(a: Task, b: Task): number {
// Tasks with priority come before tasks without
if (a.priority !== undefined && b.priority === undefined) return -1;
if (a.priority === undefined && b.priority !== undefined) return 1;
// If both have priority, lower number = higher priority
if (a.priority !== undefined && b.priority !== undefined) {
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
}
// Fall back to created date (oldest first)
return a.created.localeCompare(b.created);
}
function serializeTask(task: Task): string {
const frontmatterLines = [
"---",
@@ -31,6 +47,10 @@ function serializeTask(task: Task): string {
frontmatterLines.push(`assignee: ${task.assignee}`);
}
if (task.priority !== undefined) {
frontmatterLines.push(`priority: ${task.priority}`);
}
frontmatterLines.push("---");
const frontmatter = frontmatterLines.join("\n");
@@ -120,6 +140,8 @@ function parseTask(content: string): Task {
const assignee = getValue("assignee") as AgentType | "";
const parentId = getValue("parentId");
const priorityStr = getValue("priority");
const priority = priorityStr ? parseInt(priorityStr, 10) : undefined;
return {
id: getValue("id"),
@@ -132,6 +154,7 @@ function parseTask(content: string): Task {
notes,
created: getValue("created") || new Date().toISOString(),
assignee: assignee || undefined,
priority,
};
}
@@ -245,7 +268,7 @@ export class FileTaskStore implements TaskStore {
const allTasks = await this.list();
return allTasks
.filter((t) => t.parentId === id)
.sort((a, b) => a.created.localeCompare(b.created));
.sort(sortByPriorityThenCreated);
}
async getDescendants(id: string): Promise<Task[]> {
@@ -298,10 +321,8 @@ export class FileTaskStore implements TaskStore {
return children.every((c) => c.status === "done");
};
// Sort by created date (oldest first) for consistent ordering
return candidates.filter(isReady).sort((a, b) => {
return a.created.localeCompare(b.created);
});
// Sort by priority first (lower = higher priority), then created date
return candidates.filter(isReady).sort(sortByPriorityThenCreated);
}
async getBlocked(scopeId?: string): Promise<Task[]> {
@@ -365,6 +386,7 @@ export class FileTaskStore implements TaskStore {
notes: [],
created: new Date().toISOString(),
assignee: opts?.assignee,
priority: opts?.priority,
};
await this.writeTask(task);
@@ -385,6 +407,14 @@ export class FileTaskStore implements TaskStore {
return updated;
}
async delete(id: string): Promise<void> {
const task = await this.get(id);
if (!task) {
throw new Error(`Task not found: ${id}`);
}
await unlink(this.taskPath(id));
}
async addDep(id: string, depId: string): Promise<void> {
const task = await this.get(id);
if (!task) {
@@ -469,9 +499,6 @@ export class FileTaskStore implements TaskStore {
if (!task) {
throw new Error(`Task not found: ${id}`);
}
if (task.status !== "draft") {
throw new Error(`Cannot open task with status: ${task.status}`);
}
await this.update(id, { status: "open" });
}