@interopio/io-assist-ng 0.0.1 → 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.
@@ -0,0 +1,865 @@
1
+ # io.Assist — Internal Developer Documentation
2
+
3
+ **Audience:** Developers working on or extending `@interopio/io-assist-ng`.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Architecture Overview](#architecture-overview)
10
+ - [Project Structure](#project-structure)
11
+ - [Public API Surface](#public-api-surface)
12
+ - [Configuration & Validation](#configuration--validation)
13
+ - [Provider Registration](#provider-registration)
14
+ - [Root Component — IoAssist](#root-component--ioassist)
15
+ - [State Management (NgRx Store)](#state-management-ngrx-store)
16
+ - [Store Shape](#store-shape)
17
+ - [Facades](#facades)
18
+ - [Effects](#effects)
19
+ - [Services](#services)
20
+ - [IOConnectService](#ioconnectservice)
21
+ - [IOIntelWebService](#iointelwebservice)
22
+ - [AgentService](#agentservice)
23
+ - [AgentManagerService](#agentmanagerservice)
24
+ - [SamplingService](#samplingservice)
25
+ - [ElicitationService](#elicitationservice)
26
+ - [McpAppsService](#mcpappsservice)
27
+ - [ThreadService](#threadservice)
28
+ - [ToolsService](#toolsservice)
29
+ - [PromptService](#promptservice)
30
+ - [WorkingContextService](#workingcontextservice)
31
+ - [OverlayService](#overlayservice)
32
+ - [ConsumerAssetLoaderService](#consumerassetloaderservice)
33
+ - [ResponsiveUIService](#responsiveuiservice)
34
+ - [LoggerService](#loggerservice)
35
+ - [ComponentEffectManagerService](#componenteffectmanagerservice)
36
+ - [SubscriptionCleanupService](#subscriptioncleanupservice)
37
+ - [CopyToClipboardService](#copytoclipboardservice)
38
+ - [Components](#components)
39
+ - [Chat Layout](#chat-layout)
40
+ - [Header](#header)
41
+ - [Input Area](#input-area)
42
+ - [Messages](#messages)
43
+ - [Threads](#threads)
44
+ - [Tools](#tools)
45
+ - [Prompts](#prompts)
46
+ - [Working Context Panel](#working-context-panel)
47
+ - [MCP App Resource](#mcp-app-resource)
48
+ - [Shared UI Primitives](#shared-ui-primitives)
49
+ - [Directives](#directives)
50
+ - [AG-UI Streaming Pipeline](#ag-ui-streaming-pipeline)
51
+ - [Response Stream Store](#response-stream-store)
52
+ - [Sampling & Elicitation Flow](#sampling--elicitation-flow)
53
+ - [MCP Apps Integration](#mcp-apps-integration)
54
+ - [Styling](#styling)
55
+ - [Testing](#testing)
56
+
57
+ ---
58
+
59
+ ## Architecture Overview
60
+
61
+ `@interopio/io-assist-ng` is a standalone Angular library (no NgModule). The consumer calls `provideIoAssist(config)` once in their app config and drops `<io-assist [config]="dynamicConfig()">` into a template. Everything else — store, effects, services, UI — is bootstrapped internally.
62
+
63
+ ```
64
+ Consumer App
65
+ └── provideIoAssist(config) ← registers DI providers
66
+ ├── IO_ASSIST_CONFIG token ← validated static config
67
+ ├── IO_ASSIST_DYNAMIC_CONFIG ← WritableSignal<IoAssistDynamicConfig> (seeded empty)
68
+ ├── provideIoConnect(...) ← io.Connect Angular integration
69
+ ├── provideStore(appReducers) ← NgRx store with 8 slices
70
+ ├── provideEffects(...) ← 7 effect classes
71
+ └── provideStoreDevtools()
72
+
73
+ └── <io-assist [config]="dynamicConfig()"> ← root component
74
+ ├── input.required<IoAssistDynamicConfig>() ← runtime user + headers
75
+ ├── effect() → IO_ASSIST_DYNAMIC_CONFIG.set(config)
76
+ ├── IOConnectService ← io.Connect readiness + theme sync
77
+ ├── IOIntelWebService ← ai-web API init
78
+ ├── AppLifecycleFacade ← lifecycle state signals
79
+ └── <chat /> ← full UI tree
80
+ ```
81
+
82
+ **Key layers:**
83
+
84
+ | Layer | Responsibility |
85
+ |-------|---------------|
86
+ | **Static Config / Validation** | Zod schema validates `IoAssistStaticConfig` at provider registration time |
87
+ | **Dynamic Config** | `IoAssistDynamicConfig` passed as component input, synced to `IO_ASSIST_DYNAMIC_CONFIG` signal |
88
+ | **NgRx Store** | 8 feature slices with reducers, selectors, and facades |
89
+ | **Effects** | Async operations (API calls, streaming, lifecycle) |
90
+ | **Services** | Business logic, API integration, UI orchestration |
91
+ | **Components** | Standalone Angular components with signal-based reactivity |
92
+
93
+ ---
94
+
95
+ ## Project Structure
96
+
97
+ ```
98
+ src/
99
+ ├── public-api.ts # Package exports
100
+ ├── styles.css # Tailwind entry point
101
+ └── lib/
102
+ ├── io-assist.component.ts # Root <io-assist> component
103
+ ├── io-assist.component.html # Root template
104
+ ├── io-assist.providers.ts # provideIoAssist() function
105
+ ├── io-assist.config.ts # Config types + IO_ASSIST_CONFIG token
106
+ ├── io-assist.schema.ts # Zod validation schema
107
+ ├── io-assist.types.ts # Prompt/icon types
108
+ ├── components/
109
+ │ ├── chat/ # Main chat layout
110
+ │ ├── header/ # App header with thread toggle
111
+ │ ├── input-area/ # Message input + action bar
112
+ │ ├── messages/ # Message rendering (user, assistant, tool)
113
+ │ ├── threads/ # Thread history panel
114
+ │ ├── tool/ # Tool list + management
115
+ │ ├── prompt/ # Prompt library + favorites
116
+ │ └── working-context-panel/ # Working context inspector
117
+ └── shared/
118
+ ├── components/ # Reusable UI primitives
119
+ ├── constants/ # UI strings, config constants
120
+ ├── directives/ # Animation + accent border directives
121
+ ├── enums/ # Loading state, message roles, etc.
122
+ ├── services/ # All business logic services
123
+ ├── store/ # NgRx store (8 feature slices)
124
+ ├── styles/ # Shared SCSS/Tailwind utilities
125
+ ├── types/ # Shared type definitions
126
+ └── utils/ # Icon parsing, sanitization utilities
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Public API Surface
132
+
133
+ File: `src/public-api.ts`
134
+
135
+ Only these are exported — everything else is internal:
136
+
137
+ ```typescript
138
+ export * from './lib/io-assist.component'; // IoAssist component
139
+ export * from './lib/io-assist.providers'; // provideIoAssist()
140
+ export * from './lib/io-assist.types'; // IoAssistPrompt, IoAssistPromptCategory, IconResource, IconType
141
+ export * from './lib/io-assist.config'; // IoAssistStaticConfig, IoAssistDynamicConfig, AIWebConfig, IoAssistUserConfig, IO_ASSIST_CONFIG, IO_ASSIST_DYNAMIC_CONFIG
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Configuration & Validation
147
+
148
+ ### Config types
149
+
150
+ File: `src/lib/io-assist.config.ts`
151
+
152
+ io.Assist uses two separate configuration types:
153
+
154
+ **Static config** — passed to `provideIoAssist()` at bootstrap. Does not include user identity.
155
+
156
+ ```typescript
157
+ type IoAssistStaticConfig = {
158
+ connectConfig: IOConnectNgSettings; // io.Connect platform settings
159
+ aiWebConfig: AIWebConfig; // { agentServer, mcp? }
160
+ defaultAgentName?: string;
161
+ workingContext?: {
162
+ factory: IoIntelWorkingContextFactoryFunction;
163
+ config?: IoIntelWorkingContext.Config;
164
+ };
165
+ defaultPrompts?: IoAssistPromptCategory[];
166
+ };
167
+
168
+ type AIWebConfig = {
169
+ agentServer: Omit<IoAiWeb.WebConfig['agentServer'], 'headers'>; // headers excluded
170
+ mcp?: IoAiWeb.WebConfig['mcp'];
171
+ };
172
+ ```
173
+
174
+ **Dynamic config** — passed as `[config]` input to `<io-assist>` at runtime. Holds the active user's identity and request headers.
175
+
176
+ ```typescript
177
+ type IoAssistUserConfig = { id: string; name?: string };
178
+
179
+ type IoAssistDynamicConfig = {
180
+ user: IoAssistUserConfig;
181
+ agentServer?: {
182
+ headers?: Record<string, string>; // request headers for every agent call (e.g. auth tokens)
183
+ };
184
+ };
185
+ ```
186
+
187
+ ### DI tokens
188
+
189
+ | Token | Type | Set by |
190
+ |-------|------|--------|
191
+ | `IO_ASSIST_CONFIG` | `InjectionToken<IoAssistStaticConfig>` | `provideIoAssist()` (once, at bootstrap) |
192
+ | `IO_ASSIST_DYNAMIC_CONFIG` | `InjectionToken<WritableSignal<IoAssistDynamicConfig>>` | `provideIoAssist()` seeds with `{ user: { id: '' } }`; overwritten on each `IoAssist` component input change via `effect()` |
193
+
194
+ ### Zod validation
195
+
196
+ File: `src/lib/io-assist.schema.ts`
197
+
198
+ Two schemas are defined:
199
+
200
+ **`IoAssistStaticConfigSchema`** — validated at `provideIoAssist()` time. Key validations:
201
+
202
+ - `aiWebConfig.agentServer.baseUrl` — valid URL
203
+ - `mcp.mcpApps.sandboxProxyUrl` — non-empty string if `mcpApps` provided
204
+ - `mcp.clientsConfig.capabilities.elicitation.handler` — function type
205
+ - `defaultPrompts[].prompts[].name` — non-empty string
206
+ - `defaultPrompts[].prompts[].iconResource.type` — enum `'svg' | 'url' | 'data-url'`
207
+
208
+ If validation fails, `provideIoAssist()` throws `"Invalid IoAssist configuration provided."`.
209
+
210
+ **`IoAssistDynamicConfigSchema`** — validated in `IoAssist.ngOnInit()`. Key validations:
211
+
212
+ - `user.id` — non-empty string (required)
213
+ - `agentServer.headers` — optional record of strings
214
+
215
+ If validation fails, `ngOnInit` throws a `ZodError`.
216
+
217
+ ---
218
+
219
+ ## Provider Registration
220
+
221
+ File: `src/lib/io-assist.providers.ts`
222
+
223
+ ```typescript
224
+ function provideIoAssist(config: IoAssistStaticConfig): EnvironmentProviders
225
+ ```
226
+
227
+ Registers:
228
+
229
+ 1. `IO_ASSIST_CONFIG` — the Zod-validated static config
230
+ 2. `IO_ASSIST_DYNAMIC_CONFIG` — `useFactory: () => signal<IoAssistDynamicConfig>({ user: { id: '' } })` (empty seed; overwritten by the `IoAssist` component input)
231
+ 3. `provideBrowserGlobalErrorListeners()` — Angular global error handling
232
+ 4. `provideZonelessChangeDetection()` — zoneless Angular (signal-based)
233
+ 5. `provideStore(appReducers)` — NgRx store with all 8 reducers
234
+ 6. `provideEffects(...)` — 7 effect classes
235
+ 7. `provideStoreDevtools({ maxAge: 25 })` — Redux DevTools integration
236
+ 8. `provideIoConnect(config.connectConfig)` — io.Connect Angular bootstrap
237
+
238
+ ---
239
+
240
+ ## Root Component — IoAssist
241
+
242
+ File: `src/lib/io-assist.component.ts`
243
+
244
+ Selector: `<io-assist>`
245
+
246
+ **Input:**
247
+
248
+ | Input | Type | Description |
249
+ |-------|------|-------------|
250
+ | `config` | `IoAssistDynamicConfig` (required) | Active user identity + optional per-request headers. Validated in `ngOnInit`. |
251
+
252
+ **Lifecycle:**
253
+
254
+ 1. `effect()` — syncs the `config` input into the `IO_ASSIST_DYNAMIC_CONFIG` writable signal whenever the input changes.
255
+ 2. `ngOnInit` — validates `config` via `IoAssistDynamicConfigSchema.parse()` (throws `ZodError` if invalid), writes config to `IO_ASSIST_DYNAMIC_CONFIG`, then registers the io.Connect readiness effect.
256
+ 3. `ngAfterViewInit` — calls `ConsumerAssetLoaderService.loadConsumerAssets()` to inject fonts and Prism.js into the DOM.
257
+
258
+ **Template states:**
259
+
260
+ - **Pending** — shows spinner while io.Connect initializes and core services start
261
+ - **Error** — shows error message with "Refresh" button
262
+ - **Ready** — renders `<chat />` (the full UI tree)
263
+
264
+ **Signals used:**
265
+
266
+ | Signal | Source | Purpose |
267
+ |--------|--------|---------|
268
+ | `isIoReady` | `IOConnectService.isIoConnectReady` | io.Connect platform ready |
269
+ | `isAppCoreServicesStarted` | `AppLifecycleFacade` | ai-web initialized + agents/prompts loaded |
270
+ | `appCoreServicesError` | `AppLifecycleFacade` | Error message if init failed |
271
+ | `isAppCoreServicesPending` | `AppLifecycleFacade` | Currently initializing |
272
+
273
+ ---
274
+
275
+ ## State Management (NgRx Store)
276
+
277
+ ### Store Shape
278
+
279
+ File: `src/lib/shared/store/app.state.ts`
280
+
281
+ ```typescript
282
+ type AppState = {
283
+ threadsStore: ThreadReducerStateType;
284
+ appLifecycleStore: AppLifecycleStateType;
285
+ promptStore: PromptReducerStateType;
286
+ toolStore: ToolReducerStateType;
287
+ messageStore: MessageReducerStateType;
288
+ responseStreamStore: ResponseStreamReducerStateType;
289
+ agentStore: AgentReducerStateType;
290
+ workingContextStore: WorkingContextReducerStateType;
291
+ };
292
+ ```
293
+
294
+ ### Facades
295
+
296
+ Every store slice has a facade that wraps selectors as Angular signals and provides typed dispatch methods. Components inject facades, not the raw store.
297
+
298
+ **AgentFacade** — `store/agent/agent.facade.ts`
299
+
300
+ | Signal | Description |
301
+ |--------|-------------|
302
+ | `availableAgents` | All agents from the server |
303
+ | `selectedAgent` | Currently active agent |
304
+
305
+ | Method | Description |
306
+ |--------|-------------|
307
+ | `dispatchListAvailableAgents()` | Fetch agents from server |
308
+ | `dispatchSelectAgent(agentId)` | Switch active agent |
309
+
310
+ **AppLifecycleFacade** — `store/app-lifecycle/app-lifecycle.facade.ts`
311
+
312
+ | Signal | Description |
313
+ |--------|-------------|
314
+ | `isAppCoreServicesStarted` | Core services initialized successfully |
315
+ | `appCoreServicesLoadingState` | Loading state enum value |
316
+ | `isPendingAppCoreServicesOperation` | Currently initializing |
317
+ | `appCoreServicesErrorMessage` | Error message or null |
318
+
319
+ | Method | Description |
320
+ |--------|-------------|
321
+ | `dispatchInitAppCoreServices()` | Trigger initialization |
322
+
323
+ **MessageFacade** — `store/message/message.facade.ts`
324
+
325
+ | Signal | Description |
326
+ |--------|-------------|
327
+ | `allMessages` | All messages for active thread |
328
+ | `messageLength` | Message count |
329
+ | `lastUserMessage` | Most recent user message |
330
+ | `isGeneratingResponse` | Stream in progress |
331
+ | `isLoadingMessagesFromThread` | Loading thread messages |
332
+ | `toolTraceState` | Tool trace expand/collapse state |
333
+ | `isLastResponseSuccess` | Last response completed without error |
334
+
335
+ | Method | Description |
336
+ |--------|-------------|
337
+ | `dispatchGetResponse(params, threadId, isStream?, agent?)` | Send user message and get agent response |
338
+ | `dispatchReloadResponse(params, threadId, isStream?, agent?)` | Regenerate last response |
339
+ | `dispatchClearMessages()` | Clear all messages |
340
+ | `dispatchFetchMessagesFromThread(thread)` | Load messages from an existing thread |
341
+ | `dispatchAbortResponseGeneration(threadId)` | Cancel active stream |
342
+ | `dispatchToggleToolTrace(stateForMessageId)` | Expand/collapse tool trace |
343
+
344
+ **ThreadFacade** — `store/thread/thread.facade.ts`
345
+
346
+ | Signal | Description |
347
+ |--------|-------------|
348
+ | `allThreads` | All threads for current user |
349
+ | `activeThreadId` | Currently active thread ID |
350
+ | `activeThread` | Currently active thread object |
351
+ | `isFetchingThreads` | Loading threads |
352
+
353
+ | Method | Description |
354
+ |--------|-------------|
355
+ | `dispatchFetchThreads(agentId)` | Fetch all threads for agent + user |
356
+ | `dispatchRenameThread(thread, newTitle)` | Rename a thread |
357
+ | `dispatchDeleteThread(thread)` | Delete a thread |
358
+ | `dispatchChangeActiveThread(threadId)` | Switch active thread |
359
+
360
+ **ResponseStreamFacade** — `store/response-stream/response-stream.facade.ts`
361
+
362
+ | Signal | Description |
363
+ |--------|-------------|
364
+ | `allStreams` | All per-thread stream states |
365
+ | `threadsCurrentlyStreaming` | Thread IDs with active streams |
366
+ | `threadsWithCompletionNotification` | Threads with background completion notifications |
367
+ | `hasAnyCompletionNotification` | Any thread has a background notification |
368
+
369
+ | Method | Description |
370
+ |--------|-------------|
371
+ | `isThreadStreaming(threadId)` | Check if specific thread is streaming |
372
+ | `createIsStreamingSignal(threadIdSignal)` | Reactive signal from thread ID signal |
373
+ | `dispatchStartThreadStream(threadId, userMessage)` | Start tracking a stream |
374
+ | `dispatchCompleteThreadStream(threadId, shouldNotify)` | Mark stream complete |
375
+
376
+ **ToolFacade** — `store/tool/tool.facade.ts`
377
+
378
+ | Signal | Description |
379
+ |--------|-------------|
380
+ | `allTools` | All available tools |
381
+ | `enabledTools` | Currently enabled tools |
382
+
383
+ | Method | Description |
384
+ |--------|-------------|
385
+ | `dispatchFetchTools()` | Fetch all tools from MCP servers |
386
+ | `dispatchToggleTool(tool)` | Enable/disable a tool |
387
+
388
+ **PromptFacade** — `store/prompt/prompt.facade.ts`
389
+
390
+ | Signal | Description |
391
+ |--------|-------------|
392
+ | `allPrompts` | All parsed prompts |
393
+ | `favoritePromptNames` | Names of favorited prompts |
394
+ | `selectedPrompt` | Currently selected prompt |
395
+
396
+ | Method | Description |
397
+ |--------|-------------|
398
+ | `dispatchParsePromptsConfig()` | Parse and normalize configured prompts |
399
+ | `dispatchSelectPrompt(prompt)` | Select a prompt (fills input) |
400
+ | `dispatchToggleFavoritePrompt(name)` | Toggle favorite state |
401
+
402
+ **WorkingContextFacade** — `store/working-context/working-context.facade.ts`
403
+
404
+ | Signal | Description |
405
+ |--------|-------------|
406
+ | `workingContext` | Current working context values |
407
+ | `isWorkingContextEnabled` | Whether working context is configured |
408
+
409
+ | Method | Description |
410
+ |--------|-------------|
411
+ | `dispatchGetWorkingContext()` | Fetch current context |
412
+ | `dispatchUpdateWorkingContext(context)` | Update with new values |
413
+
414
+ ### Effects
415
+
416
+ | Effect Class | Key Operations |
417
+ |-------------|----------------|
418
+ | `AppLifecycleEffects` | Monitors IOIntelWebService init state → dispatches success/failure → triggers agent list + prompt parse |
419
+ | `AgentEffects` | Lists agents from server, selects agent (by config name or first available), fetches threads + tools on selection |
420
+ | `MessageEffects` | Handles `getResponse`/`reloadResponse` → streams AG-UI events into store actions (see [AG-UI Streaming Pipeline](#ag-ui-streaming-pipeline)) |
421
+ | `ThreadEffects` | Fetches threads for agent + user (reads `user.id` from `IO_ASSIST_DYNAMIC_CONFIG`), rename/delete, cleans up MCP app prefs on delete |
422
+ | `ToolEffects` | Fetches all tools from MCP servers, toggles tool enabled state |
423
+ | `PromptEffects` | Parses config `defaultPrompts` into UI-ready prompt objects via `PromptService` |
424
+ | `WorkingContextEffects` | Fetches and subscribes to working context changes via `WorkingContextService` |
425
+
426
+ ---
427
+
428
+ ## Services
429
+
430
+ ### IOConnectService
431
+
432
+ File: `shared/services/io/io.service.ts`
433
+
434
+ Connects to io.Connect platform. Exposes:
435
+ - `isIoConnectReady` — signal from `IOConnectStore`
436
+ - `isDarkMode` — signal synced with io.Connect theme
437
+ - `startIODependentTasks()` — subscribes to theme changes, initializes ai-web factory, dispatches lifecycle init
438
+ - `requestIOModal(config)` — wraps io.Connect modals API
439
+ - `isModalsApiAvailable()` — checks if modals library is loaded
440
+
441
+ ### IOIntelWebService
442
+
443
+ File: `shared/services/io-ai-web/io-ai-web.service.ts`
444
+
445
+ Initializes `@interopio/ai-web` API. Key behavior:
446
+ - `initialize()` — builds `IoAiWeb.WebConfig` from `IO_ASSIST_CONFIG`, calls `IoAiWebFactory`, attaches MCP apps service
447
+ - `buildMcpConfig(mcp)` — injects sampling/elicitation handlers from `SamplingService`/`ElicitationService` (custom or built-in)
448
+ - `constructConfig()` — spreads static `agentServer` fields, and sets `headers` from `IO_ASSIST_DYNAMIC_CONFIG.agentServer.headers` (the sole source of request headers)
449
+ - Exposes signals: `isInitialized`, `isInitializing`, `isError`
450
+ - Exposes API wrappers: `listAgents()`, `listThreads()`, `listTools()`, `getWorkingContext()`, etc.
451
+
452
+ ### AgentService
453
+
454
+ File: `shared/services/agent/agent.service.ts`
455
+
456
+ Builds API call parameters. Key methods:
457
+ - `getResponse(params, isStream, agent)` — calls ai-web stream/generate
458
+ - `reloadResponse(params, isStream, agent)` — regenerates last response
459
+ - `getSamplingResponse(params, skipThread)` — non-streaming generation for sampling flow
460
+ - `buildAgentParams(params, skipThreadManagement)` — constructs memory params with `thread: threadId, resource: user.id`
461
+
462
+ **Critical behavior:**
463
+ - `user.id` is read from `IO_ASSIST_DYNAMIC_CONFIG().user.id` at call time — this scopes all threads to the current user
464
+ - When `skipThreadManagement=true` (sampling), the active thread is NOT inherited to avoid polluting conversation history
465
+ - Empty assistant messages are filtered out before sending (prevents Anthropic empty-content errors)
466
+
467
+ ### AgentManagerService
468
+
469
+ File: `shared/services/agent/agent-manager/agent-manager.service.ts`
470
+
471
+ Lists available agents via `IOIntelWebService.listAgents()`.
472
+
473
+ ### SamplingService
474
+
475
+ File: `shared/services/io-ai-web/sampling/sampling.service.ts`
476
+
477
+ Handles MCP sampling requests:
478
+
479
+ 1. `selectSamplingHandler(mcp)` — returns custom handler if `mcp.clientsConfig.capabilities.sampling.handler` is provided, otherwise returns the built-in handler
480
+ 2. Built-in flow:
481
+ - Rejects requests from background threads (user not on the requesting thread)
482
+ - Shows a confirmation UI:
483
+ - **io.Connect modal** (if `IOModals` library is available) — `noInputsConfirmationDialog` template
484
+ - **Built-in panel overlay** (fallback) — `OverlayService.showPanelOverlay()` with Continue/Cancel buttons
485
+ - On accept: calls `AgentService.getSamplingResponse()`, maps finish reason → MCP stop reason, returns `SamplingSuccessResponse`
486
+ - On decline: returns `SamplingErrorResponse` with rejection message
487
+
488
+ ### ElicitationService
489
+
490
+ File: `shared/services/io-ai-web/elicitation/elicitation.service.ts`
491
+
492
+ Handles MCP elicitation requests:
493
+
494
+ 1. `selectElicitationHandler(mcp)` — returns custom handler if provided, otherwise built-in
495
+ 2. Built-in flow:
496
+ - Validates `_meta.toolName` starts with `io_connect`
497
+ - Rejects background thread requests
498
+ - Shows accept/decline/cancel UI (modal or overlay)
499
+ - Accept returns `{ action: 'accept', content: {} }` (empty content — actual form input is a TODO)
500
+ - Decline returns `{ action: 'decline' }`
501
+ - Cancel returns `{ action: 'cancel' }`
502
+
503
+ ### McpAppsService
504
+
505
+ File: `shared/services/mcp-apps/mcp-apps.service.ts`
506
+
507
+ Wraps `ai-web` MCP Apps API for Angular consumption:
508
+ - `attach(api)` — subscribes to `onAppCreated`, `onRecreateRequested`, `onAppRecreated` events
509
+ - `activeInstances` — signal tracking all live `AppInstance` objects
510
+ - Handles duplicate instance dialog (3-option: replace oldest / replace all / new instance)
511
+ - Feeds app chat messages into the message store via `MessageFacade.dispatchGetResponse()`
512
+ - Thread switch triggers `closeAll()` + re-creation of pending apps for the new thread
513
+
514
+ ### ThreadService
515
+
516
+ File: `shared/services/thread/thread.service.ts`
517
+
518
+ Thread operations:
519
+ - `fetchThreads(agentId, userId)` — lists threads from server
520
+ - `toUIThreads(threads)` — converts backend threads to UI model with relative-time labels
521
+ - `renameThread(thread, newTitle)` — renames via API
522
+ - `deleteThread(thread)` — deletes via API
523
+ - `deleteThreadState(threadId)` — cleans up persisted MCP app preferences for deleted threads
524
+
525
+ ### ToolsService
526
+
527
+ File: `shared/services/tools/tools.service.ts`
528
+
529
+ - `listTools()` — fetches all tools from MCP servers
530
+ - `toggleTool(tool)` — enables/disables a tool
531
+
532
+ ### PromptService
533
+
534
+ File: `shared/services/prompt/prompt.service.ts`
535
+
536
+ - Takes `defaultPrompts` from config and maps them to UI-ready objects
537
+ - Validates and normalizes icon resources (falls back to default icon)
538
+ - Icon sanitization: SVG strings have `fill`, `width`, `height` stripped; hardcoded colors → `currentColor`
539
+
540
+ ### WorkingContextService
541
+
542
+ File: `shared/services/working-context/working-context.service.ts`
543
+
544
+ - `getWorkingContext()` — fetches current context values
545
+ - `subscribeToWorkingContext()` — subscribes to `onChanged` callback
546
+ - `isEnabled()` — checks if working context is configured
547
+
548
+ ### OverlayService
549
+
550
+ File: `shared/services/overlay/overlay.service.ts`
551
+
552
+ CDK-based overlay system:
553
+ - `showPanelOverlay(config)` — creates centered overlay with backdrop, attaches `AppPanelComponent`
554
+ - Supports HTML content (string) or injected Angular component
555
+ - Maintains an overlay stack — backdrop click closes only the topmost overlay
556
+ - `closeCurrentOverlay()` — disposes the top overlay
557
+ - Also manages thread history panel visibility signal
558
+
559
+ ### ConsumerAssetLoaderService
560
+
561
+ File: `shared/services/consumer-asset-loader/consumer-asset-loader.service.ts`
562
+
563
+ Injects external assets into the consumer app's DOM at runtime:
564
+ - **Google Inter font** — `<link>` to Google Fonts CDN
565
+ - **Prism.js CSS** — syntax highlighting styles
566
+ - **Prism.js JS** — syntax highlighting runtime
567
+
568
+ Called in `IoAssist.ngAfterViewInit()` after the DOM is ready.
569
+
570
+ ### ResponsiveUIService
571
+
572
+ File: `shared/services/responsive-ui/responsive-ui.service.ts`
573
+
574
+ Tailwind-style breakpoint tracking:
575
+ - Uses `ResizeObserver` on the root component element
576
+ - Exposes signal-based breakpoint state matching Tailwind thresholds
577
+ - Components use it to adapt layout (e.g., compact header on small viewports)
578
+
579
+ ### LoggerService
580
+
581
+ File: `shared/services/logger/logger.service.ts`
582
+
583
+ - Wraps io.Connect logger when available
584
+ - Falls back to `console` methods
585
+ - Namespaced: `logger.get('SamplingService').info(...)`
586
+
587
+ ### ComponentEffectManagerService
588
+
589
+ File: `shared/services/component-effect-manager/component-effect-manager.service.ts`
590
+
591
+ - Named Angular `effect()` registration
592
+ - Tracks effects by name to prevent duplicates
593
+ - Auto-cleanup on service destroy
594
+
595
+ ### SubscriptionCleanupService
596
+
597
+ File: `shared/services/subscription-cleanup/subscription-cleanup.service.ts`
598
+
599
+ - Named subscription registry (`add(name, subscription)`)
600
+ - `destroy()` unsubscribes all
601
+ - Used in components/services that manage RxJS subscriptions manually
602
+
603
+ ### CopyToClipboardService
604
+
605
+ File: `shared/services/copy-to-clipboard/copy-to-clipboard.service.ts`
606
+
607
+ - Uses `navigator.clipboard.writeText()` with `<textarea>` fallback for older browsers
608
+
609
+ ---
610
+
611
+ ## Components
612
+
613
+ ### Chat Layout
614
+
615
+ File: `components/chat/chat.component.ts`
616
+
617
+ Main container component. Renders:
618
+ - Thread history panel (toggle sidebar)
619
+ - Header
620
+ - Message area
621
+ - Favorite prompts (shown on home screen when no messages)
622
+ - Input area
623
+ - AI disclaimer footer
624
+
625
+ ### Header
626
+
627
+ File: `components/header/header.component.ts`
628
+
629
+ - Thread history toggle button (with notification dot for background completions)
630
+ - Home button (starts new thread)
631
+ - Working context button (opens inspector panel)
632
+
633
+ ### Input Area
634
+
635
+ File: `components/input-area/input-area.component.ts`
636
+
637
+ - Auto-resizing textarea
638
+ - `Enter` sends, `Shift+Enter` for newlines
639
+ - `Shift+Up/Down` cycles through user's message history
640
+ - Action bar with tools panel and prompt library buttons
641
+ - Send button with loading state during streaming
642
+
643
+ ### Messages
644
+
645
+ | Component | Purpose |
646
+ |-----------|---------|
647
+ | `MessageAreaComponent` | Scrollable message list with auto-scroll |
648
+ | `UserMessageComponent` | User message bubble |
649
+ | `AssistantMessageComponent` | Markdown-rendered assistant response |
650
+ | `ToolTraceMessageComponent` | Expandable tool call trace (name + status) |
651
+ | `ToolMessageComponent` | Tool call args + result display |
652
+ | `MessageFooterComponent` | Copy + regenerate buttons |
653
+ | `McpAppResourceComponent` | Mounts MCP App iframe element |
654
+
655
+ ### Threads
656
+
657
+ | Component | Purpose |
658
+ |-----------|---------|
659
+ | `ThreadHistoryComponent` | Sidebar panel with thread list |
660
+ | `ThreadHistoryListComponent` | Grouped list with relative-time dividers |
661
+ | `ThreadHistoryListItemComponent` | Individual thread with rename/delete actions |
662
+
663
+ ### Tools
664
+
665
+ | Component | Purpose |
666
+ |-----------|---------|
667
+ | `ToolListComponent` | Searchable list of all available tools |
668
+ | `ToolListItemComponent` | Individual tool with enable/disable toggle |
669
+ | `ToolInfoComponent` | Tool metadata tooltip |
670
+
671
+ ### Prompts
672
+
673
+ | Component | Purpose |
674
+ |-----------|---------|
675
+ | `PromptListComponent` | Categorized prompt browser with search |
676
+ | `PromptListItemComponent` | Individual prompt with favorite toggle |
677
+ | `FavoritePromptListComponent` | Home screen favorites grid |
678
+
679
+ ### Working Context Panel
680
+
681
+ File: `components/working-context-panel/working-context-panel.component.ts`
682
+
683
+ - Info callout explaining what working context is
684
+ - JSON-formatted display of current context values (rendered as markdown)
685
+
686
+ ### MCP App Resource
687
+
688
+ File: `components/messages/mcp-app-resource/mcp-app-resource.component.ts`
689
+
690
+ - Receives an `AppInstance` from `McpAppsService`
691
+ - Mounts `app.element` (the sandbox proxy iframe) into the DOM
692
+ - Only renders for inline display mode
693
+
694
+ ### Shared UI Primitives
695
+
696
+ | Component | File |
697
+ |-----------|------|
698
+ | `AppIconComponent` | `shared/components/app-icon/` |
699
+ | `AppButtonComponent` | `shared/components/app-button/` |
700
+ | `AppToggleComponent` | `shared/components/app-toggle/` |
701
+ | `AppInputComponent` | `shared/components/app-input/` |
702
+ | `AppTooltipComponent` | `shared/components/app-tooltip/` |
703
+ | `AppPanelComponent` | `shared/components/app-panel/` |
704
+ | `AppSpinnerComponent` | `shared/components/app-spinner/` |
705
+ | `AppMdFormatterComponent` | `shared/components/app-md-formatter/` |
706
+ | `AppCopyToClipboardButtonComponent` | `shared/components/app-copy-to-clipboard-button/` |
707
+
708
+ ---
709
+
710
+ ## Directives
711
+
712
+ ### Animation Effect Directive
713
+
714
+ File: `shared/directives/animation/`
715
+
716
+ Applies CSS animations to host elements with:
717
+ - Configurable animation type and duration
718
+ - Start/complete event emitters
719
+ - Used for message entrance animations
720
+
721
+ ### Accent Gradient Border Directive
722
+
723
+ File: `shared/directives/accent-border/`
724
+
725
+ Applies a gradient border effect to elements. Used for visual emphasis on active/streaming states.
726
+
727
+ ---
728
+
729
+ ## AG-UI Streaming Pipeline
730
+
731
+ File: `store/message/message.effects.ts`
732
+
733
+ The core streaming loop. When `getResponse` or `reloadResponse` is dispatched:
734
+
735
+ 1. **Start** — dispatches `startThreadStream(threadId, userMessage)` to track stream state
736
+ 2. **Stream** — `AgentService.getResponse()` returns an AG-UI `StreamResponse` observable
737
+ 3. **Event processing** — each AG-UI event is mapped to store actions:
738
+
739
+ | AG-UI Event | Store Action |
740
+ |-------------|-------------|
741
+ | `TEXT_MESSAGE_START` | `addAssistantMessage` (creates placeholder) |
742
+ | `TEXT_MESSAGE_CONTENT` | `addAssistantMessage` (updates content) or `updateStreamContent` (background thread) |
743
+ | `TOOL_CALL_START` | `addToolCallMessage` |
744
+ | `TOOL_CALL_ARGS` | Accumulated in local `Map<toolCallId, args>` |
745
+ | `TOOL_CALL_END` | Parses accumulated args JSON → `updateToolCallResult` |
746
+ | `TOOL_CALL_RESULT` | Parses result → `updateToolCallResult` |
747
+
748
+ 4. **Complete** — dispatches `completeThreadStream(threadId, shouldNotify)`. Sets `shouldNotify=true` if the user switched to a different thread (background completion).
749
+ 5. **Error** — dispatches `failThreadStream` + logs error
750
+ 6. **Abort** — dispatches `abortThreadStream`
751
+
752
+ **Background thread handling:** If the user switches threads while a stream is active:
753
+ - Content deltas go to `updateStreamContent` (stored in response stream store, not visible UI)
754
+ - On completion, `shouldNotify=true` triggers a notification dot on the thread history button
755
+ - When the user switches back, the accumulated content is replayed
756
+
757
+ ---
758
+
759
+ ## Response Stream Store
760
+
761
+ File: `store/response-stream/`
762
+
763
+ Per-thread stream tracking:
764
+
765
+ ```typescript
766
+ type ThreadStreamState = {
767
+ threadId: string;
768
+ status: 'idle' | 'streaming' | 'completed' | 'error' | 'aborted';
769
+ accumulatedContent: string;
770
+ currentMessageId: string | null;
771
+ hasCompletionNotification: boolean;
772
+ errorMessage?: string;
773
+ userMessage: UIMessage | null;
774
+ toolMessages: UIToolMessage[];
775
+ };
776
+ ```
777
+
778
+ | Action | Behavior |
779
+ |--------|----------|
780
+ | `startThreadStream` | Creates/overwrites stream entry with status `streaming` |
781
+ | `updateStreamContent` | Updates accumulated content (only if `streaming`) |
782
+ | `addStreamToolMessage` | Merges tool message by ID (update existing or append) |
783
+ | `completeThreadStream` | Sets terminal status + notification flag |
784
+ | `failThreadStream` | Sets `error` status + error message |
785
+ | `abortThreadStream` | Sets `aborted` status |
786
+ | `clearCompletionNotification` | Clears notification flag (e.g., when user views the thread) |
787
+ | `untrackThreadStreamState` | Removes stream entry (only if in terminal state) |
788
+
789
+ ---
790
+
791
+ ## Sampling & Elicitation Flow
792
+
793
+ ### Sampling
794
+
795
+ ```
796
+ MCP Server → SamplingRequest → SamplingService.handleSamplingRequest()
797
+ ├── Background thread? → SamplingErrorResponse (rejected)
798
+ ├── io.Connect modal available? → noInputsConfirmationDialog
799
+ │ ├── "Continue" → AgentService.getSamplingResponse() → SamplingSuccessResponse
800
+ │ └── "Cancel" → SamplingErrorResponse
801
+ └── Fallback → OverlayService.showPanelOverlay()
802
+ ├── "Continue" → AgentService.getSamplingResponse() → SamplingSuccessResponse
803
+ └── "Cancel" → SamplingErrorResponse
804
+ ```
805
+
806
+ **Stop reason mapping:** Agent `finishReason` is mapped to MCP stop reasons:
807
+ - `'stop'` → `'endTurn'`
808
+ - `'length'` → `'maxTokens'`
809
+ - other → `'endTurn'` (default)
810
+
811
+ ### Elicitation
812
+
813
+ ```
814
+ MCP Server → ElicitationRequest → ElicitationService.handleElicitationRequest()
815
+ ├── Invalid _meta.toolName? → ElicitationRejectionResponse
816
+ ├── Background thread? → ElicitationCancelResponse
817
+ ├── io.Connect modal available? → Dialog with Accept/Decline
818
+ │ ├── "Accept" → ElicitationConfirmationResponse { action: 'accept', content: {} }
819
+ │ ├── "Decline" → ElicitationRejectionResponse { action: 'decline' }
820
+ │ └── Close → ElicitationCancelResponse { action: 'cancel' }
821
+ └── Fallback → OverlayService panel
822
+ ```
823
+
824
+ ---
825
+
826
+ ## MCP Apps Integration
827
+
828
+ ### How it works
829
+
830
+ 1. **Config** — consumer provides `mcp.mcpApps` (sandboxProxyUrl + displayMode) and `extensions['io.modelcontextprotocol/ui']` in capabilities
831
+ 2. **Init** — `IOIntelWebService.initialize()` passes config to ai-web which starts the MCP Apps controller
832
+ 3. **Attach** — `McpAppsService.attach(api)` subscribes to app lifecycle events
833
+ 4. **Stream interception** — during AG-UI streaming, ai-web's stream interceptor watches for tools with `_meta.ui.resourceUri` and auto-creates app instances
834
+ 5. **Rendering** — `McpAppResourceComponent` mounts the `AppInstance.element` (sandbox proxy iframe) for inline mode; workspace mode opens an io.Connect workspace window
835
+ 6. **Communication** — apps use `postMessage` (inline) or io.Connect interop (workspace) to exchange data with the host
836
+ 7. **Thread lifecycle** — on thread switch, `McpAppsService` closes all apps and recreates pending ones for the new thread
837
+
838
+ ### Duplicate handling
839
+
840
+ When a tool is called while an instance already exists (workspace mode), `McpAppsService` shows a 3-option dialog:
841
+ - Uses io.Connect modal if available
842
+ - Falls back to overlay panel
843
+ - Options: Replace Oldest / Replace All / New Instance
844
+
845
+ ---
846
+
847
+ ## Styling
848
+
849
+ - **Tailwind CSS** — main styling approach, built from `src/styles.css` via PostCSS
850
+ - **Exported stylesheet** — `@interopio/io-assist-ng/styles.css` must be imported by the consumer
851
+ - **Prism.js** — injected at runtime for code syntax highlighting
852
+ - **Inter font** — injected at runtime from Google Fonts CDN
853
+ - **Theme sync** — CSS variables adapt to io.Connect dark/light theme via `IOConnectService`
854
+
855
+ ---
856
+
857
+ ## Testing
858
+
859
+ - Test files are colocated with source (`*.spec.ts` alongside `*.ts`)
860
+ - Framework: Karma + Jasmine
861
+ - Run: `npm run test:disable` (currently disabled in CI)
862
+ - Coverage:
863
+ - Most component/service specs are smoke tests (`should create`)
864
+ - `icon.utils.spec.ts` has extensive behavior/security tests for SVG sanitization
865
+ - Store reducers/selectors/actions have basic coverage