AgentDocumentView.tsx4.7 KBView on GitHub
'use client';

import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';

import type { AgentTab } from '@/modules/agents/types';
import { useTRPC } from '@/providers/query-provider';
import { Skeleton } from '@/components/ui/skeleton';
import { AgentView } from './AgentView';

/**
 * A subagent DOCUMENT, opened as the agent it is.
 *
 * An agent is a document — that is the whole design (apps/server/docs/wiki/agent-document-type.md).
 * The consequence nobody had drawn until now is that opening that document should open the
 * AGENT, not a markdown editor pointed at it. Five surfaces used to render a subagent doc as
 * generic prose: the Brain knowledge explorer, the file browser's open-file view, the home
 * artifact panel, the playground playbook editor, and the CRM-updater panel. Exactly one of
 * them mounted the structured header, and all five rendered the YAML frontmatter as body
 * text whenever `HideFrontmatterExtension` bailed — which it does the moment the block is
 * not a clean `horizontalRule … horizontalRule` run, because it keys on node types rather
 * than on content.
 *
 * So the fix is not a better hider. It is that a document whose `documentType` is `agent`
 * has ONE rendering, and it is the workspace: Settings carries every frontmatter key as a
 * field, Playbook carries the instruction body with the block hidden, and the raw YAML has
 * nowhere left to appear.
 *
 * Keyed on `documentType`, not on the path. The path regex was duplicated in two files and
 * absent from the other three, and it deliberately excluded `crm-updater.md` — which is why
 * that one doc had neither a header nor hiding on any surface. `documentType: 'agent'` is
 * stamped on every subagent-path write by `resolveEffectiveDocumentType`, so it is the fact
 * that is actually true of all of them.
 */
export function AgentDocumentView({
  documentId,
  flush,
  className,
  targetUserId,
}: {
  documentId: string;
  /** Drop the workspace's own horizontal inset — the host surface already provides one. */
  flush?: boolean;
  className?: string;
  /**
   * The document's real owner, when it differs from the signed-in caller — an org
   * admin/owner viewing a teammate's agent. Passed as a PROP, deliberately not read
   * ambiently via `useTargetUserId()`: this component is mounted from general
   * surfaces (the home artifact panel, the file browser) that carry no admin
   * picker, so an ambient read here would apply whichever teammate happens to be
   * selected on a DIFFERENT screen instead of the document actually on screen.
   * `CompanyExplorer` derives this from the document's own `userId`
   * (`documents.getDoc`), which is what makes it correct regardless of picker state.
   */
  targetUserId?: string;
}) {
  const trpc = useTRPC();

  /**
   * documentId → agentId, off the list every agent surface already holds.
   *
   * `agent.list` is the authorization boundary too: it enumerates the AOPs of
   * `targetUserId` (or the caller, when absent — org-admin gated, see
   * `agent.ts`'s `resolveActorUserId`), so a subagent document belonging to
   * someone the caller cannot administer resolves to nothing here rather than
   * rendering a workspace over an agent they cannot reach.
   */
  const { data: agents, isLoading } = useQuery(
    trpc.agent.list.queryOptions({ targetUserId }),
  );
  const agentId = useMemo(
    () => agents?.find((a) => a.documentId === documentId)?.agentId,
    [agents, documentId],
  );

  /**
   * Config, and controlled.
   *
   * Opening the document is opening its configuration — that is what a subagent doc IS —
   * and the tab is owned here rather than by the URL because this view is mounted INSIDE
   * another surface's address (`?documentId=…`, the artifact panel, a file browser). Two
   * writers of `?tab=` is the bug the workspace's own note warns about.
   */
  const [tab, setTab] = useState<AgentTab>('config');

  if (isLoading) {
    return (
      <div className={className}>
        <Skeleton className="h-6 w-48" />
        <Skeleton className="mt-3 h-40 w-full" />
      </div>
    );
  }

  // Not resolvable — a stale id, another user's agent, or a doc on a subagent path that
  // never got an `agent_id`. Say so rather than falling back to a prose editor, which is
  // what would put the raw frontmatter back on screen.
  if (!agentId) {
    return (
      <div className={className}>
        <p className="text-muted-foreground text-sm">
          This is an agent document, but the agent it belongs to could not be loaded.
        </p>
      </div>
    );
  }

  return (
    <AgentView
      agentId={agentId}
      targetUserId={targetUserId}
      chrome="embedded"
      flush={flush ?? false}
      tab={tab}
      onTabChange={setTab}
      className={className}
    />
  );
}