npm package: @perstack/react
Description: React hooks and utilities for Perstack integration
Author: Wintermute Technologies, Inc.
Latest version: 0.0.72

# @perstack/react

Reusable React hooks and utilities for Perstack integration. This is the shared React library layer that provides framework-agnostic hooks consumed by both TUI and web applications.

## Installation

```bash
bun add @perstack/react
```

## Usage

### useRun

The main hook for managing Perstack run state. It processes events into:

- **activities** - Accumulated activities from RunEvent (append-only)
- **streaming** - Current streaming state for real-time display

```tsx
import { useRun } from "@perstack/react"

function ExpertRunner() {
  const { activities, streaming, isComplete, addEvent, appendHistoricalEvents } = useRun()

  // Add events from your event source
  useEffect(() => {
    const eventSource = new EventSource("/api/events")
    eventSource.onmessage = (e) => {
      addEvent(JSON.parse(e.data))
    }
    return () => eventSource.close()
  }, [addEvent])

  return (
    <div>
      {/* Show streaming content (grouped by run for parallel execution) */}
      {Object.entries(streaming.runs).map(([runId, run]) => (
        run.isReasoningActive && (
          <div key={runId}>[{run.expertKey}] Reasoning: {run.reasoning}</div>
        )
      ))}

      {/* Show accumulated activities */}
      <ActivityLog activities={activities} />

      {isComplete && <div>Run complete!</div>}
    </div>
  )
}
```

### useJobStream

Stream events for a single job. Wraps `useRun` with automatic stream connection management.

```tsx
import { useJobStream } from "@perstack/react"

function JobViewer({ jobId }: { jobId: string }) {
  const { activities, streaming, latestActivity, isConnected, error } = useJobStream({
    jobId,
    connect: (jobId, signal) => fetchStream(`/api/jobs/${jobId}/events`, signal),
  })

  return (
    <div>
      {isConnected && <span>Connected</span>}
      {error && <span>Error: {error.message}</span>}
      <ActivityLog activities={activities} />
    </div>
  )
}
```

### useJobStreams

Track multiple jobs simultaneously with lightweight summaries (latest activity only).

```tsx
import { useJobStreams } from "@perstack/react"

function JobList({ jobIds }: { jobIds: string[] }) {
  const states = useJobStreams({
    jobs: jobIds.map((id) => ({ id, enabled: true })),
    connect: (jobId, signal) => fetchStream(`/api/jobs/${jobId}/events`, signal),
  })

  return (
    <ul>
      {jobIds.map((id) => {
        const state = states.get(id)
        return (
          <li key={id}>
            {id}: {state?.isConnected ? "Connected" : "Disconnected"}
            {state?.latestActivity && ` - ${state.latestActivity.type}`}
          </li>
        )
      })}
    </ul>
  )
}
```

### Utility Functions

For advanced use cases, you can use the utility functions directly:

```tsx
import {
  createInitialActivityProcessState,
  processRunEventToActivity,
  toolToActivity,
  groupActivitiesByRun,
} from "@perstack/react"

// Create processing state
const state = createInitialActivityProcessState()

// Process RunEvent into Activity
const activities = []
processRunEventToActivity(state, event, (activity) => activities.push(activity))

// Group activities by run ID
const grouped = groupActivitiesByRun(activities)
```

## API

### useRun()

Returns an object with:

- `activities`: Array of `ActivityOrGroup` representing completed actions (append-only)
- `streaming`: Current `StreamingState` for real-time display
- `isComplete`: Whether the run is complete
- `eventCount`: Total number of processed events
- `addEvent(event)`: Add a new event to process
- `appendHistoricalEvents(events)`: Bulk load historical events
- `clearStreaming()`: Clear streaming state

**Note:** Activities are append-only and never cleared. This is required for compatibility with Ink's `<Static>` component.

### useJobStream(options)

Streams events for a single job. Parameters:

- `jobId`: Job ID to stream (or `null` to disable)
- `connect`: `StreamConnector` function `(jobId, signal) => AsyncIterable<PerstackEvent>`
- `enabled`: Whether to connect (default: `true`)

Returns:

- `activities`: Array of `ActivityOrGroup`
- `streaming`: Current `StreamingState`
- `latestActivity`: Most recent activity (or `null`)
- `isConnected`: Whether the stream is active
- `error`: Connection error (or `null`)

### useJobStreams(options)

Tracks multiple jobs with lightweight summaries. Parameters:

- `jobs`: Array of `{ id: string; enabled: boolean }`
- `connect`: `StreamConnector` function

Returns a `Map<string, JobStreamSummary>` where each summary contains:

- `latestActivity`: Most recent activity for the job
- `isConnected`: Whether the stream is active

## Types

### StreamingState

Real-time streaming state, organized by run ID to support parallel execution:

```typescript
type PerRunStreamingState = {
  expertKey: string
  reasoning?: string
  runResult?: string
  isReasoningActive?: boolean
  isRunResultActive?: boolean
}

type StreamingState = {
  runs: Record<string, PerRunStreamingState>
}
```

When multiple experts run in parallel (e.g., parallel delegation), each run's streaming content is tracked separately by its `runId`.

## License

Apache-2.0