Skip to content

feat: add OpenUI integration#8218

Open
vishxrad wants to merge 1 commit into
janhq:mainfrom
vishxrad:feat/openui-integration
Open

feat: add OpenUI integration#8218
vishxrad wants to merge 1 commit into
janhq:mainfrom
vishxrad:feat/openui-integration

Conversation

@vishxrad
Copy link
Copy Markdown

Summary

Adds an experimental OpenUI integration to Jan.

What Changed

  • Added OpenUI settings under Integrations.
  • Adds OpenUI prompt guidance automatically when OpenUI is enabled.
  • Renders assistant OpenUI Lang responses as interactive UI.
  • Routes OpenUI CTA/action clicks back into the active chat input.
  • Includes OpenUI form state in action messages sent back to the LLM.
  • Adds OpenUI logo to the settings page and integrations pane.
  • Adds tests for OpenUI CTA clicks and form submission payloads.

Validation

yarn workspace @janhq/web-app lint
yarn workspace @janhq/web-app exec vitest --run
yarn workspace @janhq/web-app build

All passed.

closes #8214

@qnixsynapse qnixsynapse self-requested a review May 28, 2026 13:51
@tokamak-pm
Copy link
Copy Markdown

tokamak-pm Bot commented Jun 3, 2026

Review: PR #8218 - feat: add OpenUI integration

Summary

This is a substantial feature PR that adds experimental OpenUI integration to Jan. When enabled, the assistant can describe UI elements in OpenUI Lang, and Jan renders them as interactive components (buttons, forms, etc.) instead of plain markdown. User interactions with these components are routed back into the conversation.

Architecture Overview

The implementation follows a clean layered approach:

  • Settings layer: New Zustand store (useOpenUISettings) persisted to localStorage, with a dedicated settings page at /settings/openui.
  • Detection layer: openui-detect.ts uses regex to identify OpenUI Lang in assistant responses (fenced code blocks or bare root = assignments).
  • Rendering layer: OpenUIResponse component wraps RenderMarkdown, lazy-loading OpenUIRenderedContent only when OpenUI content is detected. Falls back to markdown if parsing fails.
  • Action layer: openui-actions.ts dispatches user interactions as CustomEvents on window, which ChatInput listens for and auto-submits as new messages.
  • Prompt injection: custom-chat-transport.ts appends OpenUI system prompt guidance when the feature is enabled.

Detailed Findings

Strengths:

  1. Lazy loading -- The heavy OpenUI libraries (@openuidev/react-ui, @openuidev/react-lang) are code-split via React.lazy(). Users who never enable OpenUI pay no bundle cost.
  2. Graceful fallback -- If OpenUI parsing fails, the response falls back to standard markdown rendering. The Suspense boundary also shows markdown while the lazy chunk loads.
  3. Feature gating -- Disabled by default, so no impact on existing users.
  4. Test coverage -- Tests for CTA clicks, fallback to prompt, and form submission payloads in OpenUIRenderedContent.test.tsx. SettingsMenu test updated.
  5. Custom memo -- OpenUIResponse has a custom memo comparator to avoid unnecessary re-renders.

Concerns:

  1. Security -- No sanitization of rendered UI. The OpenUI renderer trusts the model output. A malicious or confused model could generate UI that triggers unintended actions. The dispatchOpenUIChatAction sends whatever the model produced back into the chat. While this is an "experimental" feature, consider:

    • What happens if the model generates a form that auto-submits on render?
    • Is there any XSS surface in the OpenUI renderer itself?
    • The window.open(url, '_blank', 'noopener,noreferrer') for OpenUrl actions is good, but the URL comes from model output unsanitized -- could a javascript: URL slip through?
  2. System prompt always appended, not opt-in per thread. When OpenUI is enabled globally, every chat gets the OpenUI system prompt appended. This increases token usage and may confuse models that are not good at generating OpenUI Lang. Consider making this per-thread or per-assistant, or at minimum noting this trade-off in the settings description.

  3. handleSendMessageRef pattern in ChatInput. The ref is updated on every render (the useEffect has no dependency array). While this is a known React pattern for "latest ref," it is somewhat fragile. A comment explaining why would help maintainability.

  4. Detection regex may false-positive. The extractOpenUIResponse function triggers on any content with root = ... followed by a capitalized function call. This could match legitimate markdown code blocks explaining assignment syntax. The fenced code block detection is more reliable. Consider tightening the bare detection or requiring both root = and a known OpenUI component name.

  5. New dependencies are heavyweight. The PR adds @openuidev/react-ui which pulls in recharts, react-day-picker, date-fns, react-syntax-highlighter (a second copy -- Jan already has one), lodash (full, not lodash-es), victory-vendor, and many Radix primitives. Even though they are lazy-loaded, this significantly increases the overall bundle size. The yarn.lock diff is substantial (800+ lines of new dependencies).

  6. MessageItem.tsx -- Global replacement of RenderMarkdown with OpenUIResponse. This means every assistant message now goes through the OpenUI detection path (regex test) even when OpenUI is disabled. The useOpenUISettings check short-circuits early, but the Zustand selector still runs per message. This is probably fine performance-wise, but worth noting.

  7. No i18n for the OpenUI settings page. All strings on the settings page ("Enable OpenUI rendering", "Component library", "About OpenUI", etc.) are hardcoded in English. Other settings pages use t() for translations. This should be consistent.

  8. The routeTree.gen.ts changes appear to be auto-generated (by TanStack Router codegen). Verify this file is regenerated correctly in CI, not manually edited.

