@interopio/io-assist-react 1.0.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/README.internal.md +467 -0
- package/README.md +503 -0
- package/changelog.md +2 -0
- package/dist/IoAssist.d.ts +12 -0
- package/dist/actions/abortActiveStream.d.ts +2 -0
- package/dist/actions/deleteThread.d.ts +2 -0
- package/dist/actions/elicitation.d.ts +13 -0
- package/dist/actions/initIoAiWeb.d.ts +3 -0
- package/dist/actions/initIoConnect.d.ts +9 -0
- package/dist/actions/mcpAppEvents.d.ts +4 -0
- package/dist/actions/newConversation.d.ts +2 -0
- package/dist/actions/processResponseStream.d.ts +3 -0
- package/dist/actions/renameThread.d.ts +2 -0
- package/dist/actions/sampling.d.ts +5 -0
- package/dist/actions/selectThread.d.ts +2 -0
- package/dist/actions/sendUserMessage.d.ts +3 -0
- package/dist/actions/toggleFavoritePrompt.d.ts +7 -0
- package/dist/actions/toggleTool.d.ts +2 -0
- package/dist/components/chat/ActivePanelModal.d.ts +6 -0
- package/dist/components/chat/AiDisclaimer.d.ts +6 -0
- package/dist/components/chat/Chat.d.ts +2 -0
- package/dist/components/chat/ConfirmModal.d.ts +8 -0
- package/dist/components/chat/WelcomeHeading.d.ts +6 -0
- package/dist/components/header/Header.d.ts +2 -0
- package/dist/components/input-area/InputArea.d.ts +8 -0
- package/dist/components/messages/AssistantMessage.d.ts +10 -0
- package/dist/components/messages/McpAppResource.d.ts +11 -0
- package/dist/components/messages/MdFormatter.d.ts +6 -0
- package/dist/components/messages/MessageArea.d.ts +7 -0
- package/dist/components/messages/ToolMessage.d.ts +9 -0
- package/dist/components/messages/ToolTraceMessage.d.ts +9 -0
- package/dist/components/messages/UserMessage.d.ts +9 -0
- package/dist/components/messages/mdUtils.d.ts +1 -0
- package/dist/components/messages/prismTwilightTheme.d.ts +2 -0
- package/dist/components/prompt/FavoritePromptList.d.ts +2 -0
- package/dist/components/prompt/PromptListItem.d.ts +8 -0
- package/dist/components/prompt/PromptListPanel.d.ts +6 -0
- package/dist/components/scroll-area/ScrollArea.d.ts +33 -0
- package/dist/components/shared/Icon.d.ts +8 -0
- package/dist/components/shared/IconButton.d.ts +11 -0
- package/dist/components/shared/Modal.d.ts +12 -0
- package/dist/components/shared/SearchInput.d.ts +8 -0
- package/dist/components/shared/ToggleInput.d.ts +8 -0
- package/dist/components/shared/Tooltip.d.ts +10 -0
- package/dist/components/shared/icons.d.ts +37 -0
- package/dist/components/threads/ThreadHistory.d.ts +6 -0
- package/dist/components/threads/ThreadHistoryListItem.d.ts +18 -0
- package/dist/components/threads/ThreadHistoryPanel.d.ts +7 -0
- package/dist/components/tool/ToolListItem.d.ts +8 -0
- package/dist/components/tool/ToolListPanel.d.ts +2 -0
- package/dist/components/working-context-panel/WorkingContextPanel.d.ts +2 -0
- package/dist/constants/modalActions.d.ts +11 -0
- package/dist/constants/uiStrings.d.ts +141 -0
- package/dist/context/IoAssistContext.d.ts +14 -0
- package/dist/files/inter-latin-wght-normal.woff2 +0 -0
- package/dist/hooks/useHoverMouseFollow.d.ts +27 -0
- package/dist/hooks/useIoAiWebApi.d.ts +19 -0
- package/dist/hooks/useIoAiWebBootstrap.d.ts +8 -0
- package/dist/hooks/useIoConnectApi.d.ts +12 -0
- package/dist/hooks/useIoConnectBootstrap.d.ts +10 -0
- package/dist/hooks/useIsMobileViewport.d.ts +1 -0
- package/dist/index.cjs +41 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3624 -0
- package/dist/index.js.map +1 -0
- package/dist/stores/agent.d.ts +15 -0
- package/dist/stores/app-lifecycle.d.ts +21 -0
- package/dist/stores/confirm-modal.d.ts +29 -0
- package/dist/stores/index.d.ts +32 -0
- package/dist/stores/mcp-apps.d.ts +13 -0
- package/dist/stores/message.d.ts +33 -0
- package/dist/stores/prompt.d.ts +16 -0
- package/dist/stores/response-stream.d.ts +31 -0
- package/dist/stores/thread.d.ts +17 -0
- package/dist/stores/tool.d.ts +15 -0
- package/dist/stores/working-context.d.ts +16 -0
- package/dist/styles.css +1 -0
- package/dist/types/agent.d.ts +8 -0
- package/dist/types/config.d.ts +40 -0
- package/dist/types/icon.d.ts +5 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/loading.d.ts +38 -0
- package/dist/types/message.d.ts +56 -0
- package/dist/types/panel.d.ts +6 -0
- package/dist/types/prompt.d.ts +17 -0
- package/dist/types/stream.d.ts +22 -0
- package/dist/types/thread.d.ts +15 -0
- package/dist/types/tool.d.ts +15 -0
- package/dist/utils/confirmModal.d.ts +5 -0
- package/dist/utils/ioModals.d.ts +21 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/mcpAppModal.d.ts +3 -0
- package/dist/utils/messageConverter.d.ts +3 -0
- package/dist/utils/messageUtils.d.ts +16 -0
- package/dist/utils/safeStringify.d.ts +1 -0
- package/dist/utils/streamUtils.d.ts +15 -0
- package/dist/utils/threadUtils.d.ts +16 -0
- package/package.json +65 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# io.Assist (React) — Internal Developer Documentation
|
|
2
|
+
|
|
3
|
+
**Audience:** Developers working on or extending `@interopio/io-assist-react`.
|
|
4
|
+
|
|
5
|
+
This is the React counterpart of [`@interopio/io-assist-ng`](../io-assist-ng/README.internal.md). It implements the same product behavior with React idioms: a single combined **Zustand** store instead of NgRx, plain **action modules** instead of effects/services, and **React hooks + Context** instead of Angular DI. Where behavior is identical, this doc points to the Angular equivalent rather than restating it.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Architecture Overview](#architecture-overview)
|
|
12
|
+
- [Project Structure](#project-structure)
|
|
13
|
+
- [Public API Surface](#public-api-surface)
|
|
14
|
+
- [Configuration](#configuration)
|
|
15
|
+
- [Root Component — IoAssist](#root-component--ioassist)
|
|
16
|
+
- [Context & Store Access](#context--store-access)
|
|
17
|
+
- [State Management (Zustand)](#state-management-zustand)
|
|
18
|
+
- [Store Composition](#store-composition)
|
|
19
|
+
- [Slices](#slices)
|
|
20
|
+
- [Bootstrap Hooks](#bootstrap-hooks)
|
|
21
|
+
- [Actions Layer](#actions-layer)
|
|
22
|
+
- [AG-UI Streaming Pipeline](#ag-ui-streaming-pipeline)
|
|
23
|
+
- [Response Stream Store & Background Threads](#response-stream-store--background-threads)
|
|
24
|
+
- [Sampling & Elicitation Flow](#sampling--elicitation-flow)
|
|
25
|
+
- [MCP Apps Integration](#mcp-apps-integration)
|
|
26
|
+
- [Components](#components)
|
|
27
|
+
- [Utilities](#utilities)
|
|
28
|
+
- [Styling](#styling)
|
|
29
|
+
- [Build & Tooling](#build--tooling)
|
|
30
|
+
- [Known Caveats](#known-caveats)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Architecture Overview
|
|
35
|
+
|
|
36
|
+
`@interopio/io-assist-react` is a single React component (`<IoAssist />`) plus a module-singleton Zustand store. The consumer passes two props — `staticConfig` and `dynamicConfig` — and everything else (io.Connect bootstrap, ai-web init, streaming, UI) runs internally.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Consumer App
|
|
40
|
+
└── <IoAssist staticConfig={...} dynamicConfig={...} />
|
|
41
|
+
└── IoAssistProvider ← puts both configs on React Context
|
|
42
|
+
└── IoAssistInner
|
|
43
|
+
├── useIoConnectBootstrap() ← initIoConnect(): platform + theme + favorites
|
|
44
|
+
├── useIoAiWebBootstrap() ← initIoAiWeb(): ai-web API + agents/threads/tools/ctx
|
|
45
|
+
└── appLoadingState gate
|
|
46
|
+
├── LOADING / NOT_STARTED → <LoadingScreen/>
|
|
47
|
+
├── ERROR → <ErrorScreen/> (reload on retry)
|
|
48
|
+
└── SUCCESS → <Chat/> ← full UI tree
|
|
49
|
+
|
|
50
|
+
ioAssistStore (module singleton, Zustand + subscribeWithSelector)
|
|
51
|
+
└── 10 slices: appLifecycle, agent, thread, message, responseStream,
|
|
52
|
+
prompt, tool, workingContext, mcpApps, confirmModal
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Key layers:**
|
|
56
|
+
|
|
57
|
+
| Layer | Responsibility | Where |
|
|
58
|
+
|-------|---------------|-------|
|
|
59
|
+
| **Config / Context** | Holds `staticConfig` + `dynamicConfig`, exposes them to the tree | `context/IoAssistContext.tsx` |
|
|
60
|
+
| **Zustand store** | Single combined store of 10 slices; module singleton | `stores/` |
|
|
61
|
+
| **Bootstrap hooks** | Run io.Connect + ai-web init once, in order | `hooks/useIoConnectBootstrap.ts`, `hooks/useIoAiWebBootstrap.ts` |
|
|
62
|
+
| **Actions** | Imperative business logic; mutate the store via `store.getState().setX()` | `actions/` |
|
|
63
|
+
| **Components** | Functional components subscribing via `useIoAssistStore(selector)` | `components/` |
|
|
64
|
+
|
|
65
|
+
**State-mutation convention:** Action modules read and write the store imperatively through `store.getState()` / `store.getState().setX(...)`. React selectors (`useIoAssistStore(selector)`) are reserved for render-coupled reads inside components. This keeps streaming and async flows out of the render path.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Project Structure
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
src/
|
|
73
|
+
├── index.ts # Package exports (IoAssist + 2 config types)
|
|
74
|
+
├── IoAssist.tsx # Root component + loading/error gates
|
|
75
|
+
├── styles/
|
|
76
|
+
│ ├── index.css # Tailwind entry + @font-face + @theme
|
|
77
|
+
│ └── defaults.css # CSS variables (light/dark)
|
|
78
|
+
├── context/
|
|
79
|
+
│ └── IoAssistContext.tsx # IoAssistProvider + config/store hooks
|
|
80
|
+
├── stores/ # Zustand slices (one file per slice) + index.ts
|
|
81
|
+
├── hooks/ # Bootstrap + UI hooks
|
|
82
|
+
├── actions/ # Imperative store-mutating logic ("services")
|
|
83
|
+
├── components/ # UI tree (chat, header, messages, threads, …)
|
|
84
|
+
├── constants/ # uiStrings, modalActions
|
|
85
|
+
├── types/ # Domain + config types (index.ts barrel)
|
|
86
|
+
└── utils/ # logger, confirmModal, message/stream/thread helpers
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Public API Surface
|
|
92
|
+
|
|
93
|
+
File: `src/index.ts`. Only three things are exported — everything else is internal:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
export { IoAssist } from './IoAssist';
|
|
97
|
+
export type { IoAssistStaticConfig, IoAssistDynamicConfig } from './types';
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The stylesheet is published separately via the package `exports` map (`./styles` → `dist/styles.css`).
|
|
101
|
+
|
|
102
|
+
> Unlike the Angular package, prompt/icon types (`IoAssistPromptCategory`, `IconResource`, …) are **not** re-exported. They are reachable structurally through `IoAssistStaticConfig`, so consumers author `defaultPrompts` as inline literals.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
File: `src/types/config.ts`.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
type IoAssistUserConfig = { id: string; name?: string };
|
|
112
|
+
|
|
113
|
+
type IoAssistStaticConfig = {
|
|
114
|
+
connectConfig: {
|
|
115
|
+
browser?: { factory: IOConnectBrowserFactoryFunction; config?: IOConnectBrowser.Config };
|
|
116
|
+
desktop?: { factory: (config?: IOConnectDesktop.Config) => Promise<IOConnectDesktop.API>; config?: IOConnectDesktop.Config };
|
|
117
|
+
};
|
|
118
|
+
aiWebConfig: {
|
|
119
|
+
agentServer: IoAiWeb.AgentServerConfig;
|
|
120
|
+
mcp?: IoAiWeb.MCPConfig;
|
|
121
|
+
};
|
|
122
|
+
defaultAgentName?: string;
|
|
123
|
+
workingContext?: IoAiWeb.WorkingContextConfig;
|
|
124
|
+
defaultPrompts?: IoAssistPromptCategory[];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type IoAssistDynamicConfig = {
|
|
128
|
+
user: IoAssistUserConfig;
|
|
129
|
+
agentServer?: { headers?: Record<string, string> };
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**No runtime validation.** The Angular package validates both configs with Zod (`provideIoAssist()` / `ngOnInit`). The React package does **not** — configs flow straight into Context. If validation is added later, do it in `IoAssist`/`IoAssistProvider` before the bootstrap hooks run.
|
|
134
|
+
|
|
135
|
+
**Config consumption:** `actions/initIoAiWeb.ts::buildWebConfig()` builds the `IoAiWeb.WebConfig`:
|
|
136
|
+
- `agentServer` = `staticConfig.aiWebConfig.agentServer` spread, with `headers` taken **only** from `dynamicConfig.agentServer?.headers` (the sole source of request headers).
|
|
137
|
+
- `mcp` (if present) → `buildMcpConfig()` injects sampling/elicitation handlers (see below).
|
|
138
|
+
- `workingContext` (if present) → set as `config.context`.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Root Component — IoAssist
|
|
143
|
+
|
|
144
|
+
File: `src/IoAssist.tsx`.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
type Props = { staticConfig: IoAssistStaticConfig; dynamicConfig: IoAssistDynamicConfig };
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- Wraps the tree in `IoAssistProvider` (puts both configs on Context).
|
|
151
|
+
- `IoAssistInner` calls `useIoConnectBootstrap()` then `useIoAiWebBootstrap()`, and reads `appLoadingState` to render one of three states:
|
|
152
|
+
|
|
153
|
+
| `appLoadingState.type` | Render |
|
|
154
|
+
|------------------------|--------|
|
|
155
|
+
| `LOADING` / `NOT_STARTED` | `<LoadingScreen/>` (spinner) |
|
|
156
|
+
| `ERROR` | `<ErrorScreen/>` — retry button calls `window.location.reload()` |
|
|
157
|
+
| `SUCCESS` | `<Chat/>` (full UI tree) |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Context & Store Access
|
|
162
|
+
|
|
163
|
+
File: `src/context/IoAssistContext.tsx`.
|
|
164
|
+
|
|
165
|
+
| Export | Purpose |
|
|
166
|
+
|--------|---------|
|
|
167
|
+
| `IoAssistProvider` | Provides `staticConfig` and `dynamicConfig` via two React Contexts. |
|
|
168
|
+
| `useIoAssistStore(selector)` | Subscribe to the Zustand store with a selector (`useStore(ioAssistStore, selector)`). |
|
|
169
|
+
| `useIoAssistConfig()` | Read the static config. Throws if used outside the provider. |
|
|
170
|
+
| `useIoAssistDynamicConfig()` | Read the dynamic config. Throws if used outside the provider. |
|
|
171
|
+
|
|
172
|
+
The store itself (`ioAssistStore`) is a **module singleton** created in `stores/index.ts`, so action modules can import and mutate it directly without going through Context.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## State Management (Zustand)
|
|
177
|
+
|
|
178
|
+
### Store Composition
|
|
179
|
+
|
|
180
|
+
File: `src/stores/index.ts`. A single store type `IoAssistStore` is the intersection of ten slices, created with the `subscribeWithSelector` middleware:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
export type IoAssistStore = AppLifecycleSlice & AgentSlice & ThreadSlice & MessageSlice &
|
|
184
|
+
ResponseStreamSlice & PromptSlice & ToolSlice & WorkingContextSlice & McpAppsSlice & ConfirmModalSlice;
|
|
185
|
+
|
|
186
|
+
export const ioAssistStore = createIoAssistStore(); // module singleton
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Each slice is a `StateCreator` in its own file. Slices freely read sibling slices (e.g. `mergeAccumulatedStreamContent` in the message slice reads `streamsByThreadId` from the response-stream slice).
|
|
190
|
+
|
|
191
|
+
### Slices
|
|
192
|
+
|
|
193
|
+
#### `app-lifecycle.ts` — `AppLifecycleSlice`
|
|
194
|
+
State: `appLoadingState`, `isIoConnectReady`, `isDarkMode`, `ioConnectApi`, `ioAiWebApi`.
|
|
195
|
+
Actions: `setAppLoadingState`, `setIsIoConnectReady`, `setIsDarkMode`, `setIoConnectApi`, `setIoAiWebApi`.
|
|
196
|
+
|
|
197
|
+
#### `agent.ts` — `AgentSlice`
|
|
198
|
+
State: `agents: UIAgent[]`, `selectedAgent`, `agentsLoadingState`.
|
|
199
|
+
Actions: `setAgents`, `setSelectedAgent`, `setAgentsLoadingState`.
|
|
200
|
+
`UIAgent` wraps the raw ai-web agent in `rawAgent` (used for `generate()` / `stream()` / `abortOperation()`).
|
|
201
|
+
|
|
202
|
+
#### `thread.ts` — `ThreadSlice`
|
|
203
|
+
State: `threads: UIThread[]`, `activeThreadId`, `threadLoadingState`.
|
|
204
|
+
Actions: `setThreads`, `updateThread`, `removeThread`, `setActiveThreadId`, `setThreadLoadingState`.
|
|
205
|
+
`UIThread` carries `update`/`delete`/`getMessages` closures bound to the raw thread.
|
|
206
|
+
|
|
207
|
+
#### `message.ts` — `MessageSlice`
|
|
208
|
+
State: `messages: UIMessage[]`, `isGeneratingResponse`, `messageLoadingState`, `isLastResponseSuccess`, `toolTraceState: ToolTraceState[]`, `scrollAnchorRequestId`.
|
|
209
|
+
Actions: `setMessages`, `addMessage`, `updateMessage`, `clearMessages`, `setIsGeneratingResponse`, `setMessageLoadingState`, `setIsLastResponseSuccess`, `setToolTraceState`, `toggleToolTrace`, `toggleToolMessage`, `mergeAccumulatedStreamContent`, `requestScrollAnchor`.
|
|
210
|
+
- Tool traces are stored **separately** from messages, keyed by the assistant message id (`stateForMessageId`), each holding `executedTools` + a `uiMessage` label + `isExpanded`.
|
|
211
|
+
- `scrollAnchorRequestId` is a monotonic counter; `ScrollArea` reacts to each tick to snap to the latest content.
|
|
212
|
+
- `mergeAccumulatedStreamContent(threadId)` flushes a background thread's buffered `StreamState` (user message, tool messages, assistant content, and a synthesised trace) into the visible `messages` when the user switches to that thread.
|
|
213
|
+
|
|
214
|
+
#### `response-stream.ts` — `ResponseStreamSlice`
|
|
215
|
+
Per-thread streaming state, keyed by thread id:
|
|
216
|
+
```typescript
|
|
217
|
+
type StreamState = {
|
|
218
|
+
status: ResponseStreamStatus; // IDLE | STREAMING | COMPLETED | ERROR | ABORTED
|
|
219
|
+
content: string;
|
|
220
|
+
messageId: string | null;
|
|
221
|
+
errorMessage?: string;
|
|
222
|
+
userMessage: UIUserMessage | null;
|
|
223
|
+
toolMessages: UIToolMessage[];
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
State: `streamsByThreadId: Record<string, StreamState>`, `completionNotifications: string[]`.
|
|
227
|
+
Actions: `startStream`, `updateStreamContent`, `addStreamToolMessage`, `completeStream(threadId, shouldNotify)`, `failStream`, `abortStream`, `untrackStream` (only removes a **terminal** entry), `setStreamState` (partial patch; seeds an `IDLE` entry if missing), `clearStreamState`, `addCompletionNotification`, `clearCompletionNotification`, `clearAllCompletionNotifications`.
|
|
228
|
+
- `updateStreamContent` / `addStreamToolMessage` are no-ops unless status is `STREAMING`.
|
|
229
|
+
- `completeStream` adds the thread to `completionNotifications` only when `shouldNotify` is true (background completion → notification dot).
|
|
230
|
+
|
|
231
|
+
#### `prompt.ts` — `PromptSlice`
|
|
232
|
+
State: `allPrompts: Prompt[]`, `favoritePromptNames: string[]`, `selectedPrompt`.
|
|
233
|
+
Actions: `setAllPrompts`, `setFavoritePromptNames`, `toggleFavoritePrompt`, `setSelectedPrompt`.
|
|
234
|
+
|
|
235
|
+
#### `tool.ts` — `ToolSlice`
|
|
236
|
+
State: `tools: UITool[]`, `toolLoadingState`.
|
|
237
|
+
Actions: `setTools`, `updateToolEnabled`, `updateToolState`, `setToolLoadingState`.
|
|
238
|
+
|
|
239
|
+
#### `working-context.ts` — `WorkingContextSlice`
|
|
240
|
+
State: `isWorkingContextEnabled`, `workingContext`, `workingContextLoadingState`.
|
|
241
|
+
Actions: `setIsWorkingContextEnabled`, `setWorkingContext`, `setWorkingContextLoadingState`.
|
|
242
|
+
|
|
243
|
+
#### `mcp-apps.ts` — `McpAppsSlice`
|
|
244
|
+
State: `mcpAppInstances: IoAiWeb.McpApps.AppInstance[]`.
|
|
245
|
+
Actions: `addMcpApp` (dedup by id), `removeMcpApp`, `resetMcpApps`.
|
|
246
|
+
|
|
247
|
+
#### `confirm-modal.ts` — `ConfirmModalSlice`
|
|
248
|
+
State: `isThreadHistoryVisible`, `activePanelContent: PanelContent` (`null | 'prompts' | 'tools' | 'working-context'`), `currentConfirmModal: ConfirmModalConfig | null`.
|
|
249
|
+
Actions: `setIsThreadHistoryVisible`, `setActivePanelContent`, `showConfirmModal`, `closeConfirmModal`.
|
|
250
|
+
This slice only holds modal/panel **presence**. The promise-based request/response bridge lives in `utils/confirmModal.ts` (see [Utilities](#utilities)).
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Bootstrap Hooks
|
|
255
|
+
|
|
256
|
+
Both hooks guard with a `useRef` so initialization runs exactly once, and run in order.
|
|
257
|
+
|
|
258
|
+
#### `useIoConnectBootstrap.ts`
|
|
259
|
+
On mount, calls `initIoConnect()` (`actions/initIoConnect.ts`). It wires callbacks that set `ioConnectApi`, `isIoConnectReady`, restore `favoritePromptNames` from io.Connect preferences, and apply the saved theme via `isDarkMode`. Also registers the logger with io.Connect's logger when available.
|
|
260
|
+
|
|
261
|
+
#### `useIoAiWebBootstrap.ts`
|
|
262
|
+
Watches `isIoConnectReady` and `appLoadingState.type`. Once io.Connect is ready and the app is still `NOT_STARTED`, calls `initIoAiWeb(staticConfig, dynamicConfig, ioAssistStore)`.
|
|
263
|
+
|
|
264
|
+
#### UI hooks
|
|
265
|
+
- `useIsMobileViewport.ts` — `(min-width: 768px)` media-query boolean, SSR-safe, updates on resize.
|
|
266
|
+
- `useHoverMouseFollow.ts` — returns `{ ref, handlers }` for a cursor-following glow effect (layered conic-gradient divs).
|
|
267
|
+
- `useIoConnectApi.ts` / `useIoAiWebApi.ts` — thin wrappers exposing the bootstrapped APIs / message helpers to components. **Internal** (not exported from the package root).
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Actions Layer
|
|
272
|
+
|
|
273
|
+
The `actions/` modules are the React equivalent of the Angular services/effects. They are plain async functions that take the store (and often the configs) and mutate state imperatively.
|
|
274
|
+
|
|
275
|
+
| Module | Responsibility |
|
|
276
|
+
|--------|---------------|
|
|
277
|
+
| `initIoConnect.ts` | Bootstraps the io.Connect platform from `connectConfig`, sets theme + favorites, registers the logger. |
|
|
278
|
+
| `initIoAiWeb.ts` | Imports `IoAiWebFactory`, builds `WebConfig`, initializes ai-web, wires MCP app events, then runs post-init loaders: `listAgents` (selects `defaultAgentName` or first; then `fetchThreads`), `loadPrompts`, `loadTools`, `checkWorkingContext`. Subscribes to `tools.onChanged` to refetch. |
|
|
279
|
+
| `sendUserMessage.ts` | Creates/reuses the active thread, appends the user message, requests a scroll anchor, calls `agent.stream(...)` with `thread`/`resource` memory params, then hands the run to `processResponseStream`. Refreshes threads on completion. |
|
|
280
|
+
| `processResponseStream.ts` | The AG-UI event loop (see next section). |
|
|
281
|
+
| `abortActiveStream.ts` | Calls `selectedAgent.rawAgent.abortOperation(threadId)`, marks the stream aborted, clears `isGeneratingResponse`. |
|
|
282
|
+
| `newConversation.ts` | Resets to a fresh empty conversation. |
|
|
283
|
+
| `selectThread.ts` | Switches `activeThreadId` and loads that thread's message history. |
|
|
284
|
+
| `deleteThread.ts` | Deletes via `thread.delete()`, removes it from state, clears its stream state, starts a new conversation if it was active. |
|
|
285
|
+
| `renameThread.ts` | Optimistically updates the title, persists via `thread.update({ title })`, reverts on failure. |
|
|
286
|
+
| `toggleTool.ts` | Enables/disables a tool (updates `enabled` + reflects to the MCP layer). |
|
|
287
|
+
| `toggleFavoritePrompt.ts` | Toggles a favorite and persists the set to io.Connect preferences. |
|
|
288
|
+
| `sampling.ts` / `elicitation.ts` | Built-in MCP request handlers + custom-handler selection (see [Sampling & Elicitation](#sampling--elicitation-flow)). |
|
|
289
|
+
| `mcpAppEvents.ts` | Wires `onAppCreated` / `onRecreateRequested` / `onAppRecreated` (see [MCP Apps](#mcp-apps-integration)). |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## AG-UI Streaming Pipeline
|
|
294
|
+
|
|
295
|
+
File: `src/actions/processResponseStream.ts`.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
processResponseStream(
|
|
299
|
+
runHandle: IoAiWeb.Agents.StreamResponse,
|
|
300
|
+
threadId: string,
|
|
301
|
+
store: IoAssistStoreInstance,
|
|
302
|
+
isActiveThread: () => boolean,
|
|
303
|
+
): Promise<void>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
It subscribes to the AG-UI `StreamResponse` and maps each event to store mutations. Loop-local state held across the subscription lifetime: `toolCallArgsMap` (per-tool accumulated args), `streamedContent`, `currentMessageId`, and `hasSyntheticPlaceholder`.
|
|
307
|
+
|
|
308
|
+
| AG-UI Event | Active-thread handling | Background-thread handling |
|
|
309
|
+
|-------------|------------------------|----------------------------|
|
|
310
|
+
| `TEXT_MESSAGE_START` | Reuse the synthetic placeholder if same-turn (matching/empty `messageId`); otherwise set a fresh `currentMessageId` and `addMessage` an empty assistant message. | Same id bookkeeping; no `addMessage`. |
|
|
311
|
+
| `TEXT_MESSAGE_CONTENT` | Append `delta` to `streamedContent`; `ensureAssistantMessage` if none exists; `updateMessage(content)`. | `setStreamState(threadId, { content, messageId })`. |
|
|
312
|
+
| `TEXT_MESSAGE_END` | Reset `hasSyntheticPlaceholder` (turn boundary). | — |
|
|
313
|
+
| `TEXT_MESSAGE_CHUNK` | Combined start+content+end shorthand; create message if `messageId` changed, append text. | Buffer into `setStreamState`. |
|
|
314
|
+
| `MESSAGES_SNAPSHOT` | Reconciliation: add/extend any assistant messages from the snapshot not already shown. | Skipped. |
|
|
315
|
+
| `TOOL_CALL_START` | Create a synthetic placeholder assistant message if none yet; `addMessage(toolMessage)`; `withToolAppendedToTrace`. | Append the tool message to `StreamState.toolMessages`. |
|
|
316
|
+
| `TOOL_CALL_ARGS` | Accumulate `delta` into `toolCallArgsMap[toolCallId].args`. | Same. |
|
|
317
|
+
| `TOOL_CALL_END` | `JSON.parse` accumulated args; `updateMessage(toolCallId, { args })`; `withExecutedToolUpdated` + `withTraceLabel`. | Patch the buffered tool message's `args`. |
|
|
318
|
+
| `TOOL_CALL_RESULT` | Read result off `event.content` (string → `JSON.parse`, falling back to `{ type: TEXT, text }`); `updateMessage(toolCallId, { result })`; update trace. | Patch the buffered tool message's `result`. |
|
|
319
|
+
|
|
320
|
+
**Terminal handling:**
|
|
321
|
+
- `error` → `failStream`, clear `isGeneratingResponse`, set `isLastResponseSuccess=false`, set `messageLoadingState=ERROR`, unsubscribe, `reject`.
|
|
322
|
+
- `complete` → finalize all trace labels (active thread only) via `withAllTraceLabelsFinalized`, `completeStream(threadId, shouldNotify=!isActiveThread())`, clear `isGeneratingResponse`, set success state, unsubscribe, `resolve`.
|
|
323
|
+
|
|
324
|
+
A `log.debug('AG-UI event …')` line at the top of `next()` enumerates every received event — the first thing to check when diagnosing missing output after sampling/tool continuations.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Response Stream Store & Background Threads
|
|
329
|
+
|
|
330
|
+
A single `agent.stream()` call can run multiple turns (text → tool → tool result → text) under one subscription, and the user may switch threads mid-stream. The pipeline routes accordingly:
|
|
331
|
+
|
|
332
|
+
- **Active thread** → writes straight to `messages` + `toolTraceState` (visible UI).
|
|
333
|
+
- **Background thread** → buffers into `streamsByThreadId[threadId]` (`StreamState`). On completion with `shouldNotify`, the thread id is added to `completionNotifications`, surfacing a notification dot on the thread-history control.
|
|
334
|
+
- **Switching back** → `mergeAccumulatedStreamContent(threadId)` reconciles the buffered user message, tool messages, assistant content, and a synthesised trace into the visible `messages`.
|
|
335
|
+
|
|
336
|
+
`isActiveThread` is passed as a **callback** (not a boolean) so it re-evaluates on every event — the active thread can change between events.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Sampling & Elicitation Flow
|
|
341
|
+
|
|
342
|
+
Files: `src/actions/sampling.ts`, `src/actions/elicitation.ts`. Both are injected into the ai-web MCP config by `initIoAiWeb.ts::buildMcpConfig()`:
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
capabilities: {
|
|
346
|
+
...mcp.clientsConfig?.capabilities,
|
|
347
|
+
sampling: { handler: selectSampling(mcp) },
|
|
348
|
+
elicitation: { handler: selectElicitation(mcp) },
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`selectSampling` / `selectElicitation` return the consumer's `mcp.clientsConfig.capabilities.{sampling|elicitation}.handler` if it's a function, otherwise the built-in handler.
|
|
353
|
+
|
|
354
|
+
### Sampling (built-in)
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
handleSamplingRequest(serverName, request)
|
|
358
|
+
├── background thread? (request._meta.threadId !== activeThreadId) → { code: -1, … } (rejected)
|
|
359
|
+
├── isModalsAvailable()? → io.Connect 'noInputsConfirmationDialog' (Continue/Cancel)
|
|
360
|
+
│ else → showConfirmModal() built-in overlay (Continue/Cancel)
|
|
361
|
+
├── accepted → getSamplingResponse(request):
|
|
362
|
+
│ formats messages (extractTextContent) + systemPrompt → selectedAgent.rawAgent.generate(params)
|
|
363
|
+
│ → SamplingSuccessResponse { role:'assistant', content:{type:'text',text}, model, stopReason:'endTurn' }
|
|
364
|
+
└── declined / preempted / error → { code: -1, message }
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`getSamplingResponse` calls `generate()` with `tools.autoIncludeEnabled: false` and passes through `maxTokens`, `temperature`, `modelPreferences`, and `_meta.structuredOutput`.
|
|
368
|
+
|
|
369
|
+
### Elicitation (built-in)
|
|
370
|
+
|
|
371
|
+
Validates `_meta.toolName` starts with `io_connect`, rejects background-thread requests, shows an accept/decline/cancel dialog (io.Connect modal or built-in overlay), and returns the corresponding `{ action, content }`.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## MCP Apps Integration
|
|
376
|
+
|
|
377
|
+
Files: `src/actions/mcpAppEvents.ts`, `src/stores/mcp-apps.ts`, `src/components/messages/McpAppResource.tsx`, `src/utils/mcpAppModal.ts`.
|
|
378
|
+
|
|
379
|
+
`wireMcpAppEvents(ioIntelWeb, store, dynamicConfig)` is called synchronously right after `IoAiWebFactory` resolves (before the post-init `Promise.all`) so no early events are missed:
|
|
380
|
+
|
|
381
|
+
1. **`onAppCreated(app)`** — `addMcpApp(app)`; subscribe `app.onMessage` and forward app-originated chat messages through `sendUserMessage`. Unsubscribe closures are tracked in a `Map<appId, () => void>`.
|
|
382
|
+
2. **`onRecreateRequested(event)`** — show a "recreate vs new instance" modal (`mcpAppModal.ts` → io.Connect modal or built-in `ConfirmModal`). While the modal is open, duplicate recreate events auto-resolve to avoid stacking.
|
|
383
|
+
3. **`onAppRecreated(event)`** — `removeMcpApp(oldId)` and clean up its message listener.
|
|
384
|
+
|
|
385
|
+
`McpAppResource.tsx` mounts the SDK-provided `AppInstance.element` (the sandbox-proxy iframe) into a container `div` for **inline** display; **workspace** mode opens an io.Connect workspace window. The component only attaches/detaches the element — ownership stays with the SDK.
|
|
386
|
+
|
|
387
|
+
Display mode + the `io.modelcontextprotocol/ui` capability extension come from the consumer's `aiWebConfig.mcp` (see the public README's MCP Apps section).
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Components
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
components/
|
|
395
|
+
├── chat/
|
|
396
|
+
│ ├── Chat.tsx # Main layout: header, message area, input, welcome/favorites
|
|
397
|
+
│ ├── ActivePanelModal.tsx # Hosts the active side panel (prompts / tools / working context)
|
|
398
|
+
│ ├── ConfirmModal.tsx # Renders currentConfirmModal; buttons resolve the confirmModal promise
|
|
399
|
+
│ ├── WelcomeHeading.tsx # Greeting shown on the empty home screen
|
|
400
|
+
│ └── AiDisclaimer.tsx # AI-usage disclaimer footer
|
|
401
|
+
├── header/Header.tsx # Thread-history toggle (notification dot), new-chat, working-context buttons
|
|
402
|
+
├── input-area/InputArea.tsx # Auto-sizing textarea, Enter/Shift+Enter, history cycling, send/stop
|
|
403
|
+
├── messages/
|
|
404
|
+
│ ├── MessageArea.tsx # Scrollable list; buildRenderList() merges messages + traces + apps
|
|
405
|
+
│ ├── UserMessage.tsx # User bubble
|
|
406
|
+
│ ├── AssistantMessage.tsx # Markdown-rendered assistant bubble
|
|
407
|
+
│ ├── ToolTraceMessage.tsx # Collapsible per-turn tool trace
|
|
408
|
+
│ ├── ToolMessage.tsx # Single tool call (args + result)
|
|
409
|
+
│ ├── McpAppResource.tsx # Mounts an inline MCP App element
|
|
410
|
+
│ ├── MdFormatter.tsx # react-markdown + remark-gfm; code via prism-react-renderer
|
|
411
|
+
│ ├── mdUtils.ts # Markdown helpers
|
|
412
|
+
│ └── prismTwilightTheme.ts # Prism color theme
|
|
413
|
+
├── threads/ # ThreadHistoryPanel / ThreadHistory / ThreadHistoryListItem
|
|
414
|
+
├── tool/ # ToolListPanel / ToolListItem
|
|
415
|
+
├── prompt/ # PromptListPanel / PromptListItem / FavoritePromptList
|
|
416
|
+
├── working-context-panel/WorkingContextPanel.tsx
|
|
417
|
+
├── scroll-area/ScrollArea.tsx # Scroll container reacting to scrollAnchorRequestId
|
|
418
|
+
└── shared/ # Icon, IconButton, Modal, SearchInput, ToggleInput, Tooltip, icons.tsx
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Components subscribe with narrow selectors via `useIoAssistStore(s => …)` to avoid broad re-renders (the store uses `subscribeWithSelector`).
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Utilities
|
|
426
|
+
|
|
427
|
+
File reference for `src/utils/`:
|
|
428
|
+
|
|
429
|
+
| File | Purpose |
|
|
430
|
+
|------|---------|
|
|
431
|
+
| `confirmModal.ts` | Promise bridge for the built-in modal. `showConfirmModal(config)` calls the `confirmModal` slice's `showConfirmModal` and returns a `Promise<buttonId \| MODAL_PREEMPTED>`; `ConfirmModal.tsx` resolves it on click. A new request **preempts** an unresolved one with the `MODAL_PREEMPTED` sentinel. |
|
|
432
|
+
| `ioModals.ts` | io.Connect Modals wrapper: `isModalsAvailable()`, `requestModalDialog(options)` (uses `noInputsConfirmationDialog`). |
|
|
433
|
+
| `mcpAppModal.ts` | MCP App recreate prompt (io.Connect modal → built-in fallback). |
|
|
434
|
+
| `messageConverter.ts` | Converts raw backend thread messages → `UIMessage[]`; `extractTextContent()` pulls text from mixed content shapes (also used by sampling). |
|
|
435
|
+
| `messageUtils.ts` | `buildRenderList(...)` merges messages, tool traces, and MCP apps into an ordered render list. |
|
|
436
|
+
| `streamUtils.ts` | Pure tool-trace helpers: `ensureAssistantMessage`, `withToolAppendedToTrace`, `withExecutedToolUpdated`, `withTraceLabel`, `withAllTraceLabelsFinalized`. |
|
|
437
|
+
| `threadUtils.ts` | Groups threads into relative-time buckets for the history panel. |
|
|
438
|
+
| `safeStringify.ts` | `JSON.stringify` with a `String(value)` fallback (for logging). |
|
|
439
|
+
| `logger.ts` | Namespaced logger (`logger.get(name)`), io.Connect logger when registered, console fallback otherwise. |
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Styling
|
|
444
|
+
|
|
445
|
+
- **Tailwind CSS** — `src/styles/index.css` is the entry (`@import 'tailwindcss'`, `@font-face`, `@theme`); `src/styles/defaults.css` defines the CSS variables that back the theme for light and dark mode.
|
|
446
|
+
- **Theme sync** — `initIoConnect` reads/sets `isDarkMode`; the dark-mode class on the document toggles the variable set.
|
|
447
|
+
- **Exported stylesheet** — `@interopio/io-assist-react/styles` (`exports["./styles"]` → `dist/styles.css`) must be imported by the consumer.
|
|
448
|
+
- **Fonts** — Inter is bundled via `@fontsource-variable/inter` (no external CDN fetch, unlike the Angular package).
|
|
449
|
+
- **Code highlighting** — `prism-react-renderer` renders fenced code blocks in `MdFormatter.tsx` using `prismTwilightTheme`.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Build & Tooling
|
|
454
|
+
|
|
455
|
+
- **Bundler** — Vite library build (`vite.config.ts`). `npm run build` → `dist/`; `npm run dev` → `vite build --watch`.
|
|
456
|
+
- **Entry points** — ESM (`dist/index.js`), CJS (`dist/index.cjs`), types (`dist/index.d.ts`), styles (`dist/styles.css`).
|
|
457
|
+
- **Peer deps** — only `react` / `react-dom` (`>=18`); all io.Connect/ai-web/UI packages are bundled `dependencies`.
|
|
458
|
+
- **Lint** — `npm run lint` (ESLint flat config), `npm run lint:fix`.
|
|
459
|
+
- **Tests** — none in this package at present.
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Known Caveats
|
|
464
|
+
|
|
465
|
+
- **Cross-turn placeholder leakage.** Because one `agent.stream()` runs N turns under a single subscription, `currentMessageId` / `streamedContent` / `hasSyntheticPlaceholder` persist across turns. The `TEXT_MESSAGE_START` "same-turn" check and the `TEXT_MESSAGE_END` reset exist specifically to stop a later turn's text from being appended to the previous turn's bubble (a recurring source of "nothing rendered after I clicked Continue" bugs in sampling/tool-continuation flows). Preserve this logic when refactoring the pipeline.
|
|
466
|
+
- **Tool result field.** AG-UI delivers tool results on `event.content` (string or object), **not** `event.result`. `TOOL_CALL_RESULT` reads `event.content` accordingly.
|
|
467
|
+
- **No config validation.** Malformed configs fail later and less obviously than in the Angular package (which uses Zod). Consider validating in the provider if this becomes a support burden.
|