Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
Fix/mothership 3#3610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Fix/mothership 3 #3610
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -12,6 +12,7 @@ import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types' | ||
| import { isWorkflowToolName } from '@/lib/copilot/workflow-tools' | ||
| import { getNextWorkflowColor } from '@/lib/workflows/colors' | ||
| import { invalidateResourceQueries } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' | ||
| import { deploymentKeys } from '@/hooks/queries/deployments' | ||
| import { | ||
| type TaskChatHistory, | ||
| type TaskStoredContentBlock, | ||
| @@ -22,6 +23,7 @@ import { | ||
| useChatHistory, | ||
| } from '@/hooks/queries/tasks' | ||
| import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order' | ||
| import { workflowKeys } from '@/hooks/queries/workflows' | ||
| import { useExecutionStream } from '@/hooks/use-execution-stream' | ||
| import { useExecutionStore } from '@/stores/execution/store' | ||
| import { useFolderStore } from '@/stores/folders/store' | ||
| @@ -74,6 +76,8 @@ const STATE_TO_STATUS: Record<string, ToolCallStatus> = { | ||
| skipped: 'success', | ||
| } as const | ||
| const DEPLOY_TOOL_NAMES = new Set(['deploy_api', 'deploy_chat', 'deploy_mcp', 'redeploy']) | ||
| function mapStoredBlock(block: TaskStoredContentBlock): ContentBlock { | ||
| const mapped: ContentBlock = { | ||
| type: block.type as ContentBlockType, | ||
| @@ -361,6 +365,15 @@ export function useChat( | ||
| useEffect(() => { | ||
| if (!chatHistory || appliedChatIdRef.current === chatHistory.id) return | ||
| const activeStreamId = chatHistory.activeStreamId | ||
| const snapshot = chatHistory.streamSnapshot | ||
| if (activeStreamId && !snapshot && !sendingRef.current) { | ||
| queryClient.invalidateQueries({ queryKey: taskKeys.detail(chatHistory.id) }) | ||
| return | ||
| } | ||
TheodoreSpeaks marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| appliedChatIdRef.current = chatHistory.id | ||
| setMessages(chatHistory.messages.map(mapStoredMessage)) | ||
| @@ -374,11 +387,6 @@ export function useChat( | ||
| } | ||
| } | ||
| // Kick off stream reconnection immediately if there's an active stream. | ||
| // The stream snapshot was fetched in parallel with the chat history (same | ||
| // API call), so there's no extra round-trip. | ||
| const activeStreamId = chatHistory.activeStreamId | ||
| const snapshot = chatHistory.streamSnapshot | ||
| if (activeStreamId && !sendingRef.current) { | ||
| const gen = ++streamGenRef.current | ||
| const abortController = new AbortController() | ||
| @@ -396,8 +404,7 @@ export function useChat( | ||
| const batchEvents = snapshot?.events ?? [] | ||
| const streamStatus = snapshot?.status ?? '' | ||
| if (!snapshot || (batchEvents.length === 0 && streamStatus === 'unknown')) { | ||
| // No snapshot available — stream buffer expired. Clean up. | ||
| if (batchEvents.length === 0 && streamStatus === 'unknown') { | ||
| const cid = chatIdRef.current | ||
| if (cid) { | ||
| fetch('/api/mothership/chat/stop', { | ||
| @@ -462,7 +469,7 @@ export function useChat( | ||
| } | ||
| reconnect() | ||
| } | ||
| }, [chatHistory, workspaceId]) | ||
| }, [chatHistory, workspaceId, queryClient]) | ||
| useEffect(() => { | ||
| if (resources.length === 0) { | ||
| @@ -686,6 +693,33 @@ export function useChat( | ||
| onResourceEventRef.current?.() | ||
| } | ||
| } | ||
| if (DEPLOY_TOOL_NAMES.has(tc.name) && tc.status === 'success') { | ||
| const output = tc.result?.output as Record<string, unknown> | undefined | ||
| const deployedWorkflowId = (output?.workflowId as string) ?? undefined | ||
| if (deployedWorkflowId && typeof output?.isDeployed === 'boolean') { | ||
| const isDeployed = output.isDeployed as boolean | ||
| const serverDeployedAt = output.deployedAt | ||
| ? new Date(output.deployedAt as string) | ||
| : undefined | ||
| useWorkflowRegistry | ||
| .getState() | ||
| .setDeploymentStatus( | ||
| deployedWorkflowId, | ||
| isDeployed, | ||
| isDeployed ? (serverDeployedAt ?? new Date()) : undefined | ||
| ) | ||
| queryClient.invalidateQueries({ | ||
| queryKey: deploymentKeys.info(deployedWorkflowId), | ||
| }) | ||
| queryClient.invalidateQueries({ | ||
| queryKey: deploymentKeys.versions(deployedWorkflowId), | ||
| }) | ||
| queryClient.invalidateQueries({ | ||
| queryKey: workflowKeys.list(workspaceId), | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| break | ||
| @@ -1116,11 +1150,6 @@ export function useChat( | ||
| useEffect(() => { | ||
| return () => { | ||
| streamGenRef.current++ | ||
| // Only drop the browser→Sim read; the Sim→Go stream stays open | ||
| // so the backend can finish persisting. Explicit abort is only | ||
| // triggered by the stop button via /api/copilot/chat/abort. | ||
| abortControllerRef.current?.abort() | ||
| abortControllerRef.current = null | ||
| sendingRef.current = false | ||
| } | ||
| }, []) | ||
TheodoreSpeaks marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -87,7 +87,16 @@ export async function executeDeployChat( | ||
| return { success: false, error: 'Unauthorized chat access' } | ||
| } | ||
| await db.delete(chat).where(eq(chat.id, existing[0].id)) | ||
| return { success: true, output: { success: true, action: 'undeploy', isDeployed: false } } | ||
| return { | ||
| success: true, | ||
| output: { | ||
| workflowId, | ||
| success: true, | ||
| action: 'undeploy', | ||
| isDeployed: true, | ||
| isChatDeployed: false, | ||
| }, | ||
| } | ||
TheodoreSpeaks marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| const { hasAccess } = await checkWorkflowAccessForChatCreation(workflowId, context.userId) | ||
| @@ -199,9 +208,11 @@ export async function executeDeployChat( | ||
| return { | ||
| success: true, | ||
| output: { | ||
| workflowId, | ||
| success: true, | ||
| action: 'deploy', | ||
| isDeployed: true, | ||
| isChatDeployed: true, | ||
| identifier, | ||
| chatUrl: `${baseUrl}/chat/${identifier}`, | ||
| apiEndpoint: `${baseUrl}/api/workflows/${workflowId}/run`, | ||
| @@ -252,6 +263,8 @@ export async function executeDeployMcp( | ||
| mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId }) | ||
| // Intentionally omits `isDeployed` — removing from an MCP server does not | ||
| // affect the workflow's API deployment. | ||
| return { | ||
| success: true, | ||
| output: { workflowId, serverId, action: 'undeploy', removed: true }, | ||
| @@ -335,9 +348,12 @@ export async function executeDeployMcp( | ||
| } | ||
| } | ||
| export async function executeRedeploy(context: ExecutionContext): Promise<ToolCallResult> { | ||
| export async function executeRedeploy( | ||
| params: { workflowId?: string }, | ||
| context: ExecutionContext | ||
| ): Promise<ToolCallResult> { | ||
| try { | ||
| const workflowId = context.workflowId | ||
| const workflowId = params.workflowId || context.workflowId | ||
| if (!workflowId) { | ||
| return { success: false, error: 'workflowId is required' } | ||
| } | ||
| @@ -352,6 +368,7 @@ export async function executeRedeploy(context: ExecutionContext): Promise<ToolCa | ||
| success: true, | ||
| output: { | ||
| workflowId, | ||
| isDeployed: true, | ||
| deployedAt: result.deployedAt || null, | ||
| version: result.version, | ||
| apiEndpoint: `${baseUrl}/api/workflows/${workflowId}/run`, | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Misleading error message when authorization fails
When a
workflowIdis supplied butauthorizeWorkflowByWorkspacePermissionreturnsallowed: false,resolvedbecomesnulland the function returns the error"workflowId is required for build. Call create_workflow first."This message implies the caller forgot to provide a workflow ID, when the actual problem is a permissions failure. An LLM acting as an MCP client will likely misinterpret this and try creating a new workflow instead of flagging an access error.Consider returning a distinct message for the authorization-failed path:
…and checking
resolved?.errorseparately from the missing-ID case.