GHSA-72qq-p3r5-f7wq: the basic catalog openUrl action accepted any URL
scheme and passed it to window.open, so an agent-supplied A2UI Button
carrying a javascript: URI would execute script in the app origin on click.
0.10.2 resolves the URL and refuses anything that is not http or https.
* fix: make dashboard chat fill the main column (fixes#322)
Chat and Chat+Canvas built-in pages declare layout: full-bleed. selectPage()
toggles a db-fill class on #db-main-content and skips the duplicate page
header, so .chat-root sizes from flex allocation instead of
calc(100vh - 140px). Session rail stacks above the thread below 900px.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: make full-bleed page metadata test non-vacuous
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
The frontend build gate added on 2026-09-23 (166091b) has failed on every
main run since: five April PRs left code that never compiled — imports of
shadcn components that were never added (ui/alert, ui/textarea), seven
example widgets that never existed (CustomElementHost), react-router-dom
(not a dependency), a useSSE call written for a different hook, a wrong
ChatMessages prop set, and unused locals under noUnusedLocals.
- add the standard shadcn alert and textarea components
- CustomElementHost: empty registry instead of imports of missing widgets
- useWindowMessage: small local EventSource hook for /sse/window-message
- SharedThread: read the token from /shared/{token}; pass the props
ChatMessages takes; type messages as ChatMessage[]
- drop unused imports/locals
None of these files are imported by the app today, so nothing users see
changes; the gate now builds and catches real breakage again.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
An agent created without name= records agent_name null. The Replay map
skipped every such event, so watch(Agent(instructions=...)) showed
"Nothing to map yet" and its steps were narrated as "The run".
- agentOf(ev): agent_name, else the package's id (agent:Agent from the
companion core fix), else tool:None/<tool> or nothing in 4.7.19 traces,
falling back to "Agent" for an agent's own run events.
- deriveGraph / nodeIdsForEvent / narrate use it, so the agent and its tools
are drawn and lit, and steps read "Agent started.".
- Only when no graph was recorded: a team graph leaves unnamed agents out on
purpose, and they must not reappear as "Outside the team".
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- create_replay_app(run=None): the Replay view as a small ASGI app (no dashboard)
to mount in any FastAPI/Starlette app; /?run=<id> pins one run (iframe-friendly).
With run(input, run_id) it shows a Run box: POST /api/replay/run (JSON only,
one run at a time) starts a run and the page follows it live.
- render(el, {base, run, runner, approve}) so the view works under a mount prefix.
- Map nodes show their state at the cursor: not reached / running / done / failed /
skipped, and "×N" counts real runs instead of events (a step that ran once
no longer reads "×2"). Scrubbing or Play rewinds the map with the cursor.
- ▶ Play steps through a run on its own.
- events?wait=1 answers a not-yet-started run without a 404.
- Node names stay whole (squeezed or clipped) so accessible names contain the
visible text (WCAG 2.5.3; axe flagged truncated tool names).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- At <=1023 px the hidden sidebar still took 260 px of the flex row,
because buildSidebar() sets an inline position:relative that beat the
mobile position:fixed; and the shell header slot took another 53 px.
Every page's content was squeezed to a few pixels at phone width. The
sidebar is now fixed (!important), the header slot gets its own row,
and phones get a 16 px gutter.
- Pages may set icon_svg next to the text icon. The sidebar and page
header parse it as SVG and drop it if it holds scripts, foreignObject,
on* handlers or javascript: URLs; other UIs keep using `icon`.
- Replay's heading uses its own inline mark (three lanes and a cursor)
instead of the emoji, and its description no longer says "why" (it
made Playwright's text=Why match the page header, not the tab).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
From a review against real recorded runs:
- Timeline: an agent's LLM span encloses its tool calls in real runs, so
tool blocks covered the LLM label. Each lane now has an LLM row with a
thinner tool row under it, blocks are packed into tracks in pixel space
so nothing overlaps, and labels are ellipsized; the full label is in
the title, the accessible name and a hover/focus tip. Visible labels
sit in a decorative layer so names never contain a cut-off word.
- Theme: defaults to the system setting (prefers-color-scheme, tracked
live in CSS) with a System / Light / Dark menu as an override.
- Map: fits the whole graph on open (shares -/Fit/+), drag to pan, never
below a readable scale, and gets the full width with the step list
underneath. Agents seen in events but not in the graph (e.g. a
hand-off to an agent outside the team) are added by observedGraph()
and laid out by layoutRunMap() as an "Outside the team" group.
- Phone: 16 px gutter, stacked layout, no horizontal page scroll.
- Standalone boot waits for DOMContentLoaded so the export works with
either script order, takes data.run_id as the title, and keeps the
documented #replay-root / window.__REPLAY_DATA__ contract.
- New fixture team_outside.jsonl with the real-run shape; node tests
for observedGraph/layoutRunMap; browser tests for no-overlap, the Why
tab by role and keyboard, system theme, map fit, phone width and the
wrapper-style export in both script orders.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The fit-to-width minimum (480 px of plot) made the timeline overflow in
the dashboard's narrower content column; lower it to 260 px. Pending
approvals and running blocks showed "Duration 0 ms"; show "still waiting"
or "still running" instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
_UIStoreBackend returned "Awaiting UI approval" (a denial) immediately, so
any tool needing approval was always denied and the UI's Approve button
never reached the agent. It also dropped request.approval_id and any
edited arguments.
- each request is stored under request.approval_id and the caller blocks
until /api/approvals/{id}/approve|deny resolves it: sync callers on a
threading.Event, async callers on a future of their own loop
- no answer before the timeout (registry timeout, default 300 s) is a
denial and the entry is resolved as denied (fail-closed)
- a sync call made on an event-loop thread is denied at once rather than
freezing that loop
- POST /approve accepts modified_args (object) and the decision carries
it as ApprovalDecision(modified_args=...); the handler change is two
lines so the auth checks in #312 rebase cleanly
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
SDKTracingManager looked up praisonaiagents.trace.list_traces, list_spans
and get_trace, which do not exist, so /api/traces never showed a recorded
run and silently fell back to an empty in-memory store.
JsonlTracingManager reads the same directory as Replay: one file is one
trace, spans come from start/end pairs (parent_seq on schema 1.1, sequence
order on 1.0), and records posted to /api/traces/record still work.
health() reports source: jsonl|memory. SDKTracingManager stays as a
deprecated alias.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
New `replay` feature (contract v1 §5), read-only:
- GET /api/replay/runs: run-picker rows, newest first, with a live flag
- GET /api/replay/runs/{id}/events?after_seq=N: {events, graph, run}
- GET /api/replay/runs/{id}/stream: SSE tail of the JSONL (stdlib seeks),
`ctx` events with id=sequence_num, ping every 15 s, `end` after the run
finishes plus 2 s quiet; honours after_seq and Last-Event-ID
- ids must match ^[A-Za-z0-9._-]+$ and resolve inside the traces dir
- traces dir: PRAISONAI_TRACES_DIR > PRAISON_HOME/traces > ~/.praison/traces
- half-written last lines, unknown fields/types and schema 1.0 are tolerated
Plain-JS view `replay` (no React, no CDN): replay-core.js holds pure
buildModel/narrate/layoutMap/deriveGraph (node --test); replay.js draws the
timeline (agent, Manager and You lanes, LLM/tool blocks, hand-off arrows,
parallel sub-lanes, xN repeats, task/step chapters, scrubber, live follow),
the narration list, What happened/Why/Raw JSON drawer, cost/latency
overlay, map view, and Approve/Deny/Edit-args for pending approvals.
Registered in BUILTIN_VIEWS and as a Control page; /#/replay?run=<id> deep
links resolve to /replay?run=<id>. It also boots standalone from
window.__REPLAY_DATA__; standalone_html() inlines both files for exports.
praisonaiui/replay_app.py + configure_replay_app() open aiui straight on
Replay for `praisonai replay view`.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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]>
The approve/deny endpoints only store `reason`; map the selected option or
typed custom answer into it so the human's decision note is recorded, while
keeping `option`/`custom_answer` for a future richer backend.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Adds a `frontend` job to CI that runs `npm ci && npm run build` for
src/frontend on every push/PR to main, failing on TypeScript/Vite build
errors. Uses Node 22 with npm caching keyed on the frontend lockfile.
Also documents the Node version, lockfile, and build/ship path in
src/frontend/README.md.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Add self-contained toApprovalModel() normalizer (ApprovalPromptModel shape)
- Render pending items as an Approval Card: risk accent, question, options,
custom answer and confidence copy — surfaced only when the API supplies them
- Preserve /api/approvals approve|deny endpoints and skill-approval deep links
- Escape all interpolated API values via esc() (fixes prior XSS gap)
- Improve a11y: role=group/radiogroup, aria-labels on buttons/inputs
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Channels configured with env:VAR token references silently resolved to
empty strings when the dashboard was started without the shell exporting
the tokens, so bots showed 'Connected' but never replied. aiui run now
loads the standard PraisonAI .env files (~/.praisonai/.env, then ./.env)
into the process environment at startup, matching the PraisonAI CLI
convention. Existing process vars are never overridden and parsing is
dependency-free (no python-dotenv).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The chat bridge installed over bot._session.chat used a fixed
(agent, user_id, text) signature. When the bot internals invoked the
session with per-conversation keyword arguments (chat_id, platform,
thread_id, ...) the wrapper raised
`TypeError: _wrapped_chat() got an unexpected keyword argument 'chat_id'`
and the bot silently failed to reply across all platforms (Telegram,
Slack, Discord).
Add `*args, **kwargs` to `_wrapped_chat` and forward them untouched to
the original session chat so memory/session continuity is preserved.
Minimal, backward-compatible change; no new params or dependencies.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
_start_channel_bot registers the task in gateway _channel_tasks, but
_stop_channel_bot only cleaned up _channel_bots. For the list-based
gateway registry this left the old cancelled task appended on every
restart, accumulating orphaned task references. Mirror start-time
registration by removing the task from _channel_tasks (dict pop / list
remove) on stop. Adds regression tests for both dict and list forms.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
_run_and_broadcast never inserted its task into ChatManager._active_runs
and never emitted run_started, so the Stop button, POST /api/chat/abort,
and the chat_abort WS message always returned no_active_run while tokens
were still streaming.
- Register the running task under run_id at run start
- Broadcast run_started up-front so the frontend captures run_id and can
enable the Stop button before the first token
- Deregister the run in a finally on completion, error, or abort
Existing CancelledError handler already broadcasts run_cancelled once the
task is actually cancelled.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a guard at the top of ChannelsFeature._start_channel_bot that stops any
bot already running for the same channel_id before creating a new one. This
prevents duplicate bots (with duplicate event handlers) when a channel is
toggled off/on, re-added, or when autostart races with a manual start.
Also make _stop_channel_bot await the cancelled task so the previous bot is
fully torn down before a restart proceeds, eliminating zombie asyncio tasks.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
SDKFileDataStore only copied toolCalls/elements into SDK message
metadata on write and only restored those two keys on read, so
channel/voice fields (platform, sender, icon, channel_id) were
silently dropped after reload — despite being persisted correctly by
the SQLAlchemy backend.
Write path now preserves all non-core keys (excluding None), and the
read path restores every persisted metadata key (skipping internal
sentinel keys, without clobbering role/content), matching the
SQLAlchemy backend. Backward compatible: existing toolCalls/elements
round-trips are unchanged and no migration is required.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Startup hooks ran with an unbounded await while shutdown hooks were
already protected by AIUI_SHUTDOWN_TIMEOUT. A single hung startup hook
blocked the Starlette lifespan from reaching yield, so /health/live never
responded and the dashboard never loaded.
Mirror the shutdown pattern: wrap startup hooks in asyncio.wait_for with
AIUI_STARTUP_TIMEOUT (default 60s). On timeout, log the error and mark
startup completed so the server still reaches lifespan yield. Shutdown
behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Store the AsyncExitStack on self._exit_stack before entering the stdio/SSE
contexts. On an asyncio.wait_for timeout the inner connect() is cancelled with
CancelledError (a BaseException), which bypasses the except-Exception cleanup,
so self._exit_stack was never set and MCPClientManager.connect()'s timeout
branch called client.disconnect() as a no-op — leaking the stdio subprocess /
SSE streams. Assigning the stack up front lets disconnect() always close it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
asyncio.wait_for raises asyncio.TimeoutError, which is only aliased to
the builtin TimeoutError on Python 3.11+. On the 3.10 CI leg the bare
'except TimeoutError' missed it, so the timeout fell through to the
generic handler with an empty last_error and no client.disconnect()
cleanup. Catch asyncio.TimeoutError to restore the helpful message and
subprocess/stream cleanup across all supported Pythons.
Co-Authored-By: Claude Opus 4.8 <[email protected]>