From 64cdab24f77329cfca667c97875df6286f951482 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 14 Apr 2026 10:04:08 -0700 Subject: [PATCH 1/3] fix(ui): handle long file paths and names in search modal (#4155) * fix(ui): handle long file paths and names in search modal * Handle long subfolder names * fix memo --- .../search-modal/components/command-items.tsx | 34 ++++++++++++------- .../search-modal/components/search-groups.tsx | 2 +- .../sidebar/components/search-modal/utils.ts | 2 +- .../w/components/sidebar/sidebar.tsx | 8 ++--- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx index 7b845995acb..a618748eb6d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx @@ -58,7 +58,7 @@ export const MemoizedWorkflowItem = memo( onSelect: () => void color: string name: string - folderPath?: string + folderPath?: string[] isCurrent?: boolean }) { return ( @@ -71,13 +71,21 @@ export const MemoizedWorkflowItem = memo( backgroundClip: 'padding-box', }} /> - - {name} - {isCurrent && ' (current)'} + + {name} + {isCurrent && (current)} - {folderPath && ( - - {folderPath} + {folderPath && folderPath.length > 0 && ( + + {folderPath.length > 1 && ( + <> + + {folderPath.slice(0, -1).join(' / ')} + + / + + )} + {folderPath[folderPath.length - 1]} )} @@ -87,8 +95,10 @@ export const MemoizedWorkflowItem = memo( prev.value === next.value && prev.color === next.color && prev.name === next.name && - prev.folderPath === next.folderPath && - prev.isCurrent === next.isCurrent + prev.isCurrent === next.isCurrent && + (prev.folderPath === next.folderPath || + (prev.folderPath?.length === next.folderPath?.length && + (prev.folderPath ?? []).every((segment, i) => segment === next.folderPath?.[i]))) ) export const MemoizedTaskItem = memo( @@ -127,9 +137,9 @@ export const MemoizedWorkspaceItem = memo( }) { return ( - - {name} - {isCurrent && ' (current)'} + + {name} + {isCurrent && (current)} ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups.tsx index 4fd1ceaf9cb..1fd4707fabb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups.tsx @@ -163,7 +163,7 @@ export const WorkflowsGroup = memo(function WorkflowsGroup({ {items.map((workflow) => ( onSelect(workflow)} color={workflow.color} name={workflow.name} 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 538d90c1a92..7d44c1d9550 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 @@ -11,7 +11,7 @@ export interface WorkflowItem { name: string href: string color: string - folderPath?: string + folderPath?: string[] isCurrent?: boolean } 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 f899d6f0895..587d17cc9e3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -637,16 +637,14 @@ export const Sidebar = memo(function Sidebar() { () => regularWorkflows.map((workflow) => { const folderPath = workflow.folderId - ? getFolderPath(folderMap, workflow.folderId) - .map((folder) => folder.name) - .join(' / ') - : '' + ? getFolderPath(folderMap, workflow.folderId).map((folder) => folder.name) + : [] return { id: workflow.id, name: workflow.name, href: `/workspace/${workspaceId}/w/${workflow.id}`, color: workflow.color, - folderPath: folderPath || undefined, + folderPath: folderPath.length > 0 ? folderPath : undefined, isCurrent: workflow.id === workflowId, } }), From ff2e369c20f677d7a1ce42d311e3b84101e05b6a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:11:53 -0700 Subject: [PATCH 2/3] fix(mothership): fix workflow vfs reads (#4156) * v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha * fix build error * improvement(mothership): new agent loop (#3920) * feat(transport): replace shared chat transport with mothership-stream module * improvement(contracts): regenerate contracts from go * feat(tools): add tool catalog codegen from go tool contracts * feat(tools): add tool-executor dispatch framework for sim side tool routing * feat(orchestrator): rewrite tool dispatch with catalog-driven executor and simplified resume loop * feat(orchestrator): checkpoint resume flow * refactor(copilot): consolidate orchestrator into request/ layer * refactor(mothership): reorganize lib/copilot into structured subdirectories * refactor(mothership): canonical transcript layer, dead code cleanup, type consolidation * refactor(mothership): rebase onto latest staging * refactor(mothership): rename request continue to lifecycle * feat(trace): add initial version of request traces * improvement(stream): batch stream from redis * fix(resume): fix the resume checkpoint * fix(resume): fix resume client tool * fix(subagents): subagent resume should join on existing subagent text block * improvement(reconnect): harden reconnect logic * fix(superagent): fix superagent integration tools * improvement(stream): improve stream perf * Rebase with origin dev * fix(tests): fix failing test * fix(build): fix type errors * fix(build): fix build errors * fix(build): fix type errors * feat(mothership): add cli execution * fix(mothership): fix function execute tests * Force redeploy * feat(motheship): add docx support * feat(mothership): append * Add deps * improvement(mothership): docs * File types * Add client retry logic * Fix stream reconnect * Eager tool streaming * Fix client side tools * Security * Fix shell var injection * Remove auto injected tasks * Fix 10mb tool response limit * Fix trailing leak * Remove dead tools * file/folder tools * Folder tools * Hide function code inline * Dont show internal tool result reads * Fix spacing * Auth vfs * Empty folders should show in vfs * Fix run workflow * change to node runtime * revert back to bun runtime * Fix * Appends * Remove debug logs * Patch * Fix patch tool * Temp * Checkpoint * File writes * Fix * Remove tool truncation limits * Bad hook * replace react markdown with streamdown * Checkpoitn * fix code block * fix stream persistence * temp * Fix file tools * tool joining * cleanup subagent + streaming issues * streamed text change * Tool display intetns * Fix dev * Fix tests * Fix dev * Speed up dev ci * Add req id * Fix persistence * Tool call names * fix payload accesses * Fix name * fix snapshot crash bug * fix * Fix * remove worker code * Clickable resources * Options ordering * Folder vfs * Restore and mass delete tools * Fix * lint * Update request tracing and skills and handlers * Fix editable * fix type error * Html code * fix(chat): make inline code inherit parent font size in markdown headers Co-Authored-By: Claude Opus 4.6 * improved autolayout * durable stream for files * one more fix * POSSIBLE BREAKAGE: SCROLLING * Fixes * Fixes * Lint fix * fix(resource): fix resource view disappearing on ats (#4103) Co-authored-by: Theodore Li * Fixes * feat(mothership): add execution logs as a resource type Adds `log` as a first-class mothership resource type so copilot can open and display workflow execution logs as tabs alongside workflows, tables, files, and knowledge bases. - Add `log` to MothershipResourceType, all Zod enums, and VALID_RESOURCE_TYPES - Register log in RESOURCE_REGISTRY (Library icon) and RESOURCE_INVALIDATORS - Add EmbeddedLog and EmbeddedLogActions components in resource-content - Export WorkflowOutputSection from log-details for reuse in EmbeddedLog - Add log resolution branch in open_resource handler via new getLogById service - Include log id in get_workflow_logs response and extract resources from output - Exclude log from manual add-resource dropdown (enters via copilot tools only) - Regenerate copilot contracts after adding log to open_resource Go enum * Fix perf and message queueing * Fix abort * fix(ui): dont delete resource on clearing from context, set resource closed on new task (#4113) Co-authored-by: Theodore Li * improvement(mothership): structure sim side typing * address comments * reactive text editor tweaks * Fix file read and tool call name persistence bug * Fix code stream + create file opening resource * fix use chat race + headless trace issues * Fix type issue * Fix mothership block req lifecycle * Fix build * Move copy reqid * Fix * fix(ui): fix resource tag transition from home to task (#4132) Co-authored-by: Theodore Li * Fix persistence * Clean code, fix bugs * Fix * Fixes --------- Co-authored-by: Waleed Co-authored-by: Theodore Li Co-authored-by: Vikhyath Mondreti Co-authored-by: Claude Opus 4.6 Co-authored-by: Theodore Li --- apps/sim/app/api/mcp/copilot/route.ts | 13 ++- apps/sim/app/api/v1/copilot/chat/route.ts | 8 +- .../components/agent-group/agent-group.tsx | 8 +- .../message-content/message-content.tsx | 12 +- .../generic-resource-content.tsx | 6 + .../[workspaceId]/home/hooks/use-chat.ts | 108 +++++++++++++----- .../app/workspace/[workspaceId]/home/types.ts | 2 + .../lib/copilot/chat/display-message.test.ts | 58 ++++++++++ apps/sim/lib/copilot/chat/display-message.ts | 4 +- apps/sim/lib/copilot/chat/post.test.ts | 1 + apps/sim/lib/copilot/chat/post.ts | 6 +- .../sim/lib/copilot/chat/workspace-context.ts | 31 ++++- apps/sim/lib/copilot/vfs/serializers.ts | 4 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 1 + apps/sim/lib/credentials/environment.ts | 3 + apps/sim/lib/workflows/utils.ts | 85 ++++++++++++-- 16 files changed, 293 insertions(+), 57 deletions(-) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index ad72860b3bb..ebff9bb80b6 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -688,11 +688,16 @@ async function handleBuildToolCall( userId, action: 'read', }) - return authorization.allowed ? { workflowId } : null + return authorization.allowed + ? { status: 'resolved' as const, workflowId } + : { + status: 'not_found' as const, + message: 'workflowId is required for build. Call create_workflow first.', + } })() : await resolveWorkflowIdForUser(userId) - if (!resolved?.workflowId) { + if (!resolved || resolved.status !== 'resolved') { return { content: [ { @@ -700,7 +705,9 @@ async function handleBuildToolCall( text: JSON.stringify( { success: false, - error: 'workflowId is required for build. Call create_workflow first.', + error: + resolved?.message ?? + 'workflowId is required for build. Call create_workflow first.', }, null, 2 diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index dc8370ff83d..d7af2d8cdc7 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -29,8 +29,8 @@ const RequestSchema = z.object({ * * workflowId is optional - if not provided: * - If workflowName is provided, finds that workflow - * - Otherwise uses the user's first workflow as context - * - The copilot can still operate on any workflow using list_user_workflows + * - If exactly one workflow is available, uses that workflow as context + * - Otherwise requires workflowId or workflowName to disambiguate */ export async function POST(req: NextRequest) { let messageId: string | undefined @@ -54,11 +54,11 @@ export async function POST(req: NextRequest) { parsed.workflowName, auth.keyType === 'workspace' ? auth.workspaceId : undefined ) - if (!resolved) { + if (resolved.status !== 'resolved') { return NextResponse.json( { success: false, - error: 'No workflows found. Create a workflow first or provide a valid workflowId.', + error: resolved.message, }, { status: 400 } ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index 9de719a7f13..4f1d8dc5b87 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -21,7 +21,13 @@ interface AgentGroupProps { } function isToolDone(status: ToolCallData['status']): boolean { - return status === 'success' || status === 'error' || status === 'cancelled' + return ( + status === 'success' || + status === 'error' || + status === 'cancelled' || + status === 'skipped' || + status === 'rejected' + ) } export function AgentGroup({ diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 60624d43130..433bb388a65 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -70,7 +70,13 @@ function resolveAgentLabel(key: string): string { } function isToolDone(status: ToolCallData['status']): boolean { - return status === 'success' || status === 'error' || status === 'cancelled' + return ( + status === 'success' || + status === 'error' || + status === 'cancelled' || + status === 'skipped' || + status === 'rejected' + ) } function isDelegatingTool(tc: NonNullable): boolean { @@ -87,6 +93,10 @@ function mapToolStatusToClientState( return ClientToolCallState.error case 'cancelled': return ClientToolCallState.cancelled + case 'skipped': + return ClientToolCallState.aborted + case 'rejected': + return ClientToolCallState.rejected default: return ClientToolCallState.executing } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/generic-resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/generic-resource-content.tsx index 383b05f9a13..804284d4eb2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/generic-resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/generic-resource-content.tsx @@ -41,6 +41,12 @@ export function GenericResourceContent({ data }: GenericResourceContentProps) { {entry.status === 'error' && ( Error )} + {entry.status === 'skipped' && ( + Skipped + )} + {entry.status === 'rejected' && ( + Rejected + )} {entry.streamingArgs && (
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 6a2ca9e2c31..2318b00249e 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
@@ -119,6 +119,7 @@ import type {
   MothershipResourceType,
   QueuedMessage,
 } from '../types'
+import { ToolCallStatus } from '../types'
 
 const FILE_SUBAGENT_ID = 'file'
 
@@ -610,6 +611,28 @@ function getToolUI(ui?: MothershipStreamV1ToolUI): StreamToolUI | undefined {
   }
 }
 
