open-slack-channel-from-task.ts2.7 KBView on GitHub
'use client';

/**
 * Opening a Slack task's channel — the one definition, shared by every surface that opens a task.
 *
 * A Slack task's output is a message in a channel, so it opens THE CHANNEL: the same
 * `ChannelThreadView` the unibox opens, reached through the same display-artifact mechanism an
 * email task reaches its thread with. Three things have to happen together, which is why this is
 * one function rather than a closure copied into each caller:
 *
 *   1. the drafted message is handed to the channel's composer,
 *   2. the channel's identity is left where the view can find it (`slackChannelHint`), and
 *   3. the artifact is set, which is what actually opens it.
 *
 * ── Which composer ──
 *
 * `seedChannelDraft`, NOT `openSlackDraftInChannel`. There are two Slack composers and they are
 * fed by different mechanisms: the per-deal timeline composer takes a ProseMirror doc through
 * `seedComposerDoc`, and the unibox's `ChannelThreadView` takes raw mrkdwn through
 * `pending-channel-draft`. This path opens the unibox one, so seeding the deal's composer put
 * the draft somewhere the user was never sent — and left the channel that DID open with an empty
 * box. See `documents/table/open-output-in-channel.ts`, which addresses a channel the same way.
 *
 * ── A sent message is not re-seeded ──
 *
 * `sendSlackDraftFromTask` posts the message and completes the task, but leaves the text on
 * `taskActionData`. Re-opening a done Slack task would therefore drop an already-posted message
 * back into the composer, one Enter from a duplicate. Completion is the sent signal (there is no
 * `sentAt` on the output), so a done task opens its channel with the box empty.
 */

import { seedChannelDraft } from '@/modules/inbox/store/pending-channel-draft';
import { useCedarStore } from '@/modules/store';

export interface SlackChannelTarget {
  conversationId: string | null;
  channelId: string;
  channelName?: string;
  /** The channel cannot be READ without it — the messages query is keyed by workspace + channel. */
  workspaceId?: string;
  message?: string;
  /** The task is finished, so its message is already on Slack — see above. */
  alreadySent?: boolean;
}

export function openSlackChannelFromTask({
  conversationId,
  channelId,
  channelName,
  workspaceId,
  message,
  alreadySent,
}: SlackChannelTarget): void {
  const store = useCedarStore.getState();
  if (message && !alreadySent) seedChannelDraft(channelId, message);
  // How to render a channel the feed has not loaded. The real feed row still wins when it lands.
  store.setSlackChannelHint({
    channelId,
    workspaceId: workspaceId ?? '',
    channelName: channelName ?? null,
    conversationId,
  });
  store.setSelectedArtifact({ kind: 'slack_thread', id: channelId });
}