Skip to main content

Overview

The Observatory widget provides a real-time, in-browser visualization of your AI agent’s execution. It displays runs, steps, and tool calls as they happen, making it easy to debug and monitor your agent during development. The widget is built with Preact and uses Shadow DOM for style isolation, ensuring it won’t conflict with your application’s styles.
The widget is designed for local development and requires local mode to be enabled.

Installation

1

Install the package

npm install @contextcompany/widget
2

Choose your integration method

You can use the widget in two ways:Option 1: Auto-init script (Recommended)
app/layout.tsx
import Script from "next/script";

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        {process.env.NODE_ENV === "development" && (
          <Script src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" />
        )}
      </head>
      <body>{children}</body>
    </html>
  );
}
Option 2: Manual initialization
import { initWidget } from "@contextcompany/widget";

initWidget({
  enabled: true,
  onMount: () => console.log("Widget mounted")
});

How It Works

The widget architecture consists of several key components:

Shadow DOM Isolation

The widget uses Shadow DOM to prevent style conflicts:
const rootContainer = document.createElement("div");
rootContainer.id = "tcc-widget-shadow-host";

const shadowRoot = rootContainer.attachShadow({ mode: "open" });

// Inject compiled Tailwind styles
const styleEl = document.createElement("style");
styleEl.textContent = styles;
shadowRoot.appendChild(styleEl);

WebSocket Connection

The widget automatically discovers and connects to the local WebSocket server:
  1. Port Scanning: Scans preferred ports (8081, 3001, 3002, etc.)
  2. Connection: Connects to the first available server
  3. Initial State: Receives the current data store on connection
  4. Real-time Updates: Subscribes to new runs, steps, and tool calls

State Management

The widget uses Preact signals for reactive state:
// Widget state signals
widgetPositionSignal    // { x: number, y: number }
widgetDimensionsSignal  // { width: number, height: number }
widgetExpandedSignal    // boolean
widgetDockedSignal      // 'left' | 'right' | 'top' | 'bottom' | null
hasUnseenFailuresSignal // boolean

Usage

Basic Setup

With the auto-init script, the widget appears automatically in development:
app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        {process.env.NODE_ENV === "development" && (
          <Script src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" />
        )}
      </head>
      <body>{children}</body>
    </html>
  );
}

Manual Control

For more control, use the programmatic API:
import { initWidget, cleanup } from "@contextcompany/widget";

// Initialize widget
const cleanupFn = initWidget({
  enabled: true,
  onMount: () => {
    console.log("Widget is ready");
  }
});

// Later: cleanup when done
if (cleanupFn) cleanupFn();

// Or use the global cleanup
cleanup();

Conditional Loading

Only load the widget in specific environments:
{/* Only in development */}
{process.env.NODE_ENV === "development" && (
  <Script src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" />
)}

{/* Only for specific users/teams */}
{isInternalUser && (
  <Script src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" />
)}

{/* With feature flag */}
{enableObservatory && (
  <Script src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" />
)}

Customization

Debug Mode

Enable debug logging to see widget internals:
<Script 
  src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" 
  data-debug="true"
/>
This logs:
  • WebSocket connection attempts
  • Port scanning results
  • Data synchronization events

Position and Size

The widget is draggable and can be docked to screen edges:
  • Drag: Click and drag the widget to reposition
  • Snap to corner: Release near a corner to snap
  • Dock to edge: Drag to screen edge to dock
  • Expand/Collapse: Click to toggle the detailed view

Widget State

The widget state is persisted in localStorage:
// Position
localStorage.getItem('tcc-widget-position')
// { x: number, y: number }

// Dimensions
localStorage.getItem('tcc-widget-dimensions')
// { width: number, height: number }

// Docked position
localStorage.getItem('tcc-widget-docked')
// 'left' | 'right' | 'top' | 'bottom' | null

Features

Run Viewer

The widget displays:
  • Runs List: All agent runs with status and duration
  • Steps: Individual steps within each run
  • Tool Calls: Tool invocations with inputs and outputs
  • Failures: Quick access to failed runs

Real-time Updates

New data appears instantly as your agent executes:
// Widget subscribes to local exporter
tccLocalExporter.subscribe((newItems) => {
  // Update UI with new runs, steps, tool calls
  updateStore(newItems);
});

Failure Notifications

The widget highlights when failures occur:
  • Visual indicator: Red background on widget icon
  • Badge: Failure count badge
  • Quick access: Click to jump to failures list

Data Inspection

Expanded view shows detailed information:
  • Metadata: Trace ID, span ID, parent relationships
  • Attributes: All span attributes and metadata
  • Events: Span events with timestamps
  • Status: Success/error status with codes
  • Timing: Duration and timestamps

API Reference

initWidget(options)

Initialize the widget programmatically.
interface WidgetOptions {
  enabled?: boolean;      // Enable/disable widget (default: true)
  onMount?: () => void;   // Callback when widget mounts
}

const cleanup = initWidget(options);
Returns: Cleanup function to remove the widget

cleanup()

Remove the widget from the DOM.
import { cleanup } from "@contextcompany/widget";

cleanup();

Global API

When using the auto script, a global API is available:
// Access via window
window.TCCWidget.init({ enabled: true });
window.TCCWidget.cleanup();

WebSocket Protocol

The widget communicates with the local server using WebSocket:

Server → Client Messages

Initial Store
{
  "type": "initialStore",
  "data": {
    "[traceId]": {
      "run": { /* UIRun */ },
      "steps": [ /* UIStep[] */ ],
      "toolCalls": [ /* UIToolCall[] */ ]
    }
  }
}
New Items
{
  "type": "newItems",
  "data": {
    "runs": [ /* UIRun[] */ ],
    "steps": [ /* UIStep[] */ ],
    "toolCalls": [ /* UIToolCall[] */ ]
  }
}

Client → Server Messages

Anonymous Events
{
  "event": "widget_expand_event",
  "action": "expand"
}

Troubleshooting

Widget Not Appearing

1

Check Script Tag

Verify the script is loaded in your layout:
<Script src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" />
2

Check WebSocket Connection

Open browser console and look for:
Found TCC WebSocket server on port: 8081
If you see an error, ensure local mode is enabled in instrumentation.
3

Enable Debug Mode

Add debug attribute to see connection attempts:
<Script 
  src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" 
  data-debug="true"
/>

Widget Stuck Loading

  • Ensure your Next.js app is running with instrumentation enabled
  • Check that local mode is active: registerOTelTCC({ local: true })
  • Verify no firewall is blocking localhost WebSocket connections

Style Conflicts

The widget uses Shadow DOM to prevent style conflicts, but if you experience issues:
  1. Check for global styles affecting Shadow DOM (rare)
  2. Ensure you’re using a modern browser with Shadow DOM support
  3. Check for !important rules that might leak into Shadow DOM

Performance Issues

If the widget impacts performance:
  • Limit the number of concurrent runs displayed
  • Use the cleanup function when navigating away
  • Consider disabling in production environments

Best Practices

Always gate the widget behind environment checks:
{process.env.NODE_ENV === "development" && (
  <Script src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" />
)}
If initializing manually, always cleanup:
const cleanup = initWidget();

// Later
if (cleanup) cleanup();
Enable debug mode when first setting up to diagnose connection issues:
<Script 
  src="https://unpkg.com/@contextcompany/widget/dist/auto.global.js" 
  data-debug="true"
/>

Next Steps

Local Mode

Learn how to set up local-first observability

OpenTelemetry

Understand the OpenTelemetry integration

Build docs developers (and LLMs) love