19 KiB
A2UI Implementation Plan — Full Rework
Credits
- Plan: Claude Opus 4.6 (2026-02-26)
- A2UI specification: Google (https://a2ui.org, https://github.com/google/A2UI, Apache 2.0)
- A2UI SDK types: @a2ui-sdk community packages (https://a2ui-sdk.js.org/)
Context
The current feature-agui branch claims to implement "A2UI" but does not implement the actual Google A2UI protocol (https://a2ui.org, https://github.com/google/A2UI). Instead, it has a custom protocol envelope system where the LLM is instructed to output a monolithic JSON blob, which is then validated with hyper-strict Zod schemas. This approach fundamentally doesn't work:
- The LLM can't reliably produce the exact JSON schema required
- Raw JSON streams to the chat UI before being replaced (visible flicker)
- Strict validation rejects almost everything, so UI elements never render
- A failed retry doubles API cost and latency
- The 11 files in
src/main/agentic/are elaborate infrastructure built on a broken premise
Goal: Replace the broken custom protocol with a proper implementation of Google's A2UI v0.9 protocol — streaming, flat-component, data-binding — so the assistant can progressively render rich interactive UI in both chat surfaces.
Architecture Overview
Real A2UI (v0.9) Key Concepts
- JSONL streaming: Server sends a stream of JSON messages, each a complete A2UI message
- 4 message types:
createSurface,updateComponents,updateDataModel,deleteSurface - Flat component model: Components are a flat list with ID references (adjacency list), not nested trees
- Data binding: Components bind to a data model via JSON Pointer paths (RFC 6901)
- Component catalog: Client declares which components it supports; agent can only use those
- Actions: User interactions dispatch events back to the server
How This Maps to Our Electron App
User types message
↓
Renderer → IPC → Main Process (OpenCodeManager)
↓
Main Process calls LLM API (text + tool calls)
↓
LLM responds with text + calls UI tools (render_chart, render_form, etc.)
↓
Main Process A2UI Generator creates A2UI messages from tool results
↓
A2UI messages sent to renderer via IPC events (one event per message)
↓
Renderer A2UI Engine processes messages:
- createSurface → initialize surface state
- updateComponents → add/update components in flat buffer
- updateDataModel → update data model store
↓
Renderer A2UI Renderer resolves component tree from flat buffer
↓
React components render (reusing existing widget implementations)
↓
User clicks button → action event → IPC → Main Process → feed back to LLM
Key insight: IPC IS the transport. We don't need JSONL parsing libraries or SSE. The main process generates A2UI message objects and sends them individually via webContents.send(). The renderer receives them as JavaScript objects via ipcRenderer.on().
Tool-Driven UI Generation (Not Free-Text JSON)
Instead of asking the LLM to produce A2UI JSON as free text (unreliable), we add UI-rendering tools that the LLM calls via the existing tool-use mechanism:
| Tool | Purpose | A2UI Output |
|---|---|---|
render_chart |
Show bar/stacked-bar/line/pie chart | updateComponents with chart component |
render_table |
Show data table | updateComponents with table rows |
render_form |
Show input form | updateComponents with form fields |
render_card |
Show info card | updateComponents with card component |
render_metric |
Show key-value metric | updateComponents with metric display |
render_list |
Show item list | updateComponents with list items |
render_tabs |
Show tabbed content | updateComponents with tabs |
The LLM calls these tools with structured parameters (validated by the API provider), and our code translates tool results into proper A2UI messages. This is reliable because tool call schemas ARE validated by Claude/GPT APIs.
Dependencies
Install
| Package | Purpose | Why |
|---|---|---|
@a2ui-sdk/types |
TypeScript types for A2UI v0.9 messages | Type-safe message handling without full renderer |
Evaluate (may not need)
| Package | Purpose | Decision Point |
|---|---|---|
@a2ui-sdk/react |
React renderer + hooks | If it supports component overrides without forcing Tailwind/shadcn. Our app uses VSCode theme variables. If it forces Tailwind, skip and build custom renderer using existing AssistantPanelControls widgets. |
Do NOT install
| Package | Why Not |
|---|---|
ndjson / JSONL parsers |
IPC is the transport — no JSONL wire format needed |
| CopilotKit / AG-UI | Full-stack framework, too heavyweight for integration into existing Electron app |
Files to DELETE (broken protocol machinery)
All of src/main/agentic/ — 11 files that implement the broken custom protocol:
src/main/agentic/protocol/types.ts→ replaced by@a2ui-sdk/typessrc/main/agentic/protocol/errors.ts→ no longer neededsrc/main/agentic/protocol/validator.ts→ replaced by schema validation in A2UI enginesrc/main/agentic/protocol/uiSchema.ts→ duplicate of renderer schema, deletedsrc/main/agentic/protocol/uiSpecParser.ts→ replaced by A2UI message parsersrc/main/agentic/protocol/responseBuilder.ts→ replaced by A2UI generatorsrc/main/agentic/capabilities/registry.ts→ replaced by A2UI client capabilitiessrc/main/agentic/workflow/turnStateMachine.ts→ not neededsrc/main/agentic/workflow/checkpointStore.ts→ not neededsrc/main/agentic/policy/actionPolicy.ts→ action policies move into A2UI action handlersrc/main/agentic/observability/protocolTelemetry.ts→ not needed initially
Also delete these test files for removed code:
tests/engine/agentic/protocol/responseBuilder.test.tstests/engine/OpenCodeManager.protocol.test.ts- Any other tests in
tests/that test the deleted agentic/ modules
Files to MODIFY
src/main/engine/OpenCodeManager.ts
- Remove
protocolBoundaryInstructions(line 152-159) - Remove protocol retry mechanism (lines 408-438)
- Remove
ProtocolResponseBuilderusage - Remove
CapabilityRegistryService,AgentTurnStateMachine,WorkflowCheckpointStore,protocolTelemetryusage - Remove the
protocolVersion/envelopefields fromSendMessageResult - Add UI-rendering tools to
getToolDefinitions():render_chart,render_table,render_form,render_card,render_metric,render_list,render_tabs - Add
executeToolhandlers that convert tool args into A2UI messages and emit them via IPC - Keep text streaming (
onDelta) as-is for conversational responses - Add new callback:
onA2UIMessagefor streaming A2UI messages to renderer
src/main/ipc/chatHandlers.ts
- Add new IPC event:
a2ui-messagefor streaming A2UI messages - Add new IPC handler:
a2ui-actionfor receiving user actions from renderer - Wire
onA2UIMessagecallback towebContents.send('a2ui-message', ...)
src/main/preload.ts
- Add
onA2UIMessage(callback)listener towindow.electronAPI.chat - Add
dispatchA2UIAction(surfaceId, action)method - Add to type definitions
src/renderer/components/ChatPanel/ChatPanel.tsx
- Remove protocol envelope handling (
result.envelope,buildActionPoliciesFromEnvelope,toClarificationElements) - Subscribe to
onA2UIMessageevents - Feed A2UI messages to new A2UI surface state manager
- Render A2UI surface alongside chat transcript
- Handle A2UI actions (dispatch back via IPC)
src/renderer/components/AssistantSidebar/AssistantSidebar.tsx
- Same changes as ChatPanel — A2UI surface rendering, remove protocol envelope
src/main/engine/ChatEngine.ts
- Remove
getBuiltInSystemPrompt()references to "AGUI payload" and protocol envelope - Update system prompt to describe available UI tools instead
- Keep the rest (conversation CRUD, message persistence) as-is
src/renderer/navigation/assistantPanelSpec.ts
- Keep the Zod schemas and
AssistantPanelElementtypes — these become our component catalog definitions - Remove
extractAssistantResponseContent()andextractAssistantPanelSpec()(free-text JSON parsing) — no longer needed - Export schemas for use by A2UI component registry
src/renderer/components/AssistantPanelControls/AssistantPanelControls.tsx
- Refactor into individual component files that can be registered in an A2UI catalog
- Each component becomes a standalone renderer:
A2UIText,A2UIChart,A2UIForm, etc. - These map A2UI flat components (with data binding) to existing widget rendering
Files to CREATE
src/main/a2ui/types.ts
A2UI message types for our app. If @a2ui-sdk/types provides adequate types, re-export from there. Otherwise define:
A2UIServerMessage(union ofCreateSurface | UpdateComponents | UpdateDataModel | DeleteSurface)A2UIClientAction(action events from user interactions)A2UIComponent(flat component with ID + type + properties)BDSCatalogId— our custom catalog identifier
src/main/a2ui/generator.ts
Converts tool call results into A2UI messages:
createChartSurface(toolArgs)→[createSurface, updateComponents, updateDataModel]createFormSurface(toolArgs)→[createSurface, updateComponents]createTableSurface(toolArgs)→[createSurface, updateComponents, updateDataModel]- etc. Each function returns an array of A2UI messages to stream to the renderer.
src/main/a2ui/catalog.ts
Defines the bDS component catalog — what component types we support:
- Maps A2UI basic catalog components to our implementations
- Adds custom components (chart, metric) not in A2UI basic catalog
- This is sent as client capabilities to inform the LLM what's available
src/renderer/a2ui/A2UISurfaceManager.ts
Client-side state manager for A2UI surfaces:
- Maintains per-surface component buffer (Map<ComponentId, Component>)
- Maintains per-surface data model (JSON object)
- Processes incoming A2UI messages:
createSurface→ initialize new surfaceupdateComponents→ merge components into bufferupdateDataModel→ update data at JSON Pointer pathdeleteSurface→ clean up
- Resolves flat component list into tree (using
childrenID references) - Resolves data bindings (JSON Pointer → value)
- Emits render-ready component tree
src/renderer/a2ui/useA2UISurface.ts
React hook that wraps A2UISurfaceManager:
- Subscribes to
onA2UIMessageIPC events - Feeds messages into surface manager
- Returns render-ready component tree + dispatch function for actions
- Handles progressive rendering (re-render as components arrive)
src/renderer/a2ui/A2UIRenderer.tsx
React component that renders an A2UI surface:
- Takes component tree from
useA2UISurface - Maps each A2UI component type to a React component (from our catalog)
- Handles data binding for input components
- Dispatches actions on user interaction
src/renderer/a2ui/components/ (directory)
Individual component renderers, refactored from AssistantPanelControls:
A2UIText.tsx— renders Text (with Markdown support)A2UIButton.tsx— renders Button with actionA2UICard.tsx— renders Card with title/body/actionsA2UIChart.tsx— renders Chart (custom, not in A2UI basic catalog)A2UIForm.tsx— renders form with fieldsA2UITable.tsx— renders data tableA2UITabs.tsx— renders tabbed interfaceA2UITextField.tsx— renders text input with data bindingA2UICheckBox.tsx— renders checkbox with data bindingA2UIDateTimeInput.tsx— renders date pickerA2UIImage.tsx— renders image with captionA2UIMetric.tsx— renders metric display (custom)A2UIList.tsx— renders item listA2UIRow.tsx/A2UIColumn.tsx— layout containers
tests/a2ui/ (directory)
generator.test.ts— test A2UI message generation from tool callssurfaceManager.test.ts— test surface state management, component tree resolution, data bindingcatalog.test.ts— test component catalog registration
Implementation Phases
Phase 1: Foundation — A2UI Types, Surface Manager, IPC Transport
Goal: Get A2UI messages flowing from main process to renderer and being processed correctly.
- Install
@a2ui-sdk/types(or define our own types if the package doesn't cover v0.9 well) - Create
src/main/a2ui/types.tswith message types - Create
src/renderer/a2ui/A2UISurfaceManager.ts— process messages, maintain state, resolve tree - Write tests for surface manager (TDD: red → green → refactor)
- Add
a2ui-messageIPC event tochatHandlers.tsand preload - Add
a2ui-actionIPC handler for action dispatch - Delete
src/main/agentic/directory and all its tests
Phase 2: A2UI Generator — Tool-Driven UI Creation
Goal: LLM can trigger rich UI by calling tools.
- Create
src/main/a2ui/generator.ts— converts tool args to A2UI messages - Create
src/main/a2ui/catalog.ts— defines our component catalog - Add UI-rendering tools to
OpenCodeManager.getToolDefinitions():render_chart({ chartType, title, series })— chartType includesbar,stacked-bar,line,pierender_table({ title, columns, rows })render_form({ title, fields, submitAction })render_card({ title, body, subtitle, actions })render_metric({ label, value })render_list({ title, items })render_tabs({ tabs: [{ label, content }] })
- Add
executeToolhandlers that call generator and emit A2UI messages viaonA2UIMessagecallback - Write tests for generator (TDD)
- Remove
protocolBoundaryInstructions, protocol retry, envelope building fromOpenCodeManager - Update
SendMessageResult— removeenvelope/protocolVersion/traceId
Phase 3: A2UI Renderer — React Component Catalog
Goal: A2UI surfaces render as interactive UI in the chat.
- Refactor
AssistantPanelControlsinto individual component files undersrc/renderer/a2ui/components/ - Create
src/renderer/a2ui/A2UIRenderer.tsx— maps component types to React components - Create
src/renderer/a2ui/useA2UISurface.ts— React hook for surface state - Integrate into
ChatPanel.tsx:- Subscribe to
onA2UIMessage - Render
A2UIRendererfor each active surface - Handle actions
- Subscribe to
- Integrate into
AssistantSidebar.tsx(same pattern) - Remove old protocol envelope handling from both components
- Write component tests
Phase 4: System Prompt and LLM Integration
Goal: LLM knows about and uses UI tools effectively.
- Update
ChatEngine.getBuiltInSystemPrompt():- Remove all "AGUI payload" / "protocol envelope" instructions
- Add descriptions of UI tools and when to use them
- Include examples: "When showing statistics, use render_chart. When showing a list of posts, use render_table."
- Update system prompt to describe the component catalog available
- Test end-to-end: user asks for a chart → LLM calls
render_chart→ A2UI messages → UI renders
Phase 5: Actions, Data Binding, and Polish
Goal: Full interactivity — forms submit, buttons trigger actions, data flows both ways.
- Implement action dispatch: renderer → IPC → main process → feed back to LLM as tool result
- Implement two-way data binding for form inputs:
- User edits input → local data model updates
- Submit action includes current data model values
- Add action confirmation policies (keep the existing silent/confirm/danger concept)
- Handle surface lifecycle (delete surfaces when conversation changes)
- Clean up: remove unused imports, dead code, duplicate schemas
- Update
API.mdand Python API bindings if affected
Phase 6: Cleanup and Tests
Goal: Zero failing tests, clean build, no dead code.
- Delete all files listed in "Files to DELETE"
- Remove all imports of deleted modules across the codebase
- Run full test suite, fix all failures
- Run
npm run build, fix all build errors - Update
protocolActionPolicies.tsandprotocolNeedsInput.ts— either adapt for A2UI actions or delete if superseded - Delete
src/renderer/python/pythonApiContractV1.tschanges if they reference the old protocol - Final review: no unused code, no commented-out code
Component Catalog Mapping: Existing Widgets → A2UI Components
| Existing Widget | A2UI Component | Notes |
|---|---|---|
text |
Text |
Direct mapping, add Markdown support |
metric |
Custom Metric |
Not in A2UI basic catalog — register as custom component |
list |
List |
A2UI has List container |
table |
Custom Table |
Not in A2UI basic catalog — register as custom |
action |
Button |
A2UI uses Button with action events |
chart |
Custom Chart |
Not in A2UI basic catalog — register as custom |
input |
TextField / CheckBox / DateTimeInput / ChoicePicker |
A2UI splits by type |
form |
Column + form fields + Button |
A2UI doesn't have a Form primitive — compose from layout + inputs |
card |
Card |
Direct mapping |
image |
Image |
Direct mapping |
tabs |
Tabs |
Direct mapping |
datePicker |
DateTimeInput |
A2UI equivalent |
(new) Row |
Row |
Layout container |
(new) Column |
Column |
Layout container |
(new) Divider |
Divider |
Visual separator |
Verification
After each phase, verify:
- Tests pass:
npm test— zero failures - Build succeeds:
npm run build— no errors - Manual test: Send chat messages, verify:
- Text responses render normally in chat
- "Show me post statistics as a chart" → LLM calls
render_chart→ chart renders - "List my recent posts in a table" → LLM calls
render_table→ table renders - Forms render with inputs, submit works
- Cards with action buttons work
- Tab navigation works
- Both ChatPanel and AssistantSidebar work
- No regression: Existing tool calls (search_posts, read_post, etc.) still work
- No raw JSON visible: Users never see protocol JSON in the chat
Risk Mitigation
| Risk | Mitigation |
|---|---|
@a2ui-sdk/types doesn't cover v0.9 well |
Define our own types — A2UI messages are simple JSON |
| LLM doesn't call UI tools reliably | Good tool descriptions + examples in system prompt; text fallback always works |
| Performance: many small IPC messages | Batch updateComponents messages; A2UI supports sending multiple components per message |
| Breaking existing functionality | Phase 1 deletes old code; each subsequent phase adds new functionality. Keep existing tool calls (search_posts etc.) unchanged. |
| A2UI spec changes (v0.9 is draft) | Our implementation is a subset; the flat component + data binding model is stable |