Missing

  • No e2e/integration tests for the full flow (enable OpenUI -> send message -> get OpenUI response -> interact with rendered component -> verify follow-up message).
  • No documentation for users about what OpenUI is and how to use it.
  • The merge_commit_sha is null and no reviewers have approved yet, suggesting CI may not have run successfully. The PR says validation passed locally.

Verdict

This is a well-architected feature with good code separation and lazy loading. However, it introduces a large dependency footprint and has security considerations around unsanitized model-generated UI that need attention before merging. The lack of i18n on the settings page is inconsistent with the rest of the codebase.

Recommendation: fix needed

Priority action items:

  1. Validate that the OpenUI renderer sanitizes URLs (prevent javascript: protocol) and does not allow arbitrary HTML/script injection.
  2. Add i18n strings for the OpenUI settings page.
  3. Consider the bundle size impact of the @openuidev/react-ui dependency tree (recharts, lodash, date-fns, etc.) and whether lighter alternatives exist.
  4. Add a comment explaining the handleSendMessageRef pattern in ChatInput.

@vishxrad vishxrad force-pushed the feat/openui-integration branch from 7908d7f to 1985c8f Compare June 4, 2026 11:22
@vishxrad
Copy link
Copy Markdown
Author

vishxrad commented Jun 4, 2026

Thanks for the review, I will refactor the pr and push the changes soon.

@vishxrad vishxrad force-pushed the feat/openui-integration branch from 1985c8f to 80f761b Compare June 5, 2026 10:17
@vishxrad vishxrad force-pushed the feat/openui-integration branch from 80f761b to 143da66 Compare June 5, 2026 22:06
@tokamak-pm
Copy link
Copy Markdown

tokamak-pm Bot commented Jun 6, 2026

Re-review: PR #8218 - feat: add OpenUI integration

Updated review after the author's changes (commit 143da66 on 2026-06-05). Comparing against findings from the previous review on 2026-06-03.

What improved since the last review

The author addressed several of the original concerns:

  1. URL sanitization -- FIXED. openui-url.ts now validates all model-generated URLs via getSafeOpenUIUrl(), allowing only http: and https: protocols. javascript:, data:, file:, and relative URLs are blocked. There are dedicated tests in openui-url.test.ts confirming this. The OpenUIRenderedContent.test.tsx tests also verify that unsafe URLs are blocked and safe URLs pass through.

  2. i18n -- FIXED. All strings on the OpenUI settings page now use t() with proper keys in settings:openui.* and common:openui. All 17 locale files (ca, cs, de-DE, en, es, fr, hi, id, it, ja, ko, pl, pt-BR, ru, vn, zh-CN, zh-TW) have been updated with translated strings.

  3. Detection tightening -- IMPROVED. The openui-detect.ts detection now validates against a known OPENUI_COMPONENT_NAMES list instead of matching any capitalized identifier. Bare root = content must start at the beginning of the trimmed string (via ROOT_AT_START_RE), reducing false positives from prose containing root = ... mid-paragraph. Tests confirm this rejects unknown component names and prose-embedded examples.

  4. Documentation -- ADDED. docs/src/pages/docs/desktop/integrations/openui.mdx provides user-facing documentation covering setup, rendering behavior, safety, and custom libraries.

Remaining concerns

1. handleSendMessageRef pattern still lacks a comment.
In ChatInput.tsx, the ref is updated via useEffect with no dependency array. This is a valid "latest ref" pattern, but it is unusual enough that a brief comment explaining why would help future maintainers:

// Keep ref in sync with latest closure so the OpenUI event listener
// (registered once with []) always calls the current handleSendMessage.
const handleSendMessageRef = useRef(handleSendMessage)
useEffect(() => { handleSendMessageRef.current = handleSendMessage })

