Skip to content

feat(ios): add inline Adaptive Card rendering in chat#42350

Open
VikrantSingh01 wants to merge 3 commits intoopenclaw:mainfrom
VikrantSingh01:feat/adaptive-cards-ios-rendering
Open

feat(ios): add inline Adaptive Card rendering in chat#42350
VikrantSingh01 wants to merge 3 commits intoopenclaw:mainfrom
VikrantSingh01:feat/adaptive-cards-ios-rendering

Conversation

@VikrantSingh01
Copy link
Copy Markdown

@VikrantSingh01 VikrantSingh01 commented Mar 10, 2026

Summary

Adds native Adaptive Card rendering to the iOS and macOS apps. When a chat message contains <!--adaptive-card--> markers, the card is parsed and rendered inline in the chat bubble using SwiftUI.

Replaces closed #42306 (closed due to force push during review fixes).

New files (in apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/)

  • AdaptiveCardModels.swift — Codable models with recursive AnyCodableAction for complex data payloads
  • AdaptiveCardParser.swift — marker extraction + JSON decode
  • AdaptiveCardView.swift — SwiftUI renderer with dark/light mode, column width support, image fallback

Modified

  • ChatMessageViews.swift — cached parsedCard computed property (not in body), think-block filtering before card parsing

Review feedback addressed

  • Parsing cached outside SwiftUI body (performance)
  • Action.Submit disabled with help text (no non-functional buttons)
  • AnyCodableAction handles Dict/Array/String/Double/Bool/Int (no data loss)
  • Column width applied (auto vs stretch)
  • Font case fixed (extralarge not extraLarge)
  • Image fallback label for invalid URLs
  • Think-block guard (cards inside <think> don't leak)

macOS gets this for free (reuses OpenClawChatView).

Ecosystem Context

This PR is part of the Adaptive Cards feature set powered by:

Package Version Role
adaptive-cards-mcp v2.3.0 Shared core — v1.6 JSON Schema validation, 7 host profiles, WCAG a11y scoring (0-100), 21 layout patterns, 924 tests
openclaw-adaptive-cards v4.0.0 OpenClaw plugin — adaptive_card tool, MCP bridge, channel-aware prompt guidance, fallback text generation, action routing

The plugin emits <!--adaptive-card--> markers in tool result text. This PR adds the iOS/macOS client-side parser and SwiftUI renderer that consumes those markers.

Element support

TextBlock, FactSet, ColumnSet, Container, Image, Action.Submit (disabled with help text), Action.OpenUrl

Performance target

<100ms parse + render for typical cards.

Related PRs

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 10, 2026

Greptile Summary

This PR adds native Adaptive Card rendering to the iOS/macOS chat UI. When an assistant message contains <!--adaptive-card--> markers, the JSON payload is parsed and rendered inline via a new SwiftUI AdaptiveCardView. The implementation is well-structured overall — marker extraction, think-block filtering, dark/light mode theming, async image loading, and graceful handling of partial/invalid JSON during streaming are all handled correctly. A few issues were found:

  • AnyCodableAction decodes integers as Double (logic bug): Double is attempted before Bool and Int in AnyCodableAction.init(from:). Swift's JSONDecoder decodes any JSON integer (e.g., 42) as Double successfully, so the Int branch at line 140 of AdaptiveCardModels.swift is unreachable dead code. Integer values in Action.Submit data will silently round-trip as 42.0 rather than 42, which will be incorrect once Action.Submit is enabled.
  • filteredText / parsedCard are not cached (performance): Both are plain computed properties on the SwiftUI struct. Despite the PR description claiming "parsing cached outside SwiftUI body", computed properties have no memoization and are recomputed on every render pass. During streaming this means full string scanning (AssistantTextParser.segments) and JSON decoding (JSONDecoder.decode) run on every frame redraw — potentially dozens of times per second for long messages.
  • encode(to:) silently drops unrecognised values: If AnyCodableAction.value holds a type outside the handled list, encode(to:) completes without writing anything to the container, producing an empty/null JSON value without any error signal.

Confidence Score: 3/5

  • Safe to merge with the understanding that Action.Submit data will have integer-to-double coercion and rendering may be slow during streaming of long messages; neither blocks the core feature.
  • The core rendering path is correct and gracefully handles edge cases (partial streaming, invalid JSON, unknown element types, think-block leaking). The identified logic bug (dead Int branch) only affects Action.Submit payload fidelity, and since Action.Submit is currently disabled in the UI the impact is deferred. The performance concern with uncached computed properties is real but tolerable at current card sizes; it should be addressed before this pattern is used with larger payloads or more frequent re-renders.
  • AdaptiveCardModels.swift (dead Int branch and silent encode drop) and ChatMessageViews.swift (computed-property recomputation during streaming).

Last reviewed commit: 72ceae6

Comment thread apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72ceae6993

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift Outdated
New files in apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/:
- AdaptiveCardModels.swift — Codable models for AC v1.5 (TextBlock, FactSet,
  ColumnSet, Container, Image, Actions)
- AdaptiveCardParser.swift — marker extraction from message text
- AdaptiveCardView.swift — SwiftUI renderer with dark/light mode support

Modified:
- ChatMessageViews.swift — card detection in ChatAssistantTextBody before
  markdown render; inline AdaptiveCardView when card found

Element mapping: TextBlock->Text, FactSet->label-value rows, ColumnSet->HStack,
Container->VStack, Image->AsyncImage, Action.Submit->Button, Action.OpenUrl->Link

macOS gets this for free (reuses OpenClawChatView from shared kit).
- Update version comment from v1.5 to v1.6
- Fix AnyCodableAction to handle nested dicts, arrays, Bool, Int, Double
- Add Table, RichTextBlock, CodeBlock, ImageSet, ActionSet elements
- Add Action.Execute alongside Action.Submit
- Extract template data from adaptive-card-data markers in parser
- Remove redundant extraLarge case in textBlockFont (already lowercased)
- Disable Action.Submit/Execute buttons with help text (no server routing)
- Add renderers for all new element types
@VikrantSingh01 VikrantSingh01 force-pushed the feat/adaptive-cards-ios-rendering branch from 3e4f307 to 3c88350 Compare March 19, 2026 22:08
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Mar 19, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c88350cbe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor Column.width when laying out ColumnSet

The renderColumnSet path forces every column to .frame(maxWidth: .infinity) and never reads cs.columns[i].width, so cards that specify auto, stretch, or weighted widths all render as equal columns. This breaks Adaptive Card layout semantics (for example, icon+content two-column cards) and can cause content crowding or incorrect alignment.

Useful? React with 👍 / 👎.

Comment on lines +134 to +135
if let url = URL(string: img.url) {
AsyncImage(url: url) { phase in
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show fallback when image URL cannot be parsed

renderImage only renders when URL(string: img.url) succeeds, but there is no else branch for invalid URL strings. In that case the image element silently disappears (including any altText), which drops card content instead of showing the placeholder label used in the AsyncImage failure case.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80a0674033

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

struct Column: Codable {
let width: String?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept numeric Column.width values during decoding

CardElement.Column.width is declared as String?, but Adaptive Card payloads commonly send weighted widths as numbers (for example "width": 2). Swift Codable treats that as a type mismatch, so decoding a ColumnSet throws and AdaptiveCardParser.parseAdaptiveCardMarkers returns nil, which prevents inline rendering for otherwise valid cards and falls back to raw marker text. Please decode width as a string-or-number union (and normalize) so valid weighted layouts do not break parsing.

Useful? React with 👍 / 👎.

BradGroux

This comment was marked as duplicate.

Copy link
Copy Markdown
Member

@BradGroux BradGroux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: iOS/macOS Adaptive Card Renderer (SwiftUI)

853 lines adding a SwiftUI renderer in the shared OpenClawChatUI package. Includes a full Codable model layer with discriminated unions (manual init(from:) keyed on "type"), marker parsing, and SwiftUI rendering. The Codable approach with typed enums is excellent and is the right pattern for Swift. SF Symbols integration for icons is a nice touch.

Blockers

1. Strict CodeBlock decoding fails entire card parse for valid variant payloads

struct CodeBlock: Codable {
    let codeSnippet: String
    let language: String?
}

The model expects a codeSnippet key, but card producers commonly use code or text as the key name for code block content. When a card uses one of these variants, the Codable decode fails for the CodeBlock element, and because the element is decoded as part of the body array, the entire AdaptiveCard parse returns nil. The user sees nothing instead of a degraded card.

Fix options:

  • Add CodingKeys mapping codeSnippet, code, and text as alternatives
  • Use a custom init(from:) that tries multiple keys
  • Make codeSnippet optional and fall back to code or text if absent

2. Merge conflict with #41735 in docs/plugins/community.md

This PR modifies community.md to add the Adaptive Cards entry. PR #41735 is dedicated to exactly this change. Both PRs touch the same lines. Merge #41735 first, then rebase this PR and remove the community.md change from it to avoid conflicts and duplicate entries.

Suggestions

3. Invalid image URLs render blank space with no fallback

private func renderImage(_ img: CardElement.ImageElement) -> some View {
    if let url = URL(string: img.url) {
        AsyncImage(url: url) { ... }
    }
}

When URL(string:) returns nil (malformed URL string), the if let falls through and nothing is rendered. The user sees blank space with no indication that an image was intended. Add an else branch that renders a placeholder icon or descriptive text like "[Image unavailable]".

4. ColumnSet ignores width semantics

ForEach(cs.columns.indices, id: \.self) { ... }
  .frame(maxWidth: .infinity, alignment: .leading)

All columns are rendered with equal width regardless of the width property specified in the card JSON (e.g., "auto", "stretch", "50px"). This means cards designed with intentional column sizing will render incorrectly. At minimum, handle "auto" (size to content) vs "stretch" (fill remaining), which covers the majority of real-world cards.

5. ImageSet grid uses height value as column width minimum

GridItem(.adaptive(minimum: self.imageMaxHeight(size)))

The imageMaxHeight function returns a height-based value, but it is being used as the minimum column width for the LazyVGrid. These are different dimensions. On devices with different aspect ratios, this produces unexpected grid density. Use a width-based calculation instead, or derive the grid minimum from the image size property directly.

Nits

  • Unknown elements and unknown actions are silently ignored with no debug logging. For development and troubleshooting, consider a #if DEBUG log statement when an unrecognized type is encountered. This makes it much easier to diagnose schema drift between server and client.

Summary

The Swift Codable model layer with discriminated unions is genuinely well-designed and is the cleanest typed approach across all the platform PRs. The main concerns are the strict CodeBlock decoding (which will drop entire cards in practice) and the community.md conflict. Fix those, handle image fallbacks, and address the column/grid sizing issues for a solid merge.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 27, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 27, 2026

Codex review: found issues before merge.

Summary
The PR adds Swift Codable Adaptive Card models, marker parsing, a SwiftUI renderer wired into iOS/macOS chat messages, and a community plugin docs entry.

Reproducibility: yes. Source inspection gives high-confidence paths: a schema-valid card with numeric "width": 2 reaches the String? decode mismatch, and a marker inside <think> reaches the parse-before-visibility path.

Next step before merge
Maintainer review is needed because the remaining blockers include product-boundary and client security-policy decisions, not only mechanical code fixes.

Security
Needs attention: Model/plugin-authored card JSON can trigger native URL opens and remote image loads without a documented client policy gate.

Review findings

  • [P1] Decode string-or-number column widths — apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardModels.swift:47-67
  • [P2] Parse cards after thinking visibility filtering — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift:623
  • [P2] Honor column widths during layout — apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardView.swift:116
Review details

Best possible solution:

Define the native rich-message contract and client URL/image policy, then land a rebased Swift implementation that preserves thinking visibility, supports schema-valid layouts, adds focused Swift tests, and includes the required changelog entry.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection gives high-confidence paths: a schema-valid card with numeric "width": 2 reaches the String? decode mismatch, and a marker inside <think> reaches the parse-before-visibility path.

Is this the best way to solve the issue?

No. Native iOS/macOS rendering may be a valid direction, but this branch is not the best merge shape until schema compatibility, thinking visibility, URL/image policy, docs policy, tests, and changelog are aligned.

Full review comments:

  • [P1] Decode string-or-number column widths — apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardModels.swift:47-67
    Adaptive Cards allow weighted numeric widths such as "width": 2, but Column.width and TableColumnDefinition.width are String?. JSONDecoder rejects numeric values here, so otherwise valid cards fail parsing and fall back to raw marker text.
    Confidence: 0.95
  • [P2] Parse cards after thinking visibility filtering — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift:623
    This parses adaptive-card markers from raw assistant text before the existing AssistantTextParser visibility policy runs. With thinking hidden, a card embedded in <think> content can still be decoded and rendered.
    Confidence: 0.91
  • [P2] Honor column widths during layout — apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardView.swift:116
    The ColumnSet renderer gives every column an infinite-width frame and never reads the decoded width value. Cards using auto, stretch, pixel, or weighted widths render as equal columns, breaking common layouts.
    Confidence: 0.92
  • [P2] Render fallback for invalid image URLs — apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardView.swift:138-156
    The image renderer only emits a view when URL(string:) succeeds. If the URL string is malformed, the image element and alt text disappear instead of showing the same fallback label used for load failures.
    Confidence: 0.86
  • [P3] Add the required changelog entry — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift:623-640
    This wires a user-facing iOS/macOS chat feature into assistant message rendering, but the PR does not update CHANGELOG.md. Repo policy requires user-facing feature changes to include a changelog entry before merge.
    Confidence: 0.83

Overall correctness: patch is incorrect
Overall confidence: 0.9

Security concerns:

  • [medium] Gate Action.OpenUrl schemes before opening — apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardView.swift:355
    Action.OpenUrl values come from assistant/plugin-emitted card JSON and are passed to SwiftUI openURL. Without an allowlist, confirmation, or documented policy, cards can drive arbitrary URL schemes or deep links when tapped.
    Confidence: 0.88
  • [medium] Constrain remote image fetching — apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardView.swift:139
    Card image URLs are rendered with AsyncImage, so a card producer can cause native clients to request arbitrary remote URLs during chat rendering. This also bypasses the current markdown path that flattens remote images into text.
    Confidence: 0.87

Acceptance criteria:

  • cd apps/shared/OpenClawKit && swift test
  • pnpm check:changed in Testbox after any accepted Swift/docs implementation changes

What I checked:

Likely related people:

  • vincentkoc: Current local blame for the iOS shared chat rendering path, markdown renderer, image-filtering tests, and community plugin docs points to recent maintenance in commit ef79347. (role: recent maintainer; confidence: medium; commits: ef793477636a; files: apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift, apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift, apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift)
  • mbelinky: Related merged PR iOS Chat: clean UI noise and format tool outputs #22122 changed the iOS chat UI, ChatMessageViews, ChatMarkdownPreprocessor, and tool-result formatting that this PR builds on. (role: adjacent owner; confidence: medium; commits: 34dd87b0c05c; files: apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift, apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift, apps/shared/OpenClawKit/Sources/OpenClawChatUI/ToolResultTextFormatter.swift)

Remaining risk / open question:

  • Native rendering of plugin-authored card JSON needs an explicit URL-opening and remote-image policy before merge.
  • The PR introduces a user-facing app feature without current Swift regression coverage in the provided diff.
  • The docs listing part conflicts with the current ClawHub-first community plugin discovery policy.

Codex review notes: model gpt-5.5, reasoning high; reviewed against a7c5a0425988.

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

Labels

docs Improvements or additions to documentation size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants