Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
558c99e
feat(swift-lang): add OpenUI Lang parser for Swift
NandishwarSingh Oct 4, 2026
cbd0b2b
feat(swift-lang): add evaluator, store, actions and queries
NandishwarSingh Oct 4, 2026
3319604
feat(swift-lang): define component libraries and generate prompts in …
NandishwarSingh Oct 4, 2026
b82598e
feat(swift-lang): add the chat library component schemas
NandishwarSingh Oct 5, 2026
f40d62c
feat(swift-lang): render OpenUI responses with SwiftUI
NandishwarSingh Oct 5, 2026
99c9d91
feat(swift-lang): port mergeStatements and OpenUI Cloud's config block
NandishwarSingh Oct 6, 2026
317a930
feat(swift-lang): report errors and parse results like react-lang's R…
NandishwarSingh Oct 6, 2026
8a42f34
test(swift-lang): cover query recovery and input identity end to end
NandishwarSingh Oct 6, 2026
e3009cb
feat(swift-lang): ship the chat prompt options and the schema generator
NandishwarSingh Oct 6, 2026
2095871
fix(swift-lang): match react-ui's component layouts and behavior
NandishwarSingh Oct 6, 2026
cd4808a
refactor(swift-lang): bind optionals instead of force-unwrapping afte…
NandishwarSingh Oct 6, 2026
8cc561b
fix(swift-lang): report data in component slots like lang-core
NandishwarSingh Oct 6, 2026
2d15b11
docs(swift-lang): add a README
NandishwarSingh Oct 5, 2026
4d2370c
ci: build and test the Swift package
NandishwarSingh Oct 5, 2026
7b04f84
fix(swift-lang): render a finished response when the renderer is nested
NandishwarSingh Oct 6, 2026
1e9dbf1
feat(swift-lang): animate streamed responses like react-ui
NandishwarSingh Oct 6, 2026
91fd9f7
fix(swift-lang): show a finished response on the first frame
NandishwarSingh Oct 7, 2026
9fd29d3
feat(swift-lang): take accent and chart colors from the theme
NandishwarSingh Oct 7, 2026
2e0a652
feat(swift-lang): port react-ui's slider values and range slider
NandishwarSingh Oct 7, 2026
8f2a772
feat(swift-lang): snap carousels and fade their edges like react-ui
NandishwarSingh Oct 7, 2026
5062f8d
feat(swift-lang): port OpenUI Cloud's message helpers
NandishwarSingh Oct 7, 2026
a080518
feat(swift-lang): highlight code like react-ui
NandishwarSingh Oct 7, 2026
2f70ff1
feat(swift-lang): add react-ui's gallery mosaic and viewer
NandishwarSingh Oct 7, 2026
cb1cfce
docs(swift-lang): document theme colors, Cloud messages and transcripts
NandishwarSingh Oct 7, 2026
7e166b4
ci: run the Swift checks when react-ui's sentinelParser changes
NandishwarSingh Oct 7, 2026
5d049c2
feat(swift-lang): add react-ui's chart tooltips and legends
NandishwarSingh Oct 7, 2026
f345252
feat(swift-lang): add react-ui's general openuiLibrary
NandishwarSingh Oct 7, 2026
30f948e
feat(swift-lang): add an MCP tool provider
NandishwarSingh Oct 7, 2026
a2fa3cd
feat(swift-lang): port the observability bus and stream events
NandishwarSingh Oct 7, 2026
1de6387
fix(swift-lang): only count changed editable table cells as edits
NandishwarSingh Oct 7, 2026
ee8ea12
feat(swift-lang): move between editable table cells with the keyboard
NandishwarSingh Oct 7, 2026
23d77c7
feat(swift-lang): add per-chart palettes and status colors to the theme
NandishwarSingh Oct 7, 2026
3453482
feat(swift-lang): add react-ui's hover states for pointers
NandishwarSingh Oct 7, 2026
f94bebb
docs(swift-lang): list the deliberate differences from react-ui
NandishwarSingh Oct 7, 2026
478c206
fix(swift-lang): word tool errors like lang-core's err.message
NandishwarSingh Oct 7, 2026
d668047
fix(swift-lang): keep card block cards readable on narrow screens
NandishwarSingh Oct 7, 2026
17b40d1
fix(swift-lang): keep primary buttons readable when gray
NandishwarSingh Oct 7, 2026
bb2ef3e
fix(swift-lang): give each node its full height in fixed-height layouts
NandishwarSingh Oct 7, 2026
54e15a6
fix(swift-lang): let horizontal bar charts grow to fit their bars
NandishwarSingh Oct 7, 2026
663e3b6
fix(swift-lang): thin crowded chart axis labels like react-ui
NandishwarSingh Oct 7, 2026
20feb8b
fix(swift-lang): thin axis labels until they fit instead of cutting t…
NandishwarSingh Oct 7, 2026
2485549
fix(swift-lang): keep numbers a layout can't use from crashing views
NandishwarSingh Oct 7, 2026
5a65f61
fix(swift-lang): move range slider thumbs with the drag
NandishwarSingh Oct 7, 2026
fed393d
fix(swift-lang): keep long tags inside their block
NandishwarSingh Oct 7, 2026
269094d
refactor(swift-lang): read markdown heading levels in a helper
NandishwarSingh Oct 7, 2026
c59e47c
refactor(swift-lang): drop code nothing reads
NandishwarSingh Oct 7, 2026
20a9fb0
fix(swift-lang): keep a date range's ends from crossing
NandishwarSingh Oct 7, 2026
5b4273d
fix(swift-lang): skip a visual card's text panel when it has no text
NandishwarSingh Oct 8, 2026
8779745
test(swift-lang): write fixtures one case per line
NandishwarSingh Oct 8, 2026
0959ff1
fix(swift-lang): let a mouse scroll card carousels
NandishwarSingh Oct 8, 2026
0f9f68a
fix(swift-lang): keep a slider whose range is shorter than a step fro…
NandishwarSingh Oct 9, 2026
7160f33
test(swift-lang): compile the renderer tests for iOS again
NandishwarSingh Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/build-swift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Build Swift