+function resolveLiveToolStatus(
+  payload: Partial<{
+    status: string
+    success: boolean
+  }>
+): ToolCallStatus {
+  switch (payload.status) {
+    case MothershipStreamV1ToolOutcome.success:
+      return ToolCallStatus.success
+    case MothershipStreamV1ToolOutcome.error:
+      return ToolCallStatus.error
+    case MothershipStreamV1ToolOutcome.cancelled:
+      return ToolCallStatus.cancelled
+    case MothershipStreamV1ToolOutcome.skipped:
+      return ToolCallStatus.skipped
+    case MothershipStreamV1ToolOutcome.rejected:
+      return ToolCallStatus.rejected
+    default:
+      return payload.success === true ? ToolCallStatus.success : ToolCallStatus.error
+  }
+}
+
 /** Adds a workflow to the React Query cache with a top-insertion sort order if it doesn't already exist. */
 function ensureWorkflowInRegistry(resourceId: string, title: string, workspaceId: string): boolean {
   const workflows = getWorkflows(workspaceId)
@@ -1396,6 +1419,7 @@ export function useChat(
       let activeSubagent: string | undefined
       let activeSubagentParentToolCallId: string | undefined
       let activeCompactionId: string | undefined
+      const subagentByParentToolCallId = new Map()
 
       if (preserveState) {
         for (let i = blocks.length - 1; i >= 0; i--) {
@@ -1418,20 +1442,32 @@ export function useChat(
         streamingBlocksRef.current = []
       }
 
-      const ensureTextBlock = (): ContentBlock => {
+      const ensureTextBlock = (subagentName?: string): ContentBlock => {
         const last = blocks[blocks.length - 1]
-        if (last?.type === 'text' && last.subagent === activeSubagent) return last
+        if (last?.type === 'text' && last.subagent === subagentName) return last
         const b: ContentBlock = { type: 'text', content: '' }
+        if (subagentName) b.subagent = subagentName
         blocks.push(b)
         return b
       }
 
-      const appendInlineErrorTag = (tag: string) => {
+      const resolveScopedSubagent = (
+        agentId: string | undefined,
+        parentToolCallId: string | undefined
+      ): string | undefined => {
+        if (agentId) return agentId
+        if (parentToolCallId) {
+          const scoped = subagentByParentToolCallId.get(parentToolCallId)
+          if (scoped) return scoped
+        }
+        return activeSubagent
+      }
+
+      const appendInlineErrorTag = (tag: string, subagentName?: string) => {
         if (runningText.includes(tag)) return
-        const tb = ensureTextBlock()
+        const tb = ensureTextBlock(subagentName)
         const prefix = runningText.length > 0 && !runningText.endsWith('\n') ? '\n' : ''
         tb.content = `${tb.content ?? ''}${prefix}${tag}`
-        if (activeSubagent) tb.subagent = activeSubagent
         runningText += `${prefix}${tag}`
         streamingContentRef.current = runningText
         flush()
@@ -1545,6 +1581,13 @@ export function useChat(
           }
 
           logger.debug('SSE event received', parsed)
+          const scopedParentToolCallId =
+            typeof parsed.scope?.parentToolCallId === 'string'
+              ? parsed.scope.parentToolCallId
+              : undefined
+          const scopedAgentId =
+            typeof parsed.scope?.agentId === 'string' ? parsed.scope.agentId : undefined
+          const scopedSubagent = resolveScopedSubagent(scopedAgentId, scopedParentToolCallId)
           switch (parsed.type) {
             case MothershipStreamV1EventType.session: {
               const payload = parsed.payload
@@ -1600,16 +1643,15 @@ export function useChat(
             case MothershipStreamV1EventType.text: {
               const chunk = parsed.payload.text
               if (chunk) {
-                const contentSource: 'main' | 'subagent' = activeSubagent ? 'subagent' : 'main'
+                const contentSource: 'main' | 'subagent' = scopedSubagent ? 'subagent' : 'main'
                 const needsBoundaryNewline =
                   lastContentSource !== null &&
                   lastContentSource !== contentSource &&
                   runningText.length > 0 &&
                   !runningText.endsWith('\n')
-                const tb = ensureTextBlock()
+                const tb = ensureTextBlock(scopedSubagent)
                 const normalizedChunk = needsBoundaryNewline ? `\n${chunk}` : chunk
                 tb.content = (tb.content ?? '') + normalizedChunk
-                if (activeSubagent) tb.subagent = activeSubagent
                 runningText += normalizedChunk
                 lastContentSource = contentSource
                 streamingContentRef.current = runningText
@@ -1800,22 +1842,24 @@ export function useChat(
                 }
                 const tc = blocks[idx].toolCall!
                 const outputObj = asPayloadRecord(payload.output)
-                const success =
-                  payload.success ?? payload.status === MothershipStreamV1ToolOutcome.success
                 const isCancelled =
                   outputObj?.reason === 'user_cancelled' ||
                   outputObj?.cancelledByUser === true ||
                   payload.status === MothershipStreamV1ToolOutcome.cancelled
+                const status = isCancelled
+                  ? ToolCallStatus.cancelled
+                  : resolveLiveToolStatus(payload)
+                const isSuccess = status === ToolCallStatus.success
 
-                if (isCancelled) {
-                  tc.status = 'cancelled'
+                if (status === ToolCallStatus.cancelled) {
+                  tc.status = ToolCallStatus.cancelled
                   tc.displayTitle = 'Stopped by user'
                 } else {
-                  tc.status = success ? 'success' : 'error'
+                  tc.status = status
                 }
                 tc.streamingArgs = undefined
                 tc.result = {
-                  success: !!success,
+                  success: isSuccess,
                   output: payload.output,
                   error: typeof payload.error === 'string' ? payload.error : undefined,
                 }
@@ -1902,7 +1946,7 @@ export function useChat(
                     })
                     setActiveResourceId(fileResource.id)
                     invalidateResourceQueries(queryClient, workspaceId, 'file', fileResource.id)
-                  } else if (!activeSubagent || activeSubagent !== FILE_SUBAGENT_ID) {
+                  } else if (tc.calledBy !== FILE_SUBAGENT_ID) {
                     setResources((rs) => rs.filter((r) => r.id !== 'streaming-file'))
                   }
                 }
@@ -1948,7 +1992,7 @@ export function useChat(
                     status: 'executing',
                     displayTitle,
                     params: args,
-                    calledBy: activeSubagent,
+                    calledBy: scopedSubagent,
                   },
                 })
                 if (name === ReadTool.id || isResourceToolName(name)) {
@@ -2064,23 +2108,18 @@ export function useChat(
               }
               const spanData = asPayloadRecord(payload.data)
               const parentToolCallId =
-                typeof parsed.scope?.parentToolCallId === 'string'
-                  ? parsed.scope.parentToolCallId
-                  : typeof spanData?.tool_call_id === 'string'
-                    ? spanData.tool_call_id
-                    : undefined
+                scopedParentToolCallId ??
+                (typeof spanData?.tool_call_id === 'string' ? spanData.tool_call_id : undefined)
               const isPendingPause = spanData?.pending === true
-              const name =
-                typeof payload.agent === 'string'
-                  ? payload.agent
-                  : typeof parsed.scope?.agentId === 'string'
-                    ? parsed.scope.agentId
-                    : undefined
+              const name = typeof payload.agent === 'string' ? payload.agent : scopedAgentId
               if (payload.event === MothershipStreamV1SpanLifecycleEvent.start && name) {
                 const isSameActiveSubagent =
                   activeSubagent === name &&
                   activeSubagentParentToolCallId &&
                   parentToolCallId === activeSubagentParentToolCallId
+                if (parentToolCallId) {
+                  subagentByParentToolCallId.set(parentToolCallId, name)
+                }
                 activeSubagent = name
                 activeSubagentParentToolCallId = parentToolCallId
                 if (!isSameActiveSubagent) {
@@ -2104,6 +2143,9 @@ export function useChat(
                 if (isPendingPause) {
                   break
                 }
+                if (parentToolCallId) {
+                  subagentByParentToolCallId.delete(parentToolCallId)
+                }
                 if (previewSessionRef.current && !activePreviewSessionIdRef.current) {
                   const lastFileResource = resourcesRef.current.find(
                     (r) => r.type === 'file' && r.id !== 'streaming-file'
@@ -2113,8 +2155,14 @@ export function useChat(
                     setActiveResourceId(lastFileResource.id)
                   }
                 }
-                activeSubagent = undefined
-                activeSubagentParentToolCallId = undefined
+                if (
+                  !parentToolCallId ||
+                  parentToolCallId === activeSubagentParentToolCallId ||
+                  name === activeSubagent
+                ) {
+                  activeSubagent = undefined
+                  activeSubagentParentToolCallId = undefined
+                }
                 blocks.push({ type: 'subagent_end' })
                 flush()
               }
@@ -2123,7 +2171,7 @@ export function useChat(
             case MothershipStreamV1EventType.error: {
               sawStreamError = true
               setError(parsed.payload.message || parsed.payload.error || 'An error occurred')
-              appendInlineErrorTag(buildInlineErrorTag(parsed.payload))
+              appendInlineErrorTag(buildInlineErrorTag(parsed.payload), scopedSubagent)
               break
             }
             case MothershipStreamV1EventType.complete: {
diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts
index 5ef7747969e..b3ed394fb70 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/types.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts
@@ -59,6 +59,8 @@ export const ToolCallStatus = {
   success: 'success',
   error: 'error',
   cancelled: 'cancelled',
+  skipped: 'skipped',
+  rejected: 'rejected',
 } as const
 export type ToolCallStatus = (typeof ToolCallStatus)[keyof typeof ToolCallStatus]
 
diff --git a/apps/sim/lib/copilot/chat/display-message.test.ts b/apps/sim/lib/copilot/chat/display-message.test.ts
index b7e99b4804c..4e02c3af27f 100644
--- a/apps/sim/lib/copilot/chat/display-message.test.ts
+++ b/apps/sim/lib/copilot/chat/display-message.test.ts
@@ -87,4 +87,62 @@ describe('display-message', () => {
 
     expect(display.contentBlocks).toEqual([{ type: 'text', content: 'visible text' }])
   })
+
+  it('preserves skipped and rejected tool outcomes', () => {
+    const display = toDisplayMessage({
+      id: 'msg-3',
+      role: 'assistant',
+      content: '',
+      timestamp: '2024-01-01T00:00:00.000Z',
+      contentBlocks: [
+        {
+          type: 'tool',
+          phase: 'call',
+          toolCall: {
+            id: 'tool-skipped',
+            name: 'read',
+            state: 'skipped',
+            display: { title: 'Reading workflow' },
+          },
+        },
+        {
+          type: 'tool',
+          phase: 'call',
+          toolCall: {
+            id: 'tool-rejected',
+            name: 'run_workflow',
+            state: 'rejected',
+            display: { title: 'Running workflow' },
+          },
+        },
+      ],
+    })
+
+    expect(display.contentBlocks).toEqual([
+      {
+        type: 'tool_call',
+        toolCall: {
+          id: 'tool-skipped',
+          name: 'read',
+          status: 'skipped',
+          displayTitle: 'Reading workflow',
+          params: undefined,
+          calledBy: undefined,
+          result: undefined,
+        },
+      },
+      {
+        type: 'tool_call',
+        toolCall: {
+          id: 'tool-rejected',
+          name: 'run_workflow',
+          status: 'rejected',
+          displayTitle: 'Running workflow',
+          params: undefined,
+          calledBy: undefined,
+          result: undefined,
+        },
+      },
+    ])
+  })
 })
diff --git a/apps/sim/lib/copilot/chat/display-message.ts b/apps/sim/lib/copilot/chat/display-message.ts
index 87efb7800f2..a5a86c20ae3 100644
--- a/apps/sim/lib/copilot/chat/display-message.ts
+++ b/apps/sim/lib/copilot/chat/display-message.ts
@@ -21,8 +21,8 @@ const STATE_TO_STATUS: Record = {
   [MothershipStreamV1ToolOutcome.success]: ToolCallStatus.success,
   [MothershipStreamV1ToolOutcome.error]: ToolCallStatus.error,
   [MothershipStreamV1ToolOutcome.cancelled]: ToolCallStatus.cancelled,
-  [MothershipStreamV1ToolOutcome.rejected]: ToolCallStatus.error,
-  [MothershipStreamV1ToolOutcome.skipped]: ToolCallStatus.success,
+  [MothershipStreamV1ToolOutcome.rejected]: ToolCallStatus.rejected,
+  [MothershipStreamV1ToolOutcome.skipped]: ToolCallStatus.skipped,
   pending: ToolCallStatus.executing,
   executing: ToolCallStatus.executing,
 }
diff --git a/apps/sim/lib/copilot/chat/post.test.ts b/apps/sim/lib/copilot/chat/post.test.ts
index 4e819bcbd2d..c2c884c96de 100644
--- a/apps/sim/lib/copilot/chat/post.test.ts
+++ b/apps/sim/lib/copilot/chat/post.test.ts
@@ -121,6 +121,7 @@ describe('handleUnifiedChatPost', () => {
     vi.clearAllMocks()
     getSession.mockResolvedValue({ user: { id: 'user-1' } })
     resolveWorkflowIdForUser.mockResolvedValue({
+      status: 'resolved',
       workflowId: 'wf-1',
       workflowName: 'Workflow One',
     })
diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts
index f7563b4a451..b15e84db694 100644
--- a/apps/sim/lib/copilot/chat/post.ts
+++ b/apps/sim/lib/copilot/chat/post.ts
@@ -420,10 +420,8 @@ async function resolveBranch(params: {
       workflowName,
       requestedWorkspaceId
     )
-    if (!resolved) {
-      return createBadRequestResponse(
-        'No workflows found. Create a workflow first or provide a valid workflowId.'
-      )
+    if (resolved.status !== 'resolved') {
+      return createBadRequestResponse(resolved.message)
     }
 
     const resolvedWorkflowId = resolved.workflowId
diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts
index fb7128b3655..2f716f0ec30 100644
--- a/apps/sim/lib/copilot/chat/workspace-context.ts
+++ b/apps/sim/lib/copilot/chat/workspace-context.ts
@@ -11,6 +11,7 @@ import {
 } from '@sim/db/schema'
 import { createLogger } from '@sim/logger'
 import { and, count, eq, inArray, isNull } from 'drizzle-orm'
+import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment'
 import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment'
 import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace'
 import { listCustomTools } from '@/lib/workflows/custom-tools/operations'
@@ -73,6 +74,23 @@ export interface WorkspaceMdData {
   }>
 }
 
+function normalizeFolderPathForVfs(folderPath?: string | null): string | null {
+  if (!folderPath) return null
+  const segments = folderPath
+    .split('/')
+    .map((segment) => normalizeVfsSegment(segment))
+    .filter(Boolean)
+  return segments.length > 0 ? segments.join('/') : null
+}
+
+function buildWorkflowStatePath(workflowName: string, folderPath?: string | null): string {
+  const normalizedFolderPath = normalizeFolderPathForVfs(folderPath)
+  const normalizedWorkflowName = normalizeVfsSegment(workflowName)
+  return normalizedFolderPath
+    ? `workflows/${normalizedFolderPath}/${normalizedWorkflowName}/state.json`
+    : `workflows/${normalizedWorkflowName}/state.json`
+}
+
 /**
  * Pure formatting: build WORKSPACE.md content from pre-fetched data.
  * No DB access — callers are responsible for providing the data.
@@ -115,10 +133,20 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string {
       if (wf.isDeployed) flags.push('deployed')
       if (wf.lastRunAt) flags.push(`last run: ${wf.lastRunAt.toISOString().split('T')[0]}`)
       if (flags.length > 0) parts[0] += ` — ${flags.join(', ')}`
+      if (wf.folderPath) {
+        parts.push(
+          `${indent}  VFS state path: \`${buildWorkflowStatePath(wf.name, wf.folderPath)}\``
+        )
+      }
       return parts.join('\n')
     }
 
     const lines: string[] = []
+    if (data.workflows.some((workflow) => workflow.folderPath)) {
+      lines.push(
+        'Use the canonical VFS state path shown under nested workflows. Do not infer nested workflow paths from the leaf workflow name alone.'
+      )
+    }
     for (const wf of rootWorkflows) {
       lines.push(formatWf(wf, ''))
     }
@@ -379,7 +407,8 @@ export async function generateWorkspaceContext(
       const folder = folderById.get(id)
       if (!folder) return id
       const parentPath = folder.parentId ? resolveFolderPath(folder.parentId) : ''
-      const path = parentPath ? `${parentPath}/${folder.name}` : folder.name
+      const normalizedName = normalizeVfsSegment(folder.name)
+      const path = parentPath ? `${parentPath}/${normalizedName}` : normalizedName
       folderPathMap.set(id, path)
       return path
     }
diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts
index 04e6ff5a533..0a0e4bf0aaf 100644
--- a/apps/sim/lib/copilot/vfs/serializers.ts
+++ b/apps/sim/lib/copilot/vfs/serializers.ts
@@ -422,13 +422,14 @@ export function serializeBlockSchema(block: BlockConfig): string {
 
 /**
  * Serialize OAuth credentials for VFS environment/credentials.json.
- * Shows which integrations are connected — IDs and scopes, NOT tokens.
+ * Shows which integrations are connected — IDs, roles, and scopes, NOT tokens.
  */
 export function serializeCredentials(
   accounts: Array<{
     id?: string
     providerId: string
     displayName?: string | null
+    role?: string | null
     scope: string | null
     createdAt: Date
   }>
@@ -438,6 +439,7 @@ export function serializeCredentials(
       id: a.id || undefined,
       provider: a.providerId,
       displayName: a.displayName || undefined,
+      role: a.role || undefined,
       scope: a.scope || undefined,
       connectedAt: a.createdAt.toISOString(),
     })),
diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts
index 530e3f3054c..a0a75b46604 100644
--- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts
+++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts
@@ -1367,6 +1367,7 @@ export class WorkspaceVFS {
             id: c.id,
             providerId: c.providerId,
             displayName: c.displayName,
+            role: c.role,
             scope: null,
             createdAt: c.updatedAt,
           })),
diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts
index 5d945b1c317..c41ae9488e5 100644
--- a/apps/sim/lib/credentials/environment.ts
+++ b/apps/sim/lib/credentials/environment.ts
@@ -367,6 +367,7 @@ export interface AccessibleOAuthCredential {
   id: string
   providerId: string
   displayName: string
+  role: 'admin' | 'member'
   updatedAt: Date
 }
 
@@ -379,6 +380,7 @@ export async function getAccessibleOAuthCredentials(
       id: credential.id,
       providerId: credential.providerId,
       displayName: credential.displayName,
+      role: credentialMember.role,
       updatedAt: credential.updatedAt,
     })
     .from(credential)
@@ -403,6 +405,7 @@ export async function getAccessibleOAuthCredentials(
       id: row.id,
       providerId: row.providerId!,
       displayName: row.displayName,
+      role: row.role,
       updatedAt: row.updatedAt,
     }))
 }
diff --git a/apps/sim/lib/workflows/utils.ts b/apps/sim/lib/workflows/utils.ts
index 8983b5406bf..31d70991eae 100644
--- a/apps/sim/lib/workflows/utils.ts
+++ b/apps/sim/lib/workflows/utils.ts
@@ -103,12 +103,32 @@ export async function deduplicateWorkflowName(
   return `${name} (${generateId().slice(0, 6)})`
 }
 
+export type WorkflowResolutionResult =
+  | {
+      status: 'resolved'
+      workflowId: string
+      workflowName?: string
+    }
+  | {
+      status: 'not_found'
+      message: string
+    }
+  | {
+      status: 'ambiguous'
+      message: string
+      candidates: Array<{
+        workflowId: string
+        workflowName?: string
+        folderId?: string | null
+      }>
+    }
+
 export async function resolveWorkflowIdForUser(
   userId: string,
   workflowId?: string,
   workflowName?: string,
   workspaceId?: string
-): Promise<{ workflowId: string; workflowName?: string } | null> {
+): Promise {
   if (workflowId) {
     const authorization = await authorizeWorkflowByWorkspacePermission({
       workflowId,
@@ -116,10 +136,13 @@ export async function resolveWorkflowIdForUser(
       action: 'read',
     })
     if (!authorization.allowed) {
-      return null
+      return {
+        status: 'not_found',
+        message: 'No workflows found. Create a workflow first or provide a valid workflowId.',
+      }
     }
     const wf = await getWorkflowById(workflowId)
-    return { workflowId, workflowName: wf?.name || undefined }
+    return { status: 'resolved', workflowId, workflowName: wf?.name || undefined }
   }
 
   const workspaceIds = await db
@@ -132,7 +155,10 @@ export async function resolveWorkflowIdForUser(
     ? workspaceIdList.filter((candidateWorkspaceId) => candidateWorkspaceId === workspaceId)
     : workspaceIdList
   if (allowedWorkspaceIds.length === 0) {
-    return null
+    return {
+      status: 'not_found',
+      message: 'No workflows found. Create a workflow first or provide a valid workflowId.',
+    }
   }
 
   const workflows = await db
@@ -144,23 +170,62 @@ export async function resolveWorkflowIdForUser(
     .orderBy(asc(workflowTable.sortOrder), asc(workflowTable.createdAt), asc(workflowTable.id))
 
   if (workflows.length === 0) {
-    return null
+    return {
+      status: 'not_found',
+      message: 'No workflows found. Create a workflow first or provide a valid workflowId.',
+    }
   }
 
   if (workflowName) {
-    const match = workflows.find(
+    const matches = workflows.filter(
       (w) =>
         String(w.name || '')
           .trim()
           .toLowerCase() === workflowName.toLowerCase()
     )
-    if (match) {
-      return { workflowId: match.id, workflowName: match.name || undefined }
+    if (matches.length === 1) {
+      const [match] = matches
+      return {
+        status: 'resolved',
+        workflowId: match.id,
+        workflowName: match.name || undefined,
+      }
+    }
+    if (matches.length > 1) {
+      return {
+        status: 'ambiguous',
+        message: `Multiple workflows named "${workflowName}" were found. Provide workflowId to disambiguate.`,
+        candidates: matches.map((match) => ({
+          workflowId: match.id,
+          workflowName: match.name || undefined,
+          folderId: match.folderId,
+        })),
+      }
+    }
+    return {
+      status: 'not_found',
+      message: `No workflow named "${workflowName}" was found.`,
+    }
+  }
+
+  if (workflows.length === 1) {
+    return {
+      status: 'resolved',
+      workflowId: workflows[0].id,
+      workflowName: workflows[0].name || undefined,
     }
-    return null
   }
 
-  return { workflowId: workflows[0].id, workflowName: workflows[0].name || undefined }
+  return {
+    status: 'ambiguous',
+    message:
+      'Multiple workflows are available. Provide workflowId or workflowName to disambiguate.',
+    candidates: workflows.slice(0, 20).map((workflow) => ({
+      workflowId: workflow.id,
+      workflowName: workflow.name || undefined,
+      folderId: workflow.folderId,
+    })),
+  }
 }
 
 type WorkflowRecord = ReturnType extends Promise

From 367415f649f7f349c3dbe728307f580799eb28d0 Mon Sep 17 00:00:00 2001
From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Date: Tue, 14 Apr 2026 13:03:07 -0700
Subject: [PATCH 3/3] fix(mothership): tool path for nested folders (#4158)

---
 .../[workspaceId]/home/hooks/use-chat.ts      | 21 +++++++++++++++++--
 .../copilot/tools/client/store-utils.test.ts  |  6 ++++++
 .../lib/copilot/tools/client/store-utils.ts   | 16 ++++++++++++++
 3 files changed, 41 insertions(+), 2 deletions(-)

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 2318b00249e..dea4eaacc12 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
@@ -673,7 +673,10 @@ function extractResourceFromReadResult(
 ): MothershipResource | null {
   if (!path) return null
 
-  const segments = path.split('/')
+  const segments = path
+    .split('/')
+    .map((segment) => segment.trim())
+    .filter(Boolean)
   const resourceType = VFS_DIR_TO_RESOURCE[segments[0]]
   if (!resourceType || !segments[1]) return null
 
@@ -693,8 +696,22 @@ function extractResourceFromReadResult(
     }
   }
 
+  const fallbackTitle =
+    resourceType === 'workflow'
+      ? resolveLeafWorkflowPathSegment(segments)
+      : segments[1] || segments[segments.length - 1]
+
   if (!id) return null
-  return { type: resourceType, id, title: name || segments[1] }
+  return { type: resourceType, id, title: name || fallbackTitle || id }
+}
+
+function resolveLeafWorkflowPathSegment(segments: string[]): string | undefined {
+  const lastSegment = segments[segments.length - 1]
+  if (!lastSegment) return undefined
+  if (/\.[^/.]+$/.test(lastSegment) && segments.length > 1) {
+    return segments[segments.length - 2]
+  }
+  return lastSegment
 }
 
 export interface UseChatOptions {
diff --git a/apps/sim/lib/copilot/tools/client/store-utils.test.ts b/apps/sim/lib/copilot/tools/client/store-utils.test.ts
index c15bce90159..3c3bba51518 100644
--- a/apps/sim/lib/copilot/tools/client/store-utils.test.ts
+++ b/apps/sim/lib/copilot/tools/client/store-utils.test.ts
@@ -29,6 +29,12 @@ describe('resolveToolDisplay', () => {
         path: 'workflows/My Workflow/meta.json',
       })?.text
     ).toBe('Read My Workflow')
+
+    expect(
+      resolveToolDisplay(ReadTool.id, ClientToolCallState.success, {
+        path: 'workflows/Folder 1/RET XYZ/state.json',
+      })?.text
+    ).toBe('Read RET XYZ')
   })
 
   it('falls back to a humanized tool label for generic tools', () => {
diff --git a/apps/sim/lib/copilot/tools/client/store-utils.ts b/apps/sim/lib/copilot/tools/client/store-utils.ts
index 69de9f28bff..e1c04abb179 100644
--- a/apps/sim/lib/copilot/tools/client/store-utils.ts
+++ b/apps/sim/lib/copilot/tools/client/store-utils.ts
@@ -98,10 +98,26 @@ function describeReadTarget(path: string | undefined): string | undefined {
     return segments.slice(1).join('/') || segments[segments.length - 1]
   }
 
+  if (resourceType === 'workflow') {
+    return stripExtension(getLeafResourceSegment(segments))
+  }
+
   const resourceName = segments[1] || segments[segments.length - 1]
   return stripExtension(resourceName)
 }
 
+function getLeafResourceSegment(segments: string[]): string {
+  const lastSegment = segments[segments.length - 1] || ''
+  if (hasFileExtension(lastSegment) && segments.length > 1) {
+    return segments[segments.length - 2] || lastSegment
+  }
+  return lastSegment
+}
+
+function hasFileExtension(value: string): boolean {
+  return /\.[^/.]+$/.test(value)
+}
+
 function stripExtension(value: string): string {
   return value.replace(/\.[^/.]+$/, '')
 }