2. System prompt is appended to every chat when enabled globally.
This was noted in the previous review and has not changed. When OpenUI is enabled, every chat gets the OpenUI system prompt (~1,600 tokens for Chat mode, ~5,000 for Standard). The settings description now documents the token cost, which is an improvement, but it is still applied globally rather than per-thread or per-assistant. For a first "experimental" release this is acceptable, but I would suggest adding a TODO or issue to make this configurable per thread/assistant in a follow-up.

3. Bundle size from @openuidev/react-ui dependency tree.
The standard library mode pulls in recharts, react-day-picker, date-fns, react-syntax-highlighter (a second copy -- Jan already uses one), lodash (full, not lodash-es), and many Radix primitives. The yarn.lock adds ~390 lines of new dependencies. The chat mode is lighter (only @openuidev/react-lang + @openuidev/react-headless). The lazy loading via React.lazy() mitigates the impact for users who never enable the Standard library, but:

  • Users who enable Standard will pay the full bundle download cost
  • Consider whether @openuidev/react-ui could be made an optional/peer dependency rather than a direct dependency, so users who only want Chat mode do not download the Standard bundle at all

4. No XSS sanitization within the OpenUI renderer itself.
URL sanitization was addressed, but the broader question remains: can a model craft OpenUI Lang that injects arbitrary HTML or scripts into the rendered output? This depends on the @openuidev/react-lang Renderer component's internal sanitization. If the Renderer uses React's JSX (which escapes strings by default), the risk is low. But if any component uses dangerouslySetInnerHTML or equivalent, model-generated content could be a vector. This should be verified by auditing the upstream @openuidev/react-lang Renderer, or at minimum documented as a known limitation.

5. useEffect without dependency array runs on every render.
The handleSendMessageRef update effect (line ~473 in ChatInput.tsx) has no dependency array, meaning it runs after every render. While this is intentional (it keeps the ref current), it is worth noting that this adds a small amount of overhead per render in a component that re-renders frequently. A more explicit alternative would be useLayoutEffect with [handleSendMessage] as deps, but this is minor and the current approach works correctly.

6. Zustand peer dependency override in .yarnrc.yml.
The PR adds packageExtensions to widen the Zustand peer dependency range for @openuidev/react-headless and @openuidev/react-ui from ^4.5.5 to ^4.5.5 || ^5.0.0. The comment says this is needed until upstream publishes the same range. This is a clean workaround, but the team should track when upstream updates so this override can be removed.

Test coverage assessment

The PR includes solid test coverage:

  • openui-detect.test.ts -- Detection logic with positive and negative cases
  • openui-url.test.ts -- URL validation for safe and unsafe protocols
  • openui.test.ts -- System prompt injection with enabled/disabled/undefined cases, and token count assertion
  • OpenUIRenderedContent.test.tsx -- CTA clicks, prompt fallback, form submission payloads with form state, and URL safety (both allowed and blocked)
  • ChatInput.test.tsx -- OpenUI action dispatch through the active input listener
  • SettingsMenu.test.tsx -- Menu item presence

Missing: No e2e tests for the full enable-to-interact flow, but this is understandable for an experimental feature.

Architecture

The layered approach is clean and well-separated:

  • Settings (useOpenUISettings Zustand store) -- persistence, feature flag
  • Detection (openui-detect.ts) -- content identification
  • Prompt injection (openui.ts via custom-chat-transport.ts) -- system prompt augmentation
  • Rendering (OpenUIResponse -> OpenUIRenderedContent -> OpenUILibraryRenderedContent) -- lazy-loaded rendering pipeline with markdown fallback
  • Actions (openui-actions.ts) -- CustomEvent dispatch from UI interactions back to chat
  • URL safety (openui-url.ts) -- protocol allowlisting

The two library modes (Chat vs Standard) are well-isolated: Chat uses the locally defined janOpenUIChatLibrary (437 lines of custom components using Jan's design system), while Standard lazy-loads the upstream @openuidev/react-ui bundle.

Verdict

The author has addressed the most critical issues from the first review (URL sanitization, i18n, detection accuracy, documentation). The remaining items are either minor (missing comment on ref pattern), known trade-offs (bundle size, global prompt injection), or require upstream verification (XSS in Renderer).

Recommendation: improve needed

Minimum before merge:

  1. Add a brief comment on the handleSendMessageRef pattern in ChatInput.tsx explaining why it has no dependency array.
  2. Verify (or document as a known limitation) that the @openuidev/react-lang Renderer does not allow arbitrary HTML injection from model output.

Nice-to-have for follow-up:
3. Track per-thread/per-assistant OpenUI toggle as a future enhancement.
4. Consider making @openuidev/react-ui an optional dependency.
5. Track removal of the .yarnrc.yml peer dependency override once upstream updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

idea: Add optional OpenUI integration for interactive assistant responses

1 participant