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