on:
push:
branches: [main]
paths:
- "packages/swift-lang/**"
- "packages/lang-core/src/**"
- "packages/react-ui/src/genui-lib/**"
- "packages/react-ui/src/utils/sentinelParser.ts"
- "packages/react-ui/src/components/Charts/utils/styleUtils.ts"
- "packages/react-ui/src/components/Charts/shared/core/PortalTooltip/utils/**"
- ".github/workflows/build-swift.yml"
pull_request:
branches: [main]
paths:
- "packages/swift-lang/**"
- "packages/lang-core/src/**"
- "packages/react-ui/src/genui-lib/**"
- "packages/react-ui/src/utils/sentinelParser.ts"
- "packages/react-ui/src/components/Charts/utils/styleUtils.ts"
- "packages/react-ui/src/components/Charts/shared/core/PortalTooltip/utils/**"
- ".github/workflows/build-swift.yml"

permissions:
contents: read

jobs:
swift:
runs-on: macos-15
defaults:
run:
working-directory: packages/swift-lang
steps:
- uses: actions/checkout@v4

- name: Build
run: swift build

- name: Test
run: swift test

- name: Lint
run: swift format lint --strict -r Sources Tests Package.swift

- name: Build for iOS
run: xcodebuild build -scheme OpenUI-Package -destination "generic/platform=iOS Simulator" -quiet

# The Swift port is checked against fixtures generated from lang-core and the
# chat library. Fail when they no longer match what the TypeScript produces.
fixtures:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v4
with:
node-version: v24.21.0
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build lang-core and react-ui
run: pnpm --filter "@openuidev/react-ui..." --workspace-concurrency=1 run build

- name: Regenerate fixtures
run: node packages/swift-lang/Scripts/generate-fixtures.mjs
env:
OPENUI_TELEMETRY_DISABLED: "1"

