Skip to content

Commit f82f853

Browse files
rafavallsclaudeguitavanoviktormarinho
authored
feat(monitoring): redesign monitoring page — overview cards, Top Agents/Automations, Threads tab (#2953)
* feat(monitoring): redesign overview with card-based layout matching Figma Update Card component to remove default hover and lighten title weight. Rebuild monitoring overview with MonitoringMetricCard pattern: Tool Calls (full width), Latency + Errors (half width), Top Tools + Top Agents, AI Usage section with model leaderboards. Update header with Live streaming indicator and inline tabs/controls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(monitoring): add Threads tab with agent, model, user, usage and conversation view Adds a new Threads tab to the Monitor page showing a paginated list of threads with their associated agent, model (resolved from decopilot monitoring logs), user, status, and token usage (in/out/total). Clicking a thread opens a slide-over panel with the full conversation rendered using the existing chat UI components in read-only mode. Made-with: Cursor * feat(monitoring): add time range filter to Threads tab Made-with: Cursor * feat(monitoring): add search and model/user/status filters to Threads tab Made-with: Cursor * feat(monitoring): add search, filters (status/agent/user/model) and consistent table header to Threads tab - Search by title (backend ILIKE) - Filter by status, user, agent (all backend), model (client-side from logs) - FiltersPopover matches Audit tab visual style - Fix empty threads bug: updated_at is TEXT so compare as ISO string, not Date - Table headers now match Audit tab style (uppercase monospace muted) Made-with: Cursor * fix(monitoring): threads tab quality fixes - Use PersistedRunConfigSchema.passthrough() for run_config in ThreadEntitySchema instead of loose record type - Replace ThreadMessagesContent with self-contained ThreadConversationPanel that owns its header, eliminating the onModelResolved-during-render anti-pattern - Paginate thread messages (100/page with infinite scroll) to support long threads - Paginate decopilot monitoring logs internally until hasMore=false to avoid 500-log truncation for model/usage aggregation - Debounce search input (300ms) to avoid firing a query on every keystroke - Switch threads list and model logs queries from useSuspenseInfiniteQuery/useSuspenseQuery to useInfiniteQuery/useQuery so ThreadsTabContent never suspends (fixing search input focus loss) - Hide table header when threads list is empty or loading Made-with: Cursor * fix file * fix(thread): revert run_config schema to record type for storage compatibility PersistedRunConfigSchema.passthrough() infers required fields (models, agent, temperature, toolApprovalLevel) that are incompatible with the Kysely storage type Thread.run_config: Record<string, unknown> | null, breaking all thread tool handlers. Revert to z.record() with a comment directing callers to use PersistedRunConfigSchema for typed access at the point of use. Made-with: Cursor * fix monitoring error * fix(types): add agent_ids column to ThreadTable and Thread interfaces The column was added by migration 052 but missing from the TypeScript types, causing TS2345 errors in threads.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(monitoring): resolve useChatStream error in threads view MessageAssistant used useChatStream() which throws when rendered outside ActiveTaskProvider. Threads in the monitoring view display read-only historical messages with no active stream context, so isRunInProgress now safely defaults to false via useOptionalChatStream(). * fix(threads): remove stale agent_ids references, use virtual_mcp_id Migration 057 already replaced agent_ids with virtual_mcp_id but leftover references remained in types, storage queries, schema, and monitoring UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(docs): remove duplicate typescript devDependency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix agent name * fix(monitoring): reduce header-to-content spacing, constrain audit/threads width - Reduce top padding in overview tab (py-6 → pt-2 pb-6) to close the gap between header controls and the first card - Wrap audit tab content in max-w-[1200px] container with px-4/md:px-10 to match the members page table layout - Apply same container constraint to threads tab Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): align button/select sizes, unify audit/threads UX - Select component: change default height from h-10 to h-8 (32px) to match button default size; xs size from h-7 to h-6 - Header controls: remove size="sm" overrides so buttons use default h-8 (32px) consistently - Reduce header-to-content gap (gap-4 → gap-3) - Move search to shared header area below tabs for audit/threads - Convert audit from accordion (inline expand) to Sheet (slide-over panel) matching the threads pattern - Remove ThreadFiltersPopover from threads tab - Remove chevron expand column from LogRow, simplify to click-to-select - Both audit and threads now use the same interaction: click row → Sheet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): pixel-perfect card design matching Figma specs Card component (MonitoringMetricCard): - gap-8 (32px) between header and content sections - Title: text-sm font-normal text-foreground/70 - Value: text-4xl font-normal (36px, weight 400) - Content children: gap-6 (24px) between chart and table Leaderboard tables (Connection, Tool, Model): - Row height: h-10 (40px) with border-b border-border/50 - Row padding: px-3 (12px) - Icon: 24x24 with border, shadow-sm, rounded-md - Name: text-sm text-muted-foreground - Percentage: text-sm text-foreground/30 (opacity 0.3) - Count: text-sm text-foreground font-normal (not semibold) - No gap between rows, only 0.5px border separator - "See all": px-4, text-sm text-muted-foreground + arrow icon Select trigger: w-[120px] matching Figma spec Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): move search into Page.Body matching Members page style Move SearchInput from a standalone div between header and content into the Page.Body container, below the tabs row. Uses w-full md:w-[375px] matching the Members page search pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ui): TimeRangePicker trigger to use default button size (32px) Remove size="sm" and h-7 override from the trigger button so it uses the default h-8 (32px) height, matching all other header controls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): tighten spacing between tabs, search, and table Change Page.Body from pb-0 to !pb-3 so there's a consistent small gap between the search input and the table content below. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): increase gap between tabs and search Bump flex gap from gap-3 to gap-5 (20px) for better spacing between title, tabs row, and search input. Also pb-3 → pb-4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): update skeletons to match Figma card design Overview skeleton: use SkeletonCard component matching the actual card structure (gap-8, proper title/value sizes, border-b table rows with icon placeholders, "See all" row). Mirrors the full overview layout with full-width and half-width card rows. Table skeleton: wrap in max-w-[1200px] container matching the actual audit/threads table layout. Match row height (h-14) and column structure (no expand chevron column). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): move search inside gap-5 flex container The search was outside the gap-5 div, so the gap wasn't applying between tabs and search. Now it's a sibling of the title and tabs row inside the same flex-col gap-5 container. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(monitoring): clickable rows, See all navigation, AI Usage layout - Connection rows in leaderboard tables are now clickable — navigate to the connection detail page via getConnectionSlug - Tool rows navigate to their parent connection detail page - "See all" links navigate to the Audit tab - All clickable rows have hover:bg-accent/50 transition - AI Usage section: added border-t divider separating it from tool call metrics; restructured from full-width + 2-col to a single 3-column row (AI Calls, AI Latency, AI Errors) with shorter charts - Updated skeleton to match new 3-column AI layout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): correct percentage calculation in leaderboard rows The percentage was dividing the metric value (e.g. avgDurationMs=117) by total (e.g. totalCalls=2) giving nonsensical results like 5825%. Now per mode: - requests: calls / totalCalls (proportion of traffic) - latency: calls / totalCalls (proportion of traffic) + latency value - errors: errorRate % + formatted metric value Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): improve Top Tools/Connections cards and AI section header Top Tools / Top Connections: - Replaced MonitoringMetricCard (with giant hero number) with simple Card containers — just a label + the ranked list - Renamed "Top Agents Used" to "Top Connections" (more accurate) - Reduced gap-8 to gap-4 for compact list style AI Usage divider: - Replaced bare border-t with a centered section header: horizontal lines + "AI USAGE" label in uppercase tracking-wider - Added pt-4 for breathing room above Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(monitoring): replace Top Connections with Top Agents, improve layout - Remove redundant "Top Connections" card (already shown in Tool Calls) - Add "Top Agents" card listing active virtual MCPs with icons, each clickable to navigate to the agent page; "See all" goes to Threads tab - New AgentLeaderboardTable component using IntegrationIcon and virtualMcps data - Pass virtualMcps prop through to OverviewTabContent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(monitoring): replace Top Tools with Top Agents + Top Automations - Remove Top Tools card and ToolLeaderboardTable (tool data already visible in the Tool Calls connection breakdown) - Remove useMonitoringTopTools hook usage and analyticsDateRange - Top Agents card: uses MonitoringMetricCard with chart + agent list, shows active virtual MCPs count as hero metric - Top Automations card: new AutomationsCard component using useAutomationsList hook, shows automation name, trigger count, active/inactive status; clickable rows navigate to the agent page - Both cards in a 2-column layout with charts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(monitoring): Agent Sessions + Automation Runs follow Tool Calls pattern Both cards now match the Tool Calls card structure: big hero metric + chart + ranked leaderboard table with the same row styling. Agent Sessions: total calls metric, chart, agent leaderboard Automation Runs: active count metric, chart, automation leaderboard showing % share of triggers + trigger count per automation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(monitoring): redesign monitoring page with new layout and stats Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(settings): remove redundant description paragraphs from settings pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(monitoring): deduplicate virtualMcpId/agentId thread filters, add local postgres docs Merge the two redundant virtual_mcp_id WHERE clauses in thread queries so they never conflict. Also add CLAUDE.md instructions for querying the embedded postgres during development. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): remove unused files and exports flagged by knip - Delete analytics-top-tools.tsx (unused file) and its useMonitoringTopTools hook - Delete home-grid-cell.tsx (no remaining consumers after skeleton removal) - Remove StackedConnectionChart, MonitoringStatsRowSkeleton (dead code) - Un-export internal helpers in utils.ts (NICE_INTERVALS, intervalToMs, etc.) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitoring): limit thread filters to single selection matching backend The backend only accepts singular agentId/userId strings, but the UI allowed multi-select. Cap both filters to one value so the UI matches the API capability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: guitavano <tavano62@gmail.com> Co-authored-by: viktormarinho <viktormpcs@gmail.com>
1 parent e3abb28 commit f82f853

