{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opencode-activity",
  "title": "OpenCode Activity",
  "description": "Orange thoughts with a live Thinking spinner, plus muted read, web search and tool lines.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "button",
    "collapsible"
  ],
  "files": [
    {
      "path": "registry/notra/opencode/components/opencode-activity.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\n\nimport {\n  OPENCODE_ACTIVITY_GLYPH,\n  OPENCODE_ACTIVITY_LABEL,\n  OPENCODE_THINKING_FRAME_MS,\n  OPENCODE_THINKING_FRAMES,\n} from \"../constants/opencode\";\nimport { useOpencodeFrame } from \"../hooks/use-opencode-frame\";\nimport { useOpencodeReducedMotion } from \"../hooks/use-opencode-reduced-motion\";\nimport type { OpencodeActivityProps } from \"../types/opencode\";\n\nconst TRIGGER_CLASS =\n  \"focus-visible:ring-opencode-blue h-auto justify-start gap-0 rounded-none p-0 text-start text-[length:inherit] leading-[inherit] font-normal whitespace-normal text-current hover:bg-transparent hover:text-current focus-visible:border-transparent focus-visible:ring-1 aria-expanded:bg-transparent aria-expanded:text-current dark:hover:bg-transparent\";\n\nconst ThinkingGlyph = ({ reducedMotion }: { reducedMotion?: boolean }) => {\n  const reduced = useOpencodeReducedMotion(reducedMotion);\n  const frame = useOpencodeFrame(\n    OPENCODE_THINKING_FRAMES.length,\n    OPENCODE_THINKING_FRAME_MS,\n    !reduced\n  );\n  return (\n    <span aria-hidden=\"true\" className=\"inline-block w-[2ch] shrink-0\">\n      {OPENCODE_THINKING_FRAMES[frame]}\n    </span>\n  );\n};\n\nexport const OpencodeActivity = ({\n  children,\n  className,\n  defaultOpen = false,\n  detail,\n  duration,\n  kind = \"tool\",\n  label,\n  pending = false,\n  reducedMotion,\n  ...props\n}: OpencodeActivityProps) => {\n  const isThought = kind === \"thought\";\n  const glyph = OPENCODE_ACTIVITY_GLYPH[kind];\n  const name = label ?? OPENCODE_ACTIVITY_LABEL[kind];\n\n  const summary =\n    isThought && pending ? (\n      <>\n        <ThinkingGlyph reducedMotion={reducedMotion} />\n        <span>Thinking</span>\n      </>\n    ) : (\n      <>\n        {glyph && (\n          <span aria-hidden=\"true\" className=\"inline-block w-[2ch] shrink-0\">\n            {glyph}\n          </span>\n        )}\n        <span className=\"min-w-0 wrap-break-word\">\n          {name}\n          {detail && <> {detail}</>}\n          {duration && <> · {duration}</>}\n        </span>\n      </>\n    );\n\n  return (\n    <Collapsible\n      className={cn(\n        \"ps-[3ch]\",\n        isThought ? \"text-opencode-orange\" : \"text-opencode-muted\",\n        className\n      )}\n      data-kind={kind}\n      data-slot=\"opencode-activity\"\n      defaultOpen={defaultOpen}\n      {...props}\n    >\n      {children ? (\n        <CollapsibleTrigger\n          render={<Button className={TRIGGER_CLASS} variant=\"ghost\" />}\n        >\n          <span className=\"flex min-w-0\">{summary}</span>\n        </CollapsibleTrigger>\n      ) : (\n        <div className=\"flex min-w-0\">{summary}</div>\n      )}\n      {children && (\n        <CollapsibleContent>\n          <div\n            className={cn(\n              \"text-opencode-muted mt-[1lh] whitespace-pre-wrap\",\n              isThought ? \"italic\" : \"ps-[2ch]\"\n            )}\n          >\n            {children}\n          </div>\n        </CollapsibleContent>\n      )}\n    </Collapsible>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/opencode/components/opencode-activity.tsx"
    },
    {
      "path": "registry/notra/opencode/constants/opencode.ts",
      "content": "import type {\n  OpencodeActivityKind,\n  OpencodeMcpServer,\n  OpencodeMcpStatus,\n} from \"../types/opencode\";\n\nexport const OPENCODE_DEFAULT_AGENT = \"Build\";\n\nexport const OPENCODE_DEFAULT_MODEL = \"Writer 0.1\";\n\nexport const OPENCODE_DEFAULT_PROVIDER = \"Notra\";\n\nexport const OPENCODE_DEFAULT_EFFORT = \"high\";\n\nexport const OPENCODE_DEFAULT_PLACEHOLDER =\n  'Ask anything... \"Draft this week\\'s changelog\"';\n\nexport const OPENCODE_DEFAULT_VERSION = \"1.18.33\";\n\nexport const OPENCODE_DEFAULT_CWD = \"~/acme/web\";\n\nexport const OPENCODE_DEFAULT_SERVERS: OpencodeMcpServer[] = [\n  { name: \"notra\", status: \"Connected\" },\n  { name: \"github\", status: \"Connected\" },\n  { name: \"linear\", status: \"Connected\" },\n];\n\nexport const OPENCODE_MCP_STATUS_CLASS: Record<OpencodeMcpStatus, string> = {\n  Connected: \"text-opencode-green\",\n  Disconnected: \"text-opencode-muted\",\n  Failed: \"text-opencode-orange\",\n};\n\nexport const OPENCODE_ACTIVITY_GLYPH: Record<OpencodeActivityKind, string> = {\n  read: \"→\",\n  search: \"◈\",\n  thought: \"\",\n  tool: \"⚙\",\n};\n\nexport const OPENCODE_ACTIVITY_LABEL: Record<OpencodeActivityKind, string> = {\n  read: \"Read\",\n  search: \"Web Search\",\n  thought: \"Thought\",\n  tool: \"Tool\",\n};\n\nexport const OPENCODE_THINKING_FRAMES = [\n  \"⠋\",\n  \"⠙\",\n  \"⠹\",\n  \"⠸\",\n  \"⠼\",\n  \"⠴\",\n  \"⠦\",\n  \"⠧\",\n  \"⠇\",\n  \"⠏\",\n] as const;\n\nexport const OPENCODE_THINKING_FRAME_MS = 80;\n\nexport const OPENCODE_PROGRESS_CELLS = 8;\n\nexport const OPENCODE_PROGRESS_FRAME_MS = 90;\n\nexport const OPENCODE_PROGRESS_TRAIL = [1, 0.55, 0.32, 0.18] as const;\n\nexport const OPENCODE_SEARCH_HEADER_MS = 140;\n\nexport const OPENCODE_SEARCH_QUERY_MS = 280;\n\nexport const OPENCODE_SEARCH_STAGGER_MS = 56;\n\nexport const OPENCODE_SEARCH_SOURCES_MS = 160;\n\nexport const OPENCODE_CHAT_TIMING = {\n  thinkMs: 1300,\n  toolMs: 600,\n  blockMs: 260,\n  reducedMaxMs: 60,\n} as const;\n\nexport const OPENCODE_REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\n\nexport const OPENCODE_LOGO_ROWS = [\n  \".................................c.....\",\n  \"oooo.oooo.oooo.ooo..cccc.cccc.cccc.cccc\",\n  \"o++o.o++o.o++o.o..o.c....c++c.c++c.c++c\",\n  \"o++o.o++o.oooo.o..o.c....c++c.c++c.cccc\",\n  \"o++o.oooo.o....o..o.c....c++c.c++c.c...\",\n  \"oooo.o....oooo.o..o.cccc.cccc.cccc.cccc\",\n  \".....o.................................\",\n] as const;\n\nexport const OPENCODE_LOGO_CELL = 6;\n",
      "type": "registry:lib",
      "target": "components/opencode/constants/opencode.ts"
    },
    {
      "path": "registry/notra/opencode/types/opencode.ts",
      "content": "import type { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport type { Collapsible } from \"@/components/ui/collapsible\";\n\nexport type OpencodeActivityKind = \"thought\" | \"read\" | \"search\" | \"tool\";\n\nexport type OpencodeMessageAuthor = \"user\" | \"assistant\";\n\nexport type OpencodeMcpStatus = \"Connected\" | \"Disconnected\" | \"Failed\";\n\nexport type OpencodeWindowProps = ComponentProps<\"div\">;\n\nexport type OpencodeScrollAreaProps = ScrollAreaPrimitive.Root.Props;\n\nexport interface OpencodeLogoProps extends Omit<\n  ComponentProps<\"svg\">,\n  \"height\" | \"width\"\n> {\n  /** Multiplies the 234 by 42 base size. */\n  scale?: number;\n}\n\nexport interface OpencodeMessageProps extends ComponentProps<\"div\"> {\n  /** Rendered under an assistant reply. */\n  actions?: ReactNode;\n  from?: OpencodeMessageAuthor;\n  /** Rendered above an assistant reply, usually `OpencodeSources`. */\n  search?: ReactNode;\n}\n\nexport interface OpencodeTurnFooterProps extends ComponentProps<\"div\"> {\n  agent?: string;\n  /** Marks a turn that was stopped with Escape. */\n  interrupted?: boolean;\n  /** Wall time of the turn. Omit it while the turn is still running. */\n  duration?: string;\n  model?: string;\n}\n\nexport interface OpencodeActivityProps extends ComponentProps<\n  typeof Collapsible\n> {\n  /** Arguments after the label, like a path or a query. */\n  detail?: string;\n  /** Shown after a dot, like `1.4s`. */\n  duration?: string;\n  kind?: OpencodeActivityKind;\n  /** Defaults to `Thought`, `Read` or `Web Search` by kind. */\n  label?: string;\n  /** Renders a running thought as an animated `Thinking` line. */\n  pending?: boolean;\n  /** Freezes the thinking spinner. Defaults to the user's preference. */\n  reducedMotion?: boolean;\n}\n\nexport interface OpencodeProgressProps extends ComponentProps<\"span\"> {\n  /** Animates the scanner. When false every cell rests as a dot. */\n  active?: boolean;\n  /** Freezes the scanner. Defaults to the user's preference. */\n  reducedMotion?: boolean;\n}\n\nexport interface OpencodeComposerProps extends Omit<\n  ComponentProps<\"input\">,\n  \"children\" | \"className\" | \"type\"\n> {\n  agent?: string;\n  /**\n   * Shows the scanner and `esc interrupt` instead of the cwd. Enter does not\n   * send while busy and Escape calls `onStop`.\n   */\n  busy?: boolean;\n  className?: string;\n  /** Context usage on the status row, like `63.4K (6%)`. */\n  context?: string;\n  /** Working directory shown on the status row while idle. */\n  cwd?: string;\n  effort?: string;\n  inputClassName?: string;\n  model?: string;\n  /**\n   * Called with the trimmed text on Enter. An uncontrolled composer clears\n   * afterwards; with `value`, clear it yourself here.\n   */\n  onSend?: (text: string) => void;\n  /** Called on Escape while `busy`, to interrupt the running turn. */\n  onStop?: () => void;\n  provider?: string;\n}\n\nexport interface OpencodeMcpServer {\n  name: string;\n  status?: OpencodeMcpStatus;\n}\n\nexport interface OpencodeSidebarProps extends Omit<\n  ComponentProps<\"aside\">,\n  \"title\"\n> {\n  /** Git branch appended to the cwd. Its last segment is highlighted. */\n  branch?: string;\n  cwd?: string;\n  lsp?: ReactNode;\n  mcpOpen?: ComponentProps<typeof Collapsible>[\"open\"];\n  onMcpOpenChange?: ComponentProps<typeof Collapsible>[\"onOpenChange\"];\n  servers?: OpencodeMcpServer[];\n  spent?: string;\n  title?: ReactNode;\n  tokens?: string;\n  used?: string;\n  version?: string;\n}\n\nexport interface OpencodeSource {\n  domain: string;\n  title: string;\n  url?: string;\n}\n\nexport interface OpencodeSourcesLabels {\n  citedSources: (count: number) => string;\n  openSource: (title: string, domain: string) => string;\n}\n\nexport interface OpencodeSourcesProps extends Omit<\n  ComponentProps<typeof Collapsible>,\n  \"children\"\n> {\n  labels?: OpencodeSourcesLabels;\n  queries?: readonly string[];\n  reducedMotion?: boolean;\n  sequential?: boolean;\n  sources: readonly OpencodeSource[];\n}\n\nexport interface OpencodeDemoActivity {\n  body?: string;\n  detail?: string;\n  duration?: string;\n  id: string;\n  kind: OpencodeActivityKind;\n  label?: string;\n}\n\n/** A run of reply text: plain, `code` (green) or strong (orange). */\nexport type OpencodeDemoSpan = string | { code: string } | { strong: string };\n\nexport interface OpencodeDemoListItem {\n  id: string;\n  spans: OpencodeDemoSpan[];\n}\n\nexport type OpencodeDemoBlock =\n  | { heading: string; id: string }\n  | { id: string; items: OpencodeDemoListItem[] }\n  | { id: string; text: OpencodeDemoSpan[] };\n\nexport interface OpencodeDemoTurn {\n  activities: OpencodeDemoActivity[];\n  duration?: string;\n  id: string;\n  prompt: string;\n  reply: OpencodeDemoBlock[];\n}\n\nexport interface OpencodeDemoSession {\n  agent: string;\n  branch: string;\n  context: string;\n  cwd: string;\n  effort: string;\n  model: string;\n  provider: string;\n  servers: OpencodeMcpServer[];\n  spent: string;\n  title: string;\n  tokens: string;\n  used: string;\n  version: string;\n}\n\nexport type OpencodeTurnStatus =\n  | \"working\"\n  | \"streaming\"\n  | \"done\"\n  | \"interrupted\";\n\n/** A scripted answer the demo plays back when you send a prompt. */\nexport interface OpencodeReply {\n  /** Thoughts and tool calls, played in order before the reply. */\n  activities: OpencodeDemoActivity[];\n  reply: OpencodeDemoBlock[];\n}\n\nexport interface OpencodeChatActivity extends OpencodeDemoActivity {\n  /** A thought still thinking, shown with the spinner. */\n  pending?: boolean;\n  /** Still playing back. Dropped from the transcript when interrupted. */\n  running?: boolean;\n}\n\nexport interface OpencodeChatTurn {\n  activities: OpencodeChatActivity[];\n  duration?: string;\n  id: string;\n  prompt: string;\n  reply: OpencodeDemoBlock[];\n  status: OpencodeTurnStatus;\n}\n\nexport interface OpencodeChat {\n  busy: boolean;\n  send: (text: string) => Promise<void>;\n  stop: () => void;\n  turns: OpencodeChatTurn[];\n}\n",
      "type": "registry:lib",
      "target": "components/opencode/types/opencode.ts"
    },
    {
      "path": "registry/notra/opencode/hooks/use-opencode-frame.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nexport const useOpencodeFrame = (\n  frameCount: number,\n  intervalMs: number,\n  active: boolean\n): number => {\n  const [frame, setFrame] = useState(0);\n\n  useEffect(() => {\n    if (!active || frameCount < 2) {\n      return;\n    }\n    const timer = window.setInterval(() => {\n      setFrame((current) => (current + 1) % frameCount);\n    }, intervalMs);\n    return () => {\n      window.clearInterval(timer);\n      setFrame(0);\n    };\n  }, [active, frameCount, intervalMs]);\n\n  return active ? frame : 0;\n};\n",
      "type": "registry:hook",
      "target": "components/opencode/hooks/use-opencode-frame.ts"
    },
    {
      "path": "registry/notra/opencode/hooks/use-opencode-reduced-motion.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\nimport { OPENCODE_REDUCED_MOTION_QUERY } from \"../constants/opencode\";\n\nconst subscribe = (onChange: () => void) => {\n  const media = window.matchMedia(OPENCODE_REDUCED_MOTION_QUERY);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n};\n\nconst readPreference = () =>\n  window.matchMedia(OPENCODE_REDUCED_MOTION_QUERY).matches;\n\nconst readServerPreference = () => false;\n\nexport const useOpencodeReducedMotion = (override?: boolean): boolean => {\n  const prefersReduced = useSyncExternalStore(\n    subscribe,\n    readPreference,\n    readServerPreference\n  );\n  return override ?? prefersReduced;\n};\n",
      "type": "registry:hook",
      "target": "components/opencode/hooks/use-opencode-reduced-motion.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-opencode-bg": "var(--opencode-bg)",
      "color-opencode-panel": "var(--opencode-panel)",
      "color-opencode-sidebar": "var(--opencode-sidebar)",
      "color-opencode-fg": "var(--opencode-fg)",
      "color-opencode-muted": "var(--opencode-muted)",
      "color-opencode-subtle": "var(--opencode-subtle)",
      "color-opencode-blue": "var(--opencode-blue)",
      "color-opencode-orange": "var(--opencode-orange)",
      "color-opencode-green": "var(--opencode-green)",
      "color-opencode-logo-open": "var(--opencode-logo-open)",
      "color-opencode-logo-code": "var(--opencode-logo-code)",
      "color-opencode-logo-fill": "var(--opencode-logo-fill)",
      "font-opencode": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace"
    },
    "light": {
      "opencode-bg": "#ffffff",
      "opencode-panel": "#f5f5f5",
      "opencode-sidebar": "#fafafa",
      "opencode-fg": "#1a1a1a",
      "opencode-muted": "#6f6f6f",
      "opencode-subtle": "#e1e1e1",
      "opencode-blue": "#3b7dd8",
      "opencode-orange": "#c47a14",
      "opencode-green": "#2f8a4a",
      "opencode-logo-open": "#8a8a8a",
      "opencode-logo-code": "#1a1a1a",
      "opencode-logo-fill": "#dcdcdc"
    },
    "dark": {
      "opencode-bg": "#090909",
      "opencode-panel": "#191919",
      "opencode-sidebar": "#121212",
      "opencode-fg": "#e9e9e9",
      "opencode-muted": "#737373",
      "opencode-subtle": "#262626",
      "opencode-blue": "#3b91f9",
      "opencode-orange": "#fd9718",
      "opencode-green": "#4fd47b",
      "opencode-logo-open": "#737373",
      "opencode-logo-code": "#e9e9e9",
      "opencode-logo-fill": "#2a2a2a"
    }
  },
  "type": "registry:component"
}