Skip to content

feat: add MCP apps sidebar to task threads#2220

Open
fercgomes wants to merge 3 commits into
mainfrom
posthog-code/mcp-apps-sidebar
Open

feat: add MCP apps sidebar to task threads#2220
fercgomes wants to merge 3 commits into
mainfrom
posthog-code/mcp-apps-sidebar

Conversation

@fercgomes
Copy link
Copy Markdown
Contributor

@fercgomes fercgomes commented May 19, 2026

Summary

  • Adds a collapsible right-side sidebar inside 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 via the existing VirtualizedList.scrollToIndex mechanism (same as conversation search).
  • A toggle button lives in the task header next to ExternalAppsOpener.
  • A single global subscription to mcpApps.onDiscoveryComplete maintains a Set of tool keys with registered UI, so the sidebar filters to true "apps" without firing N hasUiForTool queries.
Screen.Recording.2026-05-22.at.17.30.31.mov

Files

New

  • mcp-apps/stores/mcpUiToolsStore.ts — runtime registry of MCP tool keys with UI
  • mcp-apps/hooks/useMcpUiToolsSubscription.ts — single global subscription that populates the registry
  • sessions/stores/mcpAppsSidebarStore.ts — persisted open/width via createSidebarStore
  • sessions/components/McpAppsSidebar.tsx — the panel with resize handle and clickable rows
  • task-detail/components/McpAppsToggleButton.tsx — header icon button

Modified

  • App.tsx — mounts the subscription once
  • ConversationView.tsx — single pass derives both mcpAppIndices (for keepMounted) and mcpEntries; root wrapped in a Flex to host the sidebar
  • TaskDetail.tsx — toggle button next to ExternalAppsOpener in the header

Test plan

  • pnpm --filter code typecheck passes
  • pnpm dev — open a task, generate a couple of PostHog MCP apps (insights / dashboard)
  • Click the SquaresFour icon in the task header → sidebar opens to 260px on the right
  • Sidebar lists only UI-rendering MCP calls; non-UI MCP tools are absent
  • Scroll the chat up; click an entry → conversation scrolls to that item centred, iframe state preserved
  • Drag the resize handle; reload the app → width persisted
  • Close the sidebar; reload → closed state persisted
  • Empty thread → sidebar shows empty state

fercgomes and others added 3 commits May 19, 2026 14:07
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
@fercgomes fercgomes marked this pull request as ready for review May 22, 2026 20:31
@fercgomes fercgomes requested a review from a team May 22, 2026 20:31
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 22, 2026

Prompt To Fix All With AI
Fix 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

Comment on lines +202 to +209
<Box className="p-2">
<McpAppHost
toolCall={entry.toolCall}
mcpToolName={entry.fullToolName}
serverName={entry.serverName}
toolName={entry.toolName}
/>
</Box>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +89 to +101
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);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Add a cleanup reset so isResizing returns to false when the component unmounts mid-drag.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant