Headless hooks
All five Pensieve data surfaces are available as plain React hooks that return raw data, loading state, and imperative actions. Use them when you want to build a custom UI on top of the Pensieve data layer without the bundled component chrome.
All hooks must be called inside a PensieveProvider.
usePensieveGraph
import { usePensieveGraph } from "@pensieve-ai/react";
import type { UsePensieveGraphArgs, UsePensieveGraphResult } from "@pensieve-ai/react";Loads one or more (database, graph) pairs in parallel via React Query, merges them into a unified node/edge set, and exposes expansion and search helpers.
function usePensieveGraph(args?: UsePensieveGraphArgs): UsePensieveGraphResultUsePensieveGraphArgs
| Field | Type | Default | Description |
|---|---|---|---|
graphs | Array<{ database?: string; graph: string }> | — | Explicit list of graph coordinates. |
discover | "all-databases" | — | Discover all graphs across all databases (catalog fan-out). Overrides graphs. |
realm | string | — | Passed to getOverview for realm-scoped fetches. |
limit | number | 800 | Max nodes per graph request. |
UsePensieveGraphResult
| Field | Type | Description |
|---|---|---|
nodes | GraphNode[] | Merged nodes from all loaded graphs, deduplicated by composite id. |
edges | GraphRelationship[] | Merged edges from all loaded graphs, deduplicated. |
stats | GraphStats | null | Aggregate counts (total nodes/edges, per-label and per-type breakdowns). |
namespaceCounts | Record<string, number> | Node count per database/graph namespace key. |
coords | GraphCoord[] | Resolved (database, graph) coordinates being loaded. |
progress | GraphProgress | { total, settled, hasAny } — per-namespace fetch progress. |
isLoading | boolean | True while discovery or any graph fetch is pending. |
isError | boolean | True when all fetches have settled and no data arrived. |
error | unknown | First error from any fetch, or null. |
expandNode | (compositeId: string) => Promise<void> | Fetch and append the neighbours of a node identified by its composite id (database/graph::nodeId). |
searchNodes | (text: string) => Promise<SearchHits> | Search nodes in the first loaded graph by text. |
refetch | () => void | Invalidate all graph query cache entries and refetch. |
Custom graph UI example
import { usePensieveGraph } from "@pensieve-ai/react";
function GraphStats() {
const { nodes, edges, isLoading, error } = usePensieveGraph({
discover: "all-databases",
});
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Error: {String(error)}</p>;
return (
<ul>
<li>{nodes.length} nodes</li>
<li>{edges.length} edges</li>
</ul>
);
}usePensieveQuery
import { usePensieveQuery } from "@pensieve-ai/react";
import type { PensieveQueryArgs, UsePensieveQueryResult } from "@pensieve-ai/react";Runs KQL or SQL queries via POST /v1/query (NDJSON streaming). Accumulates columns and rows as chunks arrive. An AbortController per execute() call allows mid-stream cancellation; unmount aborts automatically.
function usePensieveQuery(): UsePensieveQueryResultUsePensieveQueryResult
| Field | Type | Description |
|---|---|---|
columns | Column[] | Columns from the last/current query (name + kind). |
rows | Record<string, unknown>[] | Accumulated rows from the last/current query. |
isRunning | boolean | True while the stream is active. |
error | unknown | Error from the last execute(), or null. |
execute | (args: PensieveQueryArgs) => Promise<void> | Run a query. Aborts any previously running query. Resolves when the stream completes. |
cancel | () => void | Abort the current in-flight query. No-op if idle. |
PensieveQueryArgs
| Field | Type | Description |
|---|---|---|
database | string | Database to query. |
query | string | KQL or SQL query text. |
language | "kql" | "sql" | Query language. |
walMs | number? | Optional WAL durability hint. |
memBytes | number? | Optional memory budget hint. |
Custom query UI example
import { usePensieveQuery } from "@pensieve-ai/react";
function LiveQuery() {
const { rows, isRunning, execute, cancel } = usePensieveQuery();
return (
<div>
<button onClick={() => execute({ database: "prod", query: "errors | take 20", language: "kql" })}>
Run
</button>
{isRunning && <button onClick={cancel}>Cancel</button>}
<pre>{JSON.stringify(rows.slice(0, 5), null, 2)}</pre>
</div>
);
}usePensieveDiscover
import { usePensieveDiscover } from "@pensieve-ai/react";
import type { UsePensieveDiscoverResult } from "@pensieve-ai/react";Streaming discover search (POST /v1/explore/search) that yields typed frames (plan, source_progress, rows, histogram, source_done, done, etc.). Use this to build a custom log/event browsing UI.
function usePensieveDiscover(): UsePensieveDiscoverResultUsePensieveDiscoverResult
| Field | Type | Description |
|---|---|---|
frames | Frame[] | All frames received from the current/last search stream. |
isStreaming | boolean | True while the stream generator is running. |
error | unknown | Error from the last search(), or null. |
search | (req: SearchRequest) => Promise<void> | Start a new search. Aborts any running search first. |
abort | () => void | Abort the current in-flight stream. No-op if idle. |
Custom discover UI example (from examples/embed-demo headless view)
import { usePensieveDiscover } from "@pensieve-ai/react";
function DiscoverHeadless() {
const { frames, isStreaming, search, abort } = usePensieveDiscover();
const rows = frames
.filter((f) => f.type === "rows")
.flatMap((f) => f.rows ?? []);
return (
<div>
<button onClick={() => search({ query: "level == \"error\"", databases: ["prod"] })}>
Search errors
</button>
{isStreaming && <button onClick={abort}>Stop</button>}
<p>{rows.length} rows</p>
</div>
);
}usePensieveDashboards
import { usePensieveDashboards } from "@pensieve-ai/react";
import type { UsePensieveDashboardsResult } from "@pensieve-ai/react";React Query wrapper over client.dashboards.listDashboards(). Exposes the dashboard list and a getDashboard(id) helper for on-demand detail loads. CRUD mutations (create, update, delete) are intentionally not in this hook — drive them directly via usePensieveClient().dashboards.*.
function usePensieveDashboards(): UsePensieveDashboardsResultUsePensieveDashboardsResult
| Field | Type | Description |
|---|---|---|
dashboards | Dashboard[] | All dashboards for the current database. |
isLoading | boolean | True while the list is loading. |
error | unknown | Error from the list fetch, or null. |
getDashboard | (id: string) => Promise<DashboardWithPanels> | Fetch a single dashboard with panels. Uses the React Query cache when available. |
refetch | () => void | Invalidate and refetch the dashboard list. |
usePensieveAgent
import { usePensieveAgent } from "@pensieve-ai/react";
import type { UsePensieveAgentArgs, UsePensieveAgentResult, AgentMessage, AgentStatus } from "@pensieve-ai/react";Headless agent hook over POST /v1/agent/ask (SSE stream, Vercel AI UI Message Stream v1 protocol). Manages the message list, streaming state, and multi-turn session resume internally.
function usePensieveAgent(args?: UsePensieveAgentArgs): UsePensieveAgentResultUsePensieveAgentArgs
| Field | Type | Description |
|---|---|---|
database | string? | Database sent with each ask request. Defaults to the transport's database. |
includeThinking | boolean? | Request extended thinking tokens (model-dependent). |
UsePensieveAgentResult
| Field | Type | Description |
|---|---|---|
messages | AgentMessage[] | All messages in the conversation (user + assistant). |
status | AgentStatus | "idle" | "streaming" | "error" |
send | (text: string) => Promise<void> | Send a user message and stream the assistant reply. Aborts any prior in-flight stream. |
stop | () => void | Abort the current stream. No-op if idle. |
AgentMessage
interface AgentMessage {
id: string;
role: "user" | "assistant";
text: string; // completed text
streamingText?: string; // partially accumulated while streaming
}Custom agent UI example
import { usePensieveAgent } from "@pensieve-ai/react";
function ChatWidget() {
const { messages, status, send } = usePensieveAgent();
const [input, setInput] = useState("");
return (
<div>
{messages.map((m) => (
<p key={m.id}><strong>{m.role}</strong>: {m.streamingText ?? m.text}</p>
))}
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button disabled={status === "streaming"} onClick={() => { send(input); setInput(""); }}>
Send
</button>
</div>
);
}usePensieveCapabilities
import { usePensieveCapabilities } from "@pensieve-ai/react";React Query wrapper over GET /v1/capabilities. Returns the server's feature flag set. Used internally by CapabilityGate (e.g. to guard PensieveAgentChat).
function usePensieveCapabilities(): UseQueryResult<Capabilities>Capabilities is a flat object with boolean fields (mode, data_sources, credentials, oauth, saved_views, users_admin, explore_live). staleTime is 5 minutes.
usePensieveClient
import { usePensieveClient } from "@pensieve-ai/react";
import type { PensieveContextValue } from "@pensieve-ai/react";Returns the PensieveClient from context. Use this to call the full client API (graph, query, discover, dashboards, data sources, etc.) from any component inside the provider.
import { usePensieveClient } from "@pensieve-ai/react";
function ListDatabases() {
const client = usePensieveClient();
useEffect(() => {
client.catalog.fetchSchema().then((schema) => {
console.log(schema.databases.map((d) => d.name));
});
}, [client]);
}