From 8e3c69c05d8d3e905856e536f185ca56da7e6d14 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 8 Jun 2026 12:41:22 -0700 Subject: [PATCH 1/2] refactor(mothership-chats): rename task feature to chat, move route, add redirect - Move workspace route /task/[taskId] -> /chat/[chatId] and add a permanent redirect in next.config so existing bookmarks/deeplinks keep working - Rename client hooks queries/tasks.ts -> queries/mothership-chats.ts with the mothership-chat-prefixed family (distinct from the existing deployed-chat hooks in queries/chats.ts to avoid query-key collisions) - Rename contract mothership-tasks.ts -> mothership-chats.ts (MothershipChat) - Rename lib/copilot/tasks.ts -> chat-status.ts (chatPubSub, ChatStatusEvent) - Rename use-task-events -> use-mothership-chat-events, use-task-selection -> use-chat-selection, and folder-store chat-selection methods - Update server-generated deeplinks (oauth callback, inbox response, inbox list) - Preserve wire/persisted/analytics values that cross process or deploy boundaries: Redis channel task:status_changed, SSE event task_status, MothershipResourceType 'task', posthog event names, drag/itemType discriminants. Unrelated scheduled-tasks and email-inbox tasks untouched --- apps/sim/app/api/admin/mothership/route.ts | 2 +- .../app/api/copilot/chat/delete/route.test.ts | 4 +- apps/sim/app/api/copilot/chat/delete/route.ts | 4 +- apps/sim/app/api/copilot/chat/rename/route.ts | 4 +- .../app/api/copilot/chat/stop/route.test.ts | 4 +- apps/sim/app/api/copilot/chat/stop/route.ts | 4 +- apps/sim/app/api/copilot/chats/route.ts | 4 +- .../app/api/mothership/chat/abort/route.ts | 2 +- .../api/mothership/chat/resources/route.ts | 2 +- apps/sim/app/api/mothership/chat/route.ts | 2 +- .../sim/app/api/mothership/chat/stop/route.ts | 2 +- .../app/api/mothership/chat/stream/route.ts | 2 +- .../mothership/chats/[chatId]/fork/route.ts | 6 +- .../mothership/chats/[chatId]/route.test.ts | 4 +- .../api/mothership/chats/[chatId]/route.ts | 8 +- .../app/api/mothership/chats/read/route.ts | 2 +- .../app/api/mothership/chats/route.test.ts | 4 +- apps/sim/app/api/mothership/chats/route.ts | 6 +- apps/sim/app/api/mothership/events/route.ts | 8 +- apps/sim/app/api/mothership/execute/route.ts | 2 +- .../[taskId] => chat/[chatId]}/layout.tsx | 2 +- .../{task/[taskId] => chat/[chatId]}/page.tsx | 12 +- .../message-actions/message-actions.tsx | 16 +- .../add-resource-dropdown.tsx | 4 +- .../resource-registry/resource-registry.tsx | 4 +- .../resource-tabs/resource-tabs.tsx | 4 +- .../app/workspace/[workspaceId]/home/home.tsx | 9 +- .../[workspaceId]/home/hooks/use-chat.ts | 127 +++-- .../inbox-task-list/inbox-task-list.tsx | 2 +- .../collapsed-sidebar-menu.tsx | 32 +- .../collapsed-sidebar-menu/index.ts | 2 +- .../w/components/sidebar/components/index.ts | 2 +- .../search-modal/components/index.ts | 2 +- .../components/search-groups/index.ts | 2 +- .../search-groups/search-groups.tsx | 2 +- .../components/search-modal/search-modal.tsx | 18 +- .../sidebar/components/search-modal/utils.ts | 2 +- .../w/components/sidebar/hooks/index.ts | 2 +- .../sidebar/hooks/use-chat-selection.ts | 44 ++ .../sidebar/hooks/use-task-selection.ts | 44 -- .../w/components/sidebar/sidebar.tsx | 476 +++++++++--------- ...tasks.test.ts => mothership-chats.test.ts} | 16 +- .../queries/{tasks.ts => mothership-chats.ts} | 280 ++++++----- ....ts => use-mothership-chat-events.test.ts} | 84 ++-- ...vents.ts => use-mothership-chat-events.ts} | 55 +- ...othership-tasks.ts => mothership-chats.ts} | 6 +- apps/sim/lib/copilot/chat-status.ts | 42 ++ apps/sim/lib/copilot/chat/post.test.ts | 4 +- apps/sim/lib/copilot/chat/post.ts | 10 +- .../copilot/request/lifecycle/start.test.ts | 4 +- .../lib/copilot/request/lifecycle/start.ts | 4 +- apps/sim/lib/copilot/tasks.ts | 40 -- apps/sim/lib/copilot/tools/handlers/oauth.ts | 2 +- apps/sim/lib/mothership/inbox/executor.ts | 10 +- apps/sim/lib/mothership/inbox/response.ts | 2 +- apps/sim/next.config.ts | 9 + apps/sim/stores/folders/store.ts | 94 ++-- 57 files changed, 803 insertions(+), 743 deletions(-) rename apps/sim/app/workspace/[workspaceId]/{task/[taskId] => chat/[chatId]}/layout.tsx (62%) rename apps/sim/app/workspace/[workspaceId]/{task/[taskId] => chat/[chatId]}/page.tsx (68%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-chat-selection.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-task-selection.ts rename apps/sim/hooks/queries/{tasks.test.ts => mothership-chats.test.ts} (90%) rename apps/sim/hooks/queries/{tasks.ts => mothership-chats.ts} (62%) rename apps/sim/hooks/{use-task-events.test.ts => use-mothership-chat-events.test.ts} (83%) rename apps/sim/hooks/{use-task-events.ts => use-mothership-chat-events.ts} (66%) rename apps/sim/lib/api/contracts/{mothership-tasks.ts => mothership-chats.ts} (98%) create mode 100644 apps/sim/lib/copilot/chat-status.ts delete mode 100644 apps/sim/lib/copilot/tasks.ts diff --git a/apps/sim/app/api/admin/mothership/route.ts b/apps/sim/app/api/admin/mothership/route.ts index a3a022f9d82..a5eb0245de1 100644 --- a/apps/sim/app/api/admin/mothership/route.ts +++ b/apps/sim/app/api/admin/mothership/route.ts @@ -3,7 +3,7 @@ import { settings, user } from '@sim/db/schema' import { getErrorMessage } from '@sim/utils/errors' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { adminMothershipQuerySchema } from '@/lib/api/contracts/mothership-tasks' +import { adminMothershipQuerySchema } from '@/lib/api/contracts/mothership-chats' import { mothershipEnvironmentSchema } from '@/lib/api/contracts/user' import { searchParamsToObject, validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' diff --git a/apps/sim/app/api/copilot/chat/delete/route.test.ts b/apps/sim/app/api/copilot/chat/delete/route.test.ts index ccabaf7c521..a84de630726 100644 --- a/apps/sim/app/api/copilot/chat/delete/route.test.ts +++ b/apps/sim/app/api/copilot/chat/delete/route.test.ts @@ -19,8 +19,8 @@ vi.mock('@/lib/copilot/chat/lifecycle', () => ({ getAccessibleCopilotChatAuth: mockGetAccessibleCopilotChatAuth, })) -vi.mock('@/lib/copilot/tasks', () => ({ - taskPubSub: { publishStatusChanged: vi.fn() }, +vi.mock('@/lib/copilot/chat-status', () => ({ + chatPubSub: { publishStatusChanged: vi.fn() }, })) import { DELETE } from './route' diff --git a/apps/sim/app/api/copilot/chat/delete/route.ts b/apps/sim/app/api/copilot/chat/delete/route.ts index fd06735a91f..ccd3ba7334a 100644 --- a/apps/sim/app/api/copilot/chat/delete/route.ts +++ b/apps/sim/app/api/copilot/chat/delete/route.ts @@ -7,7 +7,7 @@ import { deleteCopilotChatContract } from '@/lib/api/contracts/copilot' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { getAccessibleCopilotChatAuth } from '@/lib/copilot/chat/lifecycle' -import { taskPubSub } from '@/lib/copilot/tasks' +import { chatPubSub } from '@/lib/copilot/chat-status' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('DeleteChatAPI') @@ -47,7 +47,7 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => { logger.info('Chat deleted', { chatId: parsed.chatId }) if (deleted.workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: deleted.workspaceId, chatId: parsed.chatId, type: 'deleted', diff --git a/apps/sim/app/api/copilot/chat/rename/route.ts b/apps/sim/app/api/copilot/chat/rename/route.ts index 6886f568be6..5022fbfce74 100644 --- a/apps/sim/app/api/copilot/chat/rename/route.ts +++ b/apps/sim/app/api/copilot/chat/rename/route.ts @@ -7,7 +7,7 @@ import { renameCopilotChatContract } from '@/lib/api/contracts/copilot' import { parseRequest, validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { getAccessibleCopilotChatAuth } from '@/lib/copilot/chat/lifecycle' -import { taskPubSub } from '@/lib/copilot/tasks' +import { chatPubSub } from '@/lib/copilot/chat-status' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('RenameChatAPI') @@ -49,7 +49,7 @@ export const PATCH = withRouteHandler(async (request: NextRequest) => { logger.info('Chat renamed', { chatId, title }) if (updated.workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: updated.workspaceId, chatId, type: 'renamed', diff --git a/apps/sim/app/api/copilot/chat/stop/route.test.ts b/apps/sim/app/api/copilot/chat/stop/route.test.ts index 452131f21e1..e734cd9cde4 100644 --- a/apps/sim/app/api/copilot/chat/stop/route.test.ts +++ b/apps/sim/app/api/copilot/chat/stop/route.test.ts @@ -16,8 +16,8 @@ vi.mock('@/lib/copilot/chat/messages-store', () => ({ appendCopilotChatMessages: mockAppendCopilotChatMessages, })) -vi.mock('@/lib/copilot/tasks', () => ({ - taskPubSub: { +vi.mock('@/lib/copilot/chat-status', () => ({ + chatPubSub: { publishStatusChanged: mockPublishStatusChanged, }, })) diff --git a/apps/sim/app/api/copilot/chat/stop/route.ts b/apps/sim/app/api/copilot/chat/stop/route.ts index 05d7303d94c..91f17dbcff3 100644 --- a/apps/sim/app/api/copilot/chat/stop/route.ts +++ b/apps/sim/app/api/copilot/chat/stop/route.ts @@ -10,6 +10,7 @@ import { withStoppedContentBlock, } from '@/lib/copilot/chat/persisted-message' import { finalizeAssistantTurn } from '@/lib/copilot/chat/terminal-state' +import { chatPubSub } from '@/lib/copilot/chat-status' import { CopilotChatFinalizeOutcome, CopilotStopOutcome, @@ -17,7 +18,6 @@ import { import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1' import { withIncomingGoSpan } from '@/lib/copilot/request/otel' -import { taskPubSub } from '@/lib/copilot/tasks' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('CopilotChatStopAPI') @@ -83,7 +83,7 @@ export const POST = withRouteHandler((req: NextRequest) => result.updated || result.outcome === CopilotChatFinalizeOutcome.AssistantAlreadyPersisted if (shouldPublishCompleted && result.workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: result.workspaceId, chatId, type: 'completed', diff --git a/apps/sim/app/api/copilot/chats/route.ts b/apps/sim/app/api/copilot/chats/route.ts index 05e4c7773db..c72bfa1c8ba 100644 --- a/apps/sim/app/api/copilot/chats/route.ts +++ b/apps/sim/app/api/copilot/chats/route.ts @@ -7,6 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { createWorkflowCopilotChatContract } from '@/lib/api/contracts/copilot' import { parseRequest, validationErrorResponse } from '@/lib/api/server' import { resolveOrCreateChat } from '@/lib/copilot/chat/lifecycle' +import { chatPubSub } from '@/lib/copilot/chat-status' import { authenticateCopilotRequestSessionOnly, createBadRequestResponse, @@ -14,7 +15,6 @@ import { createInternalServerErrorResponse, createUnauthorizedResponse, } from '@/lib/copilot/request/http' -import { taskPubSub } from '@/lib/copilot/tasks' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { assertActiveWorkspaceAccess, @@ -138,7 +138,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return createInternalServerErrorResponse('Failed to create chat') } - taskPubSub?.publishStatusChanged({ workspaceId, chatId: result.chatId, type: 'created' }) + chatPubSub?.publishStatusChanged({ workspaceId, chatId: result.chatId, type: 'created' }) return NextResponse.json({ success: true, id: result.chatId }) } catch (error) { diff --git a/apps/sim/app/api/mothership/chat/abort/route.ts b/apps/sim/app/api/mothership/chat/abort/route.ts index 4b62fa4ebd3..c505fd9ab28 100644 --- a/apps/sim/app/api/mothership/chat/abort/route.ts +++ b/apps/sim/app/api/mothership/chat/abort/route.ts @@ -1,5 +1,5 @@ import type { NextRequest } from 'next/server' -import { mothershipChatAbortEnvelopeSchema } from '@/lib/api/contracts/mothership-tasks' +import { mothershipChatAbortEnvelopeSchema } from '@/lib/api/contracts/mothership-chats' import { validationErrorResponse } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { POST as copilotAbortPost } from '@/app/api/copilot/chat/abort/route' diff --git a/apps/sim/app/api/mothership/chat/resources/route.ts b/apps/sim/app/api/mothership/chat/resources/route.ts index 169f16e4148..d377f421b22 100644 --- a/apps/sim/app/api/mothership/chat/resources/route.ts +++ b/apps/sim/app/api/mothership/chat/resources/route.ts @@ -1,5 +1,5 @@ import type { NextRequest, NextResponse } from 'next/server' -import { mothershipChatResourceEnvelopeSchema } from '@/lib/api/contracts/mothership-tasks' +import { mothershipChatResourceEnvelopeSchema } from '@/lib/api/contracts/mothership-chats' import { validationErrorResponse } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { diff --git a/apps/sim/app/api/mothership/chat/route.ts b/apps/sim/app/api/mothership/chat/route.ts index f6167b7c3e0..6351971fd8c 100644 --- a/apps/sim/app/api/mothership/chat/route.ts +++ b/apps/sim/app/api/mothership/chat/route.ts @@ -2,7 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { mothershipChatGetQuerySchema, mothershipChatPostEnvelopeSchema, -} from '@/lib/api/contracts/mothership-tasks' +} from '@/lib/api/contracts/mothership-chats' import { validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { handleUnifiedChatPost, maxDuration } from '@/lib/copilot/chat/post' diff --git a/apps/sim/app/api/mothership/chat/stop/route.ts b/apps/sim/app/api/mothership/chat/stop/route.ts index 5ffd63dd1fb..7ec211ce4f8 100644 --- a/apps/sim/app/api/mothership/chat/stop/route.ts +++ b/apps/sim/app/api/mothership/chat/stop/route.ts @@ -1,5 +1,5 @@ import type { NextRequest } from 'next/server' -import { mothershipChatStopEnvelopeSchema } from '@/lib/api/contracts/mothership-tasks' +import { mothershipChatStopEnvelopeSchema } from '@/lib/api/contracts/mothership-chats' import { validationErrorResponse } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { POST as copilotStopPost } from '@/app/api/copilot/chat/stop/route' diff --git a/apps/sim/app/api/mothership/chat/stream/route.ts b/apps/sim/app/api/mothership/chat/stream/route.ts index 4707d5dbb16..9734d56c126 100644 --- a/apps/sim/app/api/mothership/chat/stream/route.ts +++ b/apps/sim/app/api/mothership/chat/stream/route.ts @@ -1,5 +1,5 @@ import type { NextRequest } from 'next/server' -import { mothershipChatStreamQuerySchema } from '@/lib/api/contracts/mothership-tasks' +import { mothershipChatStreamQuerySchema } from '@/lib/api/contracts/mothership-chats' import { validationErrorResponse } from '@/lib/api/server' import { GET as copilotStreamGet, maxDuration } from '@/app/api/copilot/chat/stream/route' diff --git a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts index ed2bd5b59e9..44b63c7f163 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts @@ -4,10 +4,11 @@ import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { forkMothershipChatContract } from '@/lib/api/contracts/mothership-tasks' +import { forkMothershipChatContract } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' import { loadCopilotChatMessages } from '@/lib/copilot/chat/lifecycle' import { appendCopilotChatMessages } from '@/lib/copilot/chat/messages-store' +import { chatPubSub } from '@/lib/copilot/chat-status' import { fetchGo } from '@/lib/copilot/request/go/fetch' import { authenticateCopilotRequestSessionOnly, @@ -19,7 +20,6 @@ import { } from '@/lib/copilot/request/http' import type { MothershipResource } from '@/lib/copilot/resources/types' import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url' -import { taskPubSub } from '@/lib/copilot/tasks' import { env } from '@/lib/core/config/env' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' @@ -154,7 +154,7 @@ export const POST = withRouteHandler( } if (newChat.workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: newChat.workspaceId, chatId: newId, type: 'created', diff --git a/apps/sim/app/api/mothership/chats/[chatId]/route.test.ts b/apps/sim/app/api/mothership/chats/[chatId]/route.test.ts index 7e96d476220..e46b2eb1314 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/route.test.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/route.test.ts @@ -79,8 +79,8 @@ vi.mock('@/lib/copilot/chat/persisted-message', () => ({ normalizeMessage: (m: unknown) => m, })) -vi.mock('@/lib/copilot/tasks', () => ({ - taskPubSub: { publishStatusChanged: vi.fn() }, +vi.mock('@/lib/copilot/chat-status', () => ({ + chatPubSub: { publishStatusChanged: vi.fn() }, })) vi.mock('@/lib/posthog/server', () => ({ diff --git a/apps/sim/app/api/mothership/chats/[chatId]/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/route.ts index 2d7c20c1b1f..2274a9582c7 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/route.ts @@ -8,7 +8,7 @@ import { deleteMothershipChatContract, getMothershipChatContract, updateMothershipChatContract, -} from '@/lib/api/contracts/mothership-tasks' +} from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository' import { buildEffectiveChatTranscript } from '@/lib/copilot/chat/effective-transcript' @@ -18,6 +18,7 @@ import { } from '@/lib/copilot/chat/lifecycle' import { normalizeMessage } from '@/lib/copilot/chat/persisted-message' import { reconcileChatStreamMarkers } from '@/lib/copilot/chat/stream-liveness' +import { chatPubSub } from '@/lib/copilot/chat-status' import { authenticateCopilotRequestSessionOnly, createInternalServerErrorResponse, @@ -27,7 +28,6 @@ import type { FilePreviewSession } from '@/lib/copilot/request/session' import { readEvents } from '@/lib/copilot/request/session/buffer' import { readFilePreviewSessions } from '@/lib/copilot/request/session/file-preview-session' import { type StreamBatchEvent, toStreamBatchEvent } from '@/lib/copilot/request/session/types' -import { taskPubSub } from '@/lib/copilot/tasks' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' @@ -185,7 +185,7 @@ export const PATCH = withRouteHandler( if (updatedChat.workspaceId) { if (title !== undefined) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: updatedChat.workspaceId, chatId, type: 'renamed', @@ -264,7 +264,7 @@ export const DELETE = withRouteHandler( } if (deletedChat.workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: deletedChat.workspaceId, chatId, type: 'deleted', diff --git a/apps/sim/app/api/mothership/chats/read/route.ts b/apps/sim/app/api/mothership/chats/read/route.ts index 2973d1bbe89..2cda04a65f2 100644 --- a/apps/sim/app/api/mothership/chats/read/route.ts +++ b/apps/sim/app/api/mothership/chats/read/route.ts @@ -3,7 +3,7 @@ import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { markMothershipChatReadContract } from '@/lib/api/contracts/mothership-tasks' +import { markMothershipChatReadContract } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' import { authenticateCopilotRequestSessionOnly, diff --git a/apps/sim/app/api/mothership/chats/route.test.ts b/apps/sim/app/api/mothership/chats/route.test.ts index 5851d1b45df..f23d33034ea 100644 --- a/apps/sim/app/api/mothership/chats/route.test.ts +++ b/apps/sim/app/api/mothership/chats/route.test.ts @@ -47,8 +47,8 @@ vi.mock('@/lib/copilot/chat/stream-liveness', () => ({ reconcileChatStreamMarkers: mockReconcileChatStreamMarkers, })) -vi.mock('@/lib/copilot/tasks', () => ({ - taskPubSub: { publishStatusChanged: vi.fn() }, +vi.mock('@/lib/copilot/chat-status', () => ({ + chatPubSub: { publishStatusChanged: vi.fn() }, })) vi.mock('@/lib/posthog/server', () => ({ diff --git a/apps/sim/app/api/mothership/chats/route.ts b/apps/sim/app/api/mothership/chats/route.ts index c5610da215d..b29abfa514d 100644 --- a/apps/sim/app/api/mothership/chats/route.ts +++ b/apps/sim/app/api/mothership/chats/route.ts @@ -6,16 +6,16 @@ import { type NextRequest, NextResponse } from 'next/server' import { createMothershipChatContract, listMothershipChatsContract, -} from '@/lib/api/contracts/mothership-tasks' +} from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' import { reconcileChatStreamMarkers } from '@/lib/copilot/chat/stream-liveness' +import { chatPubSub } from '@/lib/copilot/chat-status' import { authenticateCopilotRequestSessionOnly, createForbiddenResponse, createInternalServerErrorResponse, createUnauthorizedResponse, } from '@/lib/copilot/request/http' -import { taskPubSub } from '@/lib/copilot/tasks' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { @@ -111,7 +111,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) .returning({ id: copilotChats.id }) - taskPubSub?.publishStatusChanged({ workspaceId, chatId: chat.id, type: 'created' }) + chatPubSub?.publishStatusChanged({ workspaceId, chatId: chat.id, type: 'created' }) captureServerEvent( userId, diff --git a/apps/sim/app/api/mothership/events/route.ts b/apps/sim/app/api/mothership/events/route.ts index bb3e1f278c8..5509358254d 100644 --- a/apps/sim/app/api/mothership/events/route.ts +++ b/apps/sim/app/api/mothership/events/route.ts @@ -8,9 +8,9 @@ */ import type { NextRequest } from 'next/server' -import { mothershipEventsQuerySchema } from '@/lib/api/contracts/mothership-tasks' +import { mothershipEventsQuerySchema } from '@/lib/api/contracts/mothership-chats' import { validationErrorResponse } from '@/lib/api/server' -import { taskPubSub } from '@/lib/copilot/tasks' +import { chatPubSub } from '@/lib/copilot/chat-status' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { createWorkspaceSSE } from '@/lib/events/sse-endpoint' @@ -21,8 +21,8 @@ const mothershipEventsHandler = createWorkspaceSSE({ subscriptions: [ { subscribe: (workspaceId, send) => { - if (!taskPubSub) return () => {} - return taskPubSub.onStatusChanged((event) => { + if (!chatPubSub) return () => {} + return chatPubSub.onStatusChanged((event) => { if (event.workspaceId !== workspaceId) return send('task_status', { chatId: event.chatId, diff --git a/apps/sim/app/api/mothership/execute/route.ts b/apps/sim/app/api/mothership/execute/route.ts index a3550718b92..43a0bdfd965 100644 --- a/apps/sim/app/api/mothership/execute/route.ts +++ b/apps/sim/app/api/mothership/execute/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { type NextRequest, NextResponse } from 'next/server' -import { mothershipExecuteContract } from '@/lib/api/contracts/mothership-tasks' +import { mothershipExecuteContract } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { buildIntegrationToolSchemas } from '@/lib/copilot/chat/payload' diff --git a/apps/sim/app/workspace/[workspaceId]/task/[taskId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/chat/[chatId]/layout.tsx similarity index 62% rename from apps/sim/app/workspace/[workspaceId]/task/[taskId]/layout.tsx rename to apps/sim/app/workspace/[workspaceId]/chat/[chatId]/layout.tsx index b317512345e..6dc5b2e20bd 100644 --- a/apps/sim/app/workspace/[workspaceId]/task/[taskId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/chat/[chatId]/layout.tsx @@ -1,3 +1,3 @@ -export default function TaskLayout({ children }: { children: React.ReactNode }) { +export default function ChatLayout({ children }: { children: React.ReactNode }) { return
{children}
} diff --git a/apps/sim/app/workspace/[workspaceId]/task/[taskId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/chat/[chatId]/page.tsx similarity index 68% rename from apps/sim/app/workspace/[workspaceId]/task/[taskId]/page.tsx rename to apps/sim/app/workspace/[workspaceId]/chat/[chatId]/page.tsx index b22949113b3..7bd786290ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/task/[taskId]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/chat/[chatId]/page.tsx @@ -6,24 +6,24 @@ export const metadata: Metadata = { title: 'Chat', } -interface TaskPageProps { +interface ChatPageProps { params: Promise<{ workspaceId: string - taskId: string + chatId: string }> searchParams: Promise<{ resource?: string }> } -export default async function TaskPage({ params, searchParams }: TaskPageProps) { - const [{ taskId }, { resource }, session] = await Promise.all([ +export default async function ChatPage({ params, searchParams }: ChatPageProps) { + const [{ chatId }, { resource }, session] = await Promise.all([ params, searchParams, getSession(), ]) return ( (null) const requestIdTimeoutRef = useRef(null) const submitFeedback = useSubmitCopilotFeedback() - const forkTask = useForkTask(params.workspaceId) + const forkChat = useForkMothershipChat(params.workspaceId) useEffect(() => { return () => { @@ -151,11 +151,11 @@ export const MessageActions = memo(function MessageActions({ } const handleFork = async () => { - if (!chatId || !messageId || forkTask.isPending) return + if (!chatId || !messageId || forkChat.isPending) return try { - const result = await forkTask.mutateAsync({ chatId, upToMessageId: messageId }) - useFolderStore.getState().clearTaskSelection() - router.push(`/workspace/${params.workspaceId}/task/${result.id}`) + const result = await forkChat.mutateAsync({ chatId, upToMessageId: messageId }) + useFolderStore.getState().clearChatSelection() + router.push(`/workspace/${params.workspaceId}/chat/${result.id}`) } catch { toast.error('Failed to fork chat') } @@ -223,8 +223,8 @@ export const MessageActions = memo(function MessageActions({ type='button' aria-label='Fork from here' onClick={handleFork} - disabled={forkTask.isPending} - className={cn(BUTTON_CLASS, forkTask.isPending && 'cursor-not-allowed opacity-50')} + disabled={forkChat.isPending} + className={cn(BUTTON_CLASS, forkChat.isPending && 'cursor-not-allowed opacity-50')} > diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx index 59f8c4063fa..889ac5ada6b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx @@ -29,8 +29,8 @@ import { listIntegrations } from '@/blocks/integration-matcher' import { useFolders } from '@/hooks/queries/folders' import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' import { useLogsList } from '@/hooks/queries/logs' +import { useMothershipChats } from '@/hooks/queries/mothership-chats' import { useTablesList } from '@/hooks/queries/tables' -import { useTasks } from '@/hooks/queries/tasks' import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFileFolders } from '@/hooks/queries/workspace-file-folders' import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' @@ -76,7 +76,7 @@ export function useAvailableResources( const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId) const { data: folders = [] } = useFolders(workspaceId) const { data: fileFolders = [] } = useWorkspaceFileFolders(workspaceId) - const { data: tasks = [] } = useTasks(workspaceId) + const { data: tasks = [] } = useMothershipChats(workspaceId) const { data: logsData } = useLogsList(workspaceId, LOG_DROPDOWN_FILTERS) const logs = useMemo(() => (logsData?.pages ?? []).flatMap((page) => page.logs), [logsData]) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx index fa6be31bce3..d41ba2a8770 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx @@ -22,8 +22,8 @@ import type { import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color' import { knowledgeKeys } from '@/hooks/queries/kb/knowledge' import { logKeys } from '@/hooks/queries/logs' +import { mothershipChatKeys } from '@/hooks/queries/mothership-chats' import { tableKeys } from '@/hooks/queries/tables' -import { taskKeys } from '@/hooks/queries/tasks' import { folderKeys } from '@/hooks/queries/utils/folder-keys' import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists' import { workspaceFileFolderKeys } from '@/hooks/queries/workspace-file-folders' @@ -239,7 +239,7 @@ const RESOURCE_INVALIDATORS: Record< qc.invalidateQueries({ queryKey: workspaceFileFolderKeys.workspaceLists(wId) }) }, task: (qc, wId) => { - qc.invalidateQueries({ queryKey: taskKeys.list(wId) }) + qc.invalidateQueries({ queryKey: mothershipChatKeys.list(wId) }) }, log: (qc, _wId, id) => { qc.invalidateQueries({ queryKey: logKeys.details() }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx index 2c3a2210f73..c8411552407 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx @@ -29,12 +29,12 @@ import type { } from '@/app/workspace/[workspaceId]/home/types' import { useFolders } from '@/hooks/queries/folders' import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' -import { useTablesList } from '@/hooks/queries/tables' import { useAddChatResource, useRemoveChatResource, useReorderChatResources, -} from '@/hooks/queries/tasks' +} from '@/hooks/queries/mothership-chats' +import { useTablesList } from '@/hooks/queries/tables' import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 396c214f807..b3bef698d2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -19,7 +19,10 @@ import { } from '@/lib/mothership/events' import { captureEvent } from '@/lib/posthog/client' import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export' -import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks' +import { + useMarkMothershipChatRead, + useMothershipChatHistory, +} from '@/hooks/queries/mothership-chats' import { useOAuthReturnRouter } from '@/hooks/use-oauth-return' import type { ChatContext } from '@/stores/panel' import { @@ -113,8 +116,8 @@ export function Home({ chatId, userName, userId, initialResourceId = null }: Hom const wasSendingRef = useRef(false) - const { isPending: isChatHistoryPending } = useChatHistory(chatId) - const { mutate: markRead } = useMarkTaskRead(workspaceId) + const { isPending: isChatHistoryPending } = useMothershipChatHistory(chatId) + const { mutate: markRead } = useMarkMothershipChatRead(workspaceId) const { mothershipRef, handleResizePointerDown, clearWidth } = useMothershipResize() diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 41284baac19..e2ca729c64c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -10,7 +10,7 @@ import { addMothershipChatResourceContract, removeMothershipChatResourceContract, reorderMothershipChatResourcesContract, -} from '@/lib/api/contracts/mothership-tasks' +} from '@/lib/api/contracts/mothership-chats' import { cancelWorkflowExecutionContract } from '@/lib/api/contracts/workflows' import { getMothershipAttachmentPreviewUrl } from '@/lib/copilot/chat/attachment-preview' import { toDisplayMessage } from '@/lib/copilot/chat/display-message' @@ -118,11 +118,11 @@ import { } from '@/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions' import { deploymentKeys } from '@/hooks/queries/deployments' import { - fetchChatHistory, - type TaskChatHistory, - taskKeys, - useChatHistory, -} from '@/hooks/queries/tasks' + fetchMothershipChatHistory, + type MothershipChatHistory, + mothershipChatKeys, + useMothershipChatHistory, +} from '@/hooks/queries/mothership-chats' import { getFolderMap } from '@/hooks/queries/utils/folder-cache' import { folderKeys } from '@/hooks/queries/utils/folder-keys' import { invalidateWorkflowSelectors } from '@/hooks/queries/utils/invalidate-workflow-lists' @@ -1095,7 +1095,7 @@ function markMessageStopped(message: PersistedMessage): PersistedMessage { }) } -function buildChatHistoryHydrationKey(chatHistory: TaskChatHistory): string { +function buildChatHistoryHydrationKey(chatHistory: MothershipChatHistory): string { const resourceKey = chatHistory.resources .map((resource) => `${resource.type}:${resource.id}:${resource.title}`) .join('|') @@ -1604,18 +1604,21 @@ export function useChat( [queryClient, workspaceId] ) - const upsertTaskChatHistory = useCallback( - (chatId: string, updater: (current: TaskChatHistory) => TaskChatHistory) => { - queryClient.setQueryData(taskKeys.detail(chatId), (current) => { - const base: TaskChatHistory = current ?? { - id: chatId, - title: null, - messages: [], - activeStreamId: null, - resources: resourcesRef.current, + const upsertChatHistory = useCallback( + (chatId: string, updater: (current: MothershipChatHistory) => MothershipChatHistory) => { + queryClient.setQueryData( + mothershipChatKeys.detail(chatId), + (current) => { + const base: MothershipChatHistory = current ?? { + id: chatId, + title: null, + messages: [], + activeStreamId: null, + resources: resourcesRef.current, + } + return updater(base) } - return updater(base) - }) + ) }, [queryClient] ) @@ -1851,7 +1854,7 @@ export function useChat( const abortControllerRef = useRef(null) const streamReaderRef = useRef | null>(null) const chatIdRef = useRef(initialChatId) - /** Panel/task selection — drives createNewChat + request chatId; may differ from chatIdRef while a stream is still finishing. */ + /** Panel/chat selection — drives createNewChat + request chatId; may differ from chatIdRef while a stream is still finishing. */ const selectedChatIdRef = useRef(initialChatId) selectedChatIdRef.current = initialChatId const appliedChatHistoryKeyRef = useRef(undefined) @@ -1923,12 +1926,14 @@ export function useChat( streamId: string, assistantId: string, afterCursor: string, - options?: { targetChatId?: string; chatHistory?: TaskChatHistory } + options?: { targetChatId?: string; chatHistory?: MothershipChatHistory } ): ReconnectReplaySelection => { const cachedHistory = options?.chatHistory ?? (options?.targetChatId - ? queryClient.getQueryData(taskKeys.detail(options.targetChatId)) + ? queryClient.getQueryData( + mothershipChatKeys.detail(options.targetChatId) + ) : undefined) const cachedLiveAssistant = cachedHistory?.messages.find( (message) => message.id === assistantId @@ -2065,17 +2070,17 @@ export function useChat( !workflowIdRef.current && typeof window !== 'undefined' ) { - window.history.replaceState(null, '', `/workspace/${workspaceId}/task/${chatId}`) + window.history.replaceState(null, '', `/workspace/${workspaceId}/chat/${chatId}`) } if (options?.invalidateList) { - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) } flushPendingResources(chatId) }, [flushPendingResources, queryClient, workspaceId] ) - const { data: chatHistory } = useChatHistory(resolvedChatId) + const { data: chatHistory } = useMothershipChatHistory(resolvedChatId) const messages = useMemo(() => { const source = chatHistory?.messages.map(toDisplayMessage) ?? pendingMessages return source.map((m) => restoreRevealedSimKeysForMessage(m, revealedSimKeysRef.current)) @@ -2290,7 +2295,7 @@ export function useChat( clearActiveTurn() setTransportIdle() if (abandonedChatId) { - queryClient.invalidateQueries({ queryKey: taskKeys.detail(abandonedChatId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.detail(abandonedChatId) }) } } else { setResolvedChatId(initialChatId) @@ -2750,7 +2755,7 @@ export function useChat( contentBlocks: blocks, ...(streamRequestId ? { requestId: streamRequestId } : {}), }) - upsertTaskChatHistory(activeChatId, (current) => { + upsertChatHistory(activeChatId, (current) => { const streamId = streamIdRef.current ?? current.activeStreamId ?? assistantId const terminalPersistedAssistantExists = current.activeStreamId !== streamId && @@ -2877,7 +2882,7 @@ export function useChat( setResolvedChatId(payloadChatId) } queryClient.invalidateQueries({ - queryKey: taskKeys.list(workspaceId), + queryKey: mothershipChatKeys.list(workspaceId), }) if (isNewChat) { const userMsg = pendingUserMsgRef.current @@ -2891,27 +2896,30 @@ export function useChat( contentBlocks: streamingBlocksRef.current, }) const seededMessages = [userMsg, assistantMessage] - queryClient.setQueryData(taskKeys.detail(payloadChatId), { - id: payloadChatId, - title: null, - messages: seededMessages, - activeStreamId, - resources: resourcesRef.current, - }) + queryClient.setQueryData( + mothershipChatKeys.detail(payloadChatId), + { + id: payloadChatId, + title: null, + messages: seededMessages, + activeStreamId, + resources: resourcesRef.current, + } + ) } setPendingMessages([]) if (!workflowIdRef.current) { window.history.replaceState( null, '', - `/workspace/${workspaceId}/task/${payloadChatId}` + `/workspace/${workspaceId}/chat/${payloadChatId}` ) } } } if (payload.kind === MothershipStreamV1SessionKind.title) { queryClient.invalidateQueries({ - queryKey: taskKeys.list(workspaceId), + queryKey: mothershipChatKeys.list(workspaceId), }) onTitleUpdateRef.current?.() } @@ -3641,7 +3649,7 @@ export function useChat( workspaceId, router, queryClient, - upsertTaskChatHistory, + upsertChatHistory, addResource, removeResource, applyPreviewSessionUpdate, @@ -3657,16 +3665,18 @@ export function useChat( chatId: string, signal?: AbortSignal ): Promise<{ loaded: boolean; streamId: string | null }> => { - const cached = queryClient.getQueryData(taskKeys.detail(chatId)) + const cached = queryClient.getQueryData( + mothershipChatKeys.detail(chatId) + ) try { const fetchSignal = combineAbortSignals( signal, createTimeoutSignal(CHAT_HISTORY_RECOVERY_TIMEOUT_MS) ) - const history = await fetchChatHistory(chatId, fetchSignal) + const history = await fetchMothershipChatHistory(chatId, fetchSignal) if (signal?.aborted || fetchSignal?.aborted) return { loaded: false, streamId: null } - queryClient.setQueryData(taskKeys.detail(chatId), history) + queryClient.setQueryData(mothershipChatKeys.detail(chatId), history) return { loaded: true, streamId: history.activeStreamId ?? null } } catch (error) { logger.warn('Failed to load chat history while recovering stream', { @@ -4185,7 +4195,9 @@ export function useChat( selectedChatIdRef.current === startingSelectedChatId && !recoveryController.signal.aborted - const cached = queryClient.getQueryData(taskKeys.detail(chatId)) + const cached = queryClient.getQueryData( + mothershipChatKeys.detail(chatId) + ) const fallbackStreamId = streamIdRef.current ?? activeTurnRef.current?.userMessageId ?? cached?.activeStreamId const loadedStream = await getActiveStreamIdForChat(chatId, recoveryController.signal) @@ -4428,10 +4440,10 @@ export function useChat( const activeChatId = options?.targetChatId ?? chatIdRef.current if (options?.includeDetail !== false && activeChatId) { queryClient.invalidateQueries({ - queryKey: taskKeys.detail(activeChatId), + queryKey: mothershipChatKeys.detail(activeChatId), }) } - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) }, [workspaceId, queryClient] ) @@ -4475,7 +4487,8 @@ export function useChat( const id = generateId() const handoffChatId = selectedChatIdRef.current ?? chatIdRef.current const cachedActiveStreamId = handoffChatId - ? queryClient.getQueryData(taskKeys.detail(handoffChatId))?.activeStreamId + ? queryClient.getQueryData(mothershipChatKeys.detail(handoffChatId)) + ?.activeStreamId : undefined const supersededStreamId = streamIdRef.current || @@ -4631,7 +4644,7 @@ export function useChat( } if (requestChatId) { - await queryClient.cancelQueries({ queryKey: taskKeys.detail(requestChatId) }) + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.detail(requestChatId) }) } const applyOptimisticSend = () => { @@ -4641,7 +4654,7 @@ export function useChat( contentBlocks: [], }) if (requestChatId) { - upsertTaskChatHistory(requestChatId, (current) => ({ + upsertChatHistory(requestChatId, (current) => ({ ...current, resources: current.resources.filter((resource) => resource.id !== 'streaming-file'), messages: [ @@ -4664,7 +4677,7 @@ export function useChat( const rollbackOptimisticSend = () => { if (requestChatId) { - upsertTaskChatHistory(requestChatId, (current) => ({ + upsertChatHistory(requestChatId, (current) => ({ ...current, messages: current.messages.filter( (persistedMessage) => @@ -4725,7 +4738,9 @@ export function useChat( throw new Error('Queued message was restored because the selected chat changed.') } if (requestChatId) { - await queryClient.cancelQueries({ queryKey: taskKeys.detail(requestChatId) }) + await queryClient.cancelQueries({ + queryKey: mothershipChatKeys.detail(requestChatId), + }) } applyOptimisticSend() } catch (err) { @@ -4921,7 +4936,7 @@ export function useChat( [ workspaceId, queryClient, - upsertTaskChatHistory, + upsertChatHistory, processSSEStream, finalize, resumeOrFinalize, @@ -5237,7 +5252,8 @@ export function useChat( streamIdRef.current || activeTurnRef.current?.userMessageId || (activeChatId - ? queryClient.getQueryData(taskKeys.detail(activeChatId))?.activeStreamId + ? queryClient.getQueryData(mothershipChatKeys.detail(activeChatId)) + ?.activeStreamId : undefined) || undefined @@ -5283,8 +5299,8 @@ export function useChat( try { if (activeChatId) { - await queryClient.cancelQueries({ queryKey: taskKeys.detail(activeChatId) }) - upsertTaskChatHistory(activeChatId, (current) => ({ + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.detail(activeChatId) }) + upsertChatHistory(activeChatId, (current) => ({ ...current, messages: current.messages.map((message) => activeAssistantMessageId && message.id === activeAssistantMessageId @@ -5482,7 +5498,7 @@ export function useChat( queryClient, resolveChatIdForStream, resetEphemeralPreviewState, - upsertTaskChatHistory, + upsertChatHistory, adoptResolvedChatId, clearActiveTurn, setTransportIdle, @@ -5653,8 +5669,9 @@ export function useChat( ? (() => { const handoffChatId = selectedChatIdRef.current ?? chatIdRef.current const cachedActiveStreamId = handoffChatId - ? queryClient.getQueryData(taskKeys.detail(handoffChatId)) - ?.activeStreamId + ? queryClient.getQueryData( + mothershipChatKeys.detail(handoffChatId) + )?.activeStreamId : undefined return { id: msg.id, diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx index aa543348625..7f87e271258 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx @@ -58,7 +58,7 @@ export function InboxTaskList() { const handleTaskClick = useCallback( (task: InboxTaskItem) => { if (task.chatId && (task.status === 'completed' || task.status === 'failed')) { - router.push(`/workspace/${workspaceId}/task/${task.chatId}`) + router.push(`/workspace/${workspaceId}/chat/${task.chatId}`) } }, [workspaceId, router] diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx index 53200176b0c..7cb4d877c4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx @@ -130,8 +130,8 @@ interface CollapsedSidebarMenuProps { } } -interface CollapsedTaskFlyoutItemProps { - task: { id: string; href: string; name: string; isActive?: boolean; isUnread?: boolean } +interface CollapsedChatFlyoutItemProps { + chat: { id: string; href: string; name: string; isActive?: boolean; isUnread?: boolean } isCurrentRoute: boolean isMenuOpen?: boolean isEditing?: boolean @@ -141,9 +141,9 @@ interface CollapsedTaskFlyoutItemProps { onEditValueChange?: (value: string) => void onEditKeyDown?: (e: React.KeyboardEvent) => void onEditBlur?: () => void - onContextMenu?: (e: ReactMouseEvent, taskId: string) => void + onContextMenu?: (e: ReactMouseEvent, chatId: string) => void onMorePointerDown?: () => void - onMoreClick?: (e: ReactMouseEvent, taskId: string) => void + onMoreClick?: (e: ReactMouseEvent, chatId: string) => void } interface CollapsedWorkflowFlyoutItemProps { @@ -213,8 +213,8 @@ export function CollapsedSidebarMenu({ ) } -export function CollapsedTaskFlyoutItem({ - task, +export function CollapsedChatFlyoutItem({ + chat, isCurrentRoute, isMenuOpen = false, isEditing = false, @@ -227,16 +227,16 @@ export function CollapsedTaskFlyoutItem({ onContextMenu, onMorePointerDown, onMoreClick, -}: CollapsedTaskFlyoutItemProps) { - const showActions = task.id !== 'new' && onMoreClick +}: CollapsedChatFlyoutItemProps) { + const showActions = chat.id !== 'new' && onMoreClick if (isEditing) { return (
onEditValueChange?.(e.target.value)} onKeyDown={onEditKeyDown} onBlur={onEditBlur} @@ -265,7 +265,7 @@ export function CollapsedTaskFlyoutItem({ onMoreClick?.(e, task.id)} + onClick={(e) => onMoreClick?.(e, chat.id)} className={cn(isMenuOpen && 'opacity-100')} > @@ -274,15 +274,15 @@ export function CollapsedTaskFlyoutItem({ } > onContextMenu(e, task.id) : undefined + chat.id !== 'new' && onContextMenu ? (e) => onContextMenu(e, chat.id) : undefined } > diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/index.ts index 5c67ffe801d..addc565dcff 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/index.ts @@ -1,7 +1,7 @@ export { + CollapsedChatFlyoutItem, CollapsedFileFolderItems, CollapsedFolderItems, CollapsedSidebarMenu, - CollapsedTaskFlyoutItem, CollapsedWorkflowFlyoutItem, } from './collapsed-sidebar-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts index b7c02cae7b3..23e217cbb84 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts @@ -1,8 +1,8 @@ export { + CollapsedChatFlyoutItem, CollapsedFileFolderItems, CollapsedFolderItems, CollapsedSidebarMenu, - CollapsedTaskFlyoutItem, CollapsedWorkflowFlyoutItem, } from './collapsed-sidebar-menu' export { FileList } from './file-list' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/index.ts index 96ee9a6b04a..18f0ffa3025 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/index.ts @@ -9,6 +9,7 @@ export { } from './command-items' export { BlocksGroup, + ChatsGroup, ConnectedAccountsGroup, DocsGroup, FilesGroup, @@ -16,7 +17,6 @@ export { KnowledgeBasesGroup, PagesGroup, TablesGroup, - TasksGroup, ToolOpsGroup, ToolsGroup, TriggersGroup, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts index 53ec8d7a095..7790ac37907 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts @@ -1,5 +1,6 @@ export { BlocksGroup, + ChatsGroup, ConnectedAccountsGroup, DocsGroup, FilesGroup, @@ -7,7 +8,6 @@ export { KnowledgeBasesGroup, PagesGroup, TablesGroup, - TasksGroup, ToolOpsGroup, ToolsGroup, TriggersGroup, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx index 82941a35075..b46f3eb4520 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx @@ -182,7 +182,7 @@ export const WorkflowsGroup = memo(function WorkflowsGroup({ ) }) -export const TasksGroup = memo(function TasksGroup({ +export const ChatsGroup = memo(function ChatsGroup({ items, onSelect, }: { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index be338972aef..7137191b934 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -35,6 +35,7 @@ import type { } from '@/stores/modals/search/types' import { BlocksGroup, + ChatsGroup, ConnectedAccountsGroup, DocsGroup, FilesGroup, @@ -42,7 +43,6 @@ import { KnowledgeBasesGroup, PagesGroup, TablesGroup, - TasksGroup, ToolOpsGroup, ToolsGroup, TriggersGroup, @@ -67,7 +67,7 @@ export function SearchModal({ onOpenChange, workflows = [], workspaces = [], - tasks = [], + chats = [], tables = [], files = [], knowledgeBases = [], @@ -298,9 +298,9 @@ export function SearchModal({ [workspaceId] ) - const handleTaskSelect = useCallback( - (task: TaskItem) => { - routerRef.current.push(task.href) + const handleChatSelect = useCallback( + (chat: TaskItem) => { + routerRef.current.push(chat.href) captureEvent(posthogRef.current, 'search_result_selected', { result_type: 'task', query_length: deferredSearchRef.current.length, @@ -481,9 +481,9 @@ export function SearchModal({ () => filterAndSort(workflows, (w) => `${w.name} workflow-${w.id}`, deferredSearch), [workflows, deferredSearch] ) - const filteredTasks = useMemo( - () => filterAndSort(tasks, (t) => `${t.name} task-${t.id}`, deferredSearch), - [tasks, deferredSearch] + const filteredChats = useMemo( + () => filterAndSort(chats, (t) => `${t.name} task-${t.id}`, deferredSearch), + [chats, deferredSearch] ) const filteredWorkspaces = useMemo( () => filterAndSort(workspaces, (w) => `${w.name} workspace-${w.id}`, deferredSearch), @@ -569,7 +569,7 @@ export function SearchModal({ - + diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts index 8653d8e549c..5baacf36d80 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts @@ -51,7 +51,7 @@ export interface SearchModalProps { onOpenChange: (open: boolean) => void workflows?: WorkflowItem[] workspaces?: WorkspaceItem[] - tasks?: TaskItem[] + chats?: TaskItem[] tables?: TaskItem[] files?: FileItem[] knowledgeBases?: TaskItem[] diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts index fc467147942..58c9f83092a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts @@ -1,4 +1,5 @@ export { useAutoScroll } from './use-auto-scroll' +export { useChatSelection } from './use-chat-selection' export { useContextMenu } from './use-context-menu' export { type DropIndicator, useDragDrop } from './use-drag-drop' export { useFlyoutInlineRename } from './use-flyout-inline-rename' @@ -14,7 +15,6 @@ export { useSidebarDragContextValue, } from './use-sidebar-drag-context' export { useSidebarResize } from './use-sidebar-resize' -export { useTaskSelection } from './use-task-selection' export { useWorkflowOperations } from './use-workflow-operations' export { useWorkflowSelection } from './use-workflow-selection' export { useWorkspaceLogoUpload } from './use-workspace-logo-upload' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-chat-selection.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-chat-selection.ts new file mode 100644 index 00000000000..baaf03c4b70 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-chat-selection.ts @@ -0,0 +1,44 @@ +import { useCallback } from 'react' +import { useFolderStore } from '@/stores/folders/store' + +interface UseChatSelectionProps { + /** + * Flat array of all chat IDs in display order + */ + chatIds: string[] +} + +/** + * Hook for managing chat selection with support for single and range selection. + * Handles shift-click for range selection. + * cmd/ctrl+click is handled by the browser (opens in new tab) and never reaches this handler. + * Uses the last selected chat as the anchor point for range selections. + * Selecting chats clears workflow/folder selections and vice versa. + */ +export function useChatSelection({ chatIds }: UseChatSelectionProps) { + const selectedChats = useFolderStore((s) => s.selectedChats) + + const handleChatClick = useCallback( + (chatId: string, shiftKey: boolean) => { + const { + selectChatOnly, + selectChatRange, + toggleChatSelection, + lastSelectedChatId: anchor, + } = useFolderStore.getState() + if (shiftKey && anchor && anchor !== chatId) { + selectChatRange(chatIds, anchor, chatId) + } else if (shiftKey) { + toggleChatSelection(chatId) + } else { + selectChatOnly(chatId) + } + }, + [chatIds] + ) + + return { + selectedChats, + handleChatClick, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-task-selection.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-task-selection.ts deleted file mode 100644 index 41dd7d0b806..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-task-selection.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useCallback } from 'react' -import { useFolderStore } from '@/stores/folders/store' - -interface UseTaskSelectionProps { - /** - * Flat array of all task IDs in display order - */ - taskIds: string[] -} - -/** - * Hook for managing task selection with support for single and range selection. - * Handles shift-click for range selection. - * cmd/ctrl+click is handled by the browser (opens in new tab) and never reaches this handler. - * Uses the last selected task as the anchor point for range selections. - * Selecting tasks clears workflow/folder selections and vice versa. - */ -export function useTaskSelection({ taskIds }: UseTaskSelectionProps) { - const selectedTasks = useFolderStore((s) => s.selectedTasks) - - const handleTaskClick = useCallback( - (taskId: string, shiftKey: boolean) => { - const { - selectTaskOnly, - selectTaskRange, - toggleTaskSelection, - lastSelectedTaskId: anchor, - } = useFolderStore.getState() - if (shiftKey && anchor && anchor !== taskId) { - selectTaskRange(taskIds, anchor, taskId) - } else if (shiftKey) { - toggleTaskSelection(taskId) - } else { - selectTaskOnly(taskId) - } - }, - [taskIds] - ) - - return { - selectedTasks, - handleTaskClick, - } -} 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 cec487a0fc9..d9a0a563938 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -48,9 +48,9 @@ import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/provide import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { + CollapsedChatFlyoutItem, CollapsedFolderItems, CollapsedSidebarMenu, - CollapsedTaskFlyoutItem, CollapsedWorkflowFlyoutItem, HelpModal, NavItemContextMenu, @@ -70,12 +70,12 @@ import { SIDEBAR_SECTION_GAP_CLASS, } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' import { + useChatSelection, useContextMenu, useFlyoutInlineRename, useFolderOperations, useHoverMenu, useSidebarResize, - useTaskSelection, useWorkflowOperations, useWorkspaceLogoUpload, useWorkspaceManagement, @@ -89,23 +89,23 @@ import { useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks' import { useWorkspaceCredentials } from '@/hooks/queries/credentials' import { useFolderMap, useFolders } from '@/hooks/queries/folders' import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' -import { useTablesList } from '@/hooks/queries/tables' import { - useCreateTask, - useDeleteTask, - useDeleteTasks, - useMarkTaskRead, - useMarkTaskUnread, - useRenameTask, - useSetTaskPinned, - useTasks, -} from '@/hooks/queries/tasks' + useCreateMothershipChat, + useDeleteMothershipChat, + useDeleteMothershipChats, + useMarkMothershipChatRead, + useMarkMothershipChatUnread, + useMothershipChats, + useRenameMothershipChat, + useSetMothershipChatPinned, +} from '@/hooks/queries/mothership-chats' +import { useTablesList } from '@/hooks/queries/tables' import { useUpdateWorkflow } from '@/hooks/queries/workflows' import type { Workspace } from '@/hooks/queries/workspace' import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' +import { useMothershipChatEvents } from '@/hooks/use-mothership-chat-events' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' -import { useTaskEvents } from '@/hooks/use-task-events' import { SIDEBAR_WIDTH } from '@/stores/constants' import { useFolderStore } from '@/stores/folders/store' import { useSearchModalStore } from '@/stores/modals/search/store' @@ -145,8 +145,8 @@ function SidebarItemSkeleton() { ) } -const SidebarTaskItem = memo(function SidebarTaskItem({ - task, +const SidebarChatItem = memo(function SidebarChatItem({ + chat, isCurrentRoute, isSelected, isActive, @@ -159,7 +159,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ onMorePointerDown, onMoreClick, }: { - task: { id: string; href: string; name: string } + chat: { id: string; href: string; name: string } isCurrentRoute: boolean isSelected: boolean isActive: boolean @@ -167,10 +167,10 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ isPinned: boolean isMenuOpen: boolean showCollapsedTooltips: boolean - onMultiSelectClick: (taskId: string, shiftKey: boolean) => void - onContextMenu: (e: React.MouseEvent, taskId: string) => void + onMultiSelectClick: (chatId: string, shiftKey: boolean) => void + onContextMenu: (e: React.MouseEvent, chatId: string) => void onMorePointerDown: () => void - onMoreClick: (e: React.MouseEvent, taskId: string) => void + onMoreClick: (e: React.MouseEvent, chatId: string) => void }) { const dragGhostRef = useRef(null) @@ -178,9 +178,9 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ e.dataTransfer.effectAllowed = 'copyMove' e.dataTransfer.setData( SIM_RESOURCES_DRAG_TYPE, - JSON.stringify([{ type: 'task', id: task.id, title: task.name }]) + JSON.stringify([{ type: 'task', id: chat.id, title: chat.name }]) ) - const ghost = createSidebarDragGhost(task.name, { kind: 'task' }) + const ghost = createSidebarDragGhost(chat.name, { kind: 'task' }) void ghost.offsetHeight e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) dragGhostRef.current = ghost @@ -194,9 +194,9 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ } return ( - + onContextMenu(e, task.id)} + onContextMenu={(e) => onContextMenu(e, chat.id)} draggable onDragStart={handleDragStart} onDragEnd={handleDragEnd} > -
{task.name}
- {task.id !== 'new' && ( +
{chat.name}
+ {chat.id !== 'new' && (
{(isActive || (!isCurrentRoute && isUnread)) && ( { e.preventDefault() e.stopPropagation() - onMoreClick(e, task.id) + onMoreClick(e, chat.id) }} className={cn( 'absolute inset-0 flex items-center justify-center rounded-sm opacity-0 transition-opacity group-hover:opacity-100', @@ -589,77 +589,77 @@ export const Sidebar = memo(function Sidebar() { } }, [activeNavItemHref]) - const createTaskMutation = useCreateTask(workspaceId) - const deleteTaskMutation = useDeleteTask(workspaceId) - const deleteTasksMutation = useDeleteTasks(workspaceId) - const markTaskReadMutation = useMarkTaskRead(workspaceId) - const markTaskUnreadMutation = useMarkTaskUnread(workspaceId) - const renameTaskMutation = useRenameTask(workspaceId) - const setTaskPinnedMutation = useSetTaskPinned(workspaceId) - const tasksHover = useHoverMenu() + const createChatMutation = useCreateMothershipChat(workspaceId) + const deleteChatMutation = useDeleteMothershipChat(workspaceId) + const deleteChatsMutation = useDeleteMothershipChats(workspaceId) + const markChatReadMutation = useMarkMothershipChatRead(workspaceId) + const markChatUnreadMutation = useMarkMothershipChatUnread(workspaceId) + const renameChatMutation = useRenameMothershipChat(workspaceId) + const setChatPinnedMutation = useSetMothershipChatPinned(workspaceId) + const chatsHover = useHoverMenu() const workflowsHover = useHoverMenu() const { - isOpen: isTaskContextMenuOpen, - position: taskContextMenuPosition, - menuRef: taskMenuRef, - handleContextMenu: handleTaskContextMenuBase, - closeMenu: closeTaskContextMenu, - preventDismiss: preventTaskDismiss, + isOpen: isChatContextMenuOpen, + position: chatContextMenuPosition, + menuRef: chatMenuRef, + handleContextMenu: handleChatContextMenuBase, + closeMenu: closeChatContextMenu, + preventDismiss: preventChatDismiss, } = useContextMenu() - const isCreatingTaskRef = useRef(false) - const contextMenuSelectionRef = useRef<{ taskIds: string[]; names: string[] }>({ - taskIds: [], + const isCreatingChatRef = useRef(false) + const contextMenuSelectionRef = useRef<{ chatIds: string[]; names: string[] }>({ + chatIds: [], names: [], }) - const [menuOpenTaskId, setMenuOpenTaskId] = useState(null) + const [menuOpenChatId, setMenuOpenChatId] = useState(null) useEffect(() => { - if (!isTaskContextMenuOpen) setMenuOpenTaskId(null) - }, [isTaskContextMenuOpen]) + if (!isChatContextMenuOpen) setMenuOpenChatId(null) + }, [isChatContextMenuOpen]) - const captureTaskSelection = useCallback((taskId: string) => { - const { selectedTasks, selectTaskOnly } = useFolderStore.getState() - if (selectedTasks.size > 0 && selectedTasks.has(taskId)) { + const captureChatSelection = useCallback((chatId: string) => { + const { selectedChats, selectChatOnly } = useFolderStore.getState() + if (selectedChats.size > 0 && selectedChats.has(chatId)) { contextMenuSelectionRef.current = { - taskIds: Array.from(selectedTasks), + chatIds: Array.from(selectedChats), names: [], } } else { - selectTaskOnly(taskId) - contextMenuSelectionRef.current = { taskIds: [taskId], names: [] } + selectChatOnly(chatId) + contextMenuSelectionRef.current = { chatIds: [chatId], names: [] } } }, []) - const handleTaskContextMenu = useCallback( - (e: React.MouseEvent, taskId: string) => { - captureTaskSelection(taskId) - setMenuOpenTaskId(taskId) - tasksHover.setLocked(true) - preventTaskDismiss() - handleTaskContextMenuBase(e) + const handleChatContextMenu = useCallback( + (e: React.MouseEvent, chatId: string) => { + captureChatSelection(chatId) + setMenuOpenChatId(chatId) + chatsHover.setLocked(true) + preventChatDismiss() + handleChatContextMenuBase(e) }, - [captureTaskSelection, handleTaskContextMenuBase, preventTaskDismiss, tasksHover] + [captureChatSelection, handleChatContextMenuBase, preventChatDismiss, chatsHover] ) - const handleTaskMorePointerDown = useCallback(() => { - if (isTaskContextMenuOpen) { - preventTaskDismiss() + const handleChatMorePointerDown = useCallback(() => { + if (isChatContextMenuOpen) { + preventChatDismiss() } - }, [isTaskContextMenuOpen, preventTaskDismiss]) + }, [isChatContextMenuOpen, preventChatDismiss]) - const handleTaskMoreClick = useCallback( - (e: React.MouseEvent, taskId: string) => { - if (isTaskContextMenuOpen) { - closeTaskContextMenu() + const handleChatMoreClick = useCallback( + (e: React.MouseEvent, chatId: string) => { + if (isChatContextMenuOpen) { + closeChatContextMenu() return } - tasksHover.setLocked(true) - captureTaskSelection(taskId) - setMenuOpenTaskId(taskId) + chatsHover.setLocked(true) + captureChatSelection(chatId) + setMenuOpenChatId(chatId) const rect = e.currentTarget.getBoundingClientRect() - handleTaskContextMenuBase({ + handleChatContextMenuBase({ preventDefault: () => {}, stopPropagation: () => {}, clientX: rect.right, @@ -667,11 +667,11 @@ export const Sidebar = memo(function Sidebar() { } as React.MouseEvent) }, [ - isTaskContextMenuOpen, - closeTaskContextMenu, - captureTaskSelection, - handleTaskContextMenuBase, - tasksHover, + isChatContextMenuOpen, + closeChatContextMenu, + captureChatSelection, + handleChatContextMenuBase, + chatsHover, ] ) @@ -793,19 +793,19 @@ export const Sidebar = memo(function Sidebar() { [navigateToSettings, getSettingsHref, setSidebarWidth] ) - const { data: fetchedTasks = [], isLoading: tasksLoading } = useTasks(workspaceId) + const { data: fetchedChats = [], isLoading: chatsLoading } = useMothershipChats(workspaceId) - useTaskEvents(workspaceId) + useMothershipChatEvents(workspaceId) - const tasks = useMemo( + const chats = useMemo( () => - fetchedTasks - ? fetchedTasks.map((t) => ({ + fetchedChats + ? fetchedChats.map((t) => ({ ...t, - href: `/workspace/${workspaceId}/task/${t.id}`, + href: `/workspace/${workspaceId}/chat/${t.id}`, })) : [], - [fetchedTasks, workspaceId] + [fetchedChats, workspaceId] ) const { data: fetchedTables = [] } = useTablesList(workspaceId) @@ -849,26 +849,26 @@ export const Sidebar = memo(function Sidebar() { [fetchedKnowledgeBases, workspaceId, permissionConfig.hideKnowledgeBaseTab] ) - const taskIds = useMemo(() => tasks.map((t) => t.id), [tasks]) + const chatIds = useMemo(() => chats.map((t) => t.id), [chats]) - const { selectedTasks, handleTaskClick } = useTaskSelection({ taskIds }) - const hasTaskMultiSelection = selectedTasks.size > 1 + const { selectedChats, handleChatClick } = useChatSelection({ chatIds }) + const hasChatMultiSelection = selectedChats.size > 1 - const isMultiTaskContextMenu = contextMenuSelectionRef.current.taskIds.length > 1 - const activeTaskContextMenuItem = - !isMultiTaskContextMenu && contextMenuSelectionRef.current.taskIds.length === 1 - ? tasks.find((task) => task.id === contextMenuSelectionRef.current.taskIds[0]) + const isMultiChatContextMenu = contextMenuSelectionRef.current.chatIds.length > 1 + const activeChatContextMenuItem = + !isMultiChatContextMenu && contextMenuSelectionRef.current.chatIds.length === 1 + ? chats.find((chat) => chat.id === contextMenuSelectionRef.current.chatIds[0]) : null - const [isTaskDeleteModalOpen, setIsTaskDeleteModalOpen] = useState(false) + const [isChatDeleteModalOpen, setIsChatDeleteModalOpen] = useState(false) - const handleDeleteTask = useCallback(() => { - const { taskIds: ids } = contextMenuSelectionRef.current + const handleDeleteChat = useCallback(() => { + const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length === 0) return - const names = ids.map((id) => tasks.find((t) => t.id === id)?.name).filter(Boolean) as string[] - contextMenuSelectionRef.current = { taskIds: ids, names } - setIsTaskDeleteModalOpen(true) - }, [tasks]) + const names = ids.map((id) => chats.find((t) => t.id === id)?.name).filter(Boolean) as string[] + contextMenuSelectionRef.current = { chatIds: ids, names } + setIsChatDeleteModalOpen(true) + }, [chats]) const navigateToPage = useCallback( (path: string) => { @@ -880,35 +880,35 @@ export const Sidebar = memo(function Sidebar() { [setSidebarWidth, router] ) - const handleConfirmDeleteTasks = () => { - const { taskIds: taskIdsToDelete } = contextMenuSelectionRef.current - if (taskIdsToDelete.length === 0) return + const handleConfirmDeleteChats = () => { + const { chatIds: chatIdsToDelete } = contextMenuSelectionRef.current + if (chatIdsToDelete.length === 0) return const currentPath = pathname ?? '' - const isViewingDeletedTask = taskIdsToDelete.some( - (id) => currentPath === `/workspace/${workspaceId}/task/${id}` + const isViewingDeletedChat = chatIdsToDelete.some( + (id) => currentPath === `/workspace/${workspaceId}/chat/${id}` ) const onDeleteSuccess = () => { - useFolderStore.getState().clearTaskSelection() - if (isViewingDeletedTask) { + useFolderStore.getState().clearChatSelection() + if (isViewingDeletedChat) { navigateToPage(`/workspace/${workspaceId}/home`) } } - if (taskIdsToDelete.length === 1) { - deleteTaskMutation.mutate(taskIdsToDelete[0], { onSuccess: onDeleteSuccess }) + if (chatIdsToDelete.length === 1) { + deleteChatMutation.mutate(chatIdsToDelete[0], { onSuccess: onDeleteSuccess }) } else { - deleteTasksMutation.mutate(taskIdsToDelete, { onSuccess: onDeleteSuccess }) + deleteChatsMutation.mutate(chatIdsToDelete, { onSuccess: onDeleteSuccess }) } - setIsTaskDeleteModalOpen(false) + setIsChatDeleteModalOpen(false) } - const [visibleTaskCount, setVisibleTaskCount] = useState(5) - const taskFlyoutRename = useFlyoutInlineRename({ + const [visibleChatCount, setVisibleChatCount] = useState(5) + const chatFlyoutRename = useFlyoutInlineRename({ itemType: 'task', - onSave: async (taskId, name) => { - await renameTaskMutation.mutateAsync({ chatId: taskId, title: name }) + onSave: async (chatId, name) => { + await renameChatMutation.mutateAsync({ chatId: chatId, title: name }) }, }) @@ -924,49 +924,49 @@ export const Sidebar = memo(function Sidebar() { }) useEffect(() => { - tasksHover.setLocked(isTaskContextMenuOpen || !!taskFlyoutRename.editingId) - }, [isTaskContextMenuOpen, taskFlyoutRename.editingId, tasksHover.setLocked]) + chatsHover.setLocked(isChatContextMenuOpen || !!chatFlyoutRename.editingId) + }, [isChatContextMenuOpen, chatFlyoutRename.editingId, chatsHover.setLocked]) useEffect(() => { workflowsHover.setLocked(!!workflowFlyoutRename.editingId) }, [workflowFlyoutRename.editingId, workflowsHover.setLocked]) - const handleTaskOpenInNewTab = useCallback(() => { - const { taskIds: ids } = contextMenuSelectionRef.current + const handleChatOpenInNewTab = useCallback(() => { + const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length !== 1) return - window.open(`/workspace/${workspaceId}/task/${ids[0]}`, '_blank', 'noopener,noreferrer') + window.open(`/workspace/${workspaceId}/chat/${ids[0]}`, '_blank', 'noopener,noreferrer') }, [workspaceId]) - const handleMarkTaskAsRead = useCallback(() => { - const { taskIds: ids } = contextMenuSelectionRef.current + const handleMarkChatAsRead = useCallback(() => { + const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length !== 1) return - markTaskReadMutation.mutate(ids[0]) + markChatReadMutation.mutate(ids[0]) }, []) - const handleMarkTaskAsUnread = useCallback(() => { - const { taskIds: ids } = contextMenuSelectionRef.current + const handleMarkChatAsUnread = useCallback(() => { + const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length !== 1) return - markTaskUnreadMutation.mutate(ids[0]) + markChatUnreadMutation.mutate(ids[0]) }, []) - const handleStartTaskRename = useCallback(() => { - const { taskIds: ids } = contextMenuSelectionRef.current + const handleStartChatRename = useCallback(() => { + const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length !== 1) return - const taskId = ids[0] - const task = tasks.find((t) => t.id === taskId) - if (!task) return - tasksHover.setLocked(true) - taskFlyoutRename.startRename({ id: taskId, name: task.name }) - }, [taskFlyoutRename, tasks, tasksHover]) - - const handleToggleTaskPin = useCallback(() => { - const { taskIds: ids } = contextMenuSelectionRef.current + const chatId = ids[0] + const chat = chats.find((t) => t.id === chatId) + if (!chat) return + chatsHover.setLocked(true) + chatFlyoutRename.startRename({ id: chatId, name: chat.name }) + }, [chatFlyoutRename, chats, chatsHover]) + + const handleToggleChatPin = useCallback(() => { + const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length !== 1) return - const taskId = ids[0] - const task = tasks.find((t) => t.id === taskId) - if (!task) return - setTaskPinnedMutation.mutate({ chatId: taskId, pinned: !task.isPinned }) - }, [tasks, setTaskPinnedMutation]) + const chatId = ids[0] + const chat = chats.find((t) => t.id === chatId) + if (!chat) return + setChatPinnedMutation.mutate({ chatId: chatId, pinned: !chat.isPinned }) + }, [chats, setChatPinnedMutation]) const handleCollapsedWorkflowOpenInNewTab = useCallback( (workflow: { id: string }) => { @@ -1123,7 +1123,7 @@ export const Sidebar = memo(function Sidebar() { [workspaces, handleLeaveWorkspace] ) - const tasksCollapsedIcon = + const chatsCollapsedIcon = const workflowsCollapsedIcon = ( @@ -1134,29 +1134,29 @@ export const Sidebar = memo(function Sidebar() { onSelect: handleCreateWorkflow, } - const handleNewTask = useCallback(async () => { - if (!workspaceId || isCreatingTaskRef.current) return - isCreatingTaskRef.current = true + const handleNewChat = useCallback(async () => { + if (!workspaceId || isCreatingChatRef.current) return + isCreatingChatRef.current = true try { - const { id } = await createTaskMutation.mutateAsync() + const { id } = await createChatMutation.mutateAsync() useMothershipDraftsStore.getState().clearDraft(`${workspaceId}:new`) - navigateToPage(`/workspace/${workspaceId}/task/${id}`) + navigateToPage(`/workspace/${workspaceId}/chat/${id}`) } catch { navigateToPage(`/workspace/${workspaceId}/home`) } finally { - isCreatingTaskRef.current = false + isCreatingChatRef.current = false } }, [workspaceId, navigateToPage]) - const tasksPrimaryAction = { + const chatsPrimaryAction = { label: 'New chat', - onSelect: handleNewTask, + onSelect: handleNewChat, } - const handleSeeMoreTasks = useCallback(() => setVisibleTaskCount((prev) => prev + 5), []) - const handleSeeLessTasks = useCallback(() => setVisibleTaskCount(5), []) + const handleSeeMoreChats = useCallback(() => setVisibleChatCount((prev) => prev + 5), []) + const handleSeeLessChats = useCallback(() => setVisibleChatCount(5), []) - const handleCloseTaskDeleteModal = useCallback(() => setIsTaskDeleteModalOpen(false), []) + const handleCloseChatDeleteModal = useCallback(() => setIsChatDeleteModalOpen(false), []) const handleEdgeKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -1175,9 +1175,9 @@ export const Sidebar = memo(function Sidebar() { captureEvent(posthog, 'docs_opened', { source: 'help_menu' }) }, [posthog]) - const handleTaskRenameBlur = useCallback( - () => void taskFlyoutRename.saveRename(), - [taskFlyoutRename.saveRename] + const handleChatRenameBlur = useCallback( + () => void chatFlyoutRename.saveRename(), + [chatFlyoutRename.saveRename] ) const handleWorkflowRenameBlur = useCallback( @@ -1244,7 +1244,7 @@ export const Sidebar = memo(function Sidebar() { { id: 'add-task', handler: () => { - handleNewTask() + handleNewChat() }, }, ]) @@ -1338,7 +1338,7 @@ export const Sidebar = memo(function Sidebar() { )} >
-
+
Chats
{!isCollapsed && ( @@ -1348,8 +1348,8 @@ export const Sidebar = memo(function Sidebar() { @@ -1365,76 +1365,76 @@ export const Sidebar = memo(function Sidebar() {
{isCollapsed ? ( - {tasksLoading ? ( + {chatsLoading ? ( Loading... - ) : tasks.length === 0 ? ( + ) : chats.length === 0 ? ( No chats yet ) : ( - tasks.map((task) => ( - ( + )) )} ) : (
- {tasksLoading ? ( + {chatsLoading ? ( ) : ( <> - {tasks.length === 0 ? ( + {chats.length === 0 ? (
No chats yet
) : null} - {/* `selectTaskOnly` populates `selectedTasks` on every click, so + {/* `selectChatOnly` populates `selectedChats` on every click, so a single entry just means "last clicked" — already conveyed by `isCurrentRoute`. Highlight from selection only for explicit multi-selection (size > 1), otherwise it lingers after navigating - away from a task. */} - {tasks.slice(0, visibleTaskCount).map((task) => { - const isCurrentRoute = pathname === task.href - const isRenaming = taskFlyoutRename.editingId === task.id + away from a chat. */} + {chats.slice(0, visibleChatCount).map((chat) => { + const isCurrentRoute = pathname === chat.href + const isRenaming = chatFlyoutRename.editingId === chat.id const isSelected = - task.id !== 'new' && - hasTaskMultiSelection && - selectedTasks.has(task.id) + chat.id !== 'new' && + hasChatMultiSelection && + selectedChats.has(chat.id) if (isRenaming) { return (
taskFlyoutRename.setValue(e.target.value)} - onKeyDown={taskFlyoutRename.handleKeyDown} - onBlur={handleTaskRenameBlur} + ref={chatFlyoutRename.inputRef} + value={chatFlyoutRename.value} + onChange={(e) => chatFlyoutRename.setValue(e.target.value)} + onKeyDown={chatFlyoutRename.handleKeyDown} + onBlur={handleChatRenameBlur} className='min-w-0 flex-1 border-none bg-transparent text-[14px] text-[var(--text-body)] outline-none' />
@@ -1442,37 +1442,37 @@ export const Sidebar = memo(function Sidebar() { } return ( - ) })} - {tasks.length > 5 && ( + {chats.length > 5 && ( )} @@ -1714,36 +1714,36 @@ export const Sidebar = memo(function Sidebar() { /> @@ -1772,7 +1772,7 @@ export const Sidebar = memo(function Sidebar() { onOpenChange={setIsSearchModalOpen} workflows={searchModalWorkflows} workspaces={searchModalWorkspaces} - tasks={tasks} + chats={chats} tables={searchModalTables} files={searchModalFiles} knowledgeBases={searchModalKnowledgeBases} diff --git a/apps/sim/hooks/queries/tasks.test.ts b/apps/sim/hooks/queries/mothership-chats.test.ts similarity index 90% rename from apps/sim/hooks/queries/tasks.test.ts rename to apps/sim/hooks/queries/mothership-chats.test.ts index 641ede02146..6555d30846d 100644 --- a/apps/sim/hooks/queries/tasks.test.ts +++ b/apps/sim/hooks/queries/mothership-chats.test.ts @@ -21,7 +21,11 @@ vi.mock('@tanstack/react-query', () => ({ useMutation: vi.fn((options) => options), })) -import { fetchChatHistory, fetchTasks, useAddChatResource } from '@/hooks/queries/tasks' +import { + fetchMothershipChatHistory, + fetchMothershipChats, + useAddChatResource, +} from '@/hooks/queries/mothership-chats' function jsonResponse(body: unknown, init?: ResponseInit): Response { return new Response(JSON.stringify(body), { @@ -60,7 +64,7 @@ describe('tasks query boundary parsing', () => { }) ) - const tasks = await fetchTasks('ws-1') + const tasks = await fetchMothershipChats('ws-1') expect(tasks).toHaveLength(1) expect(tasks[0]).toEqual( @@ -92,7 +96,9 @@ describe('tasks query boundary parsing', () => { }) ) - await expect(fetchTasks('ws-1')).rejects.toThrow('Response failed contract validation') + await expect(fetchMothershipChats('ws-1')).rejects.toThrow( + 'Response failed contract validation' + ) }) it('parses valid mothership chat history responses', async () => { @@ -114,7 +120,7 @@ describe('tasks query boundary parsing', () => { }) ) - const history = await fetchChatHistory('chat-1') + const history = await fetchMothershipChatHistory('chat-1') expect(history).toEqual({ id: 'chat-1', @@ -146,7 +152,7 @@ describe('tasks query boundary parsing', () => { }) ) - await expect(fetchChatHistory('chat-1')).rejects.toThrow( + await expect(fetchMothershipChatHistory('chat-1')).rejects.toThrow( 'Invalid chat response: chat.resources[0].type is invalid' ) }) diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/mothership-chats.ts similarity index 62% rename from apps/sim/hooks/queries/tasks.ts rename to apps/sim/hooks/queries/mothership-chats.ts index 65a1d5c4bea..0b5117ac66a 100644 --- a/apps/sim/hooks/queries/tasks.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -14,11 +14,11 @@ import { forkMothershipChatContract, getMothershipChatContract, listMothershipChatsContract, - type MothershipTask, + type MothershipChat, removeMothershipChatResourceContract, reorderMothershipChatResourcesContract, updateMothershipChatContract, -} from '@/lib/api/contracts/mothership-tasks' +} from '@/lib/api/contracts/mothership-chats' import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message' import { normalizeMessage } from '@/lib/copilot/chat/persisted-message' import { @@ -29,7 +29,7 @@ import { isStreamBatchEvent, type StreamBatchEvent } from '@/lib/copilot/request import { type MothershipResource, MothershipResourceType } from '@/lib/copilot/resources/types' import { useMothershipQueueStore } from '@/stores/mothership-queue/store' -export interface TaskMetadata { +export interface MothershipChatMetadata { id: string name: string updatedAt: Date @@ -38,7 +38,7 @@ export interface TaskMetadata { isPinned: boolean } -export interface TaskChatHistory { +export interface MothershipChatHistory { id: string title: string | null messages: PersistedMessage[] @@ -51,12 +51,13 @@ export interface TaskChatHistory { } | null } -export const taskKeys = { - all: ['tasks'] as const, - lists: () => [...taskKeys.all, 'list'] as const, - list: (workspaceId: string | undefined) => [...taskKeys.lists(), workspaceId ?? ''] as const, - details: () => [...taskKeys.all, 'detail'] as const, - detail: (chatId: string | undefined) => [...taskKeys.details(), chatId ?? ''] as const, +export const mothershipChatKeys = { + all: ['mothership-chats'] as const, + lists: () => [...mothershipChatKeys.all, 'list'] as const, + list: (workspaceId: string | undefined) => + [...mothershipChatKeys.lists(), workspaceId ?? ''] as const, + details: () => [...mothershipChatKeys.all, 'detail'] as const, + detail: (chatId: string | undefined) => [...mothershipChatKeys.details(), chatId ?? ''] as const, } function isRecord(value: unknown): value is Record { @@ -80,7 +81,7 @@ function isResourceType(value: unknown): value is MothershipResource['type'] { ) } -function parseStreamSnapshot(value: unknown): TaskChatHistory['streamSnapshot'] { +function parseStreamSnapshot(value: unknown): MothershipChatHistory['streamSnapshot'] { if (!isRecord(value)) { return null } @@ -140,7 +141,7 @@ function parseResources(value: unknown, context: string): MothershipResource[] { function parseStrictStreamSnapshot( value: unknown, context: string -): TaskChatHistory['streamSnapshot'] { +): MothershipChatHistory['streamSnapshot'] { if (value === undefined || value === null) { return null } @@ -150,7 +151,7 @@ function parseStrictStreamSnapshot( return snapshot } -function parseChatHistory(value: unknown): TaskChatHistory { +function parseChatHistory(value: unknown): MothershipChatHistory { const responseContext = 'Invalid chat response' const chatContext = `${responseContext}: chat` @@ -185,7 +186,7 @@ function parseChatResourcesResponse(value: unknown): { resources: MothershipReso } } -function mapTask(chat: MothershipTask): TaskMetadata { +function mapChat(chat: MothershipChat): MothershipChatMetadata { const updatedAt = new Date(chat.updatedAt) return { id: chat.id, @@ -199,34 +200,34 @@ function mapTask(chat: MothershipTask): TaskMetadata { } } -export async function fetchTasks( +export async function fetchMothershipChats( workspaceId: string, signal?: AbortSignal -): Promise { +): Promise { const data = await requestJson(listMothershipChatsContract, { query: { workspaceId }, signal, }) - return data.data.map(mapTask) + return data.data.map(mapChat) } /** - * Fetches mothership chat tasks for a workspace. + * Fetches mothership chat chats for a workspace. * These are workspace-scoped conversations from the Home page. */ -export function useTasks(workspaceId?: string) { +export function useMothershipChats(workspaceId?: string) { return useQuery({ - queryKey: taskKeys.list(workspaceId), - queryFn: workspaceId ? ({ signal }) => fetchTasks(workspaceId, signal) : skipToken, + queryKey: mothershipChatKeys.list(workspaceId), + queryFn: workspaceId ? ({ signal }) => fetchMothershipChats(workspaceId, signal) : skipToken, placeholderData: keepPreviousData, staleTime: 60 * 1000, }) } -export async function fetchChatHistory( +export async function fetchMothershipChatHistory( chatId: string, signal?: AbortSignal -): Promise { +): Promise { try { const data = await requestJson(getMothershipChatContract, { params: { chatId }, @@ -254,60 +255,60 @@ export async function fetchChatHistory( } /** - * Fetches chat history for a single task (mothership chat). - * Used by the task page to load an existing conversation. + * Fetches chat history for a single chat (mothership chat). + * Used by the chat page to load an existing conversation. */ -export function useChatHistory(chatId: string | undefined) { +export function useMothershipChatHistory(chatId: string | undefined) { return useQuery({ - queryKey: taskKeys.detail(chatId), - queryFn: ({ signal }) => fetchChatHistory(chatId!, signal), + queryKey: mothershipChatKeys.detail(chatId), + queryFn: ({ signal }) => fetchMothershipChatHistory(chatId!, signal), enabled: Boolean(chatId), staleTime: 30 * 1000, }) } -async function deleteTask(chatId: string): Promise { +async function deleteChat(chatId: string): Promise { await requestJson(deleteMothershipChatContract, { params: { chatId }, }) } /** - * Deletes a mothership chat task and invalidates the task list. + * Deletes a mothership chat chat and invalidates the chat list. */ -export function useDeleteTask(workspaceId?: string) { +export function useDeleteMothershipChat(workspaceId?: string) { const queryClient = useQueryClient() return useMutation({ - mutationFn: deleteTask, + mutationFn: deleteChat, onSettled: (_data, _error, chatId) => { - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) - queryClient.removeQueries({ queryKey: taskKeys.detail(chatId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) + queryClient.removeQueries({ queryKey: mothershipChatKeys.detail(chatId) }) useMothershipQueueStore.getState().clearChat(chatId) }, }) } /** - * Deletes multiple mothership chat tasks and invalidates the task list. + * Deletes multiple mothership chat chats and invalidates the chat list. */ -export function useDeleteTasks(workspaceId?: string) { +export function useDeleteMothershipChats(workspaceId?: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: async (chatIds: string[]) => { - await Promise.all(chatIds.map(deleteTask)) + await Promise.all(chatIds.map(deleteChat)) }, onSettled: (_data, _error, chatIds) => { - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) const queueStore = useMothershipQueueStore.getState() for (const chatId of chatIds) { - queryClient.removeQueries({ queryKey: taskKeys.detail(chatId) }) + queryClient.removeQueries({ queryKey: mothershipChatKeys.detail(chatId) }) queueStore.clearChat(chatId) } }, }) } -async function renameTask({ chatId, title }: { chatId: string; title: string }): Promise { +async function renameChat({ chatId, title }: { chatId: string; title: string }): Promise { await requestJson(updateMothershipChatContract, { params: { chatId }, body: { title }, @@ -315,31 +316,34 @@ async function renameTask({ chatId, title }: { chatId: string; title: string }): } /** - * Renames a mothership chat task with optimistic update. + * Renames a mothership chat chat with optimistic update. */ -export function useRenameTask(workspaceId?: string) { +export function useRenameMothershipChat(workspaceId?: string) { const queryClient = useQueryClient() return useMutation({ - mutationFn: renameTask, + mutationFn: renameChat, onMutate: async ({ chatId, title }) => { - await queryClient.cancelQueries({ queryKey: taskKeys.list(workspaceId) }) + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) - const previousTasks = queryClient.getQueryData(taskKeys.list(workspaceId)) + const previousChats = queryClient.getQueryData( + mothershipChatKeys.list(workspaceId) + ) - queryClient.setQueryData(taskKeys.list(workspaceId), (old) => - old?.map((task) => (task.id === chatId ? { ...task, name: title } : task)) + queryClient.setQueryData( + mothershipChatKeys.list(workspaceId), + (old) => old?.map((chat) => (chat.id === chatId ? { ...chat, name: title } : chat)) ) - return { previousTasks } + return { previousChats } }, onError: (_err, _variables, context) => { - if (context?.previousTasks) { - queryClient.setQueryData(taskKeys.list(workspaceId), context.previousTasks) + if (context?.previousChats) { + queryClient.setQueryData(mothershipChatKeys.list(workspaceId), context.previousChats) } }, onSettled: (_data, _error, variables) => { - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) - queryClient.invalidateQueries({ queryKey: taskKeys.detail(variables.chatId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.detail(variables.chatId) }) }, }) } @@ -360,14 +364,16 @@ export function useAddChatResource(chatId?: string) { mutationFn: addChatResource, onMutate: async ({ resource }) => { if (!chatId) return - await queryClient.cancelQueries({ queryKey: taskKeys.detail(chatId) }) - const previous = queryClient.getQueryData(taskKeys.detail(chatId)) + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.detail(chatId) }) + const previous = queryClient.getQueryData( + mothershipChatKeys.detail(chatId) + ) if (previous) { const exists = previous.resources.some( (r) => r.type === resource.type && r.id === resource.id ) if (!exists) { - queryClient.setQueryData(taskKeys.detail(chatId), { + queryClient.setQueryData(mothershipChatKeys.detail(chatId), { ...previous, resources: [...previous.resources, resource], }) @@ -377,12 +383,12 @@ export function useAddChatResource(chatId?: string) { }, onError: (_err, _variables, context) => { if (context?.previous && chatId) { - queryClient.setQueryData(taskKeys.detail(chatId), context.previous) + queryClient.setQueryData(mothershipChatKeys.detail(chatId), context.previous) } }, onSettled: () => { if (chatId) { - queryClient.invalidateQueries({ queryKey: taskKeys.detail(chatId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.detail(chatId) }) } }, }) @@ -404,10 +410,12 @@ export function useReorderChatResources(chatId?: string) { mutationFn: reorderChatResources, onMutate: async ({ resources }) => { if (!chatId) return - await queryClient.cancelQueries({ queryKey: taskKeys.detail(chatId) }) - const previous = queryClient.getQueryData(taskKeys.detail(chatId)) + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.detail(chatId) }) + const previous = queryClient.getQueryData( + mothershipChatKeys.detail(chatId) + ) if (previous) { - queryClient.setQueryData(taskKeys.detail(chatId), { + queryClient.setQueryData(mothershipChatKeys.detail(chatId), { ...previous, resources, }) @@ -416,12 +424,12 @@ export function useReorderChatResources(chatId?: string) { }, onError: (_err, _variables, context) => { if (context?.previous && chatId) { - queryClient.setQueryData(taskKeys.detail(chatId), context.previous) + queryClient.setQueryData(mothershipChatKeys.detail(chatId), context.previous) } }, onSettled: () => { if (chatId) { - queryClient.invalidateQueries({ queryKey: taskKeys.detail(chatId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.detail(chatId) }) } }, }) @@ -444,11 +452,11 @@ export function useRemoveChatResource(chatId?: string) { mutationFn: removeChatResource, onMutate: async ({ resourceType, resourceId }) => { if (!chatId) return - await queryClient.cancelQueries({ queryKey: taskKeys.detail(chatId) }) - const removed: TaskChatHistory['resources'] = [] - queryClient.setQueryData(taskKeys.detail(chatId), (prev) => { + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.detail(chatId) }) + const removed: MothershipChatHistory['resources'] = [] + queryClient.setQueryData(mothershipChatKeys.detail(chatId), (prev) => { if (!prev) return prev - const next: TaskChatHistory['resources'] = [] + const next: MothershipChatHistory['resources'] = [] for (const r of prev.resources) { if (r.type === resourceType && r.id === resourceId) removed.push(r) else next.push(r) @@ -459,26 +467,26 @@ export function useRemoveChatResource(chatId?: string) { }, onError: (_err, _variables, context) => { if (!chatId || !context?.removed.length) return - queryClient.setQueryData(taskKeys.detail(chatId), (prev) => + queryClient.setQueryData(mothershipChatKeys.detail(chatId), (prev) => prev ? { ...prev, resources: [...prev.resources, ...context.removed] } : prev ) }, onSettled: () => { if (chatId) { - queryClient.invalidateQueries({ queryKey: taskKeys.detail(chatId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.detail(chatId) }) } }, }) } -async function markTaskRead(chatId: string): Promise { +async function markChatRead(chatId: string): Promise { await requestJson(updateMothershipChatContract, { params: { chatId }, body: { isUnread: false }, }) } -async function markTaskUnread(chatId: string): Promise { +async function markChatUnread(chatId: string): Promise { await requestJson(updateMothershipChatContract, { params: { chatId }, body: { isUnread: true }, @@ -491,81 +499,87 @@ function applyUnreadFlag( chatId: string, isUnread: boolean ): void { - const current = queryClient.getQueryData(taskKeys.list(workspaceId)) + const current = queryClient.getQueryData( + mothershipChatKeys.list(workspaceId) + ) if (!current) return - queryClient.setQueryData( - taskKeys.list(workspaceId), - current.map((task) => (task.id === chatId ? { ...task, isUnread } : task)) + queryClient.setQueryData( + mothershipChatKeys.list(workspaceId), + current.map((chat) => (chat.id === chatId ? { ...chat, isUnread } : chat)) ) } /** - * Marks a task as read with optimistic update. + * Marks a chat as read with optimistic update. * * The server only updates `lastSeenAt`, never `updatedAt`, so we deliberately * do not invalidate the list cache — that would trigger a refetch that can * reorder the sidebar if any unrelated server-side update landed in between. * * If there is no cached list yet (initial fetch still in flight, e.g. on - * task-page refresh), we skip cancellation entirely so the in-flight fetch + * chat-page refresh), we skip cancellation entirely so the in-flight fetch * can resolve normally — otherwise it would be orphaned and never refetched. * `onSuccess` then reconciles whichever state the fetch produced. */ -export function useMarkTaskRead(workspaceId?: string) { +export function useMarkMothershipChatRead(workspaceId?: string) { const queryClient = useQueryClient() return useMutation({ - mutationFn: markTaskRead, + mutationFn: markChatRead, onMutate: async (chatId) => { - const previousTasks = queryClient.getQueryData(taskKeys.list(workspaceId)) - if (!previousTasks) return { previousTasks } + const previousChats = queryClient.getQueryData( + mothershipChatKeys.list(workspaceId) + ) + if (!previousChats) return { previousChats } - await queryClient.cancelQueries({ queryKey: taskKeys.list(workspaceId) }) + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) applyUnreadFlag(queryClient, workspaceId, chatId, false) - return { previousTasks } + return { previousChats } }, onSuccess: (_data, chatId) => { applyUnreadFlag(queryClient, workspaceId, chatId, false) }, onError: (_err, _variables, context) => { - if (context?.previousTasks) { - queryClient.setQueryData(taskKeys.list(workspaceId), context.previousTasks) + if (context?.previousChats) { + queryClient.setQueryData(mothershipChatKeys.list(workspaceId), context.previousChats) } }, }) } /** - * Marks a task as unread with optimistic update. + * Marks a chat as unread with optimistic update. * - * Same rationale as `useMarkTaskRead` — no list invalidation, since the server + * Same rationale as `useMarkMothershipChatRead` — no list invalidation, since the server * only flips `lastSeenAt` and the optimistic update fully reflects the change. */ -export function useMarkTaskUnread(workspaceId?: string) { +export function useMarkMothershipChatUnread(workspaceId?: string) { const queryClient = useQueryClient() return useMutation({ - mutationFn: markTaskUnread, + mutationFn: markChatUnread, onMutate: async (chatId) => { - const previousTasks = queryClient.getQueryData(taskKeys.list(workspaceId)) - if (!previousTasks) return { previousTasks } + const previousChats = queryClient.getQueryData( + mothershipChatKeys.list(workspaceId) + ) + if (!previousChats) return { previousChats } - await queryClient.cancelQueries({ queryKey: taskKeys.list(workspaceId) }) + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) applyUnreadFlag(queryClient, workspaceId, chatId, true) - return { previousTasks } + return { previousChats } }, onSuccess: (_data, chatId) => { applyUnreadFlag(queryClient, workspaceId, chatId, true) }, onError: (_err, _variables, context) => { - if (context?.previousTasks) { - queryClient.setQueryData(taskKeys.list(workspaceId), context.previousTasks) + if (context?.previousChats) { + queryClient.setQueryData(mothershipChatKeys.list(workspaceId), context.previousChats) } }, }) } -async function setTaskPinned({ +async function setChatPinned({ chatId, pinned, }: { @@ -579,39 +593,41 @@ async function setTaskPinned({ } /** - * Pins or unpins a task with optimistic update. Pinned tasks are sorted to + * Pins or unpins a chat with optimistic update. Pinned chats are sorted to * the top of the list by the server; the optimistic reducer preserves that - * ordering by partitioning pinned and unpinned tasks while keeping each + * ordering by partitioning pinned and unpinned chats while keeping each * partition in its existing order (server returns desc(updatedAt) within). */ -export function useSetTaskPinned(workspaceId?: string) { +export function useSetMothershipChatPinned(workspaceId?: string) { const queryClient = useQueryClient() return useMutation({ - mutationFn: setTaskPinned, + mutationFn: setChatPinned, onMutate: async ({ chatId, pinned }) => { - await queryClient.cancelQueries({ queryKey: taskKeys.list(workspaceId) }) - const previousTasks = queryClient.getQueryData(taskKeys.list(workspaceId)) - if (!previousTasks) return { previousTasks: undefined } + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) + const previousChats = queryClient.getQueryData( + mothershipChatKeys.list(workspaceId) + ) + if (!previousChats) return { previousChats: undefined } - const updated = previousTasks.map((task) => - task.id === chatId ? { ...task, isPinned: pinned } : task + const updated = previousChats.map((chat) => + chat.id === chatId ? { ...chat, isPinned: pinned } : chat ) - const pinnedTasks = updated.filter((task) => task.isPinned) - const unpinnedTasks = updated.filter((task) => !task.isPinned) - queryClient.setQueryData(taskKeys.list(workspaceId), [ - ...pinnedTasks, - ...unpinnedTasks, + const pinnedChats = updated.filter((chat) => chat.isPinned) + const unpinnedChats = updated.filter((chat) => !chat.isPinned) + queryClient.setQueryData(mothershipChatKeys.list(workspaceId), [ + ...pinnedChats, + ...unpinnedChats, ]) - return { previousTasks } + return { previousChats } }, onError: (_err, _variables, context) => { - if (context?.previousTasks) { - queryClient.setQueryData(taskKeys.list(workspaceId), context.previousTasks) + if (context?.previousChats) { + queryClient.setQueryData(mothershipChatKeys.list(workspaceId), context.previousChats) } }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) }, }) } @@ -621,7 +637,7 @@ async function createChat(workspaceId: string): Promise<{ id: string }> { return { id } } -export function useCreateTask(workspaceId?: string) { +export function useCreateMothershipChat(workspaceId?: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: () => { @@ -630,8 +646,10 @@ export function useCreateTask(workspaceId?: string) { }, onSuccess: (data) => { if (!workspaceId) return - const existing = queryClient.getQueryData(taskKeys.list(workspaceId)) ?? [] - const newTask: TaskMetadata = { + const existing = + queryClient.getQueryData(mothershipChatKeys.list(workspaceId)) ?? + [] + const newChat: MothershipChatMetadata = { id: data.id, name: 'New chat', updatedAt: new Date(), @@ -639,17 +657,17 @@ export function useCreateTask(workspaceId?: string) { isUnread: false, isPinned: false, } - const pinnedCount = existing.findIndex((task) => !task.isPinned) + const pinnedCount = existing.findIndex((chat) => !chat.isPinned) const insertAt = pinnedCount === -1 ? existing.length : pinnedCount - queryClient.setQueryData(taskKeys.list(workspaceId), [ + queryClient.setQueryData(mothershipChatKeys.list(workspaceId), [ ...existing.slice(0, insertAt), - newTask, + newChat, ...existing.slice(insertAt), ]) }, onSettled: () => { if (!workspaceId) return - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) }, }) } @@ -665,18 +683,20 @@ async function forkChat(params: { return { id: data.id } } -export function useForkTask(workspaceId?: string) { +export function useForkMothershipChat(workspaceId?: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: forkChat, onSuccess: async (data, variables) => { if (!workspaceId) return - await queryClient.cancelQueries({ queryKey: taskKeys.list(workspaceId) }) - const existing = queryClient.getQueryData(taskKeys.list(workspaceId)) + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) + const existing = queryClient.getQueryData( + mothershipChatKeys.list(workspaceId) + ) if (existing) { - const sourceTask = existing.find((t) => t.id === variables.chatId) - const baseName = (sourceTask?.name ?? 'New chat').replace(/^Fork \| /, '') - const optimisticTask: TaskMetadata = { + const sourceChat = existing.find((t) => t.id === variables.chatId) + const baseName = (sourceChat?.name ?? 'New chat').replace(/^Fork \| /, '') + const optimisticChat: MothershipChatMetadata = { id: data.id, name: `Fork | ${baseName}`, updatedAt: new Date(), @@ -684,18 +704,18 @@ export function useForkTask(workspaceId?: string) { isUnread: false, isPinned: false, } - const pinnedCount = existing.findIndex((task) => !task.isPinned) + const pinnedCount = existing.findIndex((chat) => !chat.isPinned) const insertAt = pinnedCount === -1 ? existing.length : pinnedCount - queryClient.setQueryData(taskKeys.list(workspaceId), [ + queryClient.setQueryData(mothershipChatKeys.list(workspaceId), [ ...existing.slice(0, insertAt), - optimisticTask, + optimisticChat, ...existing.slice(insertAt), ]) } }, onSettled: () => { if (!workspaceId) return - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) }, }) } diff --git a/apps/sim/hooks/use-task-events.test.ts b/apps/sim/hooks/use-mothership-chat-events.test.ts similarity index 83% rename from apps/sim/hooks/use-task-events.test.ts rename to apps/sim/hooks/use-mothership-chat-events.test.ts index e81edbca6dd..de511c0bfa8 100644 --- a/apps/sim/hooks/use-task-events.test.ts +++ b/apps/sim/hooks/use-mothership-chat-events.test.ts @@ -4,10 +4,10 @@ import type { QueryClient } from '@tanstack/react-query' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { taskKeys } from '@/hooks/queries/tasks' -import { handleTaskStatusEvent } from '@/hooks/use-task-events' +import { mothershipChatKeys } from '@/hooks/queries/mothership-chats' +import { handleMothershipChatStatusEvent } from '@/hooks/use-mothership-chat-events' -describe('handleTaskStatusEvent', () => { +describe('handleMothershipChatStatusEvent', () => { const queryClient = { getQueryData: vi.fn(), invalidateQueries: vi.fn().mockResolvedValue(undefined), @@ -20,7 +20,7 @@ describe('handleTaskStatusEvent', () => { }) it('invalidates the task list and detail for completed task events', () => { - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -32,10 +32,10 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.detail('chat-1'), + queryKey: mothershipChatKeys.detail('chat-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -49,7 +49,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -61,7 +61,7 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -75,7 +75,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -88,7 +88,7 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -102,7 +102,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -115,7 +115,7 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -129,7 +129,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -142,10 +142,10 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.detail('chat-1'), + queryKey: mothershipChatKeys.detail('chat-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -159,7 +159,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -172,10 +172,10 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.detail('chat-1'), + queryKey: mothershipChatKeys.detail('chat-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -189,7 +189,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -202,16 +202,16 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.detail('chat-1'), + queryKey: mothershipChatKeys.detail('chat-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) it('invalidates the task list and detail for metadata-changing task events', () => { - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -223,16 +223,16 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.detail('chat-1'), + queryKey: mothershipChatKeys.detail('chat-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) it('invalidates the task list and removes detail cache for deleted task events', () => { - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -244,16 +244,16 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.removeQueries).toHaveBeenCalledTimes(1) expect(queryClient.removeQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.detail('chat-1'), + queryKey: mothershipChatKeys.detail('chat-1'), }) }) it('invalidates the task list and detail for started task events', () => { - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -265,10 +265,10 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.detail('chat-1'), + queryKey: mothershipChatKeys.detail('chat-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -282,7 +282,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -294,7 +294,7 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -308,7 +308,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -321,7 +321,7 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -335,7 +335,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -348,7 +348,7 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) @@ -362,7 +362,7 @@ describe('handleTaskStatusEvent', () => { resources: [], }) - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -375,16 +375,16 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(2) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.detail('chat-1'), + queryKey: mothershipChatKeys.detail('chat-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) it('keeps list invalidation only for unknown task event types', () => { - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, 'ws-1', JSON.stringify({ @@ -396,13 +396,13 @@ describe('handleTaskStatusEvent', () => { expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1) expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ - queryKey: taskKeys.list('ws-1'), + queryKey: mothershipChatKeys.list('ws-1'), }) expect(queryClient.removeQueries).not.toHaveBeenCalled() }) it('does not invalidate when task event payload is invalid', () => { - handleTaskStatusEvent(queryClient, 'ws-1', '{') + handleMothershipChatStatusEvent(queryClient, 'ws-1', '{') expect(queryClient.invalidateQueries).not.toHaveBeenCalled() expect(queryClient.removeQueries).not.toHaveBeenCalled() diff --git a/apps/sim/hooks/use-task-events.ts b/apps/sim/hooks/use-mothership-chat-events.ts similarity index 66% rename from apps/sim/hooks/use-task-events.ts rename to apps/sim/hooks/use-mothership-chat-events.ts index b9a5216dad4..ad8f0d9d3fd 100644 --- a/apps/sim/hooks/use-task-events.ts +++ b/apps/sim/hooks/use-mothership-chat-events.ts @@ -3,31 +3,31 @@ import { createLogger } from '@sim/logger' import type { QueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query' import { getLiveAssistantMessageId } from '@/lib/copilot/chat/effective-transcript' -import { type TaskChatHistory, taskKeys } from '@/hooks/queries/tasks' +import { type MothershipChatHistory, mothershipChatKeys } from '@/hooks/queries/mothership-chats' -const logger = createLogger('TaskEvents') +const logger = createLogger('MothershipChatEvents') -const TASK_STATUS_TYPES = ['started', 'completed', 'created', 'deleted', 'renamed'] as const -type TaskStatusEventType = (typeof TASK_STATUS_TYPES)[number] -const TASK_STATUS_TYPE_SET = new Set(TASK_STATUS_TYPES) +const CHAT_STATUS_TYPES = ['started', 'completed', 'created', 'deleted', 'renamed'] as const +type ChatStatusEventType = (typeof CHAT_STATUS_TYPES)[number] +const CHAT_STATUS_TYPE_SET = new Set(CHAT_STATUS_TYPES) -interface TaskStatusEventPayload { +interface ChatStatusEventPayload { chatId?: string - type?: TaskStatusEventType + type?: ChatStatusEventType streamId?: string } -const DETAIL_INVALIDATING_TASK_STATUS_TYPES = new Set([ +const DETAIL_INVALIDATING_CHAT_STATUS_TYPES = new Set([ 'started', 'completed', 'renamed', ]) -function isTaskStatusEventType(value: unknown): value is TaskStatusEventType { - return typeof value === 'string' && TASK_STATUS_TYPE_SET.has(value) +function isChatStatusEventType(value: unknown): value is ChatStatusEventType { + return typeof value === 'string' && CHAT_STATUS_TYPE_SET.has(value) } -function isLocalOptimisticActiveStream(current: TaskChatHistory | undefined) { +function isLocalOptimisticActiveStream(current: MothershipChatHistory | undefined) { if (!current?.activeStreamId) return false const liveAssistantId = getLiveAssistantMessageId(current.activeStreamId) return current.messages.some((message) => message.id === liveAssistantId) @@ -39,7 +39,7 @@ function isLocalOptimisticActiveStream(current: TaskChatHistory | undefined) { * If either stream is absent from the transcript, callers should refetch * instead of inferring order from incomplete cache state. */ -function hasNewerKnownActiveStream(current: TaskChatHistory | undefined, streamId: string) { +function hasNewerKnownActiveStream(current: MothershipChatHistory | undefined, streamId: string) { if (!current?.activeStreamId || current.activeStreamId === streamId) return false const activeIndex = current.messages.findIndex((message) => message.id === current.activeStreamId) @@ -50,8 +50,8 @@ function hasNewerKnownActiveStream(current: TaskChatHistory | undefined, streamI } function shouldSkipDetailInvalidationForStreamEvent( - current: TaskChatHistory | undefined, - payload: TaskStatusEventPayload + current: MothershipChatHistory | undefined, + payload: ChatStatusEventPayload ) { if (payload.type !== 'started' && payload.type !== 'completed') return false if (!current?.activeStreamId) return false @@ -66,7 +66,7 @@ function shouldSkipDetailInvalidationForStreamEvent( ) } -function parseTaskStatusEventPayload(data: unknown): TaskStatusEventPayload | null { +function parseChatStatusEventPayload(data: unknown): ChatStatusEventPayload | null { let parsed = data if (typeof parsed === 'string') { @@ -85,43 +85,46 @@ function parseTaskStatusEventPayload(data: unknown): TaskStatusEventPayload | nu return { ...(typeof record.chatId === 'string' ? { chatId: record.chatId } : {}), - ...(isTaskStatusEventType(record.type) ? { type: record.type } : {}), + ...(isChatStatusEventType(record.type) ? { type: record.type } : {}), ...(typeof record.streamId === 'string' ? { streamId: record.streamId } : {}), } } -export function handleTaskStatusEvent( +export function handleMothershipChatStatusEvent( queryClient: Pick, workspaceId: string, data: unknown ): void { - const payload = parseTaskStatusEventPayload(data) + const payload = parseChatStatusEventPayload(data) if (!payload) { logger.warn('Received invalid task_status payload') return } - queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) if (!payload.chatId) return if (payload.type === 'deleted') { - queryClient.removeQueries({ queryKey: taskKeys.detail(payload.chatId) }) + queryClient.removeQueries({ queryKey: mothershipChatKeys.detail(payload.chatId) }) return } if (payload.type === 'started' || payload.type === 'completed') { - const current = queryClient.getQueryData(taskKeys.detail(payload.chatId)) + const current = queryClient.getQueryData( + mothershipChatKeys.detail(payload.chatId) + ) if (shouldSkipDetailInvalidationForStreamEvent(current, payload)) { return } } - if (payload.type && DETAIL_INVALIDATING_TASK_STATUS_TYPES.has(payload.type)) { - queryClient.invalidateQueries({ queryKey: taskKeys.detail(payload.chatId) }) + if (payload.type && DETAIL_INVALIDATING_CHAT_STATUS_TYPES.has(payload.type)) { + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.detail(payload.chatId) }) } } /** - * Subscribes to task status SSE events and invalidates task caches on changes. + * Subscribes to chat status SSE events and invalidates chat caches on changes. + * The SSE event name remains `task_status` for wire compatibility. */ -export function useTaskEvents(workspaceId: string | undefined) { +export function useMothershipChatEvents(workspaceId: string | undefined) { const queryClient = useQueryClient() useEffect(() => { @@ -132,7 +135,7 @@ export function useTaskEvents(workspaceId: string | undefined) { ) eventSource.addEventListener('task_status', (event) => { - handleTaskStatusEvent( + handleMothershipChatStatusEvent( queryClient, workspaceId, event instanceof MessageEvent ? event.data : undefined diff --git a/apps/sim/lib/api/contracts/mothership-tasks.ts b/apps/sim/lib/api/contracts/mothership-chats.ts similarity index 98% rename from apps/sim/lib/api/contracts/mothership-tasks.ts rename to apps/sim/lib/api/contracts/mothership-chats.ts index 6e29964cdbe..2c65c824c2c 100644 --- a/apps/sim/lib/api/contracts/mothership-tasks.ts +++ b/apps/sim/lib/api/contracts/mothership-chats.ts @@ -201,7 +201,7 @@ export const removeMothershipChatResourceContract = defineRouteContract({ }, }) -export const mothershipTaskSchema = z.object({ +export const mothershipChatSchema = z.object({ id: z.string(), title: z.string().nullable(), updatedAt: dateStringSchema, @@ -218,7 +218,7 @@ export const listMothershipChatsContract = defineRouteContract({ mode: 'json', schema: z.object({ success: z.literal(true), - data: z.array(mothershipTaskSchema), + data: z.array(mothershipChatSchema), }), }, }) @@ -343,4 +343,4 @@ export const getMothershipChatContract = defineRouteContract({ }, }) -export type MothershipTask = z.infer +export type MothershipChat = z.infer diff --git a/apps/sim/lib/copilot/chat-status.ts b/apps/sim/lib/copilot/chat-status.ts new file mode 100644 index 00000000000..221eb20ac9f --- /dev/null +++ b/apps/sim/lib/copilot/chat-status.ts @@ -0,0 +1,42 @@ +/** + * Chat Status Pub/Sub Adapter + * + * Broadcasts chat status events across processes using Redis Pub/Sub. + * Gracefully falls back to process-local EventEmitter when Redis is unavailable. + * + * The Redis channel and SSE label retain the legacy `task:status_changed` + * identifier so live status updates keep flowing across pods during a rolling + * deploy (old and new pods must publish/subscribe on the same channel). + */ + +import { createPubSubChannel, type PubSubChannel } from '@/lib/events/pubsub' + +interface ChatStatusEvent { + workspaceId: string + chatId: string + type: 'started' | 'completed' | 'created' | 'deleted' | 'renamed' + streamId?: string +} + +type ChatPubSubGlobal = typeof globalThis & { + _chatStatusChannel?: PubSubChannel | null +} + +const g = globalThis as ChatPubSubGlobal + +if (!('_chatStatusChannel' in g)) { + g._chatStatusChannel = + typeof window !== 'undefined' + ? null + : createPubSubChannel({ channel: 'task:status_changed', label: 'task' }) +} + +const channel = g._chatStatusChannel + +export const chatPubSub = channel + ? { + publishStatusChanged: (event: ChatStatusEvent) => channel.publish(event), + onStatusChanged: (handler: (event: ChatStatusEvent) => void) => channel.subscribe(handler), + dispose: () => channel.dispose(), + } + : null diff --git a/apps/sim/lib/copilot/chat/post.test.ts b/apps/sim/lib/copilot/chat/post.test.ts index c2271ece05b..e9f4ba41e30 100644 --- a/apps/sim/lib/copilot/chat/post.test.ts +++ b/apps/sim/lib/copilot/chat/post.test.ts @@ -92,8 +92,8 @@ vi.mock('@/lib/copilot/chat/messages-store', () => ({ appendCopilotChatMessages, })) -vi.mock('@/lib/copilot/tasks', () => ({ - taskPubSub: { +vi.mock('@/lib/copilot/chat-status', () => ({ + chatPubSub: { publishStatusChanged: mockPublishStatusChanged, }, })) diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index 9cf5e6b2d15..f3b8e4a9259 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -23,6 +23,7 @@ import { } from '@/lib/copilot/chat/process-contents' import { finalizeAssistantTurn } from '@/lib/copilot/chat/terminal-state' import { generateWorkspaceContext } from '@/lib/copilot/chat/workspace-context' +import { chatPubSub } from '@/lib/copilot/chat-status' import { COPILOT_REQUEST_MODES } from '@/lib/copilot/constants' import { CopilotChatFinalizeOutcome, @@ -41,7 +42,6 @@ import { } from '@/lib/copilot/request/session' import type { ExecutionContext, OrchestratorResult } from '@/lib/copilot/request/types' import { persistChatResources } from '@/lib/copilot/resources/persistence' -import { taskPubSub } from '@/lib/copilot/tasks' import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' import { getWorkflowById, resolveWorkflowIdForUser } from '@/lib/workflows/utils' @@ -352,7 +352,7 @@ async function persistUserMessage(params: { ) if (notifyWorkspaceStatus && updated && workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId, chatId, type: 'started', @@ -451,7 +451,7 @@ function buildOnComplete(params: { finalization.outcome === CopilotChatFinalizeOutcome.AssistantAlreadyPersisted if (notifyWorkspaceStatus && workspaceId && shouldPublishCompletion) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId, chatId, type: 'completed', @@ -470,7 +470,7 @@ function buildOnComplete(params: { }) if (notifyWorkspaceStatus && workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId, chatId, type: 'completed', @@ -502,7 +502,7 @@ function buildOnError(params: { await finalizeAssistantTurn({ chatId, userMessageId }) if (notifyWorkspaceStatus && workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId, chatId, type: 'completed', diff --git a/apps/sim/lib/copilot/request/lifecycle/start.test.ts b/apps/sim/lib/copilot/request/lifecycle/start.test.ts index 917c56ba339..e5307fb703d 100644 --- a/apps/sim/lib/copilot/request/lifecycle/start.test.ts +++ b/apps/sim/lib/copilot/request/lifecycle/start.test.ts @@ -107,8 +107,8 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@/lib/copilot/tasks', () => ({ - taskPubSub: null, +vi.mock('@/lib/copilot/chat-status', () => ({ + chatPubSub: null, })) import { createSSEStream } from './start' diff --git a/apps/sim/lib/copilot/request/lifecycle/start.ts b/apps/sim/lib/copilot/request/lifecycle/start.ts index 595efe15f33..69682f0019f 100644 --- a/apps/sim/lib/copilot/request/lifecycle/start.ts +++ b/apps/sim/lib/copilot/request/lifecycle/start.ts @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { eq } from 'drizzle-orm' import { createRunSegment } from '@/lib/copilot/async-runs/repository' +import { chatPubSub } from '@/lib/copilot/chat-status' import { MothershipStreamV1EventType, MothershipStreamV1SessionKind, @@ -40,7 +41,6 @@ import { import { SSE_RESPONSE_HEADERS } from '@/lib/copilot/request/session/sse' import { reportTrace, TraceCollector } from '@/lib/copilot/request/trace' import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url' -import { taskPubSub } from '@/lib/copilot/tasks' import { env } from '@/lib/core/config/env' export { SSE_RESPONSE_HEADERS } @@ -476,7 +476,7 @@ function fireTitleGeneration(params: { payload: { kind: MothershipStreamV1SessionKind.title, title }, }) if (workspaceId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId, chatId, type: 'renamed', diff --git a/apps/sim/lib/copilot/tasks.ts b/apps/sim/lib/copilot/tasks.ts deleted file mode 100644 index 252b82a61ae..00000000000 --- a/apps/sim/lib/copilot/tasks.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Task Status Pub/Sub Adapter - * - * Broadcasts task status events across processes using Redis Pub/Sub. - * Gracefully falls back to process-local EventEmitter when Redis is unavailable. - * - * Channel: `task:status_changed` - */ - -import { createPubSubChannel, type PubSubChannel } from '@/lib/events/pubsub' - -interface TaskStatusEvent { - workspaceId: string - chatId: string - type: 'started' | 'completed' | 'created' | 'deleted' | 'renamed' - streamId?: string -} - -type TaskPubSubGlobal = typeof globalThis & { - _taskStatusChannel?: PubSubChannel | null -} - -const g = globalThis as TaskPubSubGlobal - -if (!('_taskStatusChannel' in g)) { - g._taskStatusChannel = - typeof window !== 'undefined' - ? null - : createPubSubChannel({ channel: 'task:status_changed', label: 'task' }) -} - -const channel = g._taskStatusChannel - -export const taskPubSub = channel - ? { - publishStatusChanged: (event: TaskStatusEvent) => channel.publish(event), - onStatusChanged: (handler: (event: TaskStatusEvent) => void) => channel.subscribe(handler), - dispose: () => channel.dispose(), - } - : null diff --git a/apps/sim/lib/copilot/tools/handlers/oauth.ts b/apps/sim/lib/copilot/tools/handlers/oauth.ts index 15ae0d04554..3919b0ba557 100644 --- a/apps/sim/lib/copilot/tools/handlers/oauth.ts +++ b/apps/sim/lib/copilot/tools/handlers/oauth.ts @@ -109,7 +109,7 @@ async function generateOAuthLink( workflowId && workspaceId ? `${baseUrl}/workspace/${workspaceId}/w/${workflowId}` : chatId && workspaceId - ? `${baseUrl}/workspace/${workspaceId}/task/${chatId}` + ? `${baseUrl}/workspace/${workspaceId}/chat/${chatId}` : `${baseUrl}/workspace/${workspaceId}` if (providerId === 'trello') { diff --git a/apps/sim/lib/mothership/inbox/executor.ts b/apps/sim/lib/mothership/inbox/executor.ts index 67ac5296632..871dfa033a7 100644 --- a/apps/sim/lib/mothership/inbox/executor.ts +++ b/apps/sim/lib/mothership/inbox/executor.ts @@ -11,10 +11,10 @@ import { buildPersistedUserMessage, } from '@/lib/copilot/chat/persisted-message' import { generateWorkspaceContext } from '@/lib/copilot/chat/workspace-context' +import { chatPubSub } from '@/lib/copilot/chat-status' import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' import { requestChatTitle } from '@/lib/copilot/request/lifecycle/start' import type { OrchestratorResult } from '@/lib/copilot/request/types' -import { taskPubSub } from '@/lib/copilot/tasks' import { isHosted } from '@/lib/core/config/feature-flags' import * as agentmail from '@/lib/mothership/inbox/agentmail-client' import { formatEmailAsMessage } from '@/lib/mothership/inbox/format' @@ -117,7 +117,7 @@ export async function executeInboxTask(taskId: string): Promise { .then(async (title) => { if (title && chatId) { await db.update(copilotChats).set({ title }).where(eq(copilotChats.id, chatId)) - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: ws.id, chatId, type: 'renamed', @@ -128,7 +128,7 @@ export async function executeInboxTask(taskId: string): Promise { logger.warn('Failed to generate chat title', { chatId, err }) }) - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: ws.id, chatId, type: 'created', @@ -138,7 +138,7 @@ export async function executeInboxTask(taskId: string): Promise { const userMessageId = generateId() if (chatId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: ws.id, chatId, type: 'started', @@ -260,7 +260,7 @@ export async function executeInboxTask(taskId: string): Promise { .where(eq(mothershipInboxTask.id, taskId)) if (chatId) { - taskPubSub?.publishStatusChanged({ + chatPubSub?.publishStatusChanged({ workspaceId: ws.id, chatId, type: 'completed', diff --git a/apps/sim/lib/mothership/inbox/response.ts b/apps/sim/lib/mothership/inbox/response.ts index f289ec705ce..1a32f5d6355 100644 --- a/apps/sim/lib/mothership/inbox/response.ts +++ b/apps/sim/lib/mothership/inbox/response.ts @@ -32,7 +32,7 @@ export async function sendInboxResponse( } const chatUrl = inboxTask.chatId - ? `${getBaseUrl()}/workspace/${ctx.workspaceId}/task/${inboxTask.chatId}` + ? `${getBaseUrl()}/workspace/${ctx.workspaceId}/chat/${inboxTask.chatId}` : `${getBaseUrl()}/workspace/${ctx.workspaceId}/home` const text = result.success diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index d5b81c9da5c..7addcc8b697 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -315,6 +315,15 @@ const nextConfig: NextConfig = { } ) + // Legacy chat URL support: the workspace chat route was renamed from + // `/workspace/:workspaceId/task/:chatId` to `/workspace/:workspaceId/chat/:chatId`. + // Preserve existing bookmarks and deeplinks. + redirects.push({ + source: '/workspace/:workspaceId/task/:chatId', + destination: '/workspace/:workspaceId/chat/:chatId', + permanent: true, + }) + return redirects }, async rewrites() { diff --git a/apps/sim/stores/folders/store.ts b/apps/sim/stores/folders/store.ts index b3ac643cdee..5f4115418fb 100644 --- a/apps/sim/stores/folders/store.ts +++ b/apps/sim/stores/folders/store.ts @@ -9,8 +9,8 @@ interface FolderState { selectedWorkflows: Set selectedFolders: Set lastSelectedFolderId: string | null - selectedTasks: Set - lastSelectedTaskId: string | null + selectedChats: Set + lastSelectedChatId: string | null toggleExpanded: (folderId: string) => void setExpanded: (folderId: string, expanded: boolean) => void @@ -33,15 +33,15 @@ interface FolderState { selectFolderRange: (folderIds: string[], fromId: string, toId: string) => void isFolderSelected: (folderId: string) => boolean - // Task selection actions - selectTaskOnly: (taskId: string) => void - toggleTaskSelection: (taskId: string) => void - selectTaskRange: (taskIds: string[], fromId: string, toId: string) => void - clearTaskSelection: () => void - isTaskSelected: (taskId: string) => boolean + // Chat selection actions + selectChatOnly: (chatId: string) => void + toggleChatSelection: (chatId: string) => void + selectChatRange: (chatIds: string[], fromId: string, toId: string) => void + clearChatSelection: () => void + isChatSelected: (chatId: string) => boolean // Unified selection helpers - getFullSelection: () => { workflowIds: string[]; folderIds: string[]; taskIds: string[] } + getFullSelection: () => { workflowIds: string[]; folderIds: string[]; chatIds: string[] } hasAnySelection: () => boolean isMixedSelection: () => boolean clearAllSelection: () => void @@ -54,8 +54,8 @@ export const useFolderStore = create()( selectedWorkflows: new Set(), selectedFolders: new Set(), lastSelectedFolderId: null, - selectedTasks: new Set(), - lastSelectedTaskId: null, + selectedChats: new Set(), + lastSelectedChatId: null, toggleExpanded: (folderId) => set((state) => { @@ -104,9 +104,9 @@ export const useFolderStore = create()( } return { selectedWorkflows: newSelected, - ...(state.selectedTasks.size > 0 && { - selectedTasks: new Set(), - lastSelectedTaskId: null, + ...(state.selectedChats.size > 0 && { + selectedChats: new Set(), + lastSelectedChatId: null, }), } }), @@ -118,8 +118,8 @@ export const useFolderStore = create()( selectedWorkflows: new Set([workflowId]), selectedFolders: new Set(), lastSelectedFolderId: null, - selectedTasks: new Set(), - lastSelectedTaskId: null, + selectedChats: new Set(), + lastSelectedChatId: null, }), selectRange: (workflowIds, fromId, toId) => { @@ -175,9 +175,9 @@ export const useFolderStore = create()( return { selectedFolders: newSelected, lastSelectedFolderId: newLastSelected, - ...(state.selectedTasks.size > 0 && { - selectedTasks: new Set(), - lastSelectedTaskId: null, + ...(state.selectedChats.size > 0 && { + selectedChats: new Set(), + lastSelectedChatId: null, }), } }), @@ -188,8 +188,8 @@ export const useFolderStore = create()( set({ selectedFolders: new Set([folderId]), lastSelectedFolderId: folderId, - selectedTasks: new Set(), - lastSelectedTaskId: null, + selectedChats: new Set(), + lastSelectedChatId: null, }), selectFolderRange: (folderIds, fromId, toId) => { @@ -206,11 +206,11 @@ export const useFolderStore = create()( isFolderSelected: (folderId) => get().selectedFolders.has(folderId), - // Task selection actions - selectTaskOnly: (taskId) => + // Chat selection actions + selectChatOnly: (chatId) => set((state) => ({ - selectedTasks: new Set([taskId]), - lastSelectedTaskId: taskId, + selectedChats: new Set([chatId]), + lastSelectedChatId: chatId, ...(state.selectedWorkflows.size > 0 && { selectedWorkflows: new Set() }), ...(state.selectedFolders.size > 0 && { selectedFolders: new Set(), @@ -218,23 +218,23 @@ export const useFolderStore = create()( }), })), - toggleTaskSelection: (taskId) => + toggleChatSelection: (chatId) => set((state) => { - const newSelected = new Set(state.selectedTasks) + const newSelected = new Set(state.selectedChats) let newLastSelected: string | null - if (newSelected.has(taskId)) { - newSelected.delete(taskId) + if (newSelected.has(chatId)) { + newSelected.delete(chatId) newLastSelected = - state.lastSelectedTaskId === taskId + state.lastSelectedChatId === chatId ? (Array.from(newSelected)[0] ?? null) - : state.lastSelectedTaskId + : state.lastSelectedChatId } else { - newSelected.add(taskId) - newLastSelected = taskId + newSelected.add(chatId) + newLastSelected = chatId } return { - selectedTasks: newSelected, - lastSelectedTaskId: newLastSelected, + selectedChats: newSelected, + lastSelectedChatId: newLastSelected, ...(state.selectedWorkflows.size > 0 && { selectedWorkflows: new Set() }), ...(state.selectedFolders.size > 0 && { selectedFolders: new Set(), @@ -243,19 +243,19 @@ export const useFolderStore = create()( } }), - selectTaskRange: (taskIds, fromId, toId) => { - const fromIndex = taskIds.indexOf(fromId) - const toIndex = taskIds.indexOf(toId) + selectChatRange: (chatIds, fromId, toId) => { + const fromIndex = chatIds.indexOf(fromId) + const toIndex = chatIds.indexOf(toId) if (fromIndex === -1 || toIndex === -1) return const [start, end] = fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex] - const rangeIds = taskIds.slice(start, end + 1) + const rangeIds = chatIds.slice(start, end + 1) const state = get() set({ - selectedTasks: new Set(rangeIds), - lastSelectedTaskId: fromId, + selectedChats: new Set(rangeIds), + lastSelectedChatId: fromId, ...(state.selectedWorkflows.size > 0 && { selectedWorkflows: new Set() }), ...(state.selectedFolders.size > 0 && { selectedFolders: new Set(), @@ -264,21 +264,21 @@ export const useFolderStore = create()( }) }, - clearTaskSelection: () => set({ selectedTasks: new Set(), lastSelectedTaskId: null }), + clearChatSelection: () => set({ selectedChats: new Set(), lastSelectedChatId: null }), - isTaskSelected: (taskId) => get().selectedTasks.has(taskId), + isChatSelected: (chatId) => get().selectedChats.has(chatId), // Unified selection helpers getFullSelection: () => ({ workflowIds: Array.from(get().selectedWorkflows), folderIds: Array.from(get().selectedFolders), - taskIds: Array.from(get().selectedTasks), + chatIds: Array.from(get().selectedChats), }), hasAnySelection: () => get().selectedWorkflows.size > 0 || get().selectedFolders.size > 0 || - get().selectedTasks.size > 0, + get().selectedChats.size > 0, isMixedSelection: () => get().selectedWorkflows.size > 0 && get().selectedFolders.size > 0, @@ -287,8 +287,8 @@ export const useFolderStore = create()( selectedWorkflows: new Set(), selectedFolders: new Set(), lastSelectedFolderId: null, - selectedTasks: new Set(), - lastSelectedTaskId: null, + selectedChats: new Set(), + lastSelectedChatId: null, }), }), { name: 'folder-store' } From d2e26d882b6ed9584f134445b5ac486c4146a5a1 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 8 Jun 2026 13:02:19 -0700 Subject: [PATCH 2/2] refactor(mothership-chats): use skipToken in useMothershipChatHistory Drop the enabled + chatId! non-null assertion in favor of the skipToken pattern, matching useMothershipChats. Behavior-preserving. --- apps/sim/hooks/queries/mothership-chats.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/sim/hooks/queries/mothership-chats.ts b/apps/sim/hooks/queries/mothership-chats.ts index 0b5117ac66a..bc7743804d6 100644 --- a/apps/sim/hooks/queries/mothership-chats.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -261,8 +261,7 @@ export async function fetchMothershipChatHistory( export function useMothershipChatHistory(chatId: string | undefined) { return useQuery({ queryKey: mothershipChatKeys.detail(chatId), - queryFn: ({ signal }) => fetchMothershipChatHistory(chatId!, signal), - enabled: Boolean(chatId), + queryFn: chatId ? ({ signal }) => fetchMothershipChatHistory(chatId, signal) : skipToken, staleTime: 30 * 1000, }) }