Compare commits

...
Author SHA1 Message Date
MervinPraisonandClaude Opus 4.8 a3568bdb31 fix: normalize approval created_at epoch to ISO in agent-UI adapter
The /api/approvals payloads emit created_at as epoch seconds (time.time()),
but mapApprovalToCardProps used asString(), silently dropping the value for
real payloads so ApprovalPromptModel.createdAt was always undefined. Coerce
numeric epochs to an ISO-8601 string (ISO strings still pass through) and
align the unit test with the real wire shape.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 14:23:52 +00:00
MervinPraisonandClaude Opus 4.8 a3a5e06225 test: make agent-UI adapter tests runnable via npm test (#294)
The adapter unit tests use Node's built-in runner, but the documented
command (`node --test src/agent-ui/`) fails on Node 22 — it neither
strips TypeScript types nor discovers the files, so the suite never ran.

- Add a `test` script to src/frontend that runs the suite with
  `--experimental-strip-types` (8/8 passing on Node 22).
- Fix the misleading run instructions in the test header.

Closes the "unit tests" acceptance criterion of #294 with an executable
suite. Purely additive; no runtime code changed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 14:07:50 +00:00
MervinPraisonandClaude Opus 4.8 5b191a439b fix: add BeautifulUI adapter boundary + upstream pin policy (fixes #294)
Introduces the agent-UI adapter layer so BeautifulUI can be vendored
without leaking upstream types/paths across the frontend.

- src/frontend/src/agent-ui/contracts.ts: stable PraisonAIUI view models
  (AgentStreamEvent, ToolCallChipModel, ApprovalPromptModel)
- src/frontend/src/agent-ui/adapter.ts: pure mappers mapChatEventToUi()
  and mapApprovalToCardProps() from WS frames / approvals REST payloads
- src/frontend/src/agent-ui/adapter.test.ts: dependency-free unit tests
  (node:test, mock events -> props); 8 tests pass via `node --test`
- docs/frontend/: ADR (vendoring, adapter boundaries, folder layout,
  default-off feature flag + fallback) and upstream SHA/pin record
- mkdocs.yml: nav entries for the new frontend docs

Boundary rule: BeautifulUI prop types are imported only inside
src/agent-ui/; consumers depend on contracts.ts, keeping the vendored
implementation swappable. No new runtime deps; adapter is not yet
mounted (migration is #296/#297, CI gate is #295).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-23 13:47:39 +00:00
MervinPraisonandCursor 3058886312 chore: bump automation hub to v1.1.10 for merge gate fix
Co-authored-by: Cursor <[email protected]>
2026-09-22 15:30:07 +01:00
Mervin Praison 70d8610733 Merge pull request #291 from MervinPraison/claude/issue-290-20260916
docs: add SECURITY.md with private disclosure policy
2026-09-17 10:37:03 +01:00
MervinPraisonandClaude Opus 4.8 2797667e04 docs: add SECURITY.md with private disclosure policy (fixes #290)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-16 16:28:01 +00:00
16 changed files with 564 additions and 17 deletions

No files matched your search

+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
permissions:
issues: write
pull-requests: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.8
hub_ref: v1.1.10
+2 -2
View File
@@ -16,8 +16,8 @@ permissions:
jobs:
recover:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.8
hub_ref: v1.1.10
pr_number: ${{ github.event.inputs.pr_number || '' }}
+2 -2
View File
@@ -25,8 +25,8 @@ permissions:
jobs:
detect-and-trigger:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.8
hub_ref: v1.1.10
pr_number: ${{ github.event.inputs.pr_number || '' }}
+4 -4
View File
@@ -43,9 +43,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].8
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].10
with:
hub_ref: v1.1.8
hub_ref: v1.1.10
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
@@ -87,9 +87,9 @@ jobs:
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.8
hub_ref: v1.1.10
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.8
hub_ref: v1.1.10
+2 -2
View File
@@ -25,9 +25,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].8
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].10
with:
hub_ref: v1.1.8
hub_ref: v1.1.10
- name: Detect conflicting PRs and trigger Claude
uses: actions/github-script@v7
with:
+2 -2
View File
@@ -13,7 +13,7 @@ permissions:
jobs:
sync:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
secrets: inherit
with:
hub_ref: v1.1.8
hub_ref: v1.1.10
+37
View File
@@ -0,0 +1,37 @@
# Security Policy
We take the security of PraisonAIUI seriously and appreciate responsible
disclosure of vulnerabilities.
## Supported Versions
Security fixes are applied to the latest released version of PraisonAIUI.
Please make sure you are on the most recent release before reporting.
## Reporting a Vulnerability
**Please do not open a public issue for security vulnerabilities.**
Report privately through GitHub's **Private Vulnerability Reporting**:
1. Go to the [Security tab](https://github.com/MervinPraison/PraisonAIUI/security)
of this repository.
2. Click **"Report a vulnerability"** to open a private advisory.
3. Include as much detail as you can:
- Affected component(s) and version.
- Steps to reproduce.
- Impact assessment.
- Any suggested fix or mitigation.
If you are unable to use private reporting, you may open a public issue that
contains **no technical specifics** and requests a private channel, and a
maintainer will follow up.
## What to Expect
- We aim to acknowledge new reports within a few business days.
- We will keep you informed as we investigate and work on a fix.
- Once a fix is released, we are happy to credit you in the advisory (unless
you prefer to remain anonymous).
Thank you for helping keep PraisonAIUI and its users safe.
+35
View File
@@ -0,0 +1,35 @@
# BeautifulUI upstream pin record
BeautifulUI is vendored (shadcn-style registry copy), **not** an npm dependency.
This file is the single source of truth for what upstream revision is vendored.
Update it in the **same PR** that adds or bumps any vendored component, and make
sure the frontend CI gate (issue #295) passes.
See [beautifului-adapter-architecture.md](./beautifului-adapter-architecture.md)
for the adapter boundary and folder layout.
## Pin
| Field | Value |
|-------|-------|
| Upstream repo | _TBD — record on first vendor_ |
| Git SHA | _TBD_ |
| Vendored on (date) | _TBD_ |
| Registry base URL | _TBD_ |
| License | MIT (kept in-tree) |
## Vendored components
Record one row per component the first time it is copied in.
| Component | Registry URL | SHA | Vendored path | Date |
|-----------|--------------|-----|---------------|------|
| _(none yet)_ | | | | |
## Upgrade checklist
1. Update the SHA / date / registry rows above.
2. Re-copy changed components into `src/frontend/src/agent-ui/vendors/beautifului/`.
3. Keep all upstream imports inside `src/agent-ui/` (adapter boundary rule).
4. Run the frontend build + adapter unit tests (frontend CI gate, #295).
5. Do **not** add a `beautiful-ui` entry to `package.json` dependencies.
@@ -0,0 +1,102 @@
# ADR: BeautifulUI adapter layer & upstream pin policy
**Status:** Accepted · **Issue:** [#294](https://github.com/MervinPraison/PraisonAIUI/issues/294)
## Context
BeautifulUI is **not** a semver npm library — it is distributed as shadcn-style
registry components that are copied into the consuming repo. PraisonAIUI must
integrate it without leaking upstream types or import paths across the codebase,
and without breaking when either side updates.
## Decision
Introduce a single **adapter boundary** between transport payloads and the UI,
and vendor BeautifulUI components behind that boundary.
```
Agent APIs (Python) ──► event bus / WS messages
│
▼
Agent-UI adapter (src/agent-ui/adapter.ts)
(stable PraisonAIUI contracts)
│
▼
@praisonaiui/react re-exports
(BeautifulUI vendored components)
│
▼
Dashboard / chat mount points (React islands, then full shell)
```
### 1. Folder layout
```
src/frontend/src/agent-ui/
├── contracts.ts # Stable PraisonAIUI view models (the ONLY types consumers import)
├── adapter.ts # Pure mappers: wire payload -> contracts
├── adapter.test.ts # Unit tests (mock events -> props), node:test
├── index.ts # Barrel: contracts + adapters + (later) vendored re-exports
└── vendors/
└── beautifului/ # Vendored BeautifulUI components (added during migration)
```
**Rule:** BeautifulUI / upstream prop types must **never** be imported outside
`src/agent-ui/`. Consumers depend on `contracts.ts` only, so the vendored
implementation can be swapped later without touching call sites.
### 2. Adapter contracts
Internal, transport-agnostic types live in `contracts.ts`:
- `AgentStreamEvent` — normalized form of the WebSocket frames handled in
`chat.js` (`run_started`, `run_content`, `tool_call_started`, …). Unknown
frame `type`s degrade to `kind: 'unknown'` with `rawType` preserved instead
of throwing, so new upstream events never break the UI.
- `ToolCallChipModel` — tool-call chip view model.
- `ApprovalPromptModel` — approval card view model, mapped from the
`/api/approvals/pending` REST payload.
Mappers (`adapter.ts`): `mapChatEventToUi(frame)` and
`mapApprovalToCardProps(item)`. Both are **pure** and unit tested.
### 3. Vendoring & pin policy
| Mechanism | Purpose |
|-----------|---------|
| [`BEAUTIFULUI_UPSTREAM.md`](./BEAUTIFULUI_UPSTREAM.md) | Record git SHA, date, registry base URL for every vendored component |
| shadcn registry URLs | Per-component install, pinned in the upstream doc |
| **No** `dependencies.beautiful-ui` in `package.json` | Avoid a phantom npm package |
**Upgrade rule:** bump upstream **only** via a PR that updates
`BEAUTIFULUI_UPSTREAM.md` and passes the frontend CI gate (issue #295). Icons
stay on `lucide-react`; do not vendor paid Central icons.
### 4. Feature flag (default **off**)
Integration is gated so the existing vanilla-JS chat/approvals plugins remain
the fallback until migration milestones (#296, #297) complete.
- **Flag:** `AIUI_BEAUTIFULUI` (build-time env / dashboard config JSON).
- **Default:** off — vanilla JS renders; the adapter is imported but not mounted.
- **On:** React islands mount using `src/agent-ui` contracts.
- **Fallback:** if a vendored component fails to load, the flag is treated as
off and the vanilla path renders. No user-facing regression when off.
### 5. Design tokens
Scope agent-UI primitives under an `.aiui-agent-ui { … }` CSS layer (or extend
the existing shadcn presets in `themes.py`) so vendored component tokens cannot
collide with the global dashboard theme.
### 6. Deprecation / upstream abandonment
Vendored code stays MIT-licensed in-tree. Because consumers depend only on the
`contracts.ts` view models, a future first-party implementation can satisfy the
same contracts and replace the vendored components with no call-site churn.
## Consequences
- One place (`src/agent-ui/`) owns the upstream coupling.
- Adapters are testable without a browser or the transport layer.
- The default-off flag keeps the migration reversible at every step.
+3
View File
@@ -95,6 +95,9 @@ nav:
- Templates & Layouts: concepts/templates.md
- Slots & Components: concepts/slots.md
- Providers: concepts/providers.md
- Frontend:
- BeautifulUI Adapter: frontend/beautifului-adapter-architecture.md
- BeautifulUI Upstream Pin: frontend/BEAUTIFULUI_UPSTREAM.md
- Features:
- Dashboard: features/dashboard.md
- Gateway Chat: features/gateway-chat.md
+2 -1
View File
@@ -29,7 +29,8 @@
"build": "tsc -b && vite build",
"build:lib": "tsc -b && vite build --mode lib",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "node --experimental-strip-types --test \"src/agent-ui/**/*.test.ts\""
},
"keywords": [
"react",
+104
View File
@@ -0,0 +1,104 @@
/**
* Unit tests for the Agent-UI adapter (mock events -> stable contracts).
*
* Dependency-free: uses the Node built-in test runner so it needs no extra
* toolchain. Run with `npm test` (from src/frontend), which invokes the Node
* test runner with `--experimental-strip-types` so the TypeScript types are
* stripped on Node 22+. The frontend CI gate (see issue #295) wires this in.
*/
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { mapApprovalToCardProps, mapChatEventToUi } from './adapter.ts'
test('mapChatEventToUi normalizes a streaming token frame', () => {
const event = mapChatEventToUi({
type: 'run_content',
token: 'Hello',
agent_name: 'Researcher',
session_id: 's1',
run_id: 'r1',
})
assert.equal(event.kind, 'run-token')
assert.equal(event.text, 'Hello')
assert.equal(event.agentName, 'Researcher')
assert.equal(event.sessionId, 's1')
assert.equal(event.runId, 'r1')
})
test('mapChatEventToUi maps a started tool call to a running chip', () => {
const event = mapChatEventToUi({
type: 'tool_call_started',
name: 'search',
description: '🔎 Searching the web',
icon: '🔎',
step_number: 2,
tool_call_id: 'tc-1',
})
assert.equal(event.kind, 'tool-call')
assert.deepEqual(event.toolCall, {
id: 'tc-1',
label: '🔎 Searching the web',
icon: '🔎',
stepNumber: 2,
status: 'running',
error: undefined,
})
})
test('mapChatEventToUi marks completed tool calls as done', () => {
const event = mapChatEventToUi({ type: 'tool_call_completed', name: 'search' })
assert.equal(event.toolCall?.status, 'done')
// Falls back to the tool name for id and label when unspecified.
assert.equal(event.toolCall?.id, 'search')
assert.equal(event.toolCall?.label, 'search')
})
test('mapChatEventToUi treats team_* frames the same as their solo variant', () => {
assert.equal(mapChatEventToUi({ type: 'team_run_content', token: 'x' }).kind, 'run-token')
assert.equal(mapChatEventToUi({ type: 'team_run_error', error: 'boom' }).kind, 'run-error')
})
test('mapChatEventToUi preserves unknown frame types without throwing', () => {
const event = mapChatEventToUi({ type: 'future_event', foo: 'bar' })
assert.equal(event.kind, 'unknown')
assert.equal(event.rawType, 'future_event')
})
test('mapChatEventToUi defaults run errors to a readable message', () => {
const event = mapChatEventToUi({ type: 'run_error' })
assert.equal(event.kind, 'run-error')
assert.equal(event.error, 'Unknown error')
})
test('mapApprovalToCardProps maps a pending approval item', () => {
const card = mapApprovalToCardProps({
id: 'a-1',
tool_name: 'delete_file',
agent_name: 'Ops',
risk_level: 'high',
risk_icon: '🔥',
description: 'Deletes a file',
arguments: { path: '/tmp/x' },
// Approvals API emits epoch seconds (time.time()), not an ISO string.
created_at: 1758585600,
})
assert.equal(card.id, 'a-1')
assert.equal(card.toolName, 'delete_file')
assert.equal(card.agentName, 'Ops')
assert.equal(card.riskLevel, 'high')
assert.equal(card.riskIcon, '🔥')
assert.equal(card.argumentsJson, '{\n "path": "/tmp/x"\n}')
// Epoch seconds are normalized to an ISO-8601 string for display.
assert.equal(card.createdAt, '2025-09-23T00:00:00.000Z')
})
test('mapApprovalToCardProps falls back for missing/invalid fields', () => {
const card = mapApprovalToCardProps({ id: 'a-2', risk_level: 'nonsense' })
assert.equal(card.toolName, 'unknown')
assert.equal(card.agentName, 'Unknown')
assert.equal(card.riskLevel, 'medium')
assert.equal(card.riskIcon, '⚠️')
assert.equal(card.argumentsJson, '{}')
})
+177
View File
@@ -0,0 +1,177 @@
/**
* PraisonAIUI Agent-UI adapter
*
* Pure mapping functions from wire payloads to stable PraisonAIUI contracts.
* This is the single boundary between transport shapes (WebSocket frames from
* chat.js, REST payloads from /api/approvals/*) and the UI layer. Vendored
* BeautifulUI components are rendered from these contracts elsewhere in
* `src/agent-ui/`; consumers never import upstream prop types directly.
*
* Functions are intentionally side-effect free so they can be unit tested
* with mock events (see adapter.test.ts).
*/
import type {
AgentStreamEvent,
ApprovalPromptModel,
ApprovalRiskLevel,
ToolCallChipModel,
} from './contracts'
/** Raw WebSocket frame as parsed in chat.js (`JSON.parse(event.data)`). */
export type RawChatFrame = Record<string, unknown>
/** Raw approval item as returned by `/api/approvals/pending`. */
export type RawApprovalItem = Record<string, unknown>
const VALID_RISK_LEVELS: readonly ApprovalRiskLevel[] = [
'low',
'medium',
'high',
'critical',
]
function asString(value: unknown): string | undefined {
return typeof value === 'string' ? value : undefined
}
function asNumber(value: unknown): number | undefined {
return typeof value === 'number' ? value : undefined
}
/**
* Normalize a `created_at` value to an ISO-8601 string. The approvals API
* emits epoch seconds (`time.time()`), while some payloads already carry an
* ISO string — accept both, drop anything else.
*/
function asTimestamp(value: unknown): string | undefined {
if (typeof value === 'string') return value
if (typeof value === 'number' && Number.isFinite(value)) {
return new Date(value * 1000).toISOString()
}
return undefined
}
/** Map a raw tool-call frame to a stable chip view model. */
function mapToolCall(
frame: RawChatFrame,
status: ToolCallChipModel['status'],
): ToolCallChipModel {
const name = asString(frame.name) ?? 'tool'
return {
id: asString(frame.tool_call_id) ?? name,
label: asString(frame.description) ?? name,
icon: asString(frame.icon),
stepNumber: asNumber(frame.step_number),
status,
error: asString(frame.error),
}
}
/**
* Map a raw WebSocket chat frame to a normalized {@link AgentStreamEvent}.
*
* Unknown frame types are preserved as `kind: 'unknown'` with `rawType` set,
* so new upstream event types degrade gracefully instead of throwing.
*/
export function mapChatEventToUi(frame: RawChatFrame): AgentStreamEvent {
const rawType = asString(frame.type)
const sessionId = asString(frame.session_id)
const runId = asString(frame.run_id)
const agentName = asString(frame.agent_name)
switch (rawType) {
case 'run_started':
return { kind: 'run-started', sessionId, runId, agentName }
case 'run_content':
case 'team_run_content':
return {
kind: 'run-token',
sessionId,
runId,
agentName,
text: asString(frame.token),
}
case 'run_completed':
case 'team_run_completed':
return {
kind: 'run-completed',
sessionId,
runId,
agentName,
text: asString(frame.content),
}
case 'run_error':
case 'team_run_error':
return {
kind: 'run-error',
sessionId,
runId,
agentName,
error: asString(frame.error) ?? 'Unknown error',
}
case 'tool_call_started':
case 'team_tool_call_started':
return {
kind: 'tool-call',
sessionId,
runId,
agentName,
toolCall: mapToolCall(frame, 'running'),
}
case 'tool_call_completed':
case 'team_tool_call_completed':
return {
kind: 'tool-call',
sessionId,
runId,
agentName,
toolCall: mapToolCall(frame, 'done'),
}
case 'reasoning_step':
case 'team_reasoning_step':
return {
kind: 'reasoning-step',
sessionId,
runId,
agentName,
text: asString(frame.step),
}
default:
return { kind: 'unknown', sessionId, runId, agentName, rawType }
}
}
function normalizeRisk(value: unknown): ApprovalRiskLevel {
return VALID_RISK_LEVELS.includes(value as ApprovalRiskLevel)
? (value as ApprovalRiskLevel)
: 'medium'
}
/**
* Map a raw approval item from `/api/approvals/pending` to a stable
* {@link ApprovalPromptModel}. Arguments are pretty-printed for display and
* an unknown/missing risk level falls back to `medium`.
*/
export function mapApprovalToCardProps(
item: RawApprovalItem,
): ApprovalPromptModel {
let argumentsJson = '{}'
try {
argumentsJson = JSON.stringify(item.arguments ?? {}, null, 2)
} catch {
// Circular or non-serializable args — degrade to empty object.
argumentsJson = '{}'
}
return {
id: asString(item.id) ?? '',
toolName: asString(item.tool_name) ?? 'unknown',
agentName: asString(item.agent_name) ?? 'Unknown',
riskLevel: normalizeRisk(item.risk_level),
riskIcon: asString(item.risk_icon) ?? '⚠️',
description: asString(item.description),
argumentsJson,
createdAt: asTimestamp(item.created_at),
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* PraisonAIUI Agent-UI contracts
*
* Stable, framework-agnostic view models owned by PraisonAIUI. These are the
* ONLY types that non-adapter code (chat views, dashboards, tests) should
* depend on. BeautifulUI / vendored component prop types must never leak
* outside `src/agent-ui/` — the adapter maps to these contracts instead so
* the upstream implementation can be swapped without touching consumers.
*
* See docs/frontend/beautifului-adapter-architecture.md.
*/
/** Discriminant for a normalized agent stream event. */
export type AgentStreamEventKind =
| 'run-started'
| 'run-token'
| 'run-completed'
| 'run-error'
| 'tool-call'
| 'reasoning-step'
| 'unknown'
/**
* Normalized agent stream event — the stable shape UI code consumes,
* decoupled from the raw WebSocket frame `type` strings handled in chat.js.
*/
export interface AgentStreamEvent {
kind: AgentStreamEventKind
/** Owning session, when the frame carried one. */
sessionId?: string
/** Run identifier, when the frame carried one. */
runId?: string
/** Human-facing agent name, when present. */
agentName?: string
/** Streaming token or final content, depending on `kind`. */
text?: string
/** Populated for `kind === 'tool-call'`. */
toolCall?: ToolCallChipModel
/** Populated for `kind === 'run-error'`. */
error?: string
/** Raw frame `type` for events we do not yet normalize (`kind === 'unknown'`). */
rawType?: string
}
/** View model for a tool-call chip, independent of any component library. */
export interface ToolCallChipModel {
id: string
label: string
icon?: string
stepNumber?: number
status: 'running' | 'done' | 'error'
error?: string
}
/** Risk levels surfaced by the approvals REST API (`/api/approvals/*`). */
export type ApprovalRiskLevel = 'low' | 'medium' | 'high' | 'critical'
/** View model for an approval prompt/card, independent of any component library. */
export interface ApprovalPromptModel {
id: string
toolName: string
agentName: string
riskLevel: ApprovalRiskLevel
riskIcon: string
description?: string
/** Pretty-printed tool arguments, ready for display. */
argumentsJson: string
createdAt?: string
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Agent-UI boundary barrel.
*
* Consumers import PraisonAIUI's stable agent-UI contracts and adapters from
* here. Vendored BeautifulUI components live under `src/agent-ui/vendors/` and
* are re-exported from this folder only — no upstream prop types should be
* imported outside `src/agent-ui/`.
*/
export type {
AgentStreamEvent,
AgentStreamEventKind,
ApprovalPromptModel,
ApprovalRiskLevel,
ToolCallChipModel,
} from './contracts'
export type { RawApprovalItem, RawChatFrame } from './adapter'
export { mapApprovalToCardProps, mapChatEventToUi } from './adapter'