Repository navigation
feat(swift-lang): add native Swift and SwiftUI support - #1295
Open
NandishwarSingh wants to merge 50 commits into
Open
NandishwarSingh wants to merge 50 commits into
NandishwarSingh wants to merge 50 commits into
Conversation
|
@NandishwarSingh is attempting to deploy a commit to the thesys-devs Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Ports lang-core's parsing pipeline to a Swift package (OpenUILang): lexer, statement splitter and auto-close, Pratt expression parser, schema-aware materialization and validation, the batch parser and the incremental streaming parser. The port keeps the JavaScript semantics the TypeScript code relies on: UTF-16 indexing, JSON.parse string unescaping (with its fallback for malformed escapes), Number to String formatting, loose equality, truthiness, undefined versus null, and JavaScript object key order. Conformance fixtures are generated from @openuidev/lang-core by Scripts/generate-fixtures.mjs: 55 inputs (the chat library prompt examples plus edge cases) for batch parsing, streaming checkpoints for each, and StreamParser.set() sequences. The Swift tests compare against them.
Ports the runtime half of lang-core and react-lang's useOpenUIState to OpenUILang: - the expression evaluator (operators, builtins, @each, action steps, reactive assignments) and schema-aware prop evaluation - the state store, state fields and form rule validators - the query manager (caching, refresh intervals, mutations, MCP results) behind a ToolProvider protocol - OpenUIRuntime, which ties the streaming parser, store, evaluation, form fields and action execution together for UI layers to observe Evaluation fixtures generated from lang-core cover operators, builtins, action plans, ternary components, bindings, query results and every chat library example. One intentional difference: lang-core re-evaluates the binding of a reactive input rendered inside @each or a ternary, nesting it four levels deep, so typing writes undefined into the bound $state. The Swift port keeps the binding intact; RuntimeTests pins the expected behaviour.
…Swift Adds the library side of OpenUILang: - PropType/Prop/ComponentSchema to describe components with typed props, $binding props and defaults; schemas serialize to the same JSON Schema Zod's toJSONSchema produces, and signatures match buildSignature - Library, which compiles the parser's parameter map, answers binding lookups for evaluation, and exports a LibrarySpec a server can pass to lang-core's generateSystemPrompt - generatePrompt, a port of lang-core's prompt assembly Fixtures check the prompt text byte for byte against lang-core across option combinations (tools, bindings, edit and inline modes, groups), and check Swift-defined chat components against the Zod schemas.
ChatComponents defines all 84 openuiChatLibrary components in the Swift DSL, with the same names, prop order, types, defaults, $binding props, descriptions and prompt groups. Array props gain an optional minItems, which the card blocks use. Tests compare every schema with the Zod output, every signature with lang-core's, and the full chat prompt generated from the Swift library with lang-core's, byte for byte.
Adds OpenUISwiftUI: - OpenUIRenderer, a view that renders a (streaming) response with a component library, backed by an observable OpenUIContext around OpenUIRuntime; node identity follows statement names so inputs keep focus and local state while the response re-renders - OpenUIChatLibrary, native SwiftUI views for every openuiChatLibrary component: content, Swift Charts (bar, line, area, horizontal bar, pie, radial, stacked, scatter, plus a drawn radar chart), tables and the editable table, forms with validation, chips and option cards, buttons, lists, tabs, accordions, sections, carousels and the card blocks - lucide icon names mapped to SF Symbols with react-ui's category fallbacks - card block clicks that merge item context into the action like react-ui's withItemContext Tests cover library completeness, card actions, icon mapping, node identity, context updates and an offscreen end-to-end render of every chat library example.
Edit mode's mergeStatements and generateSystemPrompt({ cloud: true }) (with
the library checks Cloud applies) are now in OpenUILang, both checked
against fixtures generated from lang-core.
Library JSON Schemas now match Zod's key order exactly: top-level properties
in library order, $defs in Zod's depth-first discovery order, and minItems
before type. The chat schemas are listed in openuiChatLibrary's order, so
the full chat library schema and its Cloud config are byte-identical to
lang-core's output.
…enderer OpenUIRuntime gains onError and onParseResult, ported from react-lang's useOpenUIErrors: parser errors carry enrichErrors' hints, a response with no root reports parse-failed, failed Query/Mutation calls are included, and errors are reported once streaming ends and cleared when the next response starts. OpenUIRenderer passes both through and takes a custom queryLoader view, defaulting to a small spinner like the React renderer. Hints are checked against lang-core's enrichErrors, and the parser fixtures now include a response in OpenUI Cloud's content/end marker format.
A failed query recovers on refetch and clears its error, and a rendered text field stays the same AppKit view while statements stream in after it and while typing writes its $binding (the focus loss found in the Angular port's review).
ChatComponents now includes openuiChatPromptOptions (examples and additional rules), so a Swift app prompts exactly like a react-ui app: the full chat prompt and the chat Cloud config are byte-identical to react-ui's. ChatComponents.swift is generated from openuiChatLibrary by Scripts/generate-chat-components.mjs.
Rendered every chat example and 42 real OpenUI Cloud responses with both react-ui (in Storybook, at the same width) and the SwiftUI renderer, and ported the differences: - Behavior: Tabs follow the streaming tab until the user picks one, Accordion opens the newest item, SectionBlock opens sections as they stream and folds back to the first when the response ends. - Layout: card blocks and OptionCards use react-ui's responsive grid (rows of up to 2 or 3, two columns at 768pt and below, one at 480pt), clickable cards get chevrons, snippet cards are label/value rows, visual cards put the text panel over the photo, and the root Card has no frame. - Text: TextContent renders full markdown with [n] citations resolved against the card's sources, tables, line breaks and $$ math (as source); only ~~double~~ tildes strike through. - Forms: required fields get an asterisk, DatePicker shows a placeholder until something is picked, Slider shows min/max labels, Select is a full-width field, switches lead their labels, and inputs use react-ui's field look. - Data: tables fill the width with tinted alternate rows, page like react-ui and show a placeholder while a Query fills them; FollowUpBlock shows "Related Queries"; sources show as favicon cards; tags use react-ui's corners and padding. - Charts: react-ui's default palette and curves (monotone by default, no point markers), horizontal grid lines, rounded bars, gradient area fills and scatter axes fitted to the data. Theme defaults follow react-ui's spacing, radii, fills and borders.
Ports thesysdev#1287: a string, number, boolean, object or array in a slot that only accepts components is a type-mismatch and is pruned, and that aren't listed as components (zod's hoisted __schema0 shapes) don't count as components. The new lang-core cases are in the fixtures.
Covers installing the package as a local Swift package, defining components and libraries, generating prompts, rendering streamed output, the chat library, tool providers, the differences from lang-core, and how to regenerate the conformance fixtures.
Runs on changes to packages/swift-lang, lang-core sources or the chat library: builds, tests and lints the package on macOS, and regenerates the conformance fixtures from the TypeScript packages to fail when the Swift port and lang-core drift apart.
The renderer's content was a Group, and a Group's modifiers apply to its children. Before the first parse there are none, so inside a stack or a lazy list the initial onChange never ran and a response that doesn't change afterwards (a restored chat message) stayed blank. Streaming hid it, since each new chunk triggers an update.
- Charts use react-ui's motion: bars grow in, lines and areas draw from left to right, pie/radial/radar/scatter fade in, and values glide to new positions on the dataMorph spring (stiffness 480, damping 44) as data streams in. Points are keyed by series and label so they move rather than redraw. - Newly streamed nodes fade in; nodes keep their identity, so nothing else re-animates. Tabs, accordions and sections animate when they follow the stream. - Charts that have no data yet and loading images show react-ui's pulsing skeleton; images fade in when they load. - Card grids are a Layout that reads the offered width, so they lay out correctly on the first frame instead of re-flowing. Everything respects Reduce Motion.
The renderer parsed the response in onAppear/onChange, which run after the first layout. A finished response (a restored message, or one rebuilt when it scrolls back into view) laid out empty for a frame and then jumped, and a transcript scrolled to the end landed short of it. The context now preloads the response when it's created: it parses and seeds the store, but reports nothing and runs no queries, so callbacks and queries still start on the first update as before. The renderer keeps the context in a StateObject, whose initializer is an autoclosure, so the parse runs once per renderer and not on every init of the view.
OpenUITheme gets onAccent (react-ui's textAccentPrimary) and chartPalette (react-ui's defaultChartPalette). Primary and icon buttons, option rows, list actions, steps, icon badges and the editable table's save button use the theme's accent and onAccent instead of the app's accentColor, and the renderer tints controls with the accent. Charts pick from chartPalette the same way they pick from the default ramp; an empty palette falls back to the default.
The slider follows SliderBlock: the label row shows its values as number
fields (a menu of steps for discrete sliders, up to 200), typed values
get react-ui's messages ("Invalid number", "Value must be between X and
Y", "Min must be less than max"), and two values make a two-thumb range
slider that works with drag, VoiceOver and the arrow keys.
Carousel and the carousel card blocks share one scroller: items snap to the leading edge, an edge fades out while there's more to scroll that way (40pt, 48pt for card blocks), and Carousel gets react-ui's step buttons. Card widths come from containerRelativeFrame instead of a measured width, so the first frame no longer lays out at the wrong width and jumps. The fades and buttons read the scroll geometry but never change the layout.
Ports react-ui's sentinelParser and the message builders its GenUIAssistantMessage uses: split a stored message into the program and its context, wrap content and context, build the follow-up and form state messages react-ui sends, restore form state from a message, and read ]]>openui:artifact headers. Offsets are UTF-16, as in JavaScript. generate-fixtures.mjs imports sentinelParser.ts directly (Node strips the types) and writes messages.json, which the new tests check the port against.
CodeBlock uses Prism's vscDarkPlus colors and gets react-ui's copy button (shown on hover on the Mac, as on the web). Fenced code in Markdown keeps its language, uses vscDarkPlus or oneLight with the color scheme and is trimmed like react-ui's; image lines render as images. Highlighting comes from a small tokenizer for common languages rather than Prism's grammars.
ImageGallery lays out the first five images in react-ui's grid templates, the first one large, with its narrow templates below 520pt (react-ui switches on a 768px viewport, and a chat column is narrower than its window). Past five images it shows a Show All button. Tapping an image opens a viewer with one image per page (swipe, or the arrow keys), react-ui's thumbnail strip, and the image's details under it. react-ui sizes the grid from the loaded images, up to 376px. The mosaic uses a fixed shape instead so it doesn't jump when the images arrive.
Covers the theme's accent and chart palette, the OpenUI Cloud message helpers, the highlighter's limits, and laying out a chat transcript in a VStack: a LazyVStack re-measures rows inside animations (an animated scrollTo, the keyboard closing), which can block the main thread for seconds.
The message fixtures are generated from sentinelParser.ts, so a change there can make them stale without touching the paths the workflow watched.
NandishwarSingh
force-pushed
the
feat/swift-lang
branch
from
October 7, 2026 07:50
5d444cd to
7e166b4
Compare
- Hovering with a pointer or tapping shows react-ui's tooltip, with each visible series' value in its tooltip number format, and its crosshair: a band behind bars, a dashed line with dots on lines and areas. A tap rather than a drag picks, so a chart never takes over scrolling. Tapping the same category closes it, and opening another closes it. - Legends are react-ui's DefaultLegend: centered keys that fold behind "N more" when they don't fit on one line. Tapping a key hides that series, never the last one, and colors stay put. - Value axes sit on the leading side with react-ui's tick format (1.3K, 2.5M). Axis titles go above and below the plot, as in react-ui's condensed charts, instead of inside it, where an empty one still took room. Fixes found on the way: pie and radial slices are sorted largest first, as react-ui sorts them, and the single stacked bar now matches react-ui's SegmentedBar (ramp colors in input order, a 20pt track with gaps, shares in the legend, a value and share tooltip). The number format fixtures come from react-ui's formatters, so the Swift workflow also runs when those change.
NandishwarSingh
force-pushed
the
feat/swift-lang
branch
from
October 7, 2026 08:58
05f2b8a to
fc2e378
Compare
OpenUILibrary.library renders react-ui's openuiLibrary, for apps that aren't a chat. It shares the chat library's views and adds: - Stack, the root: a flex row or column with react-ui's gap tokens, align, justify and wrap. Cards in a row share the width like react-ui's flex: 1 cards; other views keep their size and shrink in proportion. - Card with Stack's layout props and its card, sunk and clear variants. - Modal: a sheet its $boolean open binding shows. Closing it (the X, Escape, a swipe) writes false back, like react-ui's useStateField. The schemas (OpenUIComponents) come from the same generator as the chat ones and reuse ChatComponents for the 76 components both libraries define the same way. The schema tests now cover both libraries against Zod, including the full JSON Schema and both prompts byte for byte, and the general library's examples join the parser, streaming, evaluation and end-to-end render fixtures.
react-lang's Renderer takes an MCP client directly as its toolProvider and unwraps each callTool result. McpToolProvider does the same for a closure that returns the client's raw result, so error results become McpToolErrors (reported as mcp-error) and structuredContent or JSON text becomes the query's data. A closure keeps the package free of any particular MCP client.
NandishwarSingh
force-pushed
the
feat/swift-lang
branch
from
October 7, 2026 09:19
66dd601 to
323e093
Compare
Observability.shared ports @openuidev/observability: sinks listen by level or to everything, and emitters skip building events when nothing listens. The runtime publishes react-lang's react-lang:stream events with the same fields (a stable id, phase, updateIndex, the response, parser metadata, errors and timings), so a sink written for the web, like observability-cloud, reads them unchanged. A settled response with errors is an error-level event. StreamObservation ports advanceStreamingObservability, and its tests are react-lang's, case for case. publishObservability: false on the renderer turns the events off, like react-lang's Renderer prop.
On the Mac a focused text field writes its text back when editing ends, so clicking or tabbing through cells stored their unchanged values as edits and the table offered to save changes nobody made. Text equal to the data is no longer an edit, and typing the original back undoes one. The bookkeeping moves into TableEdits so it can be tested on its own.
react-ui's EditableTable takes spreadsheet keys. With a hardware keyboard, Up and Down now move between rows in the same column, and Enter keeps the edit and moves down; Tab already moved across. Cells stay text fields rather than react-ui's select-then-edit, which suits touch. Escape isn't ported: a focused Mac text field keeps showing its typed text after the value is reverted, so it would look undone without being undone.
OpenUITheme gets react-ui's per-chart palettes (barChartPalette, lineChartPalette and the rest). Each chart uses its own, then chartPalette, then the default ramp, as react-ui's resolvePalette does, with an empty palette counting as none. The single stacked bar uses the bar palette, as react-ui's does. It also gets info, success, alert and danger colors for callouts, tags, icon badges, trends, form and slider errors and the code block's copied state, which used hardcoded system colors. The defaults are those same colors.
With a pointer (on the Mac, or an iPad with a trackpad), controls now react the way react-ui's do: clickable cards sink a little and visual cards zoom their photo, list rows grow their marker, chips fill in, unselected option cards darken their border, empty radios and check boxes fill, inactive tabs brighten and sources highlight. A HoverButtonStyle tracks the pointer and passes it to the label through the environment, so each control draws its own hover state. Touch is unchanged.
The section on differences covered lang-core and a few rendering limits. It now also covers the choices made for native platforms: tooltips on a tap without hover, editable table cells that stay text fields, and the gallery's fixed mosaic proportions.
Query and Mutation errors interpolated the thrown error, which for a Cocoa error prints its debug form: a location lookup that hadn't got a fix yet reported "Error Domain=kCLErrorDomain Code=0 "(null)"" to onError and to the user. They now use what lang-core's err.message corresponds to: a LocalizedError's description, a Cocoa error's localized description, and otherwise the error's own description as before.
With responsive set to false, react-ui keeps up to three cards per row and lets each card's minimum width (196px for context cards, 158px for overview cards, 15rem for composite cards) overflow its column. The port had no minimum, so on a phone three context cards squeezed to a word or two per line with hyphens. The grid now drops to as many columns as fit at the card's minimum width, keeping react-ui's breakpoints otherwise.
Primary buttons set their label to the theme's onAccent color. The system draws a disabled prominent button, and every prominent button in an inactive Mac window, gray instead of in the accent, which left white text on light gray. Those now keep the system's label color.
Offered exactly its ideal height (a fixed frame, a self-sizing table cell), a response's VStack split that height by flexibility. A row with a Spacer (a list item, callout, check box or key/value row) counts as very flexible, so wrapping text above it was offered too little and cut off mid-sentence, and the leftover height stayed empty. Each node now takes its ideal height for its width. In a scroll view, which offers unlimited height, nothing changes.
A horizontal bar chart needs a row per category, so it asked for at least 28pt each, but its frame was fixed at the chart height. With more than about eight categories the bars overflowed the frame above and below, over the axis titles and the value labels. The frame now grows to the chart's minimum instead.
Bar, line and area charts squeezed every category label into its own slot, so on a phone eight city names read "Mum…", "Kolk…" and a burndown's "Sprint 1" to "Sprint 10" all read "Sprin…". react-ui's condensed axis draws every n-th label once a category is narrower than its label (counting at most 40px) plus an 8px gap, and gives each the room of the ones skipped. The port now does the same: each label is centered under its category, and the first and last are cut at the ends of the plot and moved inside it, as react-ui's CondensedXAxis keeps them.
…hem off react-ui's rule only thins labels until each has 40px, so longer ones still truncate: a cohort chart's seven "Month n" labels read "Month…". The axis now keeps thinning while labels would be cut off, as long as three remain, and draws them in the font they're measured in, which also stops labels that fit from being truncated. The README lists this with the other differences from react-ui.
Numbers in a response can be infinite or NaN (1e999, 0/0) or far larger than a view can draw, and a few of them reached conversions that trap or loops that never end: a Query refresh interval of 1e999, a TextArea with 1e999 rows and a discrete Slider up to 1e999 crashed; a discrete Slider with a billion steps hung, building its menu options, and on the Mac laying out a tick mark per step. Bounds and sizes that aren't finite now fall back to their defaults, refresh intervals stop at setInterval's longest delay, step menus give up past 200 options without building them, and a slider with more than 100 steps snaps its own value.
A drag's translation is measured from where it began, but each update added it to where the thumb had already moved to, so the thumb ran ahead of the finger and hit the other end of the range. The drag now starts from the thumb's position when it began.
A tag wider than its container was laid out at its full width and ran off the card. Wrapping rows now give a child wider than a row the row's width, and tags stay on one line, cut short with an ellipsis, as react-ui's `max-width: 100%` and `text-overflow: ellipsis` do.
Library validation built codes and paths for its issues that only ever became one message, the generated openuiLibrary schemas aliased a rules object none of them use, and two views kept a value they never read.
Each end of a range DatePicker could be set past the other, storing a `to` before its `from`. Each end is now bounded by the other, as react-ui's range calendar does.
NandishwarSingh
force-pushed
the
feat/swift-lang
branch
from
October 7, 2026 19:10
2ed7751 to
20a9fb0
Compare
A response can bind a card's text to values that come out empty, most often by using an @each loop variable in a separate statement, which can't see it. The card then drew an empty white panel over its photo. It now shows just the photo; the README lists this with the other differences from react-ui, which draws the empty panel.
Pretty-printed, the fixtures generated from the TypeScript packages took about 39k lines, most of this package's diff, for data nobody reads value by value. They're now one case per line: still JSON with the same contents, still diffable case by case, and the CI job that regenerates them still compares byte for byte.
Card blocks laid out as a carousel had no step buttons, as in react-ui, so on a Mac with a mouse the cards past the edge were out of reach: the wheel scrolls up and down, and there's no shift-scrolling or scrollbar to fall back on like in a browser. Show the Carousel's buttons while a pointer is over the row.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Resolves #393. Adds native Swift support as a SwiftPM package in
packages/swift-langwith two libraries:OpenUILang: the parser (batch + streaming), runtime (state, bindings, actions, Query/Mutation, error reporting), component library DSL, prompt generation, OpenUI Cloud config, Cloud's message format and edit-mode merge. Plain Swift + Foundation.OpenUISwiftUI: anOpenUIRendererview plus native SwiftUI views for react-ui's two libraries,openuiChatLibrary(84 components) andopenuiLibrary(Stack root, Modal), so the same prompt or OpenUI Cloud setup that drives react-ui on web renders natively on iOS, iPadOS and macOS.Right now an Apple app has to wrap the React runtime in a WebView or write its own parser. This gives Swift apps the same flow as the JS packages: define components, generate the prompt or Cloud config, render the stream.
I saw #393 had an earlier go-ahead back in June but nothing landed since, so I went ahead and built it. Fine to change direction on anything below.
Changes
OpenUILangis a port of lang-core, not a reimplementation. Lexer, statement splitter/auto-close, Pratt expression parser, materialize + schema validation, streaming parser, evaluator, store, query manager, prompt generation,generateSystemPrompt({ cloud: true })andmergeStatementsfollow the TS file by file, so changes there map over directly. lang-core changed while this was open (lang-core: report plain objects in component slots as type-mismatch #1287, data in component slots) and that's ported too.JSON.parsestring unescaping (including its fallback for broken escapes mid-stream),Numberto string formatting,toFixedrounding, loose==, truthiness,undefinedvsnull, and JS object key order (prop order is argument order).toSpec()works withgenerateSystemPrompton a server, and its Cloud config is byte for byte what lang-core sends.OpenUIRendererwraps a framework-freeOpenUIRuntime(a port ofuseOpenUIState) and has react-lang's props:onAction,onStateUpdate,onParseResult,onError(same errors and timing asuseOpenUIErrors, with enrichErrors' hints),queryLoaderandpublishObservability. Nodes keep their identity by statement name across streamed updates, so inputs keep focus while the response re-renders (the issue that came up on the Angular port). A finished response renders on its first frame.sentinelParserand message helpers for OpenUI Cloud's message format,@openuidev/observability(the runtime emits the samereact-lang:streamevents), and an MCP tool provider like passing an MCP client to react-lang's Renderer.ChatComponents.swiftandOpenUIComponents.swiftare generated from react-ui byScripts/generate-chat-components.mjs.build-swift.ymlthat builds/tests/lints on macOS and builds for iOS, and a Linux job that regenerates the fixtures from freshly built lang-core + react-ui and fails if anything changed, so the port can't silently drift.Doesn't touch any JS package, the lockfile or shared tooling. No
package.jsoninpackages/swift-lang, so pnpm and the existing CI ignore it, and no changeset is needed.One intentional difference, and it's a lang-core bug: a reactive input inside
@Eachor a ternary (e.g.@Each($items, it, Input("n", "p", "text", null, $v))) gets its binding re-evaluated inevaluatePropCore's plain-object branch and nested four levels deep, so typing writesundefinedinto$v. The Swift port keeps the binding intact and a test pins it. I can open a separate issue/PR for the TS side. The rest are in the README's differences section (no TeX, a small highlighter instead of Prism's grammars, tooltips on tap since touch has no hover, table cells that stay text fields, the gallery's fixed proportions).Test Plan
Verified locally
swift test: 131 tests in 36 suites pass, Swift 6 strict concurrency, no warnings,swift format lint --strictclean. CI is green on macos-15 (Xcode 16.4) and the iOS build; I'm on Xcode 26.6 locally, so both toolchains are covered.Conformance fixtures generated from lang-core/react-ui: parser and streaming checkpoints for the examples of both libraries plus edge cases (fences, CRLF, emoji, broken escapes, cycles, enum/type validation, data in component slots),
set()sequences, evaluation, prompts, edit-mode merges, Cloud configs, Cloud's message format, error hints, chart number formats, and both libraries' schemas, JSON Schema and prompts byte for byte.Real model output: 42 prompts covering every component family sent to OpenUI Cloud and replayed chunk by chunk, 1,770 chunks. Swift and lang-core produce identical parse results at every chunk through
set()andpush(), and identical evaluated trees.Rendered every real response I have (42 from OpenUI Cloud, 42 from the planner app) at phone width and checked each one for layout that doesn't fill its measured height, which is how a few of the fixes in here were found (text cut off next to rows with spacers, horizontal bars spilling over their axis titles, cards squeezed to a word per line).
Fed it numbers a model can write but a layout can't use (
1e999, a slider with a billion steps, a refresh interval of1e999) and checked nothing crashes or hangs. That found three crashes and a hang, fixed in here with a test each.Bug-fix tests were checked by putting the old behavior back and watching them fail (the edit counting, a closed Modal's spacing, tool error wording, text cut off at an exact height, the refresh interval crash, a long tag running off its block).
Ran a SwiftUI planner app on real Thesys responses on iPhone, iPad and Mac, with XCUITest flows for the gallery, chart tooltips/legends, dragging a range slider, the table keys, photos, payments and follow-ups. It hit a main-thread hang in long chats that I traced to
LazyVStacktranscripts re-measuring inside animations (not the package), so the README now says to use aVStack. After that, 196 replayed runs had no stall over 0.6 s.Every commit builds, passes its tests and lints on its own. Fixture regeneration from a fresh build reproduces the committed fixtures byte for byte. JS side unchanged.
Demo
The planner app from the test plan on iPhone, iPad and Mac. These are real Thesys answers, replayed so the runs repeat, and the data in them comes from tools running on the device: Open-Meteo for the forecast, Apple Maps for places, Wikipedia for sights and photos, Razorpay's test checkout for payments. Sped up, with the waits cut.
Same answer on a Mac, an iPad and an iPhone.
On the Mac the chart follows the pointer:
More: Mac Rome, iPad charts, Rome on all three
The app lives on a branch of my fork so this PR stays package-only:
examples/app-frameworks/swift-planner. It has its own README (with more clips and screenshots) and UI tests. I can open it as its own PR, like the Angular example, if you want it in the repo.Screenshots
Top two rows are rendered offscreen on macOS at 440pt: the chat library's dashboard example in light, dark and 45% into the stream, then the trip form example, the Paris guide example and a sheet of the charts. Below that, iPhone simulator shots from the UI test runs (gallery, viewer, chart tooltips) and code blocks in dark mode.
Checklist
Where to look
It's big, so here's the order that makes sense:
Sources/OpenUILang/Parser.swift,Materialize.swift,Validation.swift,Evaluator.swift: the core of the port, side by side with their TS files.Tests/OpenUILangTests/ParserConformanceTests.swift+Scripts/generate-fixtures.mjs: how conformance is proven.Sources/OpenUILang/Runtime.swiftandSources/OpenUISwiftUI/OpenUIRenderer.swift,OpenUIContext.swift,OpenUINode.swift: the runtime and renderer.Sources/OpenUISwiftUI/Components/*: the views.The schemas files and the JSON fixtures are generated (marked in
.gitattributes, so GitHub collapses them). The commits go roughly in that order, then one per react-ui feature, if you'd rather go commit by commit. I can also split it into separate PRs (e.g.OpenUILangfirst, then the SwiftUI renderer).Two things for you to decide:
Package.swiftpointing atpackages/swift-lang, or mirroring it to a separateopenui-swiftrepo. The README says to add it as a local package for now.examples/as a follow-up PR, or something smaller.