Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useChatStore } from '@/stores/chat/store'
import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
import type { ChatContext, PanelTab } from '@/stores/panel'
import { usePanelStore } from '@/stores/panel'
import { useVariablesModalStore } from '@/stores/variables/modal'
Expand All @@ -97,6 +98,22 @@ import type { WorkflowState } from '@/stores/workflows/workflow/types'

const logger = createLogger('Panel')
const EMPTY_COPILOT_CHATS: readonly CopilotChatListItem[] = []

/**
* Builds the persisted draft key for a workflow-copilot chat.
*
* Scoped per chat, not per workflow: a draft is cleared only on submit, so a
* workflow-wide key carries one chat's typed text, contexts, and attachments
* into the next chat selected. The workflow segment stays so each workflow
* keeps its own unselected-chat (`new`) draft.
*/
function copilotDraftKey(
workspaceId: string,
workflowId: string | undefined,
chatId: string | undefined
): string | undefined {
return workflowId ? `${workspaceId}:workflow-copilot:${workflowId}:${chatId ?? 'new'}` : undefined
}
/**
* Panel component with resizable width and tab navigation that persists across page refreshes.
*
Expand Down Expand Up @@ -274,6 +291,9 @@ export const Panel = memo(function Panel() {
activeWorkflowId ?? undefined
)

const copilotDraftWorkflowId = activeWorkflowId ?? routeWorkflowId
const copilotDraftScopeKey = copilotDraftKey(workspaceId, copilotDraftWorkflowId, copilotChatId)

const { data: copilotChatList = EMPTY_COPILOT_CHATS } = useCopilotChats(
isCopilotTabAvailable ? (activeWorkflowId ?? undefined) : undefined
)
Expand Down Expand Up @@ -332,13 +352,16 @@ export const Panel = memo(function Panel() {
if (copilotChatId === chatId) {
setCopilotChatId(undefined)
}
// The draft store is persisted, so an unpruned key survives forever.
const draftKey = copilotDraftKey(workspaceId, copilotDraftWorkflowId, chatId)
if (draftKey) useMothershipDraftsStore.getState().clearDraft(draftKey)
loadCopilotChats()
})
.catch((err) => {
logger.error('Failed to delete copilot chat', { error: toError(err).message, chatId })
})
},
[copilotChatId, loadCopilotChats, setCopilotChatId]
[copilotChatId, loadCopilotChats, setCopilotChatId, workspaceId, copilotDraftWorkflowId]
)

const handleCopilotToolResult = useCallback(
Expand Down Expand Up @@ -398,10 +421,6 @@ export const Panel = memo(function Panel() {
},
})
)
const copilotDraftWorkflowId = activeWorkflowId ?? routeWorkflowId
const copilotDraftScopeKey = copilotDraftWorkflowId
? `${workspaceId}:workflow-copilot:${copilotDraftWorkflowId}`
: undefined

const handleCopilotNewChat = useCallback(() => {
if (!activeWorkflowId || !workspaceId) return
Expand Down
39 changes: 39 additions & 0 deletions apps/sim/stores/mothership-drafts/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { dropLegacyWorkflowCopilotDrafts } from '@/stores/mothership-drafts/store'

const payload = { text: 'unsent' }

describe('dropLegacyWorkflowCopilotDrafts', () => {
it('drops workflow-only copilot keys that no surface reads anymore', () => {
const { drafts } = dropLegacyWorkflowCopilotDrafts({
drafts: { 'ws-1:workflow-copilot:wf-1': payload },
})

expect(drafts).toEqual({})
})

it('keeps home drafts, whose key shape did not change', () => {
const { drafts } = dropLegacyWorkflowCopilotDrafts({
drafts: { 'ws-1:chat-1': payload, 'ws-1:new': payload },
})

expect(drafts).toEqual({ 'ws-1:chat-1': payload, 'ws-1:new': payload })
})

it('keeps per-chat copilot keys, including the unselected-chat slot', () => {
const drafts = {
'ws-1:workflow-copilot:wf-1:chat-1': payload,
'ws-1:workflow-copilot:wf-1:new': payload,
}

expect(dropLegacyWorkflowCopilotDrafts({ drafts }).drafts).toEqual(drafts)
})

it('returns an empty map when nothing was persisted', () => {
expect(dropLegacyWorkflowCopilotDrafts(null).drafts).toEqual({})
expect(dropLegacyWorkflowCopilotDrafts({}).drafts).toEqual({})
})
})
29 changes: 29 additions & 0 deletions apps/sim/stores/mothership-drafts/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,38 @@ export interface DraftPayload {
contexts?: ChatContext[]
}

/**
* Draft keys are owned by the surface that renders the input, not by this
* store. Two shapes exist: `<workspaceId>:<chatId|'new'>` for the home chat and
* `<workspaceId>:workflow-copilot:<workflowId>:<chatId|'new'>` for the workflow
* panel.
*/
interface MothershipDraftsState {
drafts: Record<string, DraftPayload>
setDraft: (key: string, payload: DraftPayload) => void
clearDraft: (key: string) => void
}

const LEGACY_WORKFLOW_COPILOT_KEY = /^[^:]+:workflow-copilot:[^:]+$/

/**
* v0 keyed workflow-panel drafts by workflow alone. Those entries are no longer
* readable by any surface, and nothing prunes a key that is never written
* again, so drop them once rather than leave them in storage forever. Home
* drafts are untouched — their key shape did not change.
*/
export function dropLegacyWorkflowCopilotDrafts(persistedState: unknown): {
drafts: Record<string, DraftPayload>
} {
const drafts = (persistedState as MothershipDraftsState | null)?.drafts
if (!drafts) return { drafts: {} }
const kept: Record<string, DraftPayload> = {}
for (const [key, payload] of Object.entries(drafts)) {
if (!LEGACY_WORKFLOW_COPILOT_KEY.test(key)) kept[key] = payload
}
return { drafts: kept }
}

function isEmpty(payload: DraftPayload): boolean {
return !payload.text && !payload.fileAttachments?.length && !payload.contexts?.length
}
Expand Down Expand Up @@ -42,6 +68,9 @@ export const useMothershipDraftsStore = create<MothershipDraftsState>()(
}),
{
name: 'mothership-drafts:v1',
version: 1,
migrate: (persistedState, version) =>
(version ?? 0) < 1 ? dropLegacyWorkflowCopilotDrafts(persistedState) : persistedState,
partialize: (state) => ({ drafts: state.drafts }),
}
),
Expand Down
Loading