npm package: @sanity/agent-directives
Description: Shared directive system for Sanity Agent
Latest version: 0.1.0

# @sanity/agent-directives

Shared directive system for Sanity Agent.

## Installation

```sh
pnpm add @sanity/agent-directives
```

## React Usage

```tsx
// kit.ts
import { createDirectiveKit } from "@sanity/agent-directives/react";

export const { defineDirective, remarkPlugin } =
  createDirectiveKit<Application>({
    useApplication: (source) => useThreadApplication(source),
  });

// directives/Document.tsx
export const DocumentDirective = defineDirective(
  "document",
  ({ id, application }) => {
    return <DocumentCard id={id} application={application} />;
  },
);

// directives/Changes.tsx - no application needed
export const ChangesDirective = defineDirective(
  "changes",
  ({ createdCount, updatedCount }) => (
    <ChangesCard created={createdCount} updated={updatedCount} />
  ),
  { requiresContext: false },
);

// AgentMessage.tsx
import { remarkPlugin } from "./kit";
import ReactMarkdown from "react-markdown";

const components = {
  Document: DocumentDirective,
  Changes: ChangesDirective,
  // directive names are converted to PascalCase
};

function AgentMessage({ content }: { content: string }) {
  return (
    <ReactMarkdown remarkPlugins={[remarkPlugin]} components={components}>
      {content}
    </ReactMarkdown>
  );
}
```

### Excluding Directives from Stack Grouping

By default, consecutive leaf directives are grouped into a `DirectivesStack` for layout purposes. You can exclude specific directives from this grouping:

```tsx
// kit.ts
export const { defineDirective, remarkPlugin } =
  createDirectiveKit<Application>({
    useApplication: (source) => useThreadApplication(source),
    excludeFromStack: ["set"], // These directives won't be grouped
  });
```

Excluded directives will render individually instead of being grouped together. This is useful for directives that need special layout treatment or shouldn't be visually grouped with other directives.

**Alternative approach:** You can also configure this directly on the remark plugin:

```tsx
import { remarkAgentDirectives } from "@sanity/agent-directives/react";

<ReactMarkdown
  remarkPlugins={[[remarkAgentDirectives, { excludeFromStack: ["set"] }]]}
>
  {content}
</ReactMarkdown>;
```

## Server-side Usage

```ts
// kit.ts
import {
  createDirectiveKit,
  createRemarkRenderer,
} from "@sanity/agent-directives/string";

interface MyContext {
  accessToken: string;
  applications: Application[];
}

interface MyOutput {
  text?: string;
  blocks?: Block[];
}

export const { defineDirective, renderDirective } = createDirectiveKit<
  MyContext,
  MyOutput
>();

export const remarkRenderDirectives = createRemarkRenderer<MyContext, Block>(
  renderDirective,
);

// directives/document.ts
defineDirective("document", async ({ props, context }) => {
  const doc = await fetchDocument(props.id, context.accessToken);
  return { blocks: [linkButton(doc.title, url)] };
});

// directives/changes.ts - simple text output
defineDirective("changes", ({ props }) => {
  return {
    text: `Created: ${props.createdCount}, Updated: ${props.updatedCount}`,
  };
});

// markdown.ts
import remarkDirective from "remark-directive";

async function toMarkdown(content: string, context: MyContext) {
  const collector = { blocks: [] };

  const processor = unified()
    .use(remarkParse)
    .use(remarkDirective)
    .use(remarkRenderDirectives, context, collector)
    .use(remarkStringify);

  const file = await processor.process(content);
  return { text: String(file), blocks: collector.blocks };
}
```

## Formatters

For generating directive strings (agent backend):

```ts
import { createDocumentDirective } from "@sanity/agent-directives/formatters";

createDocumentDirective({ id: "doc-123", type: "article" });
// => '::document{id="doc-123" type="article"}'
```

## Adding a New Directive

Edit `src/schemas.ts`:

```ts
// 1. Add schema
export const MyDirectiveSchema = z.object({
  id: z.string(),
  title: z.string().optional(),
});

// 2. Add to DirectiveSchemas
export const DirectiveSchemas = {
  // ...existing
  my: MyDirectiveSchema,
} as const;

// 3. Add to DIRECTIVE_NAMES
export const DIRECTIVE_NAMES = {
  // ...existing
  my: "my",
} as const;

// 4. Add type
export type MyDirectiveProps = z.infer<typeof MyDirectiveSchema>;

// 5. Add to DirectivePropsMap
export interface DirectivePropsMap {
  // ...existing
  my: MyDirectiveProps;
}
```

Then define the render function in your client using `defineDirective('my', ...)`.