Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/rn-renderer-bundler-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@mobile-reality/mdma-renderer-react-native": patch
"@mobile-reality/mdma-runtime": patch
"@mobile-reality/mdma-spec": patch
---

Add `main`, `module`, and `react-native` entry fields (and a `default` export condition) alongside the existing `exports` map. These packages previously exposed only an `exports` map, so bundlers that don't opt into package `exports` resolution — notably Metro/Snackager (Expo Snack) — couldn't find an entry point and failed with "Can't resolve ''". The added fields make the packages resolvable in any React Native / Metro bundler without enabling `unstable_enablePackageExports`. Fully additive and backwards-compatible.
85 changes: 69 additions & 16 deletions demo-native/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export function App() {
// Mutate the blocks of a specific assistant message.
const editBlocks = useCallback((botId: number, fn: (blocks: Block[]) => Block[]) => {
setMessages((prev) =>
prev.map((m) => (m.id === botId && m.role === 'assistant' ? { ...m, blocks: fn(m.blocks) } : m)),
prev.map((m) =>
m.id === botId && m.role === 'assistant' ? { ...m, blocks: fn(m.blocks) } : m,
),
);
}, []);

Expand Down Expand Up @@ -191,7 +193,10 @@ export function App() {
// user stopped — leave partial output as-is
} else {
const message = e instanceof Error ? e.message : String(e);
editBlocks(botId, (blocks) => [...blocks, { id: nextBlockId(), kind: 'text', text: `⚠️ ${message}` }]);
editBlocks(botId, (blocks) => [
...blocks,
{ id: nextBlockId(), kind: 'text', text: `⚠️ ${message}` },
]);
}
} finally {
abortRef.current = null;
Expand Down Expand Up @@ -224,14 +229,22 @@ export function App() {
<View style={{ flex: 1 }}>
<Text style={[styles.title, { color: c.text }]}>MDMA Agent</Text>
<Text style={[styles.subtitle, { color: live ? c.accent : '#dc2626' }]} numberOfLines={1}>
{live ? `${providerLabel} · ${model}` : `No key · set ${PROVIDERS.find((p) => p.id === provider)?.envVar}`}
{live
? `${providerLabel} · ${model}`
: `No key · set ${PROVIDERS.find((p) => p.id === provider)?.envVar}`}
</Text>
</View>
<View style={styles.headerActions}>
<Pressable onPress={() => setShowSettings((v) => !v)} style={[styles.iconBtn, { borderColor: c.border }]}>
<Pressable
onPress={() => setShowSettings((v) => !v)}
style={[styles.iconBtn, { borderColor: c.border }]}
>
<Text style={{ color: c.text, fontSize: 13 }}>⚙️</Text>
</Pressable>
<Pressable onPress={() => setTheme(dark ? 'light' : 'dark')} style={[styles.iconBtn, { borderColor: c.border }]}>
<Pressable
onPress={() => setTheme(dark ? 'light' : 'dark')}
style={[styles.iconBtn, { borderColor: c.border }]}
>
<Text style={{ color: c.text, fontSize: 13 }}>{dark ? '☀️' : '🌙'}</Text>
</Pressable>
<Pressable onPress={reset} style={[styles.iconBtn, { borderColor: c.border }]}>
Expand All @@ -242,7 +255,9 @@ export function App() {

{/* Settings — provider + model. Keys come from .env (EXPO_PUBLIC_*). */}
{showSettings ? (
<View style={[styles.settings, { backgroundColor: c.chromeBg, borderBottomColor: c.border }]}>
<View
style={[styles.settings, { backgroundColor: c.chromeBg, borderBottomColor: c.border }]}
>
<Text style={[styles.settingsLabel, { color: c.muted }]}>Provider</Text>
<View style={styles.row}>
{PROVIDERS.map((p) => {
Expand All @@ -252,7 +267,11 @@ export function App() {
<Pressable
key={p.id}
onPress={() => pickProvider(p.id)}
style={[styles.chip, { borderColor: active ? c.accent : c.border }, active && { backgroundColor: c.accent }]}
style={[
styles.chip,
{ borderColor: active ? c.accent : c.border },
active && { backgroundColor: c.accent },
]}
>
<Text style={{ color: active ? '#fff' : c.text, fontSize: 13 }}>
{hasKey ? '● ' : '○ '}
Expand All @@ -268,11 +287,14 @@ export function App() {
onChangeText={setModel}
autoCapitalize="none"
autoCorrect={false}
style={[styles.settingsInput, { color: c.text, borderColor: c.border, backgroundColor: c.pageBg }]}
style={[
styles.settingsInput,
{ color: c.text, borderColor: c.border, backgroundColor: c.pageBg },
]}
/>
<Text style={[styles.settingsHint, { color: c.muted }]}>
● = key present. Set keys in demo-native/.env (EXPO_PUBLIC_…_API_KEY) and rebuild — they are
never entered in the app.
● = key present. Set keys in demo-native/.env (EXPO_PUBLIC_…_API_KEY) and rebuild — they
are never entered in the app.
</Text>
</View>
) : null}
Expand All @@ -299,7 +321,10 @@ export function App() {
<Text style={{ color: '#fff', fontSize: 15 }}>{m.text}</Text>
</View>
) : (
<View key={m.id} style={[styles.botBubble, { backgroundColor: c.botBg, borderColor: c.border }]}>
<View
key={m.id}
style={[styles.botBubble, { backgroundColor: c.botBg, borderColor: c.border }]}
>
{m.blocks.length === 0 ? (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<ActivityIndicator size="small" color={c.muted} />
Expand All @@ -312,9 +337,23 @@ export function App() {
{b.text}
</Text>
) : b.doc ? (
<MdmaDocument key={b.id} ast={b.doc.ast} store={b.doc.store} theme={theme} style={{ gap: 8 }} />
<MdmaDocument
key={b.id}
ast={b.doc.ast}
store={b.doc.store}
theme={theme}
style={{ gap: 8 }}
/>
) : (
<View key={b.id} style={{ flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 6 }}>
<View
key={b.id}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 6,
}}
>
<ActivityIndicator size="small" color={c.muted} />
<Text style={{ color: c.muted, fontSize: 13 }}>
{b.text ?? 'Generating document…'}
Expand Down Expand Up @@ -356,7 +395,10 @@ export function App() {
editable={!busy}
onSubmitEditing={() => send(input)}
returnKeyType="send"
style={[styles.textInput, { color: c.text, borderColor: c.border, backgroundColor: c.pageBg }]}
style={[
styles.textInput,
{ color: c.text, borderColor: c.border, backgroundColor: c.pageBg },
]}
/>
{busy ? (
<Pressable onPress={stop} style={[styles.sendBtn, { backgroundColor: '#dc2626' }]}>
Expand Down Expand Up @@ -415,8 +457,19 @@ const styles = StyleSheet.create({
row: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
iconBtn: { paddingVertical: 6, paddingHorizontal: 10, borderRadius: 8, borderWidth: 1 },
settings: { padding: 12, gap: 6, borderBottomWidth: StyleSheet.hairlineWidth },
settingsLabel: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 0.4 },
settingsInput: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 8, fontSize: 14 },
settingsLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.4,
},
settingsInput: {
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 8,
fontSize: 14,
},
settingsHint: { fontSize: 12, marginTop: 2, lineHeight: 17 },
empty: { paddingVertical: 48, paddingHorizontal: 12, gap: 8 },
emptyTitle: { fontSize: 16, fontWeight: '700', textAlign: 'center' },
Expand Down
9 changes: 1 addition & 8 deletions demo-native/llm.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
streamText,
generateText,
tool,
stepCountIs,
jsonSchema,
type ModelMessage,
} from 'ai';
import { streamText, generateText, tool, stepCountIs, jsonSchema, type ModelMessage } from 'ai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import { fetch as expoFetch } from 'expo/fetch';
Expand Down
4 changes: 2 additions & 2 deletions demo/src/agent/AgentMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,8 @@ export const AgentMessage = memo(function AgentMessage({

const { blocks } = turn as AssistantTurn;

const hasContent = blocks.some(
(b) => (b.type === 'text' || b.type === 'thinking' ? b.content : (b as ToolUseBlock).document),
const hasContent = blocks.some((b) =>
b.type === 'text' || b.type === 'thinking' ? b.content : (b as ToolUseBlock).document,
);

return (
Expand Down
4 changes: 2 additions & 2 deletions demo/src/agent/AgentSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ export const AgentSettings = memo(function AgentSettings({ config, onUpdate }: A
<p className="agent-settings-note">
The entire agent runs on your self-hosted <strong>MDMA model</strong> endpoint
(OpenAI-compatible, tool-calling enabled) — no third-party model is called. Enter the
deployed model URL above; leave it blank to use the default. The{' '}
<code>/v1</code> suffix is added automatically.
deployed model URL above; leave it blank to use the default. The <code>/v1</code>{' '}
suffix is added automatically.
</p>
)}
<p className="agent-settings-note agent-settings-note--storage">
Expand Down
3 changes: 1 addition & 2 deletions demo/src/agent/anthropic-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ export interface AnthropicConfig {
* VITE_OWN_MODEL_BASE_URL.
*/
export const OWN_MODEL_DEFAULT_BASE_URL =
import.meta.env.VITE_OWN_MODEL_BASE_URL ??
'https://REDACTED.modal.run/v1';
import.meta.env.VITE_OWN_MODEL_BASE_URL ?? 'https://REDACTED.modal.run/v1';

export interface ToolDefinition {
name: string;
Expand Down
3 changes: 1 addition & 2 deletions demo/src/agent/openai-agent-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,7 @@ export async function* streamOpenAIAgentMessages(
reader.cancel().catch(() => {});
yield {
type: 'stream_error',
message:
'The model got stuck repeating itself and was stopped. Please try again.',
message: 'The model got stuck repeating itself and was stopped. Please try again.',
};
return;
}
Expand Down
5 changes: 4 additions & 1 deletion demo/src/agent/use-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,10 @@ async function chatOnce(
const baseUrl = getBaseUrlForProvider(config);
const response = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${getApiKeyForProvider(config)}` },
headers: {
'content-type': 'application/json',
authorization: `Bearer ${getApiKeyForProvider(config)}`,
},
body: JSON.stringify({
model: isOwn ? OWN_MODEL_NAME : config.model,
messages: [
Expand Down
4 changes: 2 additions & 2 deletions demo/src/docs/sections/IntegrationAgui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export function IntegrationAgui() {
AG-UI
</a>{' '}
agent and route the user's decisions back into the run. AG-UI is the{' '}
<strong>transport</strong> (suspend/resume via its <code>interrupt</code> primitive); MDMA is
the <strong>payload</strong> (validated, audited, PII-aware components).{' '}
<strong>transport</strong> (suspend/resume via its <code>interrupt</code> primitive); MDMA
is the <strong>payload</strong> (validated, audited, PII-aware components).{' '}
<code>@mobile-reality/mdma-agui</code> is the seam between them — a community-maintained
adapter, not a framework integration.
</p>
Expand Down
8 changes: 3 additions & 5 deletions demo/src/docs/sections/PromptMatrix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,12 @@ export function PromptMatrix() {
variants and Grok 4.3, the fixer would otherwise see visible "Thinking: **Topic**" prose
prepended to every response. The eval config sets{' '}
<code>passthrough.reasoning.exclude: true</code> (and the demo's{' '}
<code>usePreviewValidation</code> does the same per-provider) to strip reasoning tokens
from the response body at the API layer rather than the prompt layer.
<code>usePreviewValidation</code> does the same per-provider) to strip reasoning tokens from
the response body at the API layer rather than the prompt layer.
</p>

<h2>In Progress</h2>
<p>
The following prompt still ships without model-specific variants and is on the roadmap:
</p>
<p>The following prompt still ships without model-specific variants and is on the roadmap:</p>
<div className="docs-inprogress-list">
{[
{
Expand Down
19 changes: 13 additions & 6 deletions demo/src/docs/sections/Usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ store.dispatch({
symmetric with <code>getState()</code>.
</p>
<p>
Hydration overlays the AST defaults <strong>without emitting audit events or marking fields{' '}
<code>touched</code></strong>, so a restore never looks like fresh user activity in the
tamper-evident log. It applies only to freshly-created components, so a later streamed
re-parse never clobbers an in-flight edit.
Hydration overlays the AST defaults{' '}
<strong>
without emitting audit events or marking fields <code>touched</code>
</strong>
, so a restore never looks like fresh user activity in the tamper-evident log. It applies
only to freshly-created components, so a later streamed re-parse never clobbers an in-flight
edit.
</p>
<Code lang="ts">{`// 1. Persist — snapshot each component's values on the way out
const snapshot = Object.fromEntries(
Expand Down Expand Up @@ -87,8 +90,12 @@ const store = createDocumentStore(ast, {
</button>
)}
<p className="docs-note">
Using the AG-UI adapter? Pass the same map to <code>&lt;MdmaAgentView initialState={'{…}'}
/&gt;</code> — each replayed message hydrates only the component ids it contains.
Using the AG-UI adapter? Pass the same map to{' '}
<code>
&lt;MdmaAgentView initialState={'{…}'}
/&gt;
</code>{' '}
— each replayed message hydrates only the component ids it contains.
</p>

<h2>In a Chat</h2>
Expand Down
14 changes: 6 additions & 8 deletions demo/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1636,7 +1636,7 @@ body {
gap: 10px;
}

.ai-setting--toggle input[type='checkbox'] {
.ai-setting--toggle input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
flex: 0 0 auto;
Expand All @@ -1651,8 +1651,8 @@ body {
transition: background 0.15s;
}

.ai-setting--toggle input[type='checkbox']::after {
content: '';
.ai-setting--toggle input[type="checkbox"]::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
Expand All @@ -1663,11 +1663,11 @@ body {
transition: transform 0.15s;
}

.ai-setting--toggle input[type='checkbox']:checked {
.ai-setting--toggle input[type="checkbox"]:checked {
background: #6c5ce7;
}

.ai-setting--toggle input[type='checkbox']:checked::after {
.ai-setting--toggle input[type="checkbox"]:checked::after {
transform: translateX(16px);
}

Expand Down Expand Up @@ -1835,9 +1835,7 @@ body {
border-radius: 5px;
padding: 2px 7px;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
transition: background 0.15s, color 0.15s;
}

.agent-raw-toggle:hover {
Expand Down
Loading