39 files changed

Lines changed: 3708 additions & 3210 deletions

AGENTS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,29 @@ bun run --cwd=apps/mesh migrate
5959
bun run --cwd=apps/mesh better-auth:migrate
6060
```
6161

62+
#### Querying local postgres during development
63+
The dev server uses embedded postgres on a **dynamic port**. To query it while `bun run dev` is running:
64+
65+
1. Find the port:
66+
```bash
67+
ps aux | grep "postgres -D" | grep -v grep
68+
# Look for -p <PORT> at the end of the command
69+
```
70+
71+
2. Run queries via a bun inline script (uses the `pg` package from apps/mesh):
72+
```bash
73+
cat << 'EOF' | bun run --cwd apps/mesh -
74+
import pg from "pg";
75+
const client = new pg.Client("postgresql://postgres:postgres@localhost:<PORT>/postgres");
76+
await client.connect();
77+
const { rows } = await client.query("SELECT * FROM <table> LIMIT 5");
78+
console.log(JSON.stringify(rows, null, 2));
79+
await client.end();
80+
EOF
81+
```
82+
83+
Replace `<PORT>` with the port found in step 1. The `--cwd apps/mesh` is required so bun resolves the `pg` dependency from the mesh workspace.
84+
6285
### Build & Deploy
6386
```bash
6487
# Build runtime package

