AgentContextDebuggerTab.tsx5.5 KBView on GitHub
import { Hash, Check, Copy, ChevronDown, ChevronRight } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { useCedarStore } from '@/modules/store';
import React, { useState, useMemo } from 'react';
import { JsonTreeView } from './JsonTreeView';

interface AgentContextTabProps {
  onCopy: (text: string, id: string) => void;
  copiedId: string | null;
}

export const AgentContextDebuggerTab: React.FC<AgentContextTabProps> = ({ onCopy, copiedId }) => {
  const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set());

  // Read the compiled context that gets sent to the LLM
  const buildMergedContext = useCedarStore((s) => s.buildMergedContextForMailSendMessage);
  const additionalContext = useCedarStore((s) => s.additionalContext);
  const excludedManualContextKeys = useCedarStore((s) => s.excludedManualContextKeys);
  const activeView = useCedarStore((s) => s.getActiveView());
  const activeCanvasId = useCedarStore((s) => s.activeCanvasId);
  const activeConversationId = useCedarStore((s) => s.activeConversationId);

  const mergedContext = useMemo(() => buildMergedContext(), [
    buildMergedContext,
    additionalContext,
    excludedManualContextKeys,
    activeView,
    activeCanvasId,
    activeConversationId,
  ]);

  const toggleExpanded = (key=[redacted] => {
    setExpandedKeys((prev) => {
      const next = new Set(prev);
      if (next.has(key)) {
        next.delete(key);
      } else {
        next.add(key);
      }
      return next;
    });
  };

  const getPreview = (value: unknown): string => {
    if (value === null) return 'null';
    if (value === undefined) return 'undefined';
    if (typeof value === 'string')
      return `"${value.substring(0, 50)}${value.length > 50 ? '...' : ''}"`;
    if (typeof value === 'number' || typeof value === 'boolean') return String(value);
    if (Array.isArray(value)) return `Array(${value.length})`;
    if (typeof value === 'object') return `Object(${Object.keys(value as Record<string, unknown>).length} keys)`;
    return String(value);
  };

  const entries = Object.entries(mergedContext);

  return (
    <div className="h-full space-y-1 overflow-y-auto p-2">
      {/* Header with view stack info */}
      <div className="mb-2 rounded-lg border border-blue-200 bg-blue-50 p-2 dark:border-blue-800 dark:bg-blue-950">
        <div className="text-xs font-medium text-blue-700 dark:text-blue-300">
          Active View: {activeView ?? 'none'}
        </div>
        <div className="mt-0.5 text-xs text-blue-600 dark:text-blue-400">
          Excluded keys: {excludedManualContextKeys.size > 0 ? Array.from(excludedManualContextKeys).join(', ') : 'none'}
        </div>
      </div>

      {entries.length === 0 ? (
        <div className="py-4 text-center text-xs text-gray-500 dark:text-gray-400">
          No agent context available
        </div>
      ) : (
        entries.map(([key, value]) => {
          const isExpanded = expandedKeys.has(key);
          const copyId = `ctx-${key}`;

          return (
            <div
              key=[redacted]
              className="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-950"
            >
              <div
                className={`flex cursor-pointer items-center justify-between p-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-900/80 ${
                  isExpanded ? 'rounded-t-lg' : 'rounded-lg'
                }`}
                onClick={() => toggleExpanded(key)}
              >
                <div className="flex flex-1 items-center gap-1.5">
                  <Hash className="h-3 w-3 text-gray-500" />
                  <span className="font-mono text-xs font-medium">{key}</span>
                  {!isExpanded && (
                    <span className="ml-1 text-xs text-gray-500 dark:text-gray-500">
                      {getPreview(value)}
                    </span>
                  )}
                </div>
                <div className="flex items-center gap-1">
                  <button
                    onClick={(e) => {
                      e.stopPropagation();
                      onCopy(JSON.stringify(value, null, 2), copyId);
                    }}
                    className="rounded p-0.5 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
                  >
                    {copiedId === copyId ? (
                      <Check className="h-3 w-3 text-green-600" />
                    ) : (
                      <Copy className="h-3 w-3" />
                    )}
                  </button>
                  {isExpanded ? (
                    <ChevronDown className="h-3 w-3" />
                  ) : (
                    <ChevronRight className="h-3 w-3" />
                  )}
                </div>
              </div>

              <AnimatePresence>
                {isExpanded && (
                  <motion.div
                    initial={{ height: 0, opacity: 0 }}
                    animate={{ height: 'auto', opacity: 1 }}
                    exit={{ height: 0, opacity: 0 }}
                    transition={{ duration: 0.2 }}
                    className="overflow-hidden"
                  >
                    <div className="rounded-b-lg p-3 pt-0">
                      <JsonTreeView data={value} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-[50vh]" />
                    </div>
                  </motion.div>
                )}
              </AnimatePresence>
            </div>
          );
        })
      )}
    </div>
  );
};