diff --git a/apps/sim/app/api/mothership/chats/[chatId]/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/route.ts index b3a86bc8a2..91c88712cf 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/route.ts @@ -142,7 +142,7 @@ export const PATCH = withRouteHandler( const parsed = await parseRequest(updateMothershipChatContract, request, context) if (!parsed.success) return parsed.response const { chatId } = parsed.data.params - const { title, isUnread } = parsed.data.body + const { title, isUnread, pinned } = parsed.data.body const updates: Record = {} @@ -157,6 +157,9 @@ export const PATCH = withRouteHandler( if (isUnread !== undefined) { updates.lastSeenAt = isUnread ? null : sql`GREATEST(${copilotChats.updatedAt}, NOW())` } + if (pinned !== undefined) { + updates.pinned = pinned + } const [updatedChat] = await db .update(copilotChats) @@ -203,6 +206,16 @@ export const PATCH = withRouteHandler( } ) } + if (pinned !== undefined) { + captureServerEvent( + userId, + pinned ? 'task_pinned' : 'task_unpinned', + { workspace_id: updatedChat.workspaceId }, + { + groups: { workspace: updatedChat.workspaceId }, + } + ) + } } return NextResponse.json({ success: true }) diff --git a/apps/sim/app/api/mothership/chats/route.ts b/apps/sim/app/api/mothership/chats/route.ts index b0a068fabc..f6d2d9eae3 100644 --- a/apps/sim/app/api/mothership/chats/route.ts +++ b/apps/sim/app/api/mothership/chats/route.ts @@ -45,6 +45,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { updatedAt: copilotChats.updatedAt, activeStreamId: copilotChats.conversationId, lastSeenAt: copilotChats.lastSeenAt, + pinned: copilotChats.pinned, }) .from(copilotChats) .where( @@ -54,7 +55,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { eq(copilotChats.type, 'mothership') ) ) - .orderBy(desc(copilotChats.updatedAt)) + .orderBy(desc(copilotChats.pinned), desc(copilotChats.updatedAt)) const streamMarkers = await reconcileChatStreamMarkers( chats.map((c) => ({ chatId: c.id, streamId: c.activeStreamId })), diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index ab876adca8..5c516adc01 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -1,6 +1,7 @@ 'use client' import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Pin, PinOff } from 'lucide-react' import { Button, DropdownMenu, @@ -234,6 +235,7 @@ interface ContextMenuProps { onOpenInNewTab?: () => void onMarkAsRead?: () => void onMarkAsUnread?: () => void + onTogglePin?: () => void onRename?: () => void onCreate?: () => void onCreateFolder?: () => void @@ -245,6 +247,8 @@ interface ContextMenuProps { showOpenInNewTab?: boolean showMarkAsRead?: boolean showMarkAsUnread?: boolean + showPin?: boolean + isPinned?: boolean showRename?: boolean showCreate?: boolean showCreateFolder?: boolean @@ -288,6 +292,7 @@ export function ContextMenu({ onOpenInNewTab, onMarkAsRead, onMarkAsUnread, + onTogglePin, onRename, onCreate, onCreateFolder, @@ -299,6 +304,8 @@ export function ContextMenu({ showOpenInNewTab = false, showMarkAsRead = false, showMarkAsUnread = false, + showPin = false, + isPinned = false, showRename = true, showCreate = false, showCreateFolder = false, @@ -375,7 +382,10 @@ export function ContextMenu({ }, []) const hasNavigationSection = showOpenInNewTab && onOpenInNewTab - const hasStatusSection = (showMarkAsRead && onMarkAsRead) || (showMarkAsUnread && onMarkAsUnread) + const hasStatusSection = + (showMarkAsRead && onMarkAsRead) || + (showMarkAsUnread && onMarkAsUnread) || + (showPin && onTogglePin) const hasEditSection = (showRename && onRename) || (showCreate && onCreate) || @@ -447,6 +457,17 @@ export function ContextMenu({ Mark as unread )} + {showPin && onTogglePin && ( + { + onTogglePin() + onClose() + }} + > + {isPinned ? : } + {isPinned ? 'Unpin' : 'Pin'} + + )} {hasStatusSection && (hasEditSection || hasCopySection) && } {showRename && onRename && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 29d403eb5a..f7c81bafb6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { Compass, MoreHorizontal } from 'lucide-react' +import { Compass, MoreHorizontal, Pin } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' @@ -91,6 +91,7 @@ import { useMarkTaskRead, useMarkTaskUnread, useRenameTask, + useSetTaskPinned, useTasks, } from '@/hooks/queries/tasks' import { useUpdateWorkflow } from '@/hooks/queries/workflows' @@ -144,6 +145,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ isSelected, isActive, isUnread, + isPinned, isMenuOpen, showCollapsedTooltips, onMultiSelectClick, @@ -156,6 +158,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ isSelected: boolean isActive: boolean isUnread: boolean + isPinned: boolean isMenuOpen: boolean showCollapsedTooltips: boolean onMultiSelectClick: (taskId: string, shiftKey: boolean) => void @@ -219,6 +222,9 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ {!isActive && isUnread && !isCurrentRoute && !isMenuOpen && ( )} + {!isActive && !isUnread && isPinned && !isCurrentRoute && !isMenuOpen && ( + + )}