feat: add MCP apps sidebar to task threads#2220
Conversation
Add a collapsible right-side sidebar in the task conversation view that lists every MCP-app (UI-rendering) tool call in the thread. Clicking an entry scrolls the chat to that exact item using the existing VirtualizedList.scrollToIndex mechanism. A toggle button lives in the task header next to ExternalAppsOpener. A single global tRPC subscription to mcpApps.onDiscoveryComplete keeps a Set of tool keys with registered UI, so the sidebar can filter to true "apps" without firing N hasUiForTool queries. Generated-By: PostHog Code Task-Id: 4204eb27-ab56-4c84-b2c2-f832b4a34f06
Replace the shortcut-only sidebar rows with cards that render the actual MCP App iframe (McpAppHost) for each entry. Each card has a small header with the tool title, server/tool name, status dot, and a jump-to button that still scrolls the chat to the source tool call. - ConversationView now resolves the merged ToolCall from turnContext.toolCalls (falling back to the update itself) so the host receives rawInput/rawOutput. - Default sidebar width bumped to 380; resize range widened to 280-720. - Drop the unused inputPreview field and helper. Each rendered app gets its own iframe + bridge, so the chat keeps its inline render alongside the sidebar copy. Generated-By: PostHog Code Task-Id: 4204eb27-ab56-4c84-b2c2-f832b4a34f06
Default the sidebar to 50% of the conversation pane and persist the chosen size as a ratio (0.2-0.8) instead of an absolute pixel width. The sidebar observes its parent flex container and recomputes pixel width on resize, so the split stays proportional when the window resizes. Resize-drag updates the ratio rather than the raw pixels. Storage key bumped to mcp-apps-sidebar-storage-v2 to invalidate stale pixel-based state from the previous version. Generated-By: PostHog Code Task-Id: 4204eb27-ab56-4c84-b2c2-f832b4a34f06
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/code/src/renderer/features/sessions/components/McpAppsSidebar.tsx:202-209
**Duplicate `McpAppHost` instances for every MCP app call**
Each MCP app tool call already has a live `McpAppHost` mounted in the main conversation via `keepMounted`. Rendering another `McpAppHost` per entry in the sidebar means two active iframes and two independent `useAppBridge` connections exist simultaneously for the same call. Because `onToolResult` and `onToolCancelled` subscriptions in `McpAppHost` are keyed only by `toolKey` (not `toolCallId`), both instances receive every tool result/cancellation event and independently forward it to their respective iframes via `sendWhenReady`. This doubles IPC traffic, doubles bridge initialisation overhead, and means the MCP app receives each result twice — once in the conversation iframe and once in the sidebar iframe.
### Issue 2 of 2
apps/code/src/renderer/features/sessions/components/McpAppsSidebar.tsx:89-101
Add a cleanup reset so `isResizing` returns to `false` when the component unmounts mid-drag.
```suggestion
const onMouseUp = () => {
setIsResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
setIsResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
```
Reviews (1): Last reviewed commit: "feat: size MCP apps sidebar as a ratio o..." | Re-trigger Greptile |
| <Box className="p-2"> | ||
| <McpAppHost | ||
| toolCall={entry.toolCall} | ||
| mcpToolName={entry.fullToolName} | ||
| serverName={entry.serverName} | ||
| toolName={entry.toolName} | ||
| /> | ||
| </Box> |
There was a problem hiding this comment.
Duplicate
McpAppHost instances for every MCP app call
Each MCP app tool call already has a live McpAppHost mounted in the main conversation via keepMounted. Rendering another McpAppHost per entry in the sidebar means two active iframes and two independent useAppBridge connections exist simultaneously for the same call. Because onToolResult and onToolCancelled subscriptions in McpAppHost are keyed only by toolKey (not toolCallId), both instances receive every tool result/cancellation event and independently forward it to their respective iframes via sendWhenReady. This doubles IPC traffic, doubles bridge initialisation overhead, and means the MCP app receives each result twice — once in the conversation iframe and once in the sidebar iframe.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/sessions/components/McpAppsSidebar.tsx
Line: 202-209
Comment:
**Duplicate `McpAppHost` instances for every MCP app call**
Each MCP app tool call already has a live `McpAppHost` mounted in the main conversation via `keepMounted`. Rendering another `McpAppHost` per entry in the sidebar means two active iframes and two independent `useAppBridge` connections exist simultaneously for the same call. Because `onToolResult` and `onToolCancelled` subscriptions in `McpAppHost` are keyed only by `toolKey` (not `toolCallId`), both instances receive every tool result/cancellation event and independently forward it to their respective iframes via `sendWhenReady`. This doubles IPC traffic, doubles bridge initialisation overhead, and means the MCP app receives each result twice — once in the conversation iframe and once in the sidebar iframe.
How can I resolve this? If you propose a fix, please make it concise.| const onMouseUp = () => { | ||
| setIsResizing(false); | ||
| document.body.style.cursor = ""; | ||
| document.body.style.userSelect = ""; | ||
| }; | ||
| document.body.style.cursor = "col-resize"; | ||
| document.body.style.userSelect = "none"; | ||
| document.addEventListener("mousemove", onMouseMove); | ||
| document.addEventListener("mouseup", onMouseUp); | ||
| return () => { | ||
| document.removeEventListener("mousemove", onMouseMove); | ||
| document.removeEventListener("mouseup", onMouseUp); | ||
| }; |
There was a problem hiding this comment.
Add a cleanup reset so
isResizing returns to false when the component unmounts mid-drag.
| const onMouseUp = () => { | |
| setIsResizing(false); | |
| document.body.style.cursor = ""; | |
| document.body.style.userSelect = ""; | |
| }; | |
| document.body.style.cursor = "col-resize"; | |
| document.body.style.userSelect = "none"; | |
| document.addEventListener("mousemove", onMouseMove); | |
| document.addEventListener("mouseup", onMouseUp); | |
| return () => { | |
| document.removeEventListener("mousemove", onMouseMove); | |
| document.removeEventListener("mouseup", onMouseUp); | |
| }; | |
| const onMouseUp = () => { | |
| setIsResizing(false); | |
| document.body.style.cursor = ""; | |
| document.body.style.userSelect = ""; | |
| }; | |
| document.body.style.cursor = "col-resize"; | |
| document.body.style.userSelect = "none"; | |
| document.addEventListener("mousemove", onMouseMove); | |
| document.addEventListener("mouseup", onMouseUp); | |
| return () => { | |
| document.removeEventListener("mousemove", onMouseMove); | |
| document.removeEventListener("mouseup", onMouseUp); | |
| setIsResizing(false); | |
| document.body.style.cursor = ""; | |
| document.body.style.userSelect = ""; | |
| }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/sessions/components/McpAppsSidebar.tsx
Line: 89-101
Comment:
Add a cleanup reset so `isResizing` returns to `false` when the component unmounts mid-drag.
```suggestion
const onMouseUp = () => {
setIsResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
setIsResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
```
How can I resolve this? If you propose a fix, please make it concise.
Summary
VirtualizedList.scrollToIndexmechanism (same as conversation search).ExternalAppsOpener.mcpApps.onDiscoveryCompletemaintains aSetof tool keys with registered UI, so the sidebar filters to true "apps" without firing NhasUiForToolqueries.Screen.Recording.2026-05-22.at.17.30.31.mov
Files
New
mcp-apps/stores/mcpUiToolsStore.ts— runtime registry of MCP tool keys with UImcp-apps/hooks/useMcpUiToolsSubscription.ts— single global subscription that populates the registrysessions/stores/mcpAppsSidebarStore.ts— persisted open/width viacreateSidebarStoresessions/components/McpAppsSidebar.tsx— the panel with resize handle and clickable rowstask-detail/components/McpAppsToggleButton.tsx— header icon buttonModified
App.tsx— mounts the subscription onceConversationView.tsx— single pass derives bothmcpAppIndices(forkeepMounted) andmcpEntries; root wrapped in a Flex to host the sidebarTaskDetail.tsx— toggle button next toExternalAppsOpenerin the headerTest plan
pnpm --filter code typecheckpassespnpm dev— open a task, generate a couple of PostHog MCP apps (insights / dashboard)