Skip to content

Commit fc07922

Browse files
authored
v0.6.42: mothership nested file reads, search modal improvements
2 parents 3838b6e + 367415f commit fc07922

File tree

22 files changed

+361
-78
lines changed

22 files changed

+361
-78
lines changed

apps/sim/app/api/mcp/copilot/route.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,19 +688,26 @@ async function handleBuildToolCall(
688688
userId,
689689
action: 'read',
690690
})
691-
return authorization.allowed ? { workflowId } : null
691+
return authorization.allowed
692+
? { status: 'resolved' as const, workflowId }
693+
: {
694+
status: 'not_found' as const,
695+
message: 'workflowId is required for build. Call create_workflow first.',
696+
}
692697
})()
693698
: await resolveWorkflowIdForUser(userId)
694699

695-
if (!resolved?.workflowId) {
700+
if (!resolved || resolved.status !== 'resolved') {
696701
return {
697702
content: [
698703
{
699704
type: 'text',
700705
text: JSON.stringify(
701706
{
702707
success: false,
703-
error: 'workflowId is required for build. Call create_workflow first.',
708+
error:
709+
resolved?.message ??
710+
'workflowId is required for build. Call create_workflow first.',
704711
},
705712
null,
706713
2

apps/sim/app/api/v1/copilot/chat/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const RequestSchema = z.object({
2929
*
3030
* workflowId is optional - if not provided:
3131
* - If workflowName is provided, finds that workflow
32-
* - Otherwise uses the user's first workflow as context
33-
* - The copilot can still operate on any workflow using list_user_workflows
32+
* - If exactly one workflow is available, uses that workflow as context
33+
* - Otherwise requires workflowId or workflowName to disambiguate
3434
*/
3535
export async function POST(req: NextRequest) {
3636
let messageId: string | undefined
@@ -54,11 +54,11 @@ export async function POST(req: NextRequest) {
5454
parsed.workflowName,
5555
auth.keyType === 'workspace' ? auth.workspaceId : undefined
5656
)
57-
if (!resolved) {
57+
if (resolved.status !== 'resolved') {
5858
return NextResponse.json(
5959
{
6060
success: false,
61-
error: 'No workflows found. Create a workflow first or provide a valid workflowId.',
61+
error: resolved.message,
6262
},
6363
{ status: 400 }
6464
)

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ interface AgentGroupProps {
2121
}
2222

2323
function isToolDone(status: ToolCallData['status']): boolean {
24-
return status === 'success' || status === 'error' || status === 'cancelled'
24+
return (
25+
status === 'success' ||
26+
status === 'error' ||
27+
status === 'cancelled' ||
28+
status === 'skipped' ||
29+
status === 'rejected'
30+
)
2531
}
2632

2733
export function AgentGroup({

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,13 @@ function resolveAgentLabel(key: string): string {
7070
}
7171

7272
function isToolDone(status: ToolCallData['status']): boolean {
73-
return status === 'success' || status === 'error' || status === 'cancelled'
73+
return (
74+
status === 'success' ||
75+
status === 'error' ||
76+
status === 'cancelled' ||
77+
status === 'skipped' ||
78+
status === 'rejected'
79+
)
7480
}
7581

7682
function isDelegatingTool(tc: NonNullable<ContentBlock['toolCall']>): boolean {
@@ -87,6 +93,10 @@ function mapToolStatusToClientState(
8793
return ClientToolCallState.error
8894
case 'cancelled':
8995
return ClientToolCallState.cancelled
96+
case 'skipped':
97+
return ClientToolCallState.aborted
98+
case 'rejected':
99+
return ClientToolCallState.rejected
90100
default:
91101
return ClientToolCallState.executing
92102
}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/generic-resource-content.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export function GenericResourceContent({ data }: GenericResourceContentProps) {
4141
{entry.status === 'error' && (
4242
<span className='ml-auto text-[12px] text-[var(--text-error)]'>Error</span>
4343
)}
44+
{entry.status === 'skipped' && (
45+
<span className='ml-auto text-[12px] text-[var(--text-muted)]'>Skipped</span>
46+
)}
47+
{entry.status === 'rejected' && (
48+
<span className='ml-auto text-[12px] text-[var(--text-muted)]'>Rejected</span>
49+
)}
4450
</div>
4551
{entry.streamingArgs && (
4652
<pre className='overflow-x-auto whitespace-pre-wrap break-words font-mono text-[12px] text-[var(--text-body)]'>

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 97 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ import type {
119119
MothershipResourceType,
120120
QueuedMessage,
121121
} from '../types'
122+
import { ToolCallStatus } from '../types'
122123

123124
const FILE_SUBAGENT_ID = 'file'
124125

@@ -610,6 +611,28 @@ function getToolUI(ui?: MothershipStreamV1ToolUI): StreamToolUI | undefined {
610611
}
611612
}
612613

614+
function resolveLiveToolStatus(
615+
payload: Partial<{
616+
status: string
617+
success: boolean
618+
}>
619+
): ToolCallStatus {
620+
switch (payload.status) {
621+
case MothershipStreamV1ToolOutcome.success:
622+
return ToolCallStatus.success
623+
case MothershipStreamV1ToolOutcome.error:
624+
return ToolCallStatus.error
625+
case MothershipStreamV1ToolOutcome.cancelled:
626+
return ToolCallStatus.cancelled
627+
case MothershipStreamV1ToolOutcome.skipped:
628+
return ToolCallStatus.skipped
629+
case MothershipStreamV1ToolOutcome.rejected:
630+
return ToolCallStatus.rejected
631+
default:
632+
return payload.success === true ? ToolCallStatus.success : ToolCallStatus.error
633+
}
634+
}
635+
613636
/** Adds a workflow to the React Query cache with a top-insertion sort order if it doesn't already exist. */
614637
function ensureWorkflowInRegistry(resourceId: string, title: string, workspaceId: string): boolean {
615638
const workflows = getWorkflows(workspaceId)
@@ -650,7 +673,10 @@ function extractResourceFromReadResult(
650673
): MothershipResource | null {
651674
if (!path) return null
652675

653-
const segments = path.split('/')
676+
const segments = path
677+
.split('/')
678+
.map((segment) => segment.trim())
679+
.filter(Boolean)
654680
const resourceType = VFS_DIR_TO_RESOURCE[segments[0]]
655681
if (!resourceType || !segments[1]) return null
656682

@@ -670,8 +696,22 @@ function extractResourceFromReadResult(
670696
}
671697
}
672698

699+
const fallbackTitle =
700+
resourceType === 'workflow'
701+
? resolveLeafWorkflowPathSegment(segments)
702+
: segments[1] || segments[segments.length - 1]
703+
673704
if (!id) return null
674-
return { type: resourceType, id, title: name || segments[1] }
705+
return { type: resourceType, id, title: name || fallbackTitle || id }
706+
}
707+
708+
function resolveLeafWorkflowPathSegment(segments: string[]): string | undefined {
709+
const lastSegment = segments[segments.length - 1]
710+
if (!lastSegment) return undefined
711+
if (/\.[^/.]+$/.test(lastSegment) && segments.length > 1) {
712+
return segments[segments.length - 2]
713+
}
714+
return lastSegment
675715
}
676716

677717
export interface UseChatOptions {
@@ -1396,6 +1436,7 @@ export function useChat(
13961436
let activeSubagent: string | undefined
13971437
let activeSubagentParentToolCallId: string | undefined
13981438
let activeCompactionId: string | undefined
1439+
const subagentByParentToolCallId = new Map<string, string>()
13991440

14001441
if (preserveState) {
14011442
for (let i = blocks.length - 1; i >= 0; i--) {
@@ -1418,20 +1459,32 @@ export function useChat(
14181459
streamingBlocksRef.current = []
14191460
}
14201461

1421-
const ensureTextBlock = (): ContentBlock => {
1462+
const ensureTextBlock = (subagentName?: string): ContentBlock => {
14221463
const last = blocks[blocks.length - 1]
1423-
if (last?.type === 'text' && last.subagent === activeSubagent) return last
1464+
if (last?.type === 'text' && last.subagent === subagentName) return last
14241465
const b: ContentBlock = { type: 'text', content: '' }
1466+
if (subagentName) b.subagent = subagentName
14251467
blocks.push(b)
14261468
return b
14271469
}
14281470

1429-
const appendInlineErrorTag = (tag: string) => {
1471+
const resolveScopedSubagent = (
1472+
agentId: string | undefined,
1473+
parentToolCallId: string | undefined
1474+
): string | undefined => {
1475+
if (agentId) return agentId
1476+
if (parentToolCallId) {
1477+
const scoped = subagentByParentToolCallId.get(parentToolCallId)
1478+
if (scoped) return scoped
1479+
}
1480+
return activeSubagent
1481+
}
1482+
1483+
const appendInlineErrorTag = (tag: string, subagentName?: string) => {
14301484
if (runningText.includes(tag)) return
1431-
const tb = ensureTextBlock()
1485+
const tb = ensureTextBlock(subagentName)
14321486
const prefix = runningText.length > 0 && !runningText.endsWith('\n') ? '\n' : ''
14331487
tb.content = `${tb.content ?? ''}${prefix}${tag}`
1434-
if (activeSubagent) tb.subagent = activeSubagent
14351488
runningText += `${prefix}${tag}`
14361489
streamingContentRef.current = runningText
14371490
flush()
@@ -1545,6 +1598,13 @@ export function useChat(
15451598
}
15461599

15471600
logger.debug('SSE event received', parsed)
1601+
const scopedParentToolCallId =
1602+
typeof parsed.scope?.parentToolCallId === 'string'
1603+
? parsed.scope.parentToolCallId
1604+
: undefined
1605+
const scopedAgentId =
1606+
typeof parsed.scope?.agentId === 'string' ? parsed.scope.agentId : undefined
1607+
const scopedSubagent = resolveScopedSubagent(scopedAgentId, scopedParentToolCallId)
15481608
switch (parsed.type) {
15491609
case MothershipStreamV1EventType.session: {
15501610
const payload = parsed.payload
@@ -1600,16 +1660,15 @@ export function useChat(
16001660
case MothershipStreamV1EventType.text: {
16011661
const chunk = parsed.payload.text
16021662
if (chunk) {
1603-
const contentSource: 'main' | 'subagent' = activeSubagent ? 'subagent' : 'main'
1663+
const contentSource: 'main' | 'subagent' = scopedSubagent ? 'subagent' : 'main'
16041664
const needsBoundaryNewline =
16051665
lastContentSource !== null &&
16061666
lastContentSource !== contentSource &&
16071667
runningText.length > 0 &&
16081668
!runningText.endsWith('\n')
1609-
const tb = ensureTextBlock()
1669+
const tb = ensureTextBlock(scopedSubagent)
16101670
const normalizedChunk = needsBoundaryNewline ? `\n${chunk}` : chunk
16111671
tb.content = (tb.content ?? '') + normalizedChunk
1612-
if (activeSubagent) tb.subagent = activeSubagent
16131672
runningText += normalizedChunk
16141673
lastContentSource = contentSource
16151674
streamingContentRef.current = runningText
@@ -1800,22 +1859,24 @@ export function useChat(
18001859
}
18011860
const tc = blocks[idx].toolCall!
18021861
const outputObj = asPayloadRecord(payload.output)
1803-
const success =
1804-
payload.success ?? payload.status === MothershipStreamV1ToolOutcome.success
18051862
const isCancelled =
18061863
outputObj?.reason === 'user_cancelled' ||
18071864
outputObj?.cancelledByUser === true ||
18081865
payload.status === MothershipStreamV1ToolOutcome.cancelled
1866+
const status = isCancelled
1867+
? ToolCallStatus.cancelled
1868+
: resolveLiveToolStatus(payload)
1869+
const isSuccess = status === ToolCallStatus.success
18091870

1810-
if (isCancelled) {
1811-
tc.status = 'cancelled'
1871+
if (status === ToolCallStatus.cancelled) {
1872+
tc.status = ToolCallStatus.cancelled
18121873
tc.displayTitle = 'Stopped by user'
18131874
} else {
1814-
tc.status = success ? 'success' : 'error'
1875+
tc.status = status
18151876
}
18161877
tc.streamingArgs = undefined
18171878
tc.result = {
1818-
success: !!success,
1879+
success: isSuccess,
18191880
output: payload.output,
18201881
error: typeof payload.error === 'string' ? payload.error : undefined,
18211882
}
@@ -1902,7 +1963,7 @@ export function useChat(
19021963
})
19031964
setActiveResourceId(fileResource.id)
19041965
invalidateResourceQueries(queryClient, workspaceId, 'file', fileResource.id)
1905-
} else if (!activeSubagent || activeSubagent !== FILE_SUBAGENT_ID) {
1966+
} else if (tc.calledBy !== FILE_SUBAGENT_ID) {
19061967
setResources((rs) => rs.filter((r) => r.id !== 'streaming-file'))
19071968
}
19081969
}
@@ -1948,7 +2009,7 @@ export function useChat(
19482009
status: 'executing',
19492010
displayTitle,
19502011
params: args,
1951-
calledBy: activeSubagent,
2012+
calledBy: scopedSubagent,
19522013
},
19532014
})
19542015
if (name === ReadTool.id || isResourceToolName(name)) {
@@ -2064,23 +2125,18 @@ export function useChat(
20642125
}
20652126
const spanData = asPayloadRecord(payload.data)
20662127
const parentToolCallId =
2067-
typeof parsed.scope?.parentToolCallId === 'string'
2068-
? parsed.scope.parentToolCallId
2069-
: typeof spanData?.tool_call_id === 'string'
2070-
? spanData.tool_call_id
2071-
: undefined
2128+
scopedParentToolCallId ??
2129+
(typeof spanData?.tool_call_id === 'string' ? spanData.tool_call_id : undefined)
20722130
const isPendingPause = spanData?.pending === true
2073-
const name =
2074-
typeof payload.agent === 'string'
2075-
? payload.agent
2076-
: typeof parsed.scope?.agentId === 'string'
2077-
? parsed.scope.agentId
2078-
: undefined
2131+
const name = typeof payload.agent === 'string' ? payload.agent : scopedAgentId
20792132
if (payload.event === MothershipStreamV1SpanLifecycleEvent.start && name) {
20802133
const isSameActiveSubagent =
20812134
activeSubagent === name &&
20822135
activeSubagentParentToolCallId &&
20832136
parentToolCallId === activeSubagentParentToolCallId
2137+
if (parentToolCallId) {
2138+
subagentByParentToolCallId.set(parentToolCallId, name)
2139+
}
20842140
activeSubagent = name
20852141
activeSubagentParentToolCallId = parentToolCallId
20862142
if (!isSameActiveSubagent) {
@@ -2104,6 +2160,9 @@ export function useChat(
21042160
if (isPendingPause) {
21052161
break
21062162
}
2163+
if (parentToolCallId) {
2164+
subagentByParentToolCallId.delete(parentToolCallId)
2165+
}
21072166
if (previewSessionRef.current && !activePreviewSessionIdRef.current) {
21082167
const lastFileResource = resourcesRef.current.find(
21092168
(r) => r.type === 'file' && r.id !== 'streaming-file'
@@ -2113,8 +2172,14 @@ export function useChat(
21132172
setActiveResourceId(lastFileResource.id)
21142173
}
21152174
}
2116-
activeSubagent = undefined
2117-
activeSubagentParentToolCallId = undefined
2175+
if (
2176+
!parentToolCallId ||
2177+
parentToolCallId === activeSubagentParentToolCallId ||
2178+
name === activeSubagent
2179+
) {
2180+
activeSubagent = undefined
2181+
activeSubagentParentToolCallId = undefined
2182+
}
21182183
blocks.push({ type: 'subagent_end' })
21192184
flush()
21202185
}
@@ -2123,7 +2188,7 @@ export function useChat(
21232188
case MothershipStreamV1EventType.error: {
21242189
sawStreamError = true
21252190
setError(parsed.payload.message || parsed.payload.error || 'An error occurred')
2126-
appendInlineErrorTag(buildInlineErrorTag(parsed.payload))
2191+
appendInlineErrorTag(buildInlineErrorTag(parsed.payload), scopedSubagent)
21272192
break
21282193
}
21292194
case MothershipStreamV1EventType.complete: {

apps/sim/app/workspace/[workspaceId]/home/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export const ToolCallStatus = {
5959
success: 'success',
6060
error: 'error',
6161
cancelled: 'cancelled',
62+
skipped: 'skipped',
63+
rejected: 'rejected',
6264
} as const
6365
export type ToolCallStatus = (typeof ToolCallStatus)[keyof typeof ToolCallStatus]
6466

0 commit comments

Comments
 (0)