Skip to content
Merged
90 changes: 89 additions & 1 deletion front/components/assistant/conversation/useAutoOpenSidePanel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { useConversationSidePanelContext } from "@app/components/assistant/conversation/ConversationSidePanelContext";
import type { AgentMessageWithStreaming } from "@app/components/assistant/conversation/types";
import { useConversationMessageAction } from "@app/hooks/conversations";
import { useActiveConversationId } from "@app/hooks/useActiveConversationId";
import { isInteractiveContentFileContentOutput } from "@app/lib/actions/mcp_internal_actions/output_schemas";
import {
CONVERSATION_SIDE_PANEL_SERVER_NAME,
SET_FILES_SIDE_PANEL_TOOL_NAME,
} from "@app/lib/api/actions/servers/conversation_side_panel/metadata";
import { useAuth } from "@app/lib/auth/AuthContext";
import { useIsMobile } from "@app/lib/swr/useIsMobile";
import type { AgentMCPActionWithOutputType } from "@app/types/actions";
import { FILES_SIDE_PANEL_TYPE } from "@app/types/conversation_side_panel";
import { isInteractiveContentType } from "@app/types/files";
import { removeNulls } from "@app/types/shared/utils/general";
import React from "react";
Expand All @@ -11,6 +20,52 @@ interface UseAutoOpenSidePanelProps {
agentMessage: AgentMessageWithStreaming;
}

function isSetFilesSidePanelAction(
action: Pick<AgentMCPActionWithOutputType, "internalMCPServerName"> & {
toolName: string | null;
}
): boolean {
return (
action.internalMCPServerName === CONVERSATION_SIDE_PANEL_SERVER_NAME &&
action.toolName === SET_FILES_SIDE_PANEL_TOOL_NAME
);
}

function getFilesSidePanelVisibility(
actions: readonly AgentMCPActionWithOutputType[]
): boolean | undefined {
let latestAction: AgentMCPActionWithOutputType | undefined;

for (const action of actions) {
if (
isSetFilesSidePanelAction(action) &&
action.status === "succeeded" &&
typeof action.params.visible === "boolean" &&
(!latestAction || action.updatedAt > latestAction.updatedAt)
) {
latestAction = action;
}
}

return typeof latestAction?.params.visible === "boolean"
? latestAction.params.visible
: undefined;
}

function getLatestFilesSidePanelActionId(
agentMessage: AgentMessageWithStreaming
): string | null {
let actionId: string | null = null;

for (const step of agentMessage.activitySteps) {
if (step.type === "action" && isSetFilesSidePanelAction(step)) {
actionId = step.actionId;
}
}

return actionId;
}

