diff --git a/surfsense_backend/app/connectors/confluence_history.py b/surfsense_backend/app/connectors/confluence_history.py index 927ebffeb..e9e547453 100644 --- a/surfsense_backend/app/connectors/confluence_history.py +++ b/surfsense_backend/app/connectors/confluence_history.py @@ -14,7 +14,6 @@ from sqlalchemy.future import select from app.config import config from app.connectors.confluence_connector import ConfluenceConnector from app.db import SearchSourceConnector -from app.routes.confluence_add_connector_route import refresh_confluence_token from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.utils.oauth_security import TokenEncryption @@ -190,7 +189,9 @@ class ConfluenceHistoryConnector: f"Connector {self._connector_id} not found; cannot refresh token." ) - # Refresh token + # Lazy import to avoid circular dependency + from app.routes.confluence_add_connector_route import refresh_confluence_token + connector = await refresh_confluence_token(self._session, connector) # Reload credentials after refresh diff --git a/surfsense_backend/app/connectors/jira_history.py b/surfsense_backend/app/connectors/jira_history.py index e9f28a2c4..30162964e 100644 --- a/surfsense_backend/app/connectors/jira_history.py +++ b/surfsense_backend/app/connectors/jira_history.py @@ -14,7 +14,6 @@ from sqlalchemy.future import select from app.config import config from app.connectors.jira_connector import JiraConnector from app.db import SearchSourceConnector -from app.routes.jira_add_connector_route import refresh_jira_token from app.schemas.atlassian_auth_credentials import AtlassianAuthCredentialsBase from app.utils.oauth_security import TokenEncryption @@ -184,7 +183,9 @@ class JiraHistoryConnector: f"Connector {self._connector_id} not found; cannot refresh token." ) - # Refresh token + # Lazy import to avoid circular dependency + from app.routes.jira_add_connector_route import refresh_jira_token + connector = await refresh_jira_token(self._session, connector) # Reload credentials after refresh diff --git a/surfsense_backend/app/tasks/chat/stream_new_chat.py b/surfsense_backend/app/tasks/chat/stream_new_chat.py index e33b67902..7afc2a262 100644 --- a/surfsense_backend/app/tasks/chat/stream_new_chat.py +++ b/surfsense_backend/app/tasks/chat/stream_new_chat.py @@ -880,6 +880,12 @@ async def _stream_agent_events( "create_calendar_event", "update_calendar_event", "delete_calendar_event", + "create_jira_issue", + "update_jira_issue", + "delete_jira_issue", + "create_confluence_page", + "update_confluence_page", + "delete_confluence_page", ): yield streaming_service.format_tool_output_available( tool_call_id, diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index ce811c690..eef9f1dd0 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -90,7 +90,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import { CONNECTOR_TOOL_ICON_PATHS, getToolIcon } from "@/contracts/enums/toolIcons"; +import { CONNECTOR_ICON_TO_TYPES, CONNECTOR_TOOL_ICON_PATHS, getToolIcon } from "@/contracts/enums/toolIcons"; import type { Document } from "@/contracts/types/document.types"; import { useBatchCommentsPreload } from "@/hooks/use-comments"; import { useCommentsElectric } from "@/hooks/use-comments-electric"; @@ -603,6 +603,12 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false const setDisabledTools = useSetAtom(disabledToolsAtom); const hydrateDisabled = useSetAtom(hydrateDisabledToolsAtom); + const { data: connectors } = useAtomValue(connectorsAtom); + const connectedTypes = useMemo( + () => new Set((connectors ?? []).map((c) => c.connector_type)), + [connectors] + ); + const toggleToolGroup = useCallback( (toolNames: string[]) => { const allDisabled = toolNames.every((name) => disabledTools.includes(name)); @@ -628,6 +634,15 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false const placed = new Set(); for (const group of TOOL_GROUPS) { + if (group.connectorIcon) { + const requiredTypes = CONNECTOR_ICON_TO_TYPES[group.connectorIcon]; + const isConnected = requiredTypes?.some((t) => connectedTypes.has(t)); + if (!isConnected) { + for (const name of group.tools) placed.add(name); + continue; + } + } + const matched = group.tools.flatMap((name) => { const tool = toolsByName.get(name); if (!tool) return []; @@ -645,7 +660,7 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false } return result; - }, [filteredTools]); + }, [filteredTools, connectedTypes]); const { visibleTotal, visibleEnabled } = useMemo(() => { let total = 0; @@ -669,6 +684,30 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false hydrateDisabled(); }, [hydrateDisabled]); + useEffect(() => { + const unavailable: string[] = []; + for (const group of TOOL_GROUPS) { + if (!group.connectorIcon) continue; + const requiredTypes = CONNECTOR_ICON_TO_TYPES[group.connectorIcon]; + const isConnected = requiredTypes?.some((t) => connectedTypes.has(t)); + if (!isConnected) { + unavailable.push(...group.tools); + } + } + if (unavailable.length === 0) return; + setDisabledTools((prev) => { + const next = new Set(prev); + let changed = false; + for (const name of unavailable) { + if (!next.has(name)) { + next.add(name); + changed = true; + } + } + return changed ? [...next] : prev; + }); + }, [connectedTypes, setDisabledTools]); + const hasModelConfigured = useMemo(() => { if (!preferences) return false; const agentLlmId = preferences.agent_llm_id; @@ -1092,6 +1131,18 @@ const TOOL_GROUPS: ToolGroup[] = [ connectorIcon: "linear", tooltip: "Create, update, and delete issues in Linear.", }, + { + label: "Jira", + tools: ["create_jira_issue", "update_jira_issue", "delete_jira_issue"], + connectorIcon: "jira", + tooltip: "Create, update, and delete issues in Jira.", + }, + { + label: "Confluence", + tools: ["create_confluence_page", "update_confluence_page", "delete_confluence_page"], + connectorIcon: "confluence", + tooltip: "Create, update, and delete pages in Confluence.", + }, ]; const MessageError: FC = () => { diff --git a/surfsense_web/contracts/enums/toolIcons.tsx b/surfsense_web/contracts/enums/toolIcons.tsx index d8317345c..028c36168 100644 --- a/surfsense_web/contracts/enums/toolIcons.tsx +++ b/surfsense_web/contracts/enums/toolIcons.tsx @@ -37,4 +37,16 @@ export const CONNECTOR_TOOL_ICON_PATHS: Record = { + gmail: ["GOOGLE_GMAIL_CONNECTOR", "COMPOSIO_GMAIL_CONNECTOR"], + google_calendar: ["GOOGLE_CALENDAR_CONNECTOR", "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR"], + google_drive: ["GOOGLE_DRIVE_CONNECTOR", "COMPOSIO_GOOGLE_DRIVE_CONNECTOR"], + notion: ["NOTION_CONNECTOR"], + linear: ["LINEAR_CONNECTOR"], + jira: ["JIRA_CONNECTOR"], + confluence: ["CONFLUENCE_CONNECTOR"], };