TaskOverflowCleanup.tsx2.9 KBView on GitHub
'use client';

/**
 * The tail of a long column, collapsed into one button.
 *
 * A column with hundreds of long-overdue tasks is not a list anyone scrolls — it's a backlog. So
 * past COLUMN_VISIBLE_LIMIT the remainder collapses to a single row that says how much is hidden
 * and how stale it is, and offers to hand the whole tail to the agent.
 *
 * This is the surface for the overdue sweep that TASK_GROUPS_DESIGN deferred: the agent's job is
 * to decide, per task, whether the deal is dead (mark it closed-lost), the task is still worth
 * doing (execute or reschedule it), or it should simply be dropped.
 *
 * Clicking seeds a chat message rather than mutating anything directly — bulk-closing deals is
 * not something to do without the user reading the proposal first.
 */
import { useMemo } from 'react';
import { Sparkles } from 'lucide-react';
import { useCedarStore } from '@/modules/store';
import type { TaskCardTask } from './TaskKanbanCard';

const WEEK_MS = 7 * 24 * 60 * 60 * 1000;

export function TaskOverflowCleanup({
  columnLabel,
  tasks,
}: {
  columnLabel: string;
  tasks: TaskCardTask[];
}) {
  // `setOverrideInputContent`, not `setChatInputContent`: the latter only writes the store's
  // draft mirror, which the mounted composer re-reads on a thread switch and nowhere else — so
  // clicking this filled nothing. The override is the path that reaches the editor, and it
  // leaves the caret in it.
  const setOverrideInputContent = useCedarStore((s) => s.setOverrideInputContent);

  const staleCount = useMemo(
    () =>
      tasks.filter((t) => t.dueDate && Date.now() - new Date(t.dueDate).getTime() > WEEK_MS).length,
    [tasks],
  );

  if (tasks.length === 0) return null;

  // Only claim ">1 week overdue" when that's actually what the tail is.
  const staleMajority = staleCount > tasks.length / 2;

  const prompt =
    `I have ${tasks.length} tasks in my "${columnLabel}" lane that are buried below the fold` +
    (staleMajority ? `, ${staleCount} of them more than a week overdue` : '') +
    `. Go through them and tell me which deals are dead (so we can mark them closed-lost), ` +
    `which tasks are still worth doing (and should be rescheduled or executed), and which ` +
    `should just be dropped. Show me the proposal before changing anything.\n\n` +
    `Task ids: ${tasks.map((t) => t.id).join(', ')}`;

  return (
    <button
      type="button"
      onClick={() => setOverrideInputContent(prompt)}
      className="bg-action text-action-foreground hover:bg-action-hover w-full rounded-md px-2.5 py-2 text-left text-xs transition-colors"
    >
      <span className="block font-medium">
        + {tasks.length} {tasks.length === 1 ? 'task' : 'tasks'}
        {staleMajority ? ' · more than a week overdue' : ''}
      </span>
      <span className="mt-0.5 flex items-center gap-1 opacity-90">
        <Sparkles className="h-3 w-3 shrink-0" />
        Ask AI to clean up — mark closed-lost or run
      </span>
    </button>
  );
}