AgentInstructionsSection.tsx6.1 KBView on GitHub
'use client';

import { useMemo, useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { ChevronRight } from 'lucide-react';

import { HideFrontmatterExtension } from '@/modules/documents/agent/HideFrontmatterExtension';
import { useRichTextExtensions } from '@/modules/documents/use-rich-text-extensions';
import { AgentSettingsSection } from './AgentSettingsSection';
import { AGENT_SECTION_SURFACE } from './AgentConfigPage';
import type { AgentDetail } from '@/modules/agents/types';
import { Document } from '@/modules/documents/document';
import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils';

interface AgentInstructionsSectionProps {
  /**
   * The agent, for the settings disclosure above the editor. Its frontmatter and its body
   * are the same document, so they belong to the same section — see AgentSettingsSection.
   */
  agent?: AgentDetail;
  /** The subagent doc id — the editor's identity and its Y.js provider key. */
  documentId?: string | null;
  /** An org admin/owner viewing a teammate's agent — see `AgentView`'s prop of the same name. */
  targetUserId?: string;
  /** A view-as perspective is active, so the settings disclosure is not rendered at all. */
  readOnly?: boolean;
  isLoading?: boolean;
  className?: string;
}

/**
 * Config §3 — the agent's instructions, rendered the way an `agent` DOCUMENT is
 * rendered everywhere else, not as plain markdown.
 *
 * Three things make a subagent doc special, and this section previously had none
 * of them, which is why it read as a raw file:
 *
 *  1. `HideFrontmatterExtension` — a subagent doc opens with a YAML frontmatter
 *     block (name, model, agent_id, the grants). Without this the editor renders
 *     that block as literal text at the top of the instructions, and worse, invites
 *     someone to edit it as prose — the structured fields have their own controls in
 *     the workspace header and the ⋯ menu, and the frontmatter must survive the Yjs
 *     round trip byte-shaped (blank-line separated) or the agent loses its identity.
 *  2. The rich-text extensions — `[[` doc links, @-mentions, conversation and event
 *     nodes. An instruction that references a knowledge-base file should render as a
 *     chip here exactly as it does in Brain.
 *  3. The owned-CRM-fields callout — which fields this agent is responsible for
 *     writing. It is part of the agent document's rendering (strategic-overview
 *     phase 20) and appears on no other surface in this workspace.
 *
 * The agent's SETTINGS sit above the editor as their own collapsed container, and the
 * owned-CRM-fields callout is a row INSIDE that form rather than a card of its own — it
 * used to render in both places at once.
 */

/**
 * One duration for the disclosure's height, matching the connections card on the same page.
 *
 * A tween, never a spring: what opens here is a form's worth of rows, and overshooting a
 * height means the document below it bounces (CLAUDE.md → a container that opens animates
 * its HEIGHT).
 */
const HEIGHT_TRANSITION = { type: 'tween', duration: 0.2, ease: 'easeOut' } as const;

export function AgentInstructionsSection({
  documentId,
  agent,
  targetUserId,
  readOnly = false,
  isLoading,
  className,
}: AgentInstructionsSectionProps) {
  /**
   * Collapsed by default. The instructions are what you came to read; the settings are
   * what you came to change on the rarer visit.
   */
  const [settingsOpen, setSettingsOpen] = useState(false);

  // A subagent doc is not a conversation doc — the conversation mention would offer
  // a picker with nothing sensible to point at from an agent's instructions.
  const richTextExtensions = useRichTextExtensions({ includeConversationMention: false });

  const extensions = useMemo(
    () => [...richTextExtensions, HideFrontmatterExtension],
    [richTextExtensions],
  );

  if (isLoading || !documentId) {
    return <Skeleton className={cn('h-64 w-full', className)} />;
  }

  return (
    // TWO containers under the Playbook heading, in this order: the settings, collapsed,
    // then the document. They are the same agent but not the same KIND of thing — a set of
    // controls that write frontmatter, and the prose the agent reads — so each gets one
    // surface, and neither is nested inside the other.
    <div className={cn('flex flex-col gap-3', className)}>
      {/* SETTINGS IS ENTIRELY MUTATION — folder, default file, every frontmatter switch —
          so under a perspective it is not collapsed, it is absent. Hidden rather than
          disabled, the rule `modules/sharing/view-as.tsx` states. */}
      {readOnly ? null : (
      <div className={AGENT_SECTION_SURFACE}>
        <button
          type="button"
          onClick={() => setSettingsOpen((open) => !open)}
          aria-expanded={settingsOpen}
          className="text-muted-foreground hover:bg-sunken hover:text-foreground flex w-full cursor-pointer items-center gap-1.5 rounded-lg px-3 py-2 text-left text-xs font-medium transition-colors"
        >
          <ChevronRight
            className={cn('size-3.5 shrink-0 transition-transform', settingsOpen && 'rotate-90')}
          />
          Settings
        </button>

        <AnimatePresence initial={false}>
          {settingsOpen && (
            <motion.div
              key=[redacted]
              initial={{ height: 0 }}
              animate={{ height: 'auto' }}
              exit={{ height: 0 }}
              transition={HEIGHT_TRANSITION}
              className="overflow-hidden"
            >
              <AgentSettingsSection
                agent={agent}
                targetUserId={targetUserId}
                isLoading={isLoading}
                className="px-3 pb-2"
              />
            </motion.div>
          )}
        </AnimatePresence>
      </div>
      )}

      <div className={cn(AGENT_SECTION_SURFACE, 'px-4 py-3')}>
        <Document
          documentId={documentId}
          targetUserId={targetUserId}
          extraExtensions={extensions}
          placeholder="Write what this agent should do…"
          className="min-h-64 border-0 bg-transparent p-0 text-sm"
        />
      </div>
    </div>
  );
}