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:
@@ -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" });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user