/**
* Auto-opens the appropriate side panel when the agent generates files.
*
Expand All @@ -25,7 +80,10 @@ export function useAutoOpenSidePanel({
isLastMessage,
agentMessage,
}: UseAutoOpenSidePanelProps) {
const { openPanel, currentPanel } = useConversationSidePanelContext();
const { openPanel, closePanel, currentPanel } =
useConversationSidePanelContext();
const { workspace } = useAuth();
const conversationId = useActiveConversationId();
const isMobile = useIsMobile();

// Track the last opened fileId to prevent double-opening glitch.
Expand Down Expand Up @@ -78,6 +136,22 @@ export function useAutoOpenSidePanel({
[agentMessage.generatedFiles]
);

const filesSidePanelVisibilityFromActions = getFilesSidePanelVisibility(
agentMessage.actions
);
const filesSidePanelActionId = getLatestFilesSidePanelActionId(agentMessage);
const { action: filesSidePanelAction } = useConversationMessageAction({
conversationId: conversationId ?? "",
workspaceId: workspace.sId,
messageId: agentMessage.sId,
actionId: conversationId ? filesSidePanelActionId : null,
});
const filesSidePanelVisibility =
filesSidePanelVisibilityFromActions ??
getFilesSidePanelVisibility(
filesSidePanelAction ? [filesSidePanelAction] : []
);

// Reset interactive tracking when the message changes.
// biome-ignore lint/correctness/useExhaustiveDependencies: ignored using `--suppress`
React.useEffect(() => {
Expand Down Expand Up @@ -116,6 +190,18 @@ export function useAutoOpenSidePanel({
return;
}

if (isLastMessage && filesSidePanelVisibility === false) {
if (currentPanel === FILES_SIDE_PANEL_TYPE) {
closePanel();
}
return;
}

if (isLastMessage && filesSidePanelVisibility === true) {
openPanel({ type: FILES_SIDE_PANEL_TYPE });
return;
}

// Priority 2: file explorer — only when no interactive content is taking the panel.
if (
regularGeneratedFiles.length === 0 ||
Expand All @@ -132,9 +218,11 @@ export function useAutoOpenSidePanel({
completedInteractiveFiles,
interactiveFilesFromProgress,
regularGeneratedFiles,
filesSidePanelVisibility,
isLastMessage,
agentMessage.sId,
openPanel,
closePanel,
currentPanel,
isMobile,
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ exports[`MCP Servers Metadata Snapshot > should have stable tool billing info ac
"toolCostCategory": "advanced",
},
},
"conversation_side_panel": {
"open_frame": {
"freeUsage": true,
"toolCostCategory": "basic",
},
"set_files_side_panel": {
"freeUsage": true,
"toolCostCategory": "basic",
},
},
"data_sources_file_system": {
"cat": {
"freeUsage": false,
Expand Down Expand Up @@ -2737,6 +2747,10 @@ exports[`MCP Servers Metadata Snapshot > should have stable tool stakes across a
"list_content_nodes_and_tables": "never_ask",
"semantic_search": "never_ask",
},
"conversation_side_panel": {
"open_frame": "never_ask",
"set_files_side_panel": "never_ask",
},
"data_sources_file_system": {
"cat": "never_ask",
"find": "never_ask",
Expand Down
13 changes: 13 additions & 0 deletions front/lib/actions/mcp_internal_actions/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { CLARI_COPILOT_SERVER } from "@app/lib/api/actions/servers/clari_copilot
import { COMMON_UTILITIES_SERVER } from "@app/lib/api/actions/servers/common_utilities/metadata";
import { CONFLUENCE_SERVER } from "@app/lib/api/actions/servers/confluence/metadata";
import { CONVERSATION_FILES_SERVER } from "@app/lib/api/actions/servers/conversation_files/metadata";
import { CONVERSATION_SIDE_PANEL_SERVER } from "@app/lib/api/actions/servers/conversation_side_panel/metadata";
import { DATA_SOURCES_FILE_SYSTEM_SERVER } from "@app/lib/api/actions/servers/data_sources_file_system/metadata";
import { DATA_WAREHOUSES_SERVER } from "@app/lib/api/actions/servers/data_warehouses/metadata";
import { DATABRICKS_SERVER } from "@app/lib/api/actions/servers/databricks/metadata";
Expand Down Expand Up @@ -165,6 +166,7 @@ export const AVAILABLE_INTERNAL_MCP_SERVER_NAMES = [
"clari_copilot",
"confluence",
"conversation_files",
"conversation_side_panel",
"files",
"databricks",
"data_sources_file_system",
Expand Down Expand Up @@ -473,6 +475,17 @@ export const INTERNAL_MCP_SERVERS = ensureUniqueToolNames({
timeoutMs: undefined,
metadata: CONVERSATION_FILES_SERVER,
},
conversation_side_panel: {
id: 1044,
availability: "auto_hidden_builder",
allowMultipleInstances: false,
isRestricted: undefined,
isPreview: false,
tools_arguments_requiring_approval: undefined,
tools_retry_policies: undefined,
timeoutMs: undefined,
metadata: CONVERSATION_SIDE_PANEL_SERVER,
},
slack: {
id: 18,
availability: "manual",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
{ "name": "user_analytics", "id": 1039 },
{ "name": "activation_recommendations", "id": 1040 },
{ "name": "agent_templates", "id": 1041 },
{ "name": "user_memory", "id": 1043 }
{ "name": "user_memory", "id": 1043 },
{ "name": "conversation_side_panel", "id": 1044 }
],
"manual": [
{ "name": "github", "id": 1 },
Expand Down
17 changes: 17 additions & 0 deletions front/lib/actions/mcp_internal_actions/output_schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,22 @@ export function isInteractiveContentFileContentOutput(
return output !== undefined && output.type === "interactive_content_file";
}

const NotificationSidePanelControlSchema = z.object({
type: z.literal("side_panel_control"),
panel: z.enum(["files"]),
action: z.enum(["open", "close"]),
});

type SidePanelControlProgressOutput = z.infer<
typeof NotificationSidePanelControlSchema
>;

export function isSidePanelControlOutput(
output: ProgressNotificationOutput
): output is SidePanelControlProgressOutput {
return output !== undefined && output.type === "side_panel_control";
}

const InternalAllowedIconSchema = z.enum([...INTERNAL_ALLOWED_ICONS]);

const CustomResourceIconSchema = z.enum([...CUSTOM_RESOURCE_ALLOWED]);
Expand Down Expand Up @@ -890,6 +906,7 @@ export const ProgressNotificationOutputSchema = z
.union([
NotificationImageContentSchema,
NotificationInteractiveContentFileContentSchema,
NotificationSidePanelControlSchema,
NotificationRunAgentContentSchema,
NotificationStoreResourceContentSchema,
NotificationTextContentSchema,
Expand Down
3 changes: 3 additions & 0 deletions front/lib/actions/mcp_internal_actions/servers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { default as clariCopilotServer } from "@app/lib/api/actions/servers/clar
import { default as commonUtilitiesServer } from "@app/lib/api/actions/servers/common_utilities";
import { default as confluenceServer } from "@app/lib/api/actions/servers/confluence";
import { default as conversationFilesServer } from "@app/lib/api/actions/servers/conversation_files";
import { default as conversationSidePanelServer } from "@app/lib/api/actions/servers/conversation_side_panel";
import { default as dataSourcesFileSystemServer } from "@app/lib/api/actions/servers/data_sources_file_system";
import { default as dataWarehousesServer } from "@app/lib/api/actions/servers/data_warehouses";
import { default as databricksServer } from "@app/lib/api/actions/servers/databricks";
Expand Down Expand Up @@ -198,6 +199,8 @@ export async function getInternalMCPServer(
return dataSourcesFileSystemServer(auth, toolContext);
case "conversation_files":
return conversationFilesServer(auth, toolContext);
case "conversation_side_panel":
return conversationSidePanelServer(auth, toolContext);
case "files":
return filesServer(auth, toolContext);
case "databricks":
Expand Down
18 changes: 18 additions & 0 deletions front/lib/api/actions/servers/bm25_tool_search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,14 @@ const QUERIES: LabeledQuery[] = [
query: "read back the content of my frame",
expected: "interactive_content.retrieve_interactive_content_file",
},
{
query: "open an existing frame in the side panel without editing it",
expected: "conversation_side_panel.open_frame",
},
{
query: "show the user a frame that was already created",
expected: "conversation_side_panel.open_frame",
},
{
query: "revert my frame to the previous version",
expected: "interactive_content.revert_interactive_content_file",
Expand Down Expand Up @@ -1578,6 +1586,16 @@ const QUERIES: LabeledQuery[] = [
expected: "common_utilities.set_conversation_title",
},

// --- conversation_side_panel ---
{
query: "open the files side panel so the user can browse attachments",
expected: "conversation_side_panel.set_files_side_panel",
},
{
query: "hide the conversation files explorer panel",
expected: "conversation_side_panel.set_files_side_panel",
},

// --- exa_people_and_company ---
{
query: "find the LinkedIn profile of the CTO of Mistral AI",
Expand Down
5 changes: 5 additions & 0 deletions front/lib/api/actions/servers/bm25_tool_search_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CLARI_COPILOT_SERVER } from "@app/lib/api/actions/servers/clari_copilot
import { COMMON_UTILITIES_SERVER } from "@app/lib/api/actions/servers/common_utilities/metadata";
import { CONFLUENCE_SERVER } from "@app/lib/api/actions/servers/confluence/metadata";
import { CONVERSATION_FILES_SERVER } from "@app/lib/api/actions/servers/conversation_files/metadata";
import { CONVERSATION_SIDE_PANEL_SERVER } from "@app/lib/api/actions/servers/conversation_side_panel/metadata";
import { DATA_SOURCES_FILE_SYSTEM_SERVER } from "@app/lib/api/actions/servers/data_sources_file_system/metadata";
import { DATA_WAREHOUSES_SERVER } from "@app/lib/api/actions/servers/data_warehouses/metadata";
import { DATABRICKS_SERVER } from "@app/lib/api/actions/servers/databricks/metadata";
Expand Down Expand Up @@ -123,6 +124,10 @@ const SERVER_SOURCES: Array<{
}> = [
{ name: "agent_memory", tools: AGENT_MEMORY_SERVER.tools },
{ name: "conversation_files", tools: CONVERSATION_FILES_SERVER.tools },
{
name: "conversation_side_panel",
tools: CONVERSATION_SIDE_PANEL_SERVER.tools,
},
{ name: "google_drive", tools: GOOGLE_DRIVE_SERVER.tools },
{ name: "google_sheets", tools: GOOGLE_SHEETS_SERVER.tools },
{ name: "microsoft_drive", tools: MICROSOFT_DRIVE_SERVER.tools },
Expand Down
24 changes: 24 additions & 0 deletions front/lib/api/actions/servers/conversation_side_panel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { makeInternalMCPServer } from "@app/lib/actions/mcp_internal_actions/utils";
import { registerTool } from "@app/lib/actions/mcp_internal_actions/wrappers";
import type { ToolContext } from "@app/lib/actions/types";
import { CONVERSATION_SIDE_PANEL_SERVER_NAME } from "@app/lib/api/actions/servers/conversation_side_panel/metadata";
import { TOOLS } from "@app/lib/api/actions/servers/conversation_side_panel/tools";
import type { Authenticator } from "@app/lib/auth";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

function createServer(
auth: Authenticator,
toolContext?: ToolContext
): McpServer {
const server = makeInternalMCPServer(CONVERSATION_SIDE_PANEL_SERVER_NAME);

for (const tool of TOOLS) {
registerTool(auth, toolContext, server, tool, {
monitoringName: CONVERSATION_SIDE_PANEL_SERVER_NAME,
});
}

return server;
}

export default createServer;
Loading
Loading