apps/docs/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
},
2323
"devDependencies": {
2424
"@tailwindcss/typography": "^0.5.16",
25-
"typescript": "^5.9.3",
2625
"wrangler": "^4.40.3"
2726
},
2827
"engines": {

apps/mesh/src/storage/threads.ts

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,16 @@ export class OrgScopedThreadStorage {
6767

6868
list(
6969
createdBy?: string,
70-
options?: { limit?: number; offset?: number; virtualMcpId?: string },
70+
options?: {
71+
limit?: number;
72+
offset?: number;
73+
virtualMcpId?: string;
74+
startDate?: string;
75+
endDate?: string;
76+
search?: string;
77+
status?: string;
78+
agentId?: string;
79+
},
7180
): Promise<{ threads: Thread[]; total: number }> {
7281
return this.inner.list(this.requireOrg(), createdBy, options);
7382
}
@@ -237,7 +246,16 @@ export class SqlThreadStorage implements ThreadStoragePort {
237246
async list(
238247
organizationId: string,
239248
createdBy?: string,
240-
options?: { limit?: number; offset?: number; virtualMcpId?: string },
249+
options?: {
250+
limit?: number;
251+
offset?: number;
252+
virtualMcpId?: string;
253+
startDate?: string;
254+
endDate?: string;
255+
search?: string;
256+
status?: string;
257+
agentId?: string;
258+
},
241259
): Promise<{ threads: Thread[]; total: number }> {
242260
let query = this.db
243261
.selectFrom("threads")
@@ -249,8 +267,30 @@ export class SqlThreadStorage implements ThreadStoragePort {
249267
if (createdBy) {
250268
query = query.where("created_by", "=", createdBy);
251269
}
252-
if (options?.virtualMcpId) {
253-
query = query.where("virtual_mcp_id", "=", options.virtualMcpId);
270+
const virtualMcpFilter = options?.virtualMcpId ?? options?.agentId;
271+
if (virtualMcpFilter) {
272+
query = query.where("virtual_mcp_id", "=", virtualMcpFilter);
273+
}
274+
if (options?.startDate) {
275+
// updated_at is stored as ISO text — string comparison is correct for ISO dates
276+
query = query.where(
277+
"updated_at",
278+
">=",
279+
options.startDate as unknown as Date,
280+
);
281+
}
282+
if (options?.endDate) {
283+
query = query.where(
284+
"updated_at",
285+
"<=",
286+
options.endDate as unknown as Date,
287+
);
288+
}
289+
if (options?.search) {
290+
query = query.where("title", "ilike", `%${options.search}%`);
291+
}
292+
if (options?.status) {
293+
query = query.where("status", "=", options.status as ThreadStatus);
254294
}
255295

256296
let countQuery = this.db
@@ -262,11 +302,31 @@ export class SqlThreadStorage implements ThreadStoragePort {
262302
if (createdBy) {
263303
countQuery = countQuery.where("created_by", "=", createdBy);
264304
}
265-
if (options?.virtualMcpId) {
305+
if (virtualMcpFilter) {
306+
countQuery = countQuery.where("virtual_mcp_id", "=", virtualMcpFilter);
307+
}
308+
if (options?.startDate) {
309+
countQuery = countQuery.where(
310+
"updated_at",
311+
">=",
312+
options.startDate as unknown as Date,
313+
);
314+
}
315+
if (options?.endDate) {
316+
countQuery = countQuery.where(
317+
"updated_at",
318+
"<=",
319+
options.endDate as unknown as Date,
320+
);
321+
}
322+
if (options?.search) {
323+
countQuery = countQuery.where("title", "ilike", `%${options.search}%`);
324+
}
325+
if (options?.status) {
266326
countQuery = countQuery.where(
267-
"virtual_mcp_id",
327+
"status",
268328
"=",
269-
options.virtualMcpId,
329+
options.status as ThreadStatus,
270330
);
271331
}
272332

apps/mesh/src/tools/monitoring/stats.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ export const MONITORING_STATS = defineTool({
3333
.optional()
3434
.describe("Filter by end date (ISO 8601 datetime string)"),
3535
interval: z
36-
.enum(["1m", "1h", "1d"])
36+
.string()
37+
.regex(/^\d+[mhd]$/)
3738
.optional()
3839
.describe(
39-
"Bucket interval for timeseries data. When provided, returns timeseries array.",
40+
"Bucket interval for timeseries data (e.g. 1m, 5m, 2h, 1d). When provided, returns timeseries array.",
4041
),
4142
connectionIds: z
4243
.array(z.string())

apps/mesh/src/tools/thread/list.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@ const ThreadListInputSchema = CollectionListInputSchema.extend({
2323
virtual_mcp_id: z.string().optional(),
2424
})
2525
.optional(),
26+
startDate: z
27+
.string()
28+
.datetime()
29+
.optional()
30+
.describe("Filter threads updated at or after this ISO timestamp"),
31+
endDate: z
32+
.string()
33+
.datetime()
34+
.optional()
35+
.describe("Filter threads updated at or before this ISO timestamp"),
36+
search: z
37+
.string()
38+
.optional()
39+
.describe("Full-text search on thread title (case-insensitive)"),
40+
status: z
41+
.string()
42+
.optional()
43+
.describe("Filter by thread status (e.g. completed, failed, in_progress)"),
44+
userId: z
45+
.string()
46+
.optional()
47+
.describe("Filter by the user who created the thread"),
48+
agentId: z
49+
.string()
50+
.optional()
51+
.describe("Filter by agent (connection or virtual MCP) ID"),
2652
});
2753

2854
/**
@@ -58,7 +84,8 @@ export const COLLECTION_THREADS_LIST = defineTool({
5884
const virtualMcpId = input.where?.virtual_mcp_id;
5985
// "me" is a reserved value meaning "filter by the authenticated user"
6086
const createdBy =
61-
input.where?.created_by === "me" ? userId : input.where?.created_by;
87+
input.userId ??
88+
(input.where?.created_by === "me" ? userId : input.where?.created_by);
6289

6390
const { threads, total } = triggerIds?.length
6491
? await ctx.storage.threads.listByTriggerIds(triggerIds, {
@@ -69,6 +96,11 @@ export const COLLECTION_THREADS_LIST = defineTool({
6996
limit,
7097
offset,
7198
virtualMcpId,
99+
startDate: input.startDate,
100+
endDate: input.endDate,
101+
search: input.search,
102+
status: input.status,
103+
agentId: input.agentId,
72104
});
73105

74106
const hasMore = offset + limit < total;

apps/mesh/src/tools/thread/schema.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ export const ThreadEntitySchema = z.object({
5555
.string()
5656
.nullable()
5757
.describe("User ID who last updated the thread"),
58+
virtual_mcp_id: z
59+
.string()
60+
.optional()
61+
.describe("Virtual MCP (agent) this thread was initiated with"),
62+
// Typed as a loose record to stay compatible with the Kysely storage type
63+
// (Thread.run_config: Record<string, unknown> | null). Callers that need the
64+
// typed shape should parse with PersistedRunConfigSchema from run-config.ts.
65+
run_config: z
66+
.record(z.string(), z.unknown())
67+
.nullable()
68+
.optional()
69+
.describe("Persisted run configuration (contains agent and model info)"),
5870
});
5971

6072
export type ThreadEntity = z.infer<typeof ThreadEntitySchema>;

apps/mesh/src/web/components/chat/chat-context.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,10 @@ export function useChatStream(): ChatStreamContextValue {
754754
return ctx;
755755
}
756756

757+
export function useOptionalChatStream(): ChatStreamContextValue | null {
758+
return useContext(ChatStreamCtx);
759+
}
760+
757761
export function useChatTask(): ChatTaskContextValue {
758762
const ctx = useContext(ChatTaskCtx);
759763
if (!ctx)
@@ -768,6 +772,10 @@ export function useChatPrefs(): ChatPrefsContextValue {
768772
return ctx;
769773
}
770774

775+
export function useOptionalChatPrefs(): ChatPrefsContextValue | null {
776+
return useContext(ChatPrefsCtx);
777+
}
778+
771779
export function useChatBridge(): ChatBridgeValue {
772780
return useContext(ChatBridgeCtx);
773781
}

apps/mesh/src/web/components/chat/context.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export {
1313
ActiveTaskProvider,
1414
useChatTask,
1515
useChatStream,
16+
useOptionalChatStream,
1617
useChatPrefs,
18+
useOptionalChatPrefs,
1719
useChatBridge,
1820
type ChatStreamContextValue,
1921
type ChatTaskContextValue,

apps/mesh/src/web/components/chat/message/assistant.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { SmartAutoScroll } from "./smart-auto-scroll.tsx";
1616
import { type DataParts, useFilterParts } from "./use-filter-parts.ts";
1717
import { addUsage, emptyUsageStats } from "@decocms/mesh-sdk";
18-
import { useChatStream } from "../context.tsx";
18+
import { useOptionalChatStream } from "../context.tsx";
1919

2020
type ThinkingStage = "planning" | "thinking";
2121

@@ -374,7 +374,7 @@ export function MessageAssistant({
374374
className,
375375
isLast = false,
376376
}: MessageAssistantProps) {
377-
const { isRunInProgress } = useChatStream();
377+
const { isRunInProgress = false } = useOptionalChatStream() ?? {};
378378
const isStreaming = status === "streaming";
379379
const isSubmitted = status === "submitted";
380380
const isLoading = isStreaming || isSubmitted;

apps/mesh/src/web/components/chat/message/parts/tool-call-part/generic.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import { contentBlocksToTiptapDoc } from "@/mcp-apps/content-blocks.ts";
44
import { MCPAppRenderer as MCPAppIframeRenderer } from "@/mcp-apps/mcp-app-renderer.tsx";
55
import { getUIResourceUri } from "@/mcp-apps/types.ts";
6-
import { useChatStream, useChatPrefs } from "@/web/components/chat/context.tsx";
6+
import {
7+
useOptionalChatStream,
8+
useOptionalChatPrefs,
9+
} from "@/web/components/chat/context.tsx";
710
import { Button } from "@deco/ui/components/button.tsx";
811
import {
912
Tooltip,
@@ -29,9 +32,9 @@ import {
2932
} from "@untitledui/icons";
3033
import type { DynamicToolUIPart, ToolUIPart } from "ai";
3134
import type React from "react";
32-
import { Suspense } from "react";
35+
import { Suspense, useContext } from "react";
3336
import { ErrorBoundary } from "@/web/components/error-boundary.tsx";
34-
import { useChatPanel } from "@/web/contexts/panel-context.tsx";
37+
import { PanelContext } from "@/web/contexts/panel-context.tsx";
3538
import { getToolPartErrorText, safeStringify } from "../utils.ts";
3639
import { ToolCallShell } from "./common.tsx";
3740
import { getEffectiveState, getFriendlyToolName } from "./utils.tsx";
@@ -166,10 +169,24 @@ export function GenericToolCallPart({
166169
: part.type.replace("tool-", "") || "Tool";
167170
const friendlyName = getFriendlyToolName(toolName);
168171

169-
const { sendMessage } = useChatStream();
170-
const { selectedVirtualMcp, setAppContext, clearAppContext } = useChatPrefs();
172+
const chatStream = useOptionalChatStream();
173+
const chatPrefs = useOptionalChatPrefs();
171174
const { org } = useProjectContext();
172-
const [, setChatOpen] = useChatPanel();
175+
176+
// Panel context may not be available when rendering read-only thread history
177+
// (e.g. monitoring Threads tab), so we read the context directly.
178+
const panelControls = useContext(PanelContext);
179+
const setChatOpen = panelControls
180+
? (open: boolean) => {
181+
if (open) {
182+
panelControls.chatPanelRef.current?.resize(
183+
Math.min(panelControls.chatPanelWidth, 35),
184+
);
185+
} else {
186+
panelControls.chatPanelRef.current?.collapse();
187+
}
188+
}
189+
: undefined;
173190

174191
const uiResourceUri = getUIResourceUri(toolMeta);
175192

@@ -181,16 +198,16 @@ export function GenericToolCallPart({
181198
toolMeta.connectionId != null &&
182199
toolMeta.connectionId !== ""
183200
? String(toolMeta.connectionId)
184-
: (selectedVirtualMcp?.id ?? null);
201+
: (chatPrefs?.selectedVirtualMcp?.id ?? null);
185202

186203
const hasMCPApp = !!uiResourceUri && part.state === "output-available";
187204
const sourceId = connectionId ? `${connectionId}:${toolName}` : null;
188205

189206
const handleAppMessage = (params: McpUiMessageRequest["params"]) => {
190207
const doc = contentBlocksToTiptapDoc(params.content);
191208
if (doc.content.length > 0) {
192-
setChatOpen(true);
193-
sendMessage(doc);
209+
setChatOpen?.(true);
210+
chatStream?.sendMessage(doc);
194211
}
195212
};
196213

@@ -289,12 +306,14 @@ export function GenericToolCallPart({
289306
toolMeta={toolMeta as Record<string, unknown> | undefined}
290307
onMessage={handleAppMessage}
291308
onUpdateModelContext={
292-
sourceId
293-
? (params) => setAppContext(sourceId, params)
309+
sourceId && chatPrefs
310+
? (params) => chatPrefs.setAppContext(sourceId, params)
294311
: undefined
295312
}
296313
onTeardown={
297-
sourceId ? () => clearAppContext(sourceId) : undefined
314+
sourceId && chatPrefs
315+
? () => chatPrefs.clearAppContext(sourceId)
316+
: undefined
298317
}
299318
/>
300319
</Suspense>

0 commit comments

Comments
 (0)