@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.
Files changed (99) hide show
  1. package/README.internal.md +467 -0
  2. package/README.md +503 -0
  3. package/changelog.md +2 -0
  4. package/dist/IoAssist.d.ts +12 -0
  5. package/dist/actions/abortActiveStream.d.ts +2 -0
  6. package/dist/actions/deleteThread.d.ts +2 -0
  7. package/dist/actions/elicitation.d.ts +13 -0
  8. package/dist/actions/initIoAiWeb.d.ts +3 -0
  9. package/dist/actions/initIoConnect.d.ts +9 -0
  10. package/dist/actions/mcpAppEvents.d.ts +4 -0
  11. package/dist/actions/newConversation.d.ts +2 -0
  12. package/dist/actions/processResponseStream.d.ts +3 -0
  13. package/dist/actions/renameThread.d.ts +2 -0
  14. package/dist/actions/sampling.d.ts +5 -0
  15. package/dist/actions/selectThread.d.ts +2 -0
  16. package/dist/actions/sendUserMessage.d.ts +3 -0
  17. package/dist/actions/toggleFavoritePrompt.d.ts +7 -0
  18. package/dist/actions/toggleTool.d.ts +2 -0
  19. package/dist/components/chat/ActivePanelModal.d.ts +6 -0
  20. package/dist/components/chat/AiDisclaimer.d.ts +6 -0
  21. package/dist/components/chat/Chat.d.ts +2 -0
  22. package/dist/components/chat/ConfirmModal.d.ts +8 -0
  23. package/dist/components/chat/WelcomeHeading.d.ts +6 -0
  24. package/dist/components/header/Header.d.ts +2 -0
  25. package/dist/components/input-area/InputArea.d.ts +8 -0
  26. package/dist/components/messages/AssistantMessage.d.ts +10 -0
  27. package/dist/components/messages/McpAppResource.d.ts +11 -0
  28. package/dist/components/messages/MdFormatter.d.ts +6 -0
  29. package/dist/components/messages/MessageArea.d.ts +7 -0
  30. package/dist/components/messages/ToolMessage.d.ts +9 -0
  31. package/dist/components/messages/ToolTraceMessage.d.ts +9 -0
  32. package/dist/components/messages/UserMessage.d.ts +9 -0
  33. package/dist/components/messages/mdUtils.d.ts +1 -0
  34. package/dist/components/messages/prismTwilightTheme.d.ts +2 -0
  35. package/dist/components/prompt/FavoritePromptList.d.ts +2 -0
  36. package/dist/components/prompt/PromptListItem.d.ts +8 -0
  37. package/dist/components/prompt/PromptListPanel.d.ts +6 -0
  38. package/dist/components/scroll-area/ScrollArea.d.ts +33 -0
  39. package/dist/components/shared/Icon.d.ts +8 -0
  40. package/dist/components/shared/IconButton.d.ts +11 -0
  41. package/dist/components/shared/Modal.d.ts +12 -0
  42. package/dist/components/shared/SearchInput.d.ts +8 -0
  43. package/dist/components/shared/ToggleInput.d.ts +8 -0
  44. package/dist/components/shared/Tooltip.d.ts +10 -0
  45. package/dist/components/shared/icons.d.ts +37 -0
  46. package/dist/components/threads/ThreadHistory.d.ts +6 -0
  47. package/dist/components/threads/ThreadHistoryListItem.d.ts +18 -0
  48. package/dist/components/threads/ThreadHistoryPanel.d.ts +7 -0
  49. package/dist/components/tool/ToolListItem.d.ts +8 -0
  50. package/dist/components/tool/ToolListPanel.d.ts +2 -0
  51. package/dist/components/working-context-panel/WorkingContextPanel.d.ts +2 -0
  52. package/dist/constants/modalActions.d.ts +11 -0
  53. package/dist/constants/uiStrings.d.ts +141 -0
  54. package/dist/context/IoAssistContext.d.ts +14 -0
  55. package/dist/files/inter-latin-wght-normal.woff2 +0 -0
  56. package/dist/hooks/useHoverMouseFollow.d.ts +27 -0
  57. package/dist/hooks/useIoAiWebApi.d.ts +19 -0
  58. package/dist/hooks/useIoAiWebBootstrap.d.ts +8 -0
  59. package/dist/hooks/useIoConnectApi.d.ts +12 -0
  60. package/dist/hooks/useIoConnectBootstrap.d.ts +10 -0
  61. package/dist/hooks/useIsMobileViewport.d.ts +1 -0
  62. package/dist/index.cjs +41 -0
  63. package/dist/index.cjs.map +1 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.js +3624 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/stores/agent.d.ts +15 -0
  68. package/dist/stores/app-lifecycle.d.ts +21 -0
  69. package/dist/stores/confirm-modal.d.ts +29 -0
  70. package/dist/stores/index.d.ts +32 -0
  71. package/dist/stores/mcp-apps.d.ts +13 -0
  72. package/dist/stores/message.d.ts +33 -0
  73. package/dist/stores/prompt.d.ts +16 -0
  74. package/dist/stores/response-stream.d.ts +31 -0
  75. package/dist/stores/thread.d.ts +17 -0
  76. package/dist/stores/tool.d.ts +15 -0
  77. package/dist/stores/working-context.d.ts +16 -0
  78. package/dist/styles.css +1 -0
  79. package/dist/types/agent.d.ts +8 -0
  80. package/dist/types/config.d.ts +40 -0
  81. package/dist/types/icon.d.ts +5 -0
  82. package/dist/types/index.d.ts +15 -0
  83. package/dist/types/loading.d.ts +38 -0
  84. package/dist/types/message.d.ts +56 -0
  85. package/dist/types/panel.d.ts +6 -0
  86. package/dist/types/prompt.d.ts +17 -0
  87. package/dist/types/stream.d.ts +22 -0
  88. package/dist/types/thread.d.ts +15 -0
  89. package/dist/types/tool.d.ts +15 -0
  90. package/dist/utils/confirmModal.d.ts +5 -0
  91. package/dist/utils/ioModals.d.ts +21 -0
  92. package/dist/utils/logger.d.ts +34 -0
  93. package/dist/utils/mcpAppModal.d.ts +3 -0
  94. package/dist/utils/messageConverter.d.ts +3 -0
  95. package/dist/utils/messageUtils.d.ts +16 -0
  96. package/dist/utils/safeStringify.d.ts +1 -0
  97. package/dist/utils/streamUtils.d.ts +15 -0
  98. package/dist/utils/threadUtils.d.ts +16 -0
  99. 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.