# Status rather than diff, so a newly generated fixture file counts too.
- name: Check fixtures are up to date
run: |
fixtures="packages/swift-lang/Tests/OpenUILangTests/Fixtures packages/swift-lang/Tests/OpenUISwiftUITests/Fixtures"
if [ -n "$(git status --porcelain -- $fixtures)" ]; then
git status --short -- $fixtures
git diff -- $fixtures | head -100
exit 1
fi
5 changes: 5 additions & 0 deletions packages/swift-lang/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Generated from the TypeScript packages (see Scripts/); collapsed in diffs.
Tests/OpenUILangTests/Fixtures/*.json linguist-generated=true
Tests/OpenUISwiftUITests/Fixtures/*.json linguist-generated=true
Sources/OpenUILang/ChatComponents.swift linguist-generated=true
Sources/OpenUILang/OpenUIComponents.swift linguist-generated=true
2 changes: 2 additions & 0 deletions packages/swift-lang/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.build/
.swiftpm/
25 changes: 25 additions & 0 deletions packages/swift-lang/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version:6.0
import PackageDescription

let package = Package(
name: "OpenUI",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "OpenUILang", targets: ["OpenUILang"]),
.library(name: "OpenUISwiftUI", targets: ["OpenUISwiftUI"]),
],
targets: [
.target(name: "OpenUILang"),
.target(name: "OpenUISwiftUI", dependencies: ["OpenUILang"]),
.testTarget(
name: "OpenUILangTests",
dependencies: ["OpenUILang"],
resources: [.copy("Fixtures")]
),
.testTarget(
name: "OpenUISwiftUITests",
dependencies: ["OpenUISwiftUI"],
resources: [.copy("Fixtures")]
),
]
)
281 changes: 281 additions & 0 deletions packages/swift-lang/README.md

Large diffs are not rendered by default.

247 changes: 247 additions & 0 deletions packages/swift-lang/Scripts/generate-chat-components.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// Regenerates Sources/OpenUILang/ChatComponents.swift and OpenUIComponents.swift
// from openuiChatLibrary and openuiLibrary in @openuidev/react-ui, so the Swift
// libraries can't drift from the web ones.
//
// pnpm run build:packages
// node packages/swift-lang/Scripts/generate-chat-components.mjs
//
// The output is formatted with `swift format`. LibraryComponentsTests checks
// the result against the fixtures from generate-fixtures.mjs.

import { execFileSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const packages = join(here, "..", "..");
const sources = join(here, "..", "Sources", "OpenUILang");

// Code generation is not product usage, so keep lang-core's telemetry off.
process.env.OPENUI_TELEMETRY_DISABLED = "1";

const core = await import(join(packages, "lang-core", "dist", "index.mjs"));
const ui = await import(join(packages, "react-ui", "dist", "genui-lib", "index.mjs"));

// The library being generated; set by generate() below.
let library;
let known;

// ── Swift literals ────────────────────────────────────────────────────────

function swiftString(text) {
const escaped = [...text]
.map((c) => {
if (c === "\\") return "\\\\";
if (c === '"') return '\\"';
if (c === "\n") return "\\n";
if (c === "\r") return "\\r";
if (c === "\t") return "\\t";
const code = c.codePointAt(0);
return code < 0x20 ? `\\u{${code.toString(16)}}` : c;
})
.join("");
return `"${escaped}"`;
}

function swiftValue(value) {
if (value === null) return ".null";
if (Array.isArray(value)) return `[${value.map(swiftValue).join(", ")}]`;
if (typeof value === "object") {
const entries = Object.entries(value).map(([k, v]) => `${swiftString(k)}: ${swiftValue(v)}`);
return `[${entries.join(", ") || ":"}]`;
}
return typeof value === "string" ? swiftString(value) : JSON.stringify(value);
}

// ── JSON Schema → PropType ────────────────────────────────────────────────

const isFormRules = (s) =>
s?.type === "object" && s.properties && "minLength" in s.properties && "pattern" in s.properties;
const isActionExpression = (s) =>
Array.isArray(s?.anyOf) &&
s.anyOf.length === 3 &&
s.anyOf[0].properties?.type?.const === "open_url";

function propType(s) {
if (isFormRules(s)) return "formRules";
if (isActionExpression(s)) return ".actionExpression";
if (s.$ref) return `.component(${swiftString(s.$ref.split("/").pop())})`;
if (s.anyOf) {
const options = s.anyOf.every((o) => o.$ref)
? s.anyOf.filter((o) => known.has(o.$ref.split("/").pop()))
: s.anyOf;
return `.union([${options.map(propType).join(", ")}])`;
}
if (s.const !== undefined) return `.literal(${swiftValue(s.const)})`;
switch (s.type) {
case "string":
return s.enum ? `.enumeration([${s.enum.map(swiftString).join(", ")}])` : ".string";
case "number":
case "integer":
return ".number";
case "boolean":
return ".boolean";
case "array": {
const minItems = s.minItems === undefined ? "" : `, minItems: ${s.minItems}`;
return `.array(${propType(s.items)}${minItems})`;
}
case "object":
if (s.propertyNames) return `.record(${propType(s.additionalProperties ?? {})})`;
return `.object([${props(s).join(", ")}])`;
default:
return ".any";
}
}

function props(schema, component) {
const required = schema.required ?? [];
const shape = component ? library.components[component].props.shape : {};
return Object.entries(schema.properties ?? {}).map(([key, s]) => {
const args = [swiftString(key), propType(s)];
if ("default" in s) args.push(`.defaulted(${swiftValue(s.default)})`);
else if (!required.includes(key)) args.push(".optional");
if (shape[key] && core.isReactiveSchema(shape[key])) args.push("binding: true");
if (s.description) args.push(`description: ${swiftString(s.description)}`);
return `Prop(${args.join(", ")})`;
});
}

// ── Output ────────────────────────────────────────────────────────────────

const lowerFirst = (name) => name[0].toLowerCase() + name.slice(1);
const swiftLines = (items) => items.map((item) => ` ${swiftString(item)},`).join("\n");

/// Writes `enumName` with a schema per component, the groups and the prompt
/// options. Components defined exactly as in `shared` (same JSON Schema and
/// bindings) reuse its schemas instead of repeating them.
function generate({ lib, promptOptions, enumName, docs, shared }) {
library = lib;
const defs = lib.toJSONSchema().$defs;
const spec = lib.toSpec();
const names = Object.keys(spec.components);
known = new Set(names);
const reused = (name) => {
if (!shared?.lib.components[name]) return false;
const other = shared.lib.toJSONSchema().$defs[name];
const reactive = (l) =>
Object.entries(l.components[name].props.shape)
.filter(([, s]) => core.isReactiveSchema(s))
.map(([key]) => key)
.join();
return (
JSON.stringify(other) === JSON.stringify(defs[name]) && reactive(shared.lib) === reactive(lib)
);
};

const schemas = names
.filter((name) => !reused(name))
.map(
(name) => ` public static let ${lowerFirst(name)} = ComponentSchema(
${swiftString(name)}, description: ${swiftString(defs[name].description ?? "")},
props: [${props(defs[name], name).join(", ")}])
`,
);
const groups = spec.componentGroups.map((group) => {
const notes = group.notes?.length
? `, notes: [${group.notes.map(swiftString).join(", ")}]`
: "";
return ` ComponentGroup(name: ${swiftString(group.name)}, components: [${group.components.map(swiftString).join(", ")}]${notes}),`;
});
const all = names.map(
(name) => ` ${reused(name) ? `${shared.enumName}.` : ""}${lowerFirst(name)},`,
);
// A library reusing another's schemas aliases its rules object only when one
// of its own schemas has form rules.
const formRules = shared
? schemas.some((schema) => /\bformRules\b/.test(schema))
? ` /// The form \`rules\` object, for schemas that aren't shared.
static let formRules = ${shared.enumName}.formRules

`
: ""
: ` /// The structured \`rules\` object shared by form fields.
public static let formRules: PropType = .object(
["required", "email", "url", "numeric"].map { Prop($0, .boolean, .optional) }
+ ["min", "max", "minLength", "maxLength"].map { Prop($0, .number, .optional) }
+ [Prop("pattern", .string, .optional)])

`;
const output = join(sources, `${enumName}.swift`);
writeFileSync(
output,
`// Generated by Scripts/generate-chat-components.mjs from ${docs.source} in
// @openuidev/react-ui. Don't edit by hand.
// swift-format-ignore-file: LineLength

${docs.comment}
public enum ${enumName} {
${formRules}${schemas.join("\n")}
/// Prompt groups and notes, as in ${docs.source}.
public static let groups: [ComponentGroup] = [
${groups.join("\n")}
]

/// Every schema, in library order.
public static let all: [ComponentSchema] = [
${all.join("\n")}
]

/// Example responses for the prompt (\`${docs.examples}\`).
public static let examples: [String] = [
${swiftLines(promptOptions.examples)}
]

/// Extra prompt rules for this library (\`${docs.rules}\`).
public static let additionalRules: [String] = [
${swiftLines(promptOptions.additionalRules)}
]

/// The prompt options react-ui pairs with this library (\`${docs.options}\`).
public static let promptOptions = PromptOptions(
additionalRules: additionalRules, examples: examples)
}
`,
);
execFileSync("swift", ["format", "-i", output]);
console.log(
`wrote ${names.length} ${docs.source} schemas to ${output}` +
(shared ? ` (${names.filter(reused).length} shared with ${shared.enumName})` : ""),
);
}

const chat = {
lib: ui.openuiChatLibrary,
promptOptions: ui.openuiChatPromptOptions,
enumName: "ChatComponents",
docs: {
source: "openuiChatLibrary",
examples: "openuiChatExamples",
rules: "openuiChatAdditionalRules",
options: "openuiChatPromptOptions",
comment: `/// Schemas for the OpenUI chat library. Names, prop order, types and
/// descriptions match \`openuiChatLibrary\` in @openuidev/react-ui, so a backend
/// that prompts with either library produces responses both can render.
///
/// The schemas live in \`OpenUILang\`, without SwiftUI, so a server can build
/// the chat prompt or Cloud config for them too.`,
},
};
generate(chat);
generate({
lib: ui.openuiLibrary,
promptOptions: ui.openuiPromptOptions,
enumName: "OpenUIComponents",
shared: chat,
docs: {
source: "openuiLibrary",
examples: "openuiExamples",
rules: "openuiAdditionalRules",
options: "openuiPromptOptions",
comment: `/// Schemas for react-ui's general OpenUI library, \`openuiLibrary\`: the chat
/// library's components without the chat-only follow-ups and sections, plus
/// \`Stack\` (the root) and \`Modal\`, with \`Card\` taking Stack's layout props.
/// Components both libraries define the same way are \`ChatComponents\`'
/// schemas.`,
},
});
Loading
Loading