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;
}