a997f13601
* feat(kanban): add Kanban board with task-manager skill, modal UI, and desk clutter Implement a full Kanban board system for tracking agent tasks: - Add task-manager skill with shared JSON task store for persistence - Render board as a floating modal over the live 3D office (not immersive) - Auto-create tasks from actionable user messages with heuristic filtering - Sync task status through OpenClaw agent lifecycle events - Collapse task details panel by default, expand on card click - Add dynamic desk clutter (papers, folders, etc.) reflecting active task count - Exclude done tasks from desk clutter count - Extract KANBAN_CLUTTER_OFFSET for easy positioning adjustment - Add install flow with progress bar for the task-manager skill - Include unit and e2e test coverage Made-with: Cursor * feat(kanban): production-harden task board with AI-free classification, resilient persistence, and modal UX - Harden shared task store with atomic writes, payload size limits, and server-side enum validation - Add client resilience: request timeouts (AbortController), exponential backoff retries, poll deduplication - Implement optimistic UI with rollback on all card mutations (update, move, archive) - Add modal accessibility: focus trap, Escape to close, aria-modal, keyboard card navigation - Trust OpenClaw agent lifecycle phase=start as task classification signal instead of regex heuristics - Keep regex heuristic only as lightweight filter for direct chat events (conversational noise) - Expand verb recognition with typo tolerance and broader action vocabulary - Create tasks from agent runs even when no chat event is received (external channel support) - Merge dual header bars into single bar; reposition close button outside modal corner - Exclude done tasks from desk clutter count; make clutter position configurable via KANBAN_CLUTTER_OFFSET - Update default furniture layout to match user configuration - Ensure kanban_board furniture persists in local storage across sessions - Add comprehensive test coverage for store, API route, and controller logic Made-with: Cursor --------- Co-authored-by: iamlukethedev <lucas.guilherme@smartwayslfl.com>
138 lines
4.1 KiB
TypeScript
138 lines
4.1 KiB
TypeScript
import { createElement } from "react";
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { TaskBoardView } from "@/features/office/tasks/TaskBoardView";
|
|
import type { TaskBoardCard } from "@/features/office/tasks/types";
|
|
import type { AgentState } from "@/features/agents/state/store";
|
|
import type { CronJobSummary } from "@/lib/cron/types";
|
|
|
|
const createCard = (overrides: Partial<TaskBoardCard> = {}): TaskBoardCard => ({
|
|
id: "task-1",
|
|
title: "New task",
|
|
description: "",
|
|
status: "todo",
|
|
source: "claw3d_manual",
|
|
sourceEventId: null,
|
|
assignedAgentId: null,
|
|
createdAt: "2026-03-29T10:00:00.000Z",
|
|
updatedAt: "2026-03-29T10:00:00.000Z",
|
|
playbookJobId: null,
|
|
runId: null,
|
|
channel: null,
|
|
externalThreadId: null,
|
|
lastActivityAt: null,
|
|
notes: [],
|
|
isArchived: false,
|
|
isInferred: false,
|
|
...overrides,
|
|
});
|
|
|
|
const createAgent = (): AgentState => ({
|
|
agentId: "agent-1",
|
|
name: "Agent One",
|
|
sessionKey: "agent:agent-1:main",
|
|
status: "idle",
|
|
sessionCreated: true,
|
|
awaitingUserInput: false,
|
|
hasUnseenActivity: false,
|
|
outputLines: [],
|
|
lastResult: null,
|
|
lastDiff: null,
|
|
runId: null,
|
|
runStartedAt: null,
|
|
streamText: null,
|
|
thinkingTrace: null,
|
|
latestOverride: null,
|
|
latestOverrideKind: null,
|
|
lastAssistantMessageAt: null,
|
|
lastActivityAt: null,
|
|
latestPreview: null,
|
|
lastUserMessage: null,
|
|
draft: "",
|
|
sessionSettingsSynced: true,
|
|
historyLoadedAt: null,
|
|
historyFetchLimit: null,
|
|
historyFetchedCount: null,
|
|
historyMaybeTruncated: false,
|
|
toolCallingEnabled: true,
|
|
showThinkingTraces: true,
|
|
model: "openai/gpt-5",
|
|
thinkingLevel: "medium",
|
|
avatarSeed: "seed-1",
|
|
avatarUrl: null,
|
|
});
|
|
|
|
const createCronJob = (): CronJobSummary => ({
|
|
id: "job-1",
|
|
name: "Morning review",
|
|
agentId: "agent-1",
|
|
enabled: true,
|
|
updatedAtMs: Date.now(),
|
|
schedule: { kind: "every", everyMs: 60_000 },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: { kind: "agentTurn", message: "Review new tasks." },
|
|
state: {},
|
|
});
|
|
|
|
describe("TaskBoardView", () => {
|
|
it("routes task edits through callbacks", () => {
|
|
const onCreateCard = vi.fn();
|
|
const onMoveCard = vi.fn();
|
|
const onSelectCard = vi.fn();
|
|
const onUpdateCard = vi.fn();
|
|
const onDeleteCard = vi.fn();
|
|
const onRefreshCronJobs = vi.fn();
|
|
const selectedCard = createCard();
|
|
|
|
render(
|
|
createElement(TaskBoardView, {
|
|
title: "Kanban",
|
|
subtitle: "Track tasks.",
|
|
agents: [createAgent()],
|
|
cardsByStatus: {
|
|
todo: [selectedCard],
|
|
in_progress: [],
|
|
blocked: [],
|
|
review: [],
|
|
done: [],
|
|
},
|
|
selectedCard,
|
|
activeRuns: [{ runId: "run-1", agentId: "agent-1", label: "Agent One" }],
|
|
cronJobs: [createCronJob()],
|
|
cronLoading: false,
|
|
cronError: null,
|
|
onCreateCard,
|
|
onMoveCard,
|
|
onSelectCard,
|
|
onUpdateCard,
|
|
onDeleteCard,
|
|
onRefreshCronJobs,
|
|
})
|
|
);
|
|
|
|
fireEvent.click(screen.getAllByRole("button", { name: /new task/i })[0]!);
|
|
fireEvent.click(screen.getByRole("button", { name: /refresh/i }));
|
|
fireEvent.click(screen.getAllByRole("button", { name: /new task/i })[1]!);
|
|
fireEvent.change(screen.getByLabelText("Title"), {
|
|
target: { value: "Create marketing website" },
|
|
});
|
|
fireEvent.change(screen.getByLabelText("Status"), {
|
|
target: { value: "in_progress" },
|
|
});
|
|
fireEvent.change(screen.getByLabelText("Assigned agent"), {
|
|
target: { value: "agent-1" },
|
|
});
|
|
fireEvent.click(screen.getByRole("button", { name: /delete task/i }));
|
|
|
|
expect(onCreateCard).toHaveBeenCalledTimes(1);
|
|
expect(onRefreshCronJobs).toHaveBeenCalledTimes(1);
|
|
expect(onSelectCard).toHaveBeenCalledWith("task-1");
|
|
expect(onUpdateCard).toHaveBeenCalledWith("task-1", { title: "Create marketing website" });
|
|
expect(onMoveCard).toHaveBeenCalledWith("task-1", "in_progress");
|
|
expect(onUpdateCard).toHaveBeenCalledWith("task-1", { assignedAgentId: "agent-1" });
|
|
expect(onDeleteCard).toHaveBeenCalledWith("task-1");
|
|
});
|
|
});
|