TaskExecutionUrlSync.tsx3.9 KBView on GitHub
'use client';

/**
 * TaskExecutionUrlSync — restoring execution mode from `?task=` on a cold load.
 *
 * CLICKING a task routes it through `openTask`: an email draft opens its thread, a Slack draft its
 * message, and a task with neither opens its conversation's Inbox with the task pinned above it by
 * `OpenTaskExecutionCard`. A RELOAD, a pasted link, or a back/forward onto the same URL runs none of
 * that — nothing calls `openTask`, so LayoutUrlSync's URL→store sync claims the bare `task` display
 * artifact and the centre column becomes `TaskTicketView`: the task as its own ticket, whichever
 * kind of task it is. The link and the click disagreed about what a task opens.
 *
 * This closes that gap. Whenever `?task=` is the only artifact named in the URL, it resolves the
 * task and runs the SAME `openTask` the click runs. `openTask` is idempotent for the cases that
 * genuinely belong in the panel (a Slack task's drafted message, a task with no conversation): it
 * re-opens the panel that is already showing.
 *
 * Fires once per task id — `openTask` writes `?threadOpen=`/`?conversationId=` for the other cases,
 * which closes the gate, and the ref stops the idempotent ones from re-firing.
 *
 * Mounted beside LayoutUrlSync, whose `?task=` effect is the other half of this: that one claims
 * the `task` display artifact so a deep link has something on screen immediately, this one resolves
 * where the task actually belongs. A sibling rather than a child because it has to FETCH — folding
 * it into LayoutUrlSync would make a pure URL⇄store adapter require a QueryClient, which every
 * test that mounts it bare would then have to provide.
 */

import { useEffect, useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
import { openSlackChannelFromTask } from '@/modules/userTasks/utils/open-slack-channel-from-task';
import { useQueryState } from 'nuqs';

import { openTask, type OpenableTask } from '@/modules/userTasks/utils/open-task';
import { useTRPC } from '@/providers/query-provider';
import { useCedarStore } from '@/modules/store';

export function TaskExecutionUrlSync() {
  const trpc = useTRPC();
  const [taskParam] = useQueryState('task');
  const [threadOpen, setThreadOpen] = useQueryState('threadOpen', { history: 'push' });
  const [conversationParam] = useQueryState('conversationId');
  const updateThreadConversationId = useCedarStore((s) => s.updateThreadConversationId);
  const setSelectedArtifact = useCedarStore((s) => s.setSelectedArtifact);

  // `?task=` with no artifact of its own in the URL is the un-routed state: either a cold load, or
  // one of the cases that legitimately resolves to the task panel.
  const needsRouting = !!taskParam && !threadOpen && !conversationParam;

  const { data } = useQuery({
    ...trpc.userTasks.getTaskById.queryOptions({ taskId: taskParam ?? '' }),
    enabled: needsRouting,
  });

  const routedTaskId = useRef<string | null>(null);

  useEffect(() => {
    if (!needsRouting || !taskParam) return;
    if (routedTaskId.current === taskParam) return;
    const task = data?.task;
    // Guard on the id: the query keeps the previous task's data while the next one loads.
    if (!task || task.id !== taskParam) return;
    routedTaskId.current = taskParam;

    openTask(
      {
        id: task.id,
        conversationId: task.conversationId,
        taskActionData: task.taskActionData as OpenableTask['taskActionData'],
        status: task.status,
      },
      {
        updateThreadConversationId,
        setThreadOpen,
        // Shared with `useOpenTaskInExecutionMode` — a click and a cold `?task=` load must land
        // on the same channel, with the same draft in the box.
        openSlackChannel: openSlackChannelFromTask,
        openTaskOutput: (id) => setSelectedArtifact({ kind: 'task', id }),
      },
    );
  }, [
    needsRouting,
    taskParam,
    data,
    updateThreadConversationId,
    setThreadOpen,
    setSelectedArtifact,
  ]);

  return null;
}