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
1 change: 1 addition & 0 deletions .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"sessionId":"74da53f6-8f01-4e0f-b961-4a177c2cc25f","pid":18276,"procStart":"Sat May 9 09:08:27 2026","acquiredAt":1778500900181}
56 changes: 56 additions & 0 deletions packages/genui/a2ui-playground/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# A2UI playground examples
Comment thread
PupilTong marked this conversation as resolved.

Reference implementations that intentionally live **outside**
`@lynx-js/a2ui-reactlynx`. The package itself ships only:

- `<A2UI>` — the protocol-naive renderer.
- `MessageStore` — a pure raw-message buffer.
- The catalog + custom-component-author API.

Everything else — talking to an agent, chunking turns, theming the chat
shell — is the developer's choice. These examples show common shapes;
copy and adapt them.

## `io-mock/`

`createMockAgent(store, opts)` returns a driver that pushes a fixed
initial stream into the store and serves canned responses to user
actions. Used by the playground's `lynx-src/App.tsx` to exercise demos
without a real agent.

```ts
const store = createMessageStore();
const agent = createMockAgent(store, { initialMessages, actionMocks });
agent.start(); // streams initial messages into the buffer
agent.onAction(action); // pushes the canned response to a user action
Comment thread
PupilTong marked this conversation as resolved.
```

## Multi-turn chat shell pattern

For chat UIs, give each turn (user prompt + agent response) its own
`MessageStore` and render one `<A2UI messageStore={turnStore}>` per
agent turn. The shell only tracks turns; the renderer handles
everything inside an agent turn.

```tsx
function Conversation({ catalogs, respond }) {
const [turns, setTurns] = useState([]);
const send = async (input) => {
const store = createMessageStore();
setTurns((t) => [
...t,
{ kind: 'user', content: input },
{ kind: 'agent', store },
]);
await respond(input, store);
};
return turns.map((t) =>
t.kind === 'user'
? <view key={...}><text>{t.content}</text></view>
: <A2UI key={...} messageStore={t.store} catalogs={catalogs} />
);
}
```

Each `<A2UI>` only sees a bounded buffer; history is just a list of
turns the shell maintains.
96 changes: 96 additions & 0 deletions packages/genui/a2ui-playground/examples/io-mock/mockAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2026 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
//
// Reference mock IO module. Pushes a fixed initial stream into the store
// and serves canned responses to user actions. NOT shipped from
// `@lynx-js/a2ui-reactlynx` — copy as a starting point for tests / demos.
import type {
MessageStore,
ServerToClientMessage,
UserActionPayload,
} from '@lynx-js/a2ui-reactlynx';

export interface MockAgentOptions {
/** Streamed once after `start()`. */
initialMessages?: readonly ServerToClientMessage[];
/** Per-action response messages, keyed by action name. */
actionMocks?: Record<
string,
| readonly ServerToClientMessage[]
| ((ctx: UserActionPayload) => readonly ServerToClientMessage[])
>;
/** Delay between successive batches when streaming. */
delayMs?: number;
}

export interface MockAgent {
/**
* Begin streaming the initial messages. Idempotent — calling twice
* returns the original promise.
*/
start(): Promise<void>;
/** Forward a user action; pushes the canned response, if any. */
onAction(action: UserActionPayload): Promise<void>;
/** Stop streaming and discard any pending messages. */
stop(): void;
}

/**
* Build a mock agent driver bound to a `MessageStore`. The driver
* streams raw protocol messages into the store with a small delay
* between each, simulating an SSE-like server.
*/
export function createMockAgent(
store: MessageStore,
options: MockAgentOptions = {},
): MockAgent {
const { initialMessages, actionMocks = {}, delayMs = 800 } = options;
const abort = new AbortController();
let started: Promise<void> | null = null;

function sleep(ms: number): Promise<void> {
if (ms <= 0 || abort.signal.aborted) return Promise.resolve();
return new Promise<void>((resolve) => {
const onAbort = () => {
clearTimeout(timer);
resolve();
};
const timer = setTimeout(() => {
abort.signal.removeEventListener('abort', onAbort);
resolve();
}, ms);
abort.signal.addEventListener('abort', onAbort, { once: true });
});
}

async function streamInto(
messages: readonly ServerToClientMessage[],
): Promise<void> {
for (const msg of messages) {
if (abort.signal.aborted) return;
store.push(msg);
if (delayMs > 0) {
await sleep(delayMs);
if (abort.signal.aborted) return;
}
}
}

return {
start() {
if (started) return started;
started = streamInto(initialMessages ?? []);
return started;
},
async onAction(action) {
const mock = actionMocks[action.name];
if (!mock) return;
const stream = typeof mock === 'function' ? mock(action) : mock;
await streamInto(stream);
},
stop() {
abort.abort();
},
};
}
Loading
Loading