@aotui/mobile-ai-native 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE.md +317 -0
- package/README.md +114 -0
- package/dist/core/action/createActionRuntime.d.ts +13 -0
- package/dist/core/action/createActionRuntime.js +69 -0
- package/dist/core/action/defineAction.d.ts +21 -0
- package/dist/core/action/defineAction.js +3 -0
- package/dist/core/effect/types.d.ts +1 -0
- package/dist/core/effect/types.js +1 -0
- package/dist/core/ref/ref-index.d.ts +5 -0
- package/dist/core/ref/ref-index.js +11 -0
- package/dist/core/snapshot/createSnapshotBundle.d.ts +6 -0
- package/dist/core/snapshot/createSnapshotBundle.js +12 -0
- package/dist/core/state/createStore.d.ts +5 -0
- package/dist/core/state/createStore.js +19 -0
- package/dist/core/types.d.ts +30 -0
- package/dist/core/types.js +1 -0
- package/dist/demo/inbox/InboxGUI.d.ts +1 -0
- package/dist/demo/inbox/InboxGUI.js +7 -0
- package/dist/demo/inbox/InboxTUI.d.ts +1 -0
- package/dist/demo/inbox/InboxTUI.js +12 -0
- package/dist/demo/inbox/actions.d.ts +16 -0
- package/dist/demo/inbox/actions.js +56 -0
- package/dist/demo/inbox/createInboxApp.d.ts +20 -0
- package/dist/demo/inbox/createInboxApp.js +50 -0
- package/dist/demo/inbox/effects.d.ts +9 -0
- package/dist/demo/inbox/effects.js +12 -0
- package/dist/demo/inbox/state.d.ts +33 -0
- package/dist/demo/inbox/state.js +56 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/projection/gui/AppProvider.d.ts +20 -0
- package/dist/projection/gui/AppProvider.js +15 -0
- package/dist/projection/gui/hooks.d.ts +10 -0
- package/dist/projection/gui/hooks.js +18 -0
- package/dist/projection/tui/renderTUI.d.ts +6 -0
- package/dist/projection/tui/renderTUI.js +14 -0
- package/dist/ref/RefContext.d.ts +11 -0
- package/dist/ref/RefContext.js +15 -0
- package/dist/ref/useArrayRef.d.ts +1 -0
- package/dist/ref/useArrayRef.js +19 -0
- package/dist/ref/useDataRef.d.ts +1 -0
- package/dist/ref/useDataRef.js +8 -0
- package/dist/tool/createToolBridge.d.ts +18 -0
- package/dist/tool/createToolBridge.js +57 -0
- package/package.json +49 -0
package/GUIDE.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# GUIDE: Building an Agent Native iOS Calendar App
|
|
2
|
+
|
|
3
|
+
This guide is for a developer who wants to build a high-quality Agent Native calendar app on iOS using `@aotui/mobile-ai-native`.
|
|
4
|
+
|
|
5
|
+
The goal is simple:
|
|
6
|
+
|
|
7
|
+
- humans use a normal GUI calendar
|
|
8
|
+
- the LLM uses tools through a TUI snapshot
|
|
9
|
+
- both operate on the same app state
|
|
10
|
+
- the human can see the result of the LLM's actions
|
|
11
|
+
|
|
12
|
+
## 1. What You Are Actually Building
|
|
13
|
+
|
|
14
|
+
Do not think of this as:
|
|
15
|
+
|
|
16
|
+
- "an AI that clicks buttons"
|
|
17
|
+
|
|
18
|
+
Think of it as:
|
|
19
|
+
|
|
20
|
+
- "one calendar app with two input channels"
|
|
21
|
+
|
|
22
|
+
Those two channels are:
|
|
23
|
+
|
|
24
|
+
1. human input through GUI
|
|
25
|
+
2. LLM input through tools
|
|
26
|
+
|
|
27
|
+
They must meet in the same place:
|
|
28
|
+
|
|
29
|
+
- the same `State`
|
|
30
|
+
- the same `Action` layer
|
|
31
|
+
|
|
32
|
+
That is the whole trick.
|
|
33
|
+
|
|
34
|
+
## 2. Why This Architecture Matters
|
|
35
|
+
|
|
36
|
+
If the LLM drives the GUI by fake taps, your system becomes fragile.
|
|
37
|
+
|
|
38
|
+
Why?
|
|
39
|
+
|
|
40
|
+
- the UI layout changes and the AI breaks
|
|
41
|
+
- the AI acts on pixels, not meaning
|
|
42
|
+
- debugging becomes miserable
|
|
43
|
+
|
|
44
|
+
Instead, this framework makes the LLM act on meaning:
|
|
45
|
+
|
|
46
|
+
- `openEvent`
|
|
47
|
+
- `createEvent`
|
|
48
|
+
- `moveEvent`
|
|
49
|
+
- `searchEvents`
|
|
50
|
+
- `changeCalendarView`
|
|
51
|
+
|
|
52
|
+
The GUI is one projection of state.
|
|
53
|
+
The TUI snapshot is another projection of state.
|
|
54
|
+
|
|
55
|
+
The LLM should never guess ids from the screen.
|
|
56
|
+
It should receive semantic refs from the current `SnapshotBundle`.
|
|
57
|
+
|
|
58
|
+
## 3. The Core Mental Model
|
|
59
|
+
|
|
60
|
+
Your calendar app should follow this loop:
|
|
61
|
+
|
|
62
|
+
`State -> GUI`
|
|
63
|
+
`State -> TUI Snapshot`
|
|
64
|
+
`Tool -> Action -> Event/Effect -> State`
|
|
65
|
+
`GUI Event -> Action -> Event/Effect -> State`
|
|
66
|
+
|
|
67
|
+
That means:
|
|
68
|
+
|
|
69
|
+
- GUI controls do not own business logic
|
|
70
|
+
- TUI does not own business logic
|
|
71
|
+
- tools do not own business logic
|
|
72
|
+
- `Action` is the one real business entry
|
|
73
|
+
|
|
74
|
+
## 4. Calendar App State
|
|
75
|
+
|
|
76
|
+
A good Agent Native calendar app should have state shaped more like this:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
type CalendarState = {
|
|
80
|
+
shell: {
|
|
81
|
+
currentView: "day" | "week" | "month";
|
|
82
|
+
selectedDate: string;
|
|
83
|
+
recentTrace: string | null;
|
|
84
|
+
};
|
|
85
|
+
events: {
|
|
86
|
+
items: CalendarEvent[];
|
|
87
|
+
selectedEventId: string | null;
|
|
88
|
+
searchQuery: string;
|
|
89
|
+
isLoading: boolean;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Good rule:
|
|
95
|
+
|
|
96
|
+
- if it affects GUI, TUI, tool visibility, or trace, it belongs in framework state
|
|
97
|
+
- if it is only a tiny render helper, keep it local
|
|
98
|
+
|
|
99
|
+
## 5. Calendar Actions
|
|
100
|
+
|
|
101
|
+
Your first calendar app should expose a very small action set:
|
|
102
|
+
|
|
103
|
+
- `openEvent`
|
|
104
|
+
- `searchEvents`
|
|
105
|
+
- `createEvent`
|
|
106
|
+
- `moveEvent`
|
|
107
|
+
- `changeCalendarView`
|
|
108
|
+
|
|
109
|
+
Start smaller than you want.
|
|
110
|
+
You can always add more later.
|
|
111
|
+
|
|
112
|
+
The important rule is:
|
|
113
|
+
|
|
114
|
+
- actions should be domain actions
|
|
115
|
+
- not UI actions like `tapButton` or `scrollList`
|
|
116
|
+
|
|
117
|
+
## 6. Why Refs Matter in Calendar Apps
|
|
118
|
+
|
|
119
|
+
Calendars are full of structured data:
|
|
120
|
+
|
|
121
|
+
- one day
|
|
122
|
+
- one week
|
|
123
|
+
- one event
|
|
124
|
+
- one search result list
|
|
125
|
+
|
|
126
|
+
The LLM should see semantic markers like:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
(1:1 with Sarah at 14:00)[event:events[0]]
|
|
130
|
+
(Today)[day:visible_days[0]]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
That is what `useDataRef` and `useArrayRef` are for.
|
|
134
|
+
|
|
135
|
+
### Use `useDataRef`
|
|
136
|
+
|
|
137
|
+
Use it for one event, one selected day, one active calendar.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
const selectedEventRef = useDataRef("event", selectedEvent, "selected_event");
|
|
141
|
+
<text>{selectedEventRef("Selected event")}</text>;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Use `useArrayRef`
|
|
145
|
+
|
|
146
|
+
Use it for event lists, visible day buckets, agenda results.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
const [eventsRef, eventRef] = useArrayRef("event", visibleEvents, "events");
|
|
150
|
+
<text>{eventsRef("Visible events")}</text>
|
|
151
|
+
{visibleEvents.map((event, index) => (
|
|
152
|
+
<item key={event.id}>{eventRef(index, event.title)}</item>
|
|
153
|
+
))}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 7. Why `snapshotId` Is Non-Negotiable
|
|
157
|
+
|
|
158
|
+
The screen can change after the LLM reads it.
|
|
159
|
+
|
|
160
|
+
Maybe:
|
|
161
|
+
|
|
162
|
+
- the selected day changed
|
|
163
|
+
- search results refreshed
|
|
164
|
+
- the event was deleted
|
|
165
|
+
|
|
166
|
+
So tool execution must always be tied to the exact snapshot the LLM saw:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
await bridge.executeTool("openEvent", { event: "events[0]" }, snapshotId);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
This prevents the runtime from guessing against the latest UI.
|
|
173
|
+
|
|
174
|
+
That is a big deal.
|
|
175
|
+
It is the difference between:
|
|
176
|
+
|
|
177
|
+
- a reliable system
|
|
178
|
+
- and a haunted house
|
|
179
|
+
|
|
180
|
+
## 8. How To Build the iOS App
|
|
181
|
+
|
|
182
|
+
### Step 1: Keep this package as the core
|
|
183
|
+
|
|
184
|
+
Use `@aotui/mobile-ai-native` for:
|
|
185
|
+
|
|
186
|
+
- state
|
|
187
|
+
- refs
|
|
188
|
+
- snapshot bundle creation
|
|
189
|
+
- tool bridge
|
|
190
|
+
|
|
191
|
+
### Step 2: Build a thin React Native host
|
|
192
|
+
|
|
193
|
+
The React Native app should do only host work:
|
|
194
|
+
|
|
195
|
+
- render native GUI
|
|
196
|
+
- host the agent session
|
|
197
|
+
- ask the framework for `SnapshotBundle`
|
|
198
|
+
- pass tool calls into `executeTool`
|
|
199
|
+
|
|
200
|
+
### Step 3: Build GUI and TUI as separate projections
|
|
201
|
+
|
|
202
|
+
Do not auto-generate TUI from GUI.
|
|
203
|
+
|
|
204
|
+
For calendar apps this is especially important because:
|
|
205
|
+
|
|
206
|
+
- GUI cares about touch and spatial layout
|
|
207
|
+
- TUI cares about semantic clarity for the model
|
|
208
|
+
|
|
209
|
+
### Step 4: Start with one vertical slice
|
|
210
|
+
|
|
211
|
+
Do not build the whole calendar app at once.
|
|
212
|
+
|
|
213
|
+
Start with:
|
|
214
|
+
|
|
215
|
+
- month or week view
|
|
216
|
+
- visible event list
|
|
217
|
+
- `openEvent`
|
|
218
|
+
- `searchEvents`
|
|
219
|
+
|
|
220
|
+
Only after that is stable, add:
|
|
221
|
+
|
|
222
|
+
- create
|
|
223
|
+
- move
|
|
224
|
+
- delete
|
|
225
|
+
- recurring events
|
|
226
|
+
|
|
227
|
+
## 9. Suggested First Calendar TUI
|
|
228
|
+
|
|
229
|
+
Your first TUI should be boring and clear, not clever:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
<screen name="Calendar">
|
|
233
|
+
<text>View: week</text>
|
|
234
|
+
<text>Date: 2026-03-24</text>
|
|
235
|
+
<text>{daysRef("Visible days")}</text>
|
|
236
|
+
{events.map((event, index) => (
|
|
237
|
+
<item key={event.id}>{eventRef(index, `${event.title} at ${event.startTime}`)}</item>
|
|
238
|
+
))}
|
|
239
|
+
</screen>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The LLM does not need visual beauty.
|
|
243
|
+
It needs:
|
|
244
|
+
|
|
245
|
+
- stable structure
|
|
246
|
+
- meaningful refs
|
|
247
|
+
- clear tool choices
|
|
248
|
+
|
|
249
|
+
## 10. Quality Bar for a Good Agent Native Calendar App
|
|
250
|
+
|
|
251
|
+
Before you call the app "good", verify these:
|
|
252
|
+
|
|
253
|
+
### State correctness
|
|
254
|
+
|
|
255
|
+
- GUI and TUI always reflect the same event state
|
|
256
|
+
- tool calls only act on snapshot-scoped refs
|
|
257
|
+
|
|
258
|
+
### Tool correctness
|
|
259
|
+
|
|
260
|
+
- invisible tools are not callable
|
|
261
|
+
- stale `snapshotId` fails cleanly
|
|
262
|
+
- missing refs fail with explicit errors
|
|
263
|
+
|
|
264
|
+
### UX correctness
|
|
265
|
+
|
|
266
|
+
- human sees the result of AI actions
|
|
267
|
+
- recent AI action summary is visible
|
|
268
|
+
- event openings, searches, and edits are understandable
|
|
269
|
+
|
|
270
|
+
### Product correctness
|
|
271
|
+
|
|
272
|
+
- no tool is named after a button
|
|
273
|
+
- tools match domain intent
|
|
274
|
+
- TUI exposes enough semantic data without dumping noise
|
|
275
|
+
|
|
276
|
+
## 11. What This Package Does Not Give You Yet
|
|
277
|
+
|
|
278
|
+
Be honest with yourself:
|
|
279
|
+
|
|
280
|
+
this package is still an alpha core.
|
|
281
|
+
|
|
282
|
+
It does not yet give you:
|
|
283
|
+
|
|
284
|
+
- a full React Native adapter
|
|
285
|
+
- iOS simulator harness
|
|
286
|
+
- production persistence
|
|
287
|
+
- production networking
|
|
288
|
+
- voice or background agent integration
|
|
289
|
+
|
|
290
|
+
That is okay.
|
|
291
|
+
|
|
292
|
+
It gives you the hardest part first:
|
|
293
|
+
|
|
294
|
+
- the protocol spine
|
|
295
|
+
|
|
296
|
+
## 12. Recommended Build Order
|
|
297
|
+
|
|
298
|
+
If your friend is building the calendar app, this is the order I recommend:
|
|
299
|
+
|
|
300
|
+
1. build one RN screen shell
|
|
301
|
+
2. integrate framework state and tool bridge
|
|
302
|
+
3. implement week view with event refs
|
|
303
|
+
4. implement `openEvent`
|
|
304
|
+
5. implement `searchEvents`
|
|
305
|
+
6. add trace banner for recent AI action
|
|
306
|
+
7. add create/edit flows
|
|
307
|
+
8. only then add advanced calendar features
|
|
308
|
+
|
|
309
|
+
## 13. The One Sentence To Remember
|
|
310
|
+
|
|
311
|
+
An Agent Native iOS app is not:
|
|
312
|
+
|
|
313
|
+
- "AI controlling UI"
|
|
314
|
+
|
|
315
|
+
It is:
|
|
316
|
+
|
|
317
|
+
- "one app where GUI and LLM share the same domain actions and the same state"
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @aotui/mobile-ai-native
|
|
2
|
+
|
|
3
|
+
This package is an alpha host-agnostic core for building Agent Native mobile apps.
|
|
4
|
+
|
|
5
|
+
It currently proves the smallest end-to-end loop of the framework:
|
|
6
|
+
|
|
7
|
+
`State -> SnapshotBundle -> Tool Call(ref_id + snapshotId) -> Action -> Event/Effect -> State -> GUI/TUI refresh`
|
|
8
|
+
|
|
9
|
+
If you want a practical build guide for a real iOS app, read [GUIDE.md](./GUIDE.md).
|
|
10
|
+
|
|
11
|
+
## What This Slice Proves
|
|
12
|
+
|
|
13
|
+
- one shared state system drives both GUI and TUI
|
|
14
|
+
- TUI is handwritten and can expose semantic refs with `useDataRef` and `useArrayRef`
|
|
15
|
+
- the framework builds one atomic `SnapshotBundle`
|
|
16
|
+
- tools execute against the exact `snapshotId` the LLM saw
|
|
17
|
+
- `refIndex` stores serializable snapshot payloads, not live object references
|
|
18
|
+
- one pure local action and one effect-driven action both work
|
|
19
|
+
|
|
20
|
+
## Core Contract
|
|
21
|
+
|
|
22
|
+
The LLM does not operate on guessed ids.
|
|
23
|
+
It sees semantic markers like:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
(Welcome back)[message:messages[0]]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then it calls a tool with:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
await bridge.executeTool("openMessage", { message: "messages[0]" }, snapshotId);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The runtime resolves that `ref_id` from the `SnapshotBundle.refIndex` for the same `snapshotId`.
|
|
36
|
+
|
|
37
|
+
## Ref APIs
|
|
38
|
+
|
|
39
|
+
### `useDataRef`
|
|
40
|
+
|
|
41
|
+
Use for a single object:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
const pinnedRef = useDataRef("message", message, "messages[0]");
|
|
45
|
+
<text>{pinnedRef("Pinned message")}</text>;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `useArrayRef`
|
|
49
|
+
|
|
50
|
+
Use for an array:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
const [listRef, itemRef] = useArrayRef("message", messages, "messages");
|
|
54
|
+
<text>{listRef("Inbox messages")}</text>;
|
|
55
|
+
<item>{itemRef(0, "Welcome back")}</item>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## SnapshotBundle
|
|
59
|
+
|
|
60
|
+
The LLM-facing read model is:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
type SnapshotBundle = {
|
|
64
|
+
snapshotId: string;
|
|
65
|
+
generatedAt: number;
|
|
66
|
+
tui: string;
|
|
67
|
+
refIndex: Record<string, { type: string; value: unknown }>;
|
|
68
|
+
visibleTools: ToolDefinition[];
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This is intentionally atomic:
|
|
73
|
+
|
|
74
|
+
- `tui`
|
|
75
|
+
- `visibleTools`
|
|
76
|
+
- `refIndex`
|
|
77
|
+
|
|
78
|
+
must all come from the same render tick.
|
|
79
|
+
|
|
80
|
+
## Why `refIndex` Stores Serializable Payloads
|
|
81
|
+
|
|
82
|
+
Screens can change after the LLM reads them.
|
|
83
|
+
Live object references may already be gone.
|
|
84
|
+
|
|
85
|
+
So `refIndex` stores serializable snapshot payloads.
|
|
86
|
+
When the LLM later calls a tool, the framework reconstructs the action input from the payload attached to that `snapshotId`.
|
|
87
|
+
|
|
88
|
+
## Current Status
|
|
89
|
+
|
|
90
|
+
This is not yet a full React Native runtime or iOS shell.
|
|
91
|
+
|
|
92
|
+
What it gives you today:
|
|
93
|
+
|
|
94
|
+
- shared state core
|
|
95
|
+
- semantic refs with `useDataRef` and `useArrayRef`
|
|
96
|
+
- atomic `SnapshotBundle`
|
|
97
|
+
- snapshot-scoped tool execution
|
|
98
|
+
- a working inbox vertical slice
|
|
99
|
+
|
|
100
|
+
What you still need for a production iOS app:
|
|
101
|
+
|
|
102
|
+
- a thin React Native host adapter
|
|
103
|
+
- real GUI components
|
|
104
|
+
- model orchestration and networking
|
|
105
|
+
- product-level trace UI and persistence
|
|
106
|
+
|
|
107
|
+
## Current Demo
|
|
108
|
+
|
|
109
|
+
The inbox demo exposes:
|
|
110
|
+
|
|
111
|
+
- `openMessage`
|
|
112
|
+
- `searchMessages`
|
|
113
|
+
|
|
114
|
+
and proves that GUI and TUI both refresh from the same state after tool execution.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ActionDefinition } from "./defineAction";
|
|
2
|
+
import type { ActionResult, Store, ToolDefinition } from "../types";
|
|
3
|
+
export declare function createActionRuntime<State, Event>(config: {
|
|
4
|
+
store: Store<State, Event>;
|
|
5
|
+
actions: Array<ActionDefinition<State, Event, any>>;
|
|
6
|
+
effects?: Record<string, (ctx: {
|
|
7
|
+
getState(): State;
|
|
8
|
+
emit(event: Event): void;
|
|
9
|
+
}, input: any) => Promise<void> | void>;
|
|
10
|
+
}): {
|
|
11
|
+
executeAction: (name: string, input: Record<string, unknown>) => Promise<ActionResult>;
|
|
12
|
+
listVisibleTools: () => ToolDefinition[];
|
|
13
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function createActionRuntime(config) {
|
|
2
|
+
const actionsByName = new Map(config.actions.map((action) => [action.name, action]));
|
|
3
|
+
const trace = {
|
|
4
|
+
start(_summary) { },
|
|
5
|
+
update(_summary) { },
|
|
6
|
+
success(_summary) { },
|
|
7
|
+
fail(_summary) { },
|
|
8
|
+
};
|
|
9
|
+
async function executeAction(name, input) {
|
|
10
|
+
const action = actionsByName.get(name);
|
|
11
|
+
if (!action) {
|
|
12
|
+
return {
|
|
13
|
+
success: false,
|
|
14
|
+
error: {
|
|
15
|
+
code: "ACTION_NOT_FOUND",
|
|
16
|
+
message: `Action ${name} was not found`,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (!action.visibility(config.store.getState())) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: {
|
|
24
|
+
code: "ACTION_NOT_VISIBLE",
|
|
25
|
+
message: `Action ${name} is not currently visible`,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const parsed = action.schema.safeParse(input);
|
|
30
|
+
if (!parsed.success) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: {
|
|
34
|
+
code: "ACTION_INVALID_INPUT",
|
|
35
|
+
message: parsed.error.message,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const ctx = {
|
|
40
|
+
getState: () => config.store.getState(),
|
|
41
|
+
emit: (event) => config.store.emit(event),
|
|
42
|
+
runEffect: async (name, input) => {
|
|
43
|
+
const effect = config.effects?.[name];
|
|
44
|
+
if (!effect) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await effect({
|
|
48
|
+
getState: () => config.store.getState(),
|
|
49
|
+
emit: (event) => config.store.emit(event),
|
|
50
|
+
}, input);
|
|
51
|
+
},
|
|
52
|
+
trace,
|
|
53
|
+
};
|
|
54
|
+
return await action.handler(ctx, parsed.data);
|
|
55
|
+
}
|
|
56
|
+
function listVisibleTools() {
|
|
57
|
+
const state = config.store.getState();
|
|
58
|
+
return config.actions
|
|
59
|
+
.filter((action) => action.visibility(state))
|
|
60
|
+
.map((action) => ({
|
|
61
|
+
name: action.name,
|
|
62
|
+
description: action.description,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
executeAction,
|
|
67
|
+
listVisibleTools,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ZodType } from "zod";
|
|
2
|
+
import type { ActionResult } from "../types";
|
|
3
|
+
export type ActionContext<State, Event> = {
|
|
4
|
+
getState(): State;
|
|
5
|
+
emit(event: Event): void;
|
|
6
|
+
runEffect(name: string, input: unknown): Promise<void>;
|
|
7
|
+
trace: {
|
|
8
|
+
start(summary: string): void;
|
|
9
|
+
update(summary: string): void;
|
|
10
|
+
success(summary?: string): void;
|
|
11
|
+
fail(summary: string): void;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type ActionDefinition<State, Event, Input> = {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
schema: ZodType<Input>;
|
|
18
|
+
visibility(state: State): boolean;
|
|
19
|
+
handler(ctx: ActionContext<State, Event>, input: Input): Promise<ActionResult> | ActionResult;
|
|
20
|
+
};
|
|
21
|
+
export declare function defineAction<State, Event, Input>(config: ActionDefinition<State, Event, Input>): ActionDefinition<State, Event, Input>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type EffectMap = Record<string, (input: unknown) => Promise<void> | void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
let snapshotCounter = 0;
|
|
2
|
+
export function createSnapshotBundle(input) {
|
|
3
|
+
const generatedAt = Date.now();
|
|
4
|
+
snapshotCounter += 1;
|
|
5
|
+
return {
|
|
6
|
+
snapshotId: `snap_${generatedAt}_${snapshotCounter}`,
|
|
7
|
+
generatedAt,
|
|
8
|
+
tui: input.tui,
|
|
9
|
+
refIndex: input.refIndex,
|
|
10
|
+
visibleTools: input.visibleTools,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function createStore(config) {
|
|
2
|
+
let state = config.initialState;
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
return {
|
|
5
|
+
getState() {
|
|
6
|
+
return state;
|
|
7
|
+
},
|
|
8
|
+
emit(event) {
|
|
9
|
+
state = config.reduce(state, event);
|
|
10
|
+
listeners.forEach((listener) => listener());
|
|
11
|
+
},
|
|
12
|
+
subscribe(listener) {
|
|
13
|
+
listeners.add(listener);
|
|
14
|
+
return () => {
|
|
15
|
+
listeners.delete(listener);
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type StateReducer<State, Event> = (state: State, event: Event) => State;
|
|
2
|
+
export type Store<State, Event> = {
|
|
3
|
+
getState(): State;
|
|
4
|
+
emit(event: Event): void;
|
|
5
|
+
subscribe(listener: () => void): () => void;
|
|
6
|
+
};
|
|
7
|
+
export type RefIndexEntry = {
|
|
8
|
+
type: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
};
|
|
11
|
+
export type ToolDefinition = {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
};
|
|
15
|
+
export type SnapshotBundle = {
|
|
16
|
+
snapshotId: string;
|
|
17
|
+
generatedAt: number;
|
|
18
|
+
tui: string;
|
|
19
|
+
refIndex: Record<string, RefIndexEntry>;
|
|
20
|
+
visibleTools: ToolDefinition[];
|
|
21
|
+
};
|
|
22
|
+
export type ActionResult<T = unknown> = {
|
|
23
|
+
success: boolean;
|
|
24
|
+
message?: string;
|
|
25
|
+
data?: T;
|
|
26
|
+
error?: {
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function InboxGUI(): import("preact").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
import { useAppState } from "../../projection/gui/hooks";
|
|
4
|
+
export function InboxGUI() {
|
|
5
|
+
const { state } = useAppState();
|
|
6
|
+
return (_jsxs("gui-screen", { children: [_jsx("gui-trace", { children: state.shell.recentTrace ?? "" }), _jsx("gui-opened", { children: state.inbox.openedMessageId ?? "" }), state.inbox.items.map((item) => (_jsx("gui-item", { children: item.subject }, item.id)))] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function InboxTUI(): import("preact").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
import { useDataRef } from "../../ref/useDataRef";
|
|
4
|
+
import { useArrayRef } from "../../ref/useArrayRef";
|
|
5
|
+
import { useAppState } from "../../projection/gui/hooks";
|
|
6
|
+
export function InboxTUI() {
|
|
7
|
+
const { state } = useAppState();
|
|
8
|
+
const firstMessage = state.inbox.items[0] ?? { id: "empty", subject: "Empty", opened: false };
|
|
9
|
+
const openedRef = useDataRef("message", firstMessage, "opened_message");
|
|
10
|
+
const [listRef, itemRef] = useArrayRef("message", state.inbox.items, "messages");
|
|
11
|
+
return (_jsxs("screen", { name: "Inbox", children: [_jsx("text", { children: listRef("Inbox messages") }), _jsxs("text", { children: ["Query: ", state.inbox.query || "(empty)"] }), _jsxs("text", { children: ["Opened: ", String(Boolean(state.inbox.openedMessageId))] }), state.inbox.items.map((item, index) => (_jsx("item", { children: itemRef(index, item.subject) }, item.id))), _jsx("text", { children: openedRef("Opened message") })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { InboxEvent, InboxState } from "./state";
|
|
3
|
+
declare const inboxMessageSchema: z.ZodObject<{
|
|
4
|
+
id: z.ZodString;
|
|
5
|
+
subject: z.ZodString;
|
|
6
|
+
opened: z.ZodBoolean;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export declare function createInboxActions(): {
|
|
9
|
+
openMessage: import("../../core/action/defineAction").ActionDefinition<InboxState, InboxEvent, {
|
|
10
|
+
message: z.infer<typeof inboxMessageSchema>;
|
|
11
|
+
}>;
|
|
12
|
+
searchMessages: import("../../core/action/defineAction").ActionDefinition<InboxState, InboxEvent, {
|
|
13
|
+
query: string;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../core/action/defineAction";
|
|
3
|
+
const inboxMessageSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
subject: z.string(),
|
|
6
|
+
opened: z.boolean(),
|
|
7
|
+
});
|
|
8
|
+
export function createInboxActions() {
|
|
9
|
+
const openMessage = defineAction({
|
|
10
|
+
name: "openMessage",
|
|
11
|
+
description: "Open a message from the inbox.",
|
|
12
|
+
schema: z.object({
|
|
13
|
+
message: inboxMessageSchema,
|
|
14
|
+
}),
|
|
15
|
+
visibility(state) {
|
|
16
|
+
return state.shell.currentTab === "inbox";
|
|
17
|
+
},
|
|
18
|
+
handler(ctx, input) {
|
|
19
|
+
ctx.emit({ type: "MessageOpened", messageId: input.message.id });
|
|
20
|
+
ctx.emit({
|
|
21
|
+
type: "TraceUpdated",
|
|
22
|
+
summary: `Opened message ${input.message.subject}`,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
data: { openedMessageId: input.message.id },
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const searchMessages = defineAction({
|
|
31
|
+
name: "searchMessages",
|
|
32
|
+
description: "Search inbox messages.",
|
|
33
|
+
schema: z.object({
|
|
34
|
+
query: z.string().min(1),
|
|
35
|
+
}),
|
|
36
|
+
visibility(state) {
|
|
37
|
+
return state.shell.currentTab === "inbox";
|
|
38
|
+
},
|
|
39
|
+
async handler(ctx, input) {
|
|
40
|
+
ctx.emit({ type: "SearchStarted", query: input.query });
|
|
41
|
+
ctx.emit({
|
|
42
|
+
type: "TraceUpdated",
|
|
43
|
+
summary: `Started search for ${input.query}`,
|
|
44
|
+
});
|
|
45
|
+
await ctx.runEffect("searchMessages", input);
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
message: `Started search for ${input.query}`,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
openMessage,
|
|
54
|
+
searchMessages,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type InboxMessage } from "./state";
|
|
2
|
+
export declare function createInboxApp(config: {
|
|
3
|
+
initialMessages: InboxMessage[];
|
|
4
|
+
}): {
|
|
5
|
+
store: import("../../core/types").Store<import("./state").InboxState, import("./state").InboxEvent>;
|
|
6
|
+
bridge: {
|
|
7
|
+
listTools(): {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}[];
|
|
11
|
+
getSnapshotBundle(): import("../../core/types").SnapshotBundle;
|
|
12
|
+
executeTool(name: string, input: Record<string, unknown>, snapshotId: string): Promise<import("../../core/types").ActionResult>;
|
|
13
|
+
};
|
|
14
|
+
gui: {
|
|
15
|
+
render: () => string;
|
|
16
|
+
getVisibleSubjects(): string[];
|
|
17
|
+
getOpenedMessageId(): string | null;
|
|
18
|
+
getRecentTrace(): string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
import renderToString from "preact-render-to-string";
|
|
4
|
+
import { createActionRuntime } from "../../core/action/createActionRuntime";
|
|
5
|
+
import { createStore } from "../../core/state/createStore";
|
|
6
|
+
import { renderTUI } from "../../projection/tui/renderTUI";
|
|
7
|
+
import { AppProvider } from "../../projection/gui/AppProvider";
|
|
8
|
+
import { createToolBridge } from "../../tool/createToolBridge";
|
|
9
|
+
import { createInboxActions } from "./actions";
|
|
10
|
+
import { createInboxEffects } from "./effects";
|
|
11
|
+
import { InboxGUI } from "./InboxGUI";
|
|
12
|
+
import { InboxTUI } from "./InboxTUI";
|
|
13
|
+
import { createInitialInboxState, reduceInboxState, } from "./state";
|
|
14
|
+
export function createInboxApp(config) {
|
|
15
|
+
const store = createStore({
|
|
16
|
+
initialState: createInitialInboxState(config.initialMessages),
|
|
17
|
+
reduce: reduceInboxState,
|
|
18
|
+
});
|
|
19
|
+
const actions = createInboxActions();
|
|
20
|
+
const actionRuntime = createActionRuntime({
|
|
21
|
+
store,
|
|
22
|
+
actions: [actions.openMessage, actions.searchMessages],
|
|
23
|
+
effects: createInboxEffects(config.initialMessages),
|
|
24
|
+
});
|
|
25
|
+
const bridge = createToolBridge({
|
|
26
|
+
actionRuntime,
|
|
27
|
+
renderCurrentSnapshot() {
|
|
28
|
+
return renderTUI(_jsx(AppProvider, { store: store, actionRuntime: actionRuntime, children: _jsx(InboxTUI, {}) }), { visibleTools: actionRuntime.listVisibleTools() });
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
function renderGUI() {
|
|
32
|
+
return renderToString(_jsx(AppProvider, { store: store, actionRuntime: actionRuntime, children: _jsx(InboxGUI, {}) }));
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
store,
|
|
36
|
+
bridge,
|
|
37
|
+
gui: {
|
|
38
|
+
render: renderGUI,
|
|
39
|
+
getVisibleSubjects() {
|
|
40
|
+
return store.getState().inbox.items.map((item) => item.subject);
|
|
41
|
+
},
|
|
42
|
+
getOpenedMessageId() {
|
|
43
|
+
return store.getState().inbox.openedMessageId;
|
|
44
|
+
},
|
|
45
|
+
getRecentTrace() {
|
|
46
|
+
return store.getState().shell.recentTrace ?? "";
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InboxEvent, InboxMessage, InboxState } from "./state";
|
|
2
|
+
export declare function createInboxEffects(allMessages: InboxMessage[]): {
|
|
3
|
+
searchMessages(ctx: {
|
|
4
|
+
getState(): InboxState;
|
|
5
|
+
emit(event: InboxEvent): void;
|
|
6
|
+
}, input: {
|
|
7
|
+
query: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createInboxEffects(allMessages) {
|
|
2
|
+
return {
|
|
3
|
+
async searchMessages(ctx, input) {
|
|
4
|
+
const items = allMessages.filter((item) => item.subject.toLowerCase().includes(input.query.toLowerCase()));
|
|
5
|
+
ctx.emit({
|
|
6
|
+
type: "SearchSucceeded",
|
|
7
|
+
query: input.query,
|
|
8
|
+
items,
|
|
9
|
+
});
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type InboxMessage = {
|
|
2
|
+
id: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
opened: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type InboxState = {
|
|
7
|
+
shell: {
|
|
8
|
+
currentTab: "inbox" | "settings";
|
|
9
|
+
recentTrace: string | null;
|
|
10
|
+
};
|
|
11
|
+
inbox: {
|
|
12
|
+
query: string;
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
items: InboxMessage[];
|
|
15
|
+
openedMessageId: string | null;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type InboxEvent = {
|
|
19
|
+
type: "MessageOpened";
|
|
20
|
+
messageId: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "SearchStarted";
|
|
23
|
+
query: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: "SearchSucceeded";
|
|
26
|
+
query: string;
|
|
27
|
+
items: InboxMessage[];
|
|
28
|
+
} | {
|
|
29
|
+
type: "TraceUpdated";
|
|
30
|
+
summary: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function createInitialInboxState(messages: InboxMessage[]): InboxState;
|
|
33
|
+
export declare function reduceInboxState(state: InboxState, event: InboxEvent): InboxState;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function createInitialInboxState(messages) {
|
|
2
|
+
return {
|
|
3
|
+
shell: {
|
|
4
|
+
currentTab: "inbox",
|
|
5
|
+
recentTrace: null,
|
|
6
|
+
},
|
|
7
|
+
inbox: {
|
|
8
|
+
query: "",
|
|
9
|
+
isLoading: false,
|
|
10
|
+
items: messages,
|
|
11
|
+
openedMessageId: null,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function reduceInboxState(state, event) {
|
|
16
|
+
switch (event.type) {
|
|
17
|
+
case "MessageOpened":
|
|
18
|
+
return {
|
|
19
|
+
...state,
|
|
20
|
+
inbox: {
|
|
21
|
+
...state.inbox,
|
|
22
|
+
openedMessageId: event.messageId,
|
|
23
|
+
items: state.inbox.items.map((item) => item.id === event.messageId ? { ...item, opened: true } : item),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
case "SearchStarted":
|
|
27
|
+
return {
|
|
28
|
+
...state,
|
|
29
|
+
inbox: {
|
|
30
|
+
...state.inbox,
|
|
31
|
+
query: event.query,
|
|
32
|
+
isLoading: true,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
case "SearchSucceeded":
|
|
36
|
+
return {
|
|
37
|
+
...state,
|
|
38
|
+
inbox: {
|
|
39
|
+
...state.inbox,
|
|
40
|
+
query: event.query,
|
|
41
|
+
isLoading: false,
|
|
42
|
+
items: event.items,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
case "TraceUpdated":
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
shell: {
|
|
49
|
+
...state.shell,
|
|
50
|
+
recentTrace: event.summary,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
default:
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const VERSION = "0.0.0";
|
|
2
|
+
export { createStore } from "./core/state/createStore";
|
|
3
|
+
export { createSnapshotBundle } from "./core/snapshot/createSnapshotBundle";
|
|
4
|
+
export { defineAction } from "./core/action/defineAction";
|
|
5
|
+
export { createActionRuntime } from "./core/action/createActionRuntime";
|
|
6
|
+
export { createToolBridge } from "./tool/createToolBridge";
|
|
7
|
+
export { useDataRef } from "./ref/useDataRef";
|
|
8
|
+
export { useArrayRef } from "./ref/useArrayRef";
|
|
9
|
+
export { renderTUI } from "./projection/tui/renderTUI";
|
|
10
|
+
export { createInboxApp } from "./demo/inbox/createInboxApp";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const VERSION = "0.0.0";
|
|
2
|
+
export { createStore } from "./core/state/createStore";
|
|
3
|
+
export { createSnapshotBundle } from "./core/snapshot/createSnapshotBundle";
|
|
4
|
+
export { defineAction } from "./core/action/defineAction";
|
|
5
|
+
export { createActionRuntime } from "./core/action/createActionRuntime";
|
|
6
|
+
export { createToolBridge } from "./tool/createToolBridge";
|
|
7
|
+
export { useDataRef } from "./ref/useDataRef";
|
|
8
|
+
export { useArrayRef } from "./ref/useArrayRef";
|
|
9
|
+
export { renderTUI } from "./projection/tui/renderTUI";
|
|
10
|
+
export { createInboxApp } from "./demo/inbox/createInboxApp";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
type AppContextValue = {
|
|
3
|
+
store: {
|
|
4
|
+
getState(): unknown;
|
|
5
|
+
};
|
|
6
|
+
actionRuntime: {
|
|
7
|
+
executeAction(name: string, input: Record<string, unknown>): Promise<unknown>;
|
|
8
|
+
listVisibleTools(): Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare function AppProvider(props: {
|
|
15
|
+
store: AppContextValue["store"];
|
|
16
|
+
actionRuntime: AppContextValue["actionRuntime"];
|
|
17
|
+
children: ComponentChildren;
|
|
18
|
+
}): import("preact").JSX.Element;
|
|
19
|
+
export declare function useAppContext(): AppContextValue;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
import { createContext } from "preact";
|
|
4
|
+
import { useContext } from "preact/hooks";
|
|
5
|
+
const AppContext = createContext(null);
|
|
6
|
+
export function AppProvider(props) {
|
|
7
|
+
return (_jsx(AppContext.Provider, { value: { store: props.store, actionRuntime: props.actionRuntime }, children: props.children }));
|
|
8
|
+
}
|
|
9
|
+
export function useAppContext() {
|
|
10
|
+
const context = useContext(AppContext);
|
|
11
|
+
if (!context) {
|
|
12
|
+
throw new Error("useAppContext must be used within AppProvider");
|
|
13
|
+
}
|
|
14
|
+
return context;
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function useAppState<State>(): {
|
|
2
|
+
state: State;
|
|
3
|
+
};
|
|
4
|
+
export declare function useActions(): {
|
|
5
|
+
callAction(name: string, input: Record<string, unknown>): Promise<unknown>;
|
|
6
|
+
getVisibleTools(): {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
}[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useAppContext } from "./AppProvider";
|
|
2
|
+
export function useAppState() {
|
|
3
|
+
const { store } = useAppContext();
|
|
4
|
+
return {
|
|
5
|
+
state: store.getState(),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function useActions() {
|
|
9
|
+
const { actionRuntime } = useAppContext();
|
|
10
|
+
return {
|
|
11
|
+
callAction(name, input) {
|
|
12
|
+
return actionRuntime.executeAction(name, input);
|
|
13
|
+
},
|
|
14
|
+
getVisibleTools() {
|
|
15
|
+
return actionRuntime.listVisibleTools();
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { ComponentChild } from "preact";
|
|
3
|
+
import type { ToolDefinition } from "../../core/types";
|
|
4
|
+
export declare function renderTUI(node: ComponentChild, options: {
|
|
5
|
+
visibleTools: ToolDefinition[];
|
|
6
|
+
}): import("../../core/types").SnapshotBundle;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import renderToString from "preact-render-to-string";
|
|
3
|
+
import { createRefCollector } from "../../core/ref/ref-index";
|
|
4
|
+
import { createSnapshotBundle } from "../../core/snapshot/createSnapshotBundle";
|
|
5
|
+
import { RefProvider } from "../../ref/RefContext";
|
|
6
|
+
export function renderTUI(node, options) {
|
|
7
|
+
const collector = createRefCollector();
|
|
8
|
+
const tui = renderToString(_jsx(RefProvider, { registry: collector, children: node }));
|
|
9
|
+
return createSnapshotBundle({
|
|
10
|
+
tui,
|
|
11
|
+
refIndex: collector.snapshot(),
|
|
12
|
+
visibleTools: options.visibleTools,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
import type { RefIndexEntry } from "../core/types";
|
|
3
|
+
type RefRegistry = {
|
|
4
|
+
register(refId: string, entry: RefIndexEntry): void;
|
|
5
|
+
};
|
|
6
|
+
export declare function RefProvider(props: {
|
|
7
|
+
registry: RefRegistry;
|
|
8
|
+
children: ComponentChildren;
|
|
9
|
+
}): import("preact").JSX.Element;
|
|
10
|
+
export declare function useRefRegistry(): RefRegistry;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
import { createContext } from "preact";
|
|
4
|
+
import { useContext } from "preact/hooks";
|
|
5
|
+
const RefContext = createContext(null);
|
|
6
|
+
export function RefProvider(props) {
|
|
7
|
+
return (_jsx(RefContext.Provider, { value: props.registry, children: props.children }));
|
|
8
|
+
}
|
|
9
|
+
export function useRefRegistry() {
|
|
10
|
+
const registry = useContext(RefContext);
|
|
11
|
+
if (!registry) {
|
|
12
|
+
throw new Error("useRefRegistry must be used within RefProvider");
|
|
13
|
+
}
|
|
14
|
+
return registry;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useArrayRef(type: string, data: object[], refId: string): readonly [(content: string) => string, (index: number, content: string) => string];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useRefRegistry } from "./RefContext";
|
|
2
|
+
export function useArrayRef(type, data, refId) {
|
|
3
|
+
const registry = useRefRegistry();
|
|
4
|
+
const listRef = (content) => {
|
|
5
|
+
registry.register(refId, {
|
|
6
|
+
type: `${type}[]`,
|
|
7
|
+
value: data,
|
|
8
|
+
});
|
|
9
|
+
return `(${content})[${type}[]:${refId}]`;
|
|
10
|
+
};
|
|
11
|
+
const itemRef = (index, content) => {
|
|
12
|
+
registry.register(`${refId}[${index}]`, {
|
|
13
|
+
type,
|
|
14
|
+
value: data[index],
|
|
15
|
+
});
|
|
16
|
+
return `(${content})[${type}:${refId}[${index}]]`;
|
|
17
|
+
};
|
|
18
|
+
return [listRef, itemRef];
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useDataRef(type: string, data: object, refId: string): (content: string) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SnapshotBundle, ActionResult } from "../core/types";
|
|
2
|
+
export declare function createToolBridge(config: {
|
|
3
|
+
actionRuntime: {
|
|
4
|
+
listVisibleTools(): Array<{
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}>;
|
|
8
|
+
executeAction(name: string, input: Record<string, unknown>): Promise<ActionResult>;
|
|
9
|
+
};
|
|
10
|
+
renderCurrentSnapshot(): SnapshotBundle;
|
|
11
|
+
}): {
|
|
12
|
+
listTools(): {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
}[];
|
|
16
|
+
getSnapshotBundle(): SnapshotBundle;
|
|
17
|
+
executeTool(name: string, input: Record<string, unknown>, snapshotId: string): Promise<ActionResult>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function resolveRefArgs(input, refIndex) {
|
|
2
|
+
const resolved = {};
|
|
3
|
+
for (const [key, value] of Object.entries(input)) {
|
|
4
|
+
if (typeof value === "string" && value in refIndex) {
|
|
5
|
+
resolved[key] = refIndex[value].value;
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === "string" &&
|
|
9
|
+
(value.includes("[") || value.includes("]"))) {
|
|
10
|
+
return {
|
|
11
|
+
success: false,
|
|
12
|
+
refId: value,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
resolved[key] = value;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
data: resolved,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createToolBridge(config) {
|
|
23
|
+
const snapshots = new Map();
|
|
24
|
+
return {
|
|
25
|
+
listTools() {
|
|
26
|
+
return config.actionRuntime.listVisibleTools();
|
|
27
|
+
},
|
|
28
|
+
getSnapshotBundle() {
|
|
29
|
+
const snapshot = config.renderCurrentSnapshot();
|
|
30
|
+
snapshots.set(snapshot.snapshotId, snapshot);
|
|
31
|
+
return snapshot;
|
|
32
|
+
},
|
|
33
|
+
async executeTool(name, input, snapshotId) {
|
|
34
|
+
const snapshot = snapshots.get(snapshotId);
|
|
35
|
+
if (!snapshot) {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
error: {
|
|
39
|
+
code: "SNAPSHOT_NOT_FOUND",
|
|
40
|
+
message: `Snapshot ${snapshotId} was not found`,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const resolved = resolveRefArgs(input, snapshot.refIndex);
|
|
45
|
+
if (!resolved.success) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: {
|
|
49
|
+
code: "REF_NOT_FOUND",
|
|
50
|
+
message: `Reference ${resolved.refId} was not found in snapshot ${snapshotId}`,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return config.actionRuntime.executeAction(name, resolved.data);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aotui/mobile-ai-native",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Alpha host-agnostic core for building Agent Native mobile apps with shared GUI/TUI state and snapshot-scoped tool execution.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"build": "pnpm run clean && tsc -p tsconfig.json",
|
|
18
|
+
"test": "vitest",
|
|
19
|
+
"test:run": "vitest --run",
|
|
20
|
+
"prepublishOnly": "pnpm build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"preact": "^10.28.1",
|
|
24
|
+
"preact-render-to-string": "^6.6.6",
|
|
25
|
+
"zod": "^4.3.6"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"happy-dom": "^20.5.3",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vitest": "^3.2.4"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"GUIDE.md"
|
|
36
|
+
],
|
|
37
|
+
"keywords": [
|
|
38
|
+
"agent-native",
|
|
39
|
+
"ios",
|
|
40
|
+
"mobile",
|
|
41
|
+
"llm",
|
|
42
|
+
"tui",
|
|
43
|
+
"state-machine",
|
|
44
|
+
"preact"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|