Files
claw3d/src/features/office/screens/KanbanImmersiveScreen.tsx
T
Luke The Dev a997f13601 feat(kanban): Interactive Kanban board with real-time task tracking (#83)
* 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>
2026-03-30 22:58:18 -05:00

132 lines
4.0 KiB
TypeScript

"use client";
import { type ComponentProps, useCallback, useEffect, useRef } from "react";
import { X } from "lucide-react";
import type { AgentState } from "@/features/agents/state/store";
import { TaskBoardView } from "@/features/office/tasks/TaskBoardView";
import type { TaskBoardCard, TaskBoardStatus } from "@/features/office/tasks/types";
import type { CronJobSummary } from "@/lib/cron/types";
export function KanbanImmersiveScreen({
agents,
cardsByStatus,
selectedCard,
activeRuns,
cronJobs,
cronLoading,
cronError,
taskCaptureDebug,
onCreateCard,
onMoveCard,
onSelectCard,
onUpdateCard,
onDeleteCard,
onRefreshCronJobs,
onClose,
}: {
agents: AgentState[];
cardsByStatus: Record<TaskBoardStatus, TaskBoardCard[]>;
selectedCard: TaskBoardCard | null;
activeRuns: Array<{ runId: string; agentId: string; label: string }>;
cronJobs: CronJobSummary[];
cronLoading: boolean;
cronError: string | null;
taskCaptureDebug?: ComponentProps<typeof TaskBoardView>["taskCaptureDebug"];
onCreateCard: () => void;
onMoveCard: (cardId: string, status: TaskBoardStatus) => void;
onSelectCard: (cardId: string | null) => void;
onUpdateCard: (cardId: string, patch: Partial<TaskBoardCard>) => void;
onDeleteCard: (cardId: string) => void;
onRefreshCronJobs: () => void;
onClose: () => void;
}) {
const dialogRef = useRef<HTMLDivElement>(null);
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Escape") {
event.stopPropagation();
onClose();
}
},
[onClose]
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
const previouslyFocused = document.activeElement as HTMLElement | null;
dialog.focus();
const trapFocus = (event: FocusEvent) => {
if (!dialog.contains(event.target as Node)) {
event.stopPropagation();
dialog.focus();
}
};
document.addEventListener("focusin", trapFocus);
return () => {
document.removeEventListener("focusin", trapFocus);
previouslyFocused?.focus?.();
};
}, []);
return (
<div
role="dialog"
aria-modal="true"
aria-label="Kanban Board"
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div className="relative">
<button
type="button"
onClick={onClose}
aria-label="Close Kanban Board"
className="absolute -right-5 -top-5 z-10 flex h-10 w-10 items-center justify-center rounded-full border border-amber-400/20 bg-[#0e0b07]/90 text-amber-200/70 backdrop-blur-sm transition-colors hover:border-amber-400/40 hover:text-white"
>
<X className="h-4 w-4" />
</button>
<div
ref={dialogRef}
tabIndex={-1}
className="flex h-[min(75vh,800px)] w-[min(80vw,1280px)] flex-col overflow-hidden rounded-2xl border border-amber-500/20 bg-[#0e0b07]/85 shadow-2xl outline-none backdrop-blur-md"
>
<div className="min-h-0 flex-1">
<TaskBoardView
title="Kanban Board"
subtitle="Headquarters task routing, scheduling, and review."
agents={agents}
cardsByStatus={cardsByStatus}
selectedCard={selectedCard}
activeRuns={activeRuns}
cronJobs={cronJobs}
cronLoading={cronLoading}
cronError={cronError}
taskCaptureDebug={taskCaptureDebug}
onCreateCard={onCreateCard}
onMoveCard={onMoveCard}
onSelectCard={onSelectCard}
onUpdateCard={onUpdateCard}
onDeleteCard={onDeleteCard}
onRefreshCronJobs={onRefreshCronJobs}
/>
</div>
</div>
</div>
</div>
);
}