From 5fbce4470be64ae3c8bc95e53e19217e8d6431f9 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 13 May 2026 14:04:21 -0700 Subject: [PATCH 1/4] feat(mothership): pin tasks to keep them at the top of the sidebar --- .../api/mothership/chats/[chatId]/route.ts | 15 +- apps/sim/app/api/mothership/chats/route.ts | 3 +- .../components/context-menu/context-menu.tsx | 26 +- .../w/components/sidebar/sidebar.tsx | 22 +- apps/sim/hooks/queries/tasks.test.ts | 3 + apps/sim/hooks/queries/tasks.ts | 55 + .../sim/lib/api/contracts/mothership-tasks.ts | 11 +- .../0207_colorful_secret_warriors.sql | 1 + .../db/migrations/meta/0207_snapshot.json | 15873 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 1 + 11 files changed, 16010 insertions(+), 7 deletions(-) create mode 100644 packages/db/migrations/0207_colorful_secret_warriors.sql create mode 100644 packages/db/migrations/meta/0207_snapshot.json 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..ed201d76e4 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,9 @@ interface ContextMenuProps { showOpenInNewTab?: boolean showMarkAsRead?: boolean showMarkAsUnread?: boolean + showPin?: boolean + isPinned?: boolean + disablePin?: boolean showRename?: boolean showCreate?: boolean showCreateFolder?: boolean @@ -288,6 +293,7 @@ export function ContextMenu({ onOpenInNewTab, onMarkAsRead, onMarkAsUnread, + onTogglePin, onRename, onCreate, onCreateFolder, @@ -299,6 +305,9 @@ export function ContextMenu({ showOpenInNewTab = false, showMarkAsRead = false, showMarkAsUnread = false, + showPin = false, + isPinned = false, + disablePin = false, showRename = true, showCreate = false, showCreateFolder = false, @@ -375,7 +384,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 +459,18 @@ 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..8ac7cc29d2 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 && ( + + )}