Skip to content

Commit ea83be3

Browse files
authored
fix(mship): add folder rename tools and locked workflow status (#5126)
* fix(mship): add folder rename tools and locked workflow status * fix(mship): manage_folder bug fixes * improvement(mship): clean up deprecated fields from contracts * test(mship): update tool-call display title tests for client-derived titles Display titles now come from the Sim-side name resolver, not the stream's ui.title/phaseLabel. Update the read lifecycle test to expect the name-derived title and drop the obsolete phaseLabel-fallback test. * fix(contracts): lint
1 parent e5f3965 commit ea83be3

21 files changed

Lines changed: 617 additions & 1097 deletions

File tree

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
'use client'
22

33
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
4-
import { stripVersionSuffix } from '@sim/utils/string'
54
import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1'
65
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
76
import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils'
87
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state'
8+
import { getToolDisplayTitle, humanizeToolName } from '@/lib/copilot/tools/tool-display'
99
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
1010
import type { ContentBlock, OptionItem, ToolCallData } from '../../types'
11-
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types'
11+
import { SUBAGENT_LABELS } from '../../types'
1212
import type { AgentGroupItem } from './components'
1313
import {
1414
AgentGroup,
@@ -84,16 +84,9 @@ function isHiddenToolCall(toolName: string | undefined): boolean {
8484
return isToolHiddenInUi(toolName)
8585
}
8686

87-
function formatToolName(name: string): string {
88-
return stripVersionSuffix(name)
89-
.split('_')
90-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
91-
.join(' ')
92-
}
93-
9487
function resolveAgentLabel(key: string): string {
9588
if (key === 'mothership') return 'Sim'
96-
return SUBAGENT_LABELS[key] ?? formatToolName(key)
89+
return SUBAGENT_LABELS[key] ?? humanizeToolName(key)
9790
}
9891

9992
function isDelegatingTool(tc: NonNullable<ContentBlock['toolCall']>): boolean {
@@ -129,10 +122,7 @@ function getOverrideDisplayTitle(tc: NonNullable<ContentBlock['toolCall']>): str
129122
function toToolData(tc: NonNullable<ContentBlock['toolCall']>): ToolCallData {
130123
const overrideDisplayTitle = getOverrideDisplayTitle(tc)
131124
const displayTitle =
132-
overrideDisplayTitle ||
133-
tc.displayTitle ||
134-
TOOL_UI_METADATA[tc.name as keyof typeof TOOL_UI_METADATA]?.title ||
135-
formatToolName(tc.name)
125+
overrideDisplayTitle || tc.displayTitle || getToolDisplayTitle(tc.name, tc.params)
136126

137127
return {
138128
id: tc.id,

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts

Lines changed: 29 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,25 @@ import { createLogger } from '@sim/logger'
22
import { isRecordLike } from '@sim/utils/object'
33
import {
44
CrawlWebsite,
5-
CreateFolder,
6-
DeleteFolder,
75
DeleteWorkflow,
86
DeployApi,
97
DeployChat,
108
DeployMcp,
119
FunctionExecute,
12-
GetPageContents,
1310
Glob,
1411
Grep,
1512
ManageCredential,
1613
ManageCredentialOperation,
1714
ManageCustomTool,
1815
ManageCustomToolOperation,
16+
ManageFolder,
17+
ManageFolderOperation,
1918
ManageMcpTool,
2019
ManageMcpToolOperation,
2120
ManageScheduledTask,
2221
ManageScheduledTaskOperation,
2322
ManageSkill,
2423
ManageSkillOperation,
25-
MoveFolder,
2624
MoveWorkflow,
2725
QueryLogs,
2826
Redeploy,
@@ -36,6 +34,7 @@ import {
3634
WorkspaceFileOperation,
3735
} from '@/lib/copilot/generated/tool-catalog-v1'
3836
import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types'
37+
import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display'
3938
import type { ContentBlock, MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
4039
import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
4140
import { getWorkflowById } from '@/hooks/queries/utils/workflow-cache'
@@ -53,11 +52,7 @@ export const DEPLOY_TOOL_NAMES: Set<string> = new Set([
5352
Redeploy.id,
5453
])
5554

56-
export const FOLDER_TOOL_NAMES: Set<string> = new Set([
57-
CreateFolder.id,
58-
DeleteFolder.id,
59-
MoveFolder.id,
60-
])
55+
export const FOLDER_TOOL_NAMES: Set<string> = new Set([ManageFolder.id])
6156

6257
export const WORKFLOW_MUTATION_TOOL_NAMES: Set<string> = new Set([
6358
MoveWorkflow.id,
@@ -160,11 +155,6 @@ function stringParam(value: unknown): string | undefined {
160155
return typeof value === 'string' && value.trim() ? value.trim() : undefined
161156
}
162157

163-
function stringArrayParam(value: unknown): string[] {
164-
if (!Array.isArray(value)) return []
165-
return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
166-
}
167-
168158
function resolveWorkflowNameForDisplay(workflowId: unknown): string | undefined {
169159
const id = stringParam(workflowId)
170160
if (!id) return undefined
@@ -218,134 +208,27 @@ function functionExecuteTitle(title: string | undefined): string {
218208
return title ?? 'Running code'
219209
}
220210

221-
export function resolveToolDisplayTitle(
222-
name: string,
223-
args?: Record<string, unknown>
224-
): string | undefined {
225-
if (!args) return undefined
226-
227-
if (name === FunctionExecute.id) {
228-
return functionExecuteTitle(stringParam(args.title))
229-
}
230-
231-
if (name === WorkspaceFile.id) {
232-
const target = asPayloadRecord(args.target)
233-
return resolveWorkspaceFileDisplayTitle(args.operation, args.title, target?.fileName)
234-
}
235-
236-
if (name === SearchOnline.id) {
237-
const toolTitle = stringParam(args.toolTitle)
238-
return toolTitle ? `Searching online for ${toolTitle}` : 'Searching online'
239-
}
240-
241-
if (name === Grep.id) {
242-
const toolTitle = stringParam(args.toolTitle)
243-
return toolTitle ? `Searching for ${toolTitle}` : 'Searching'
244-
}
245-
246-
if (name === Glob.id) {
247-
const toolTitle = stringParam(args.toolTitle)
248-
return toolTitle ? `Finding ${toolTitle}` : 'Finding files'
249-
}
250-
251-
if (name === ScrapePage.id) {
252-
const url = stringParam(args.url)
253-
return url ? `Scraping ${url}` : 'Scraping page'
254-
}
255-
256-
if (name === CrawlWebsite.id) {
257-
const url = stringParam(args.url)
258-
return url ? `Crawling ${url}` : 'Crawling website'
259-
}
260-
261-
if (name === GetPageContents.id) {
262-
const urls = stringArrayParam(args.urls)
263-
if (urls.length === 1) return `Getting ${urls[0]}`
264-
if (urls.length > 1) return `Getting ${urls.length} pages`
265-
return 'Getting page contents'
266-
}
267-
268-
if (name === ManageCustomTool.id) {
269-
return resolveOperationDisplayTitle(
270-
args.operation,
271-
{
272-
[ManageCustomToolOperation.add]: 'Creating custom tool',
273-
[ManageCustomToolOperation.edit]: 'Updating custom tool',
274-
[ManageCustomToolOperation.delete]: 'Deleting custom tool',
275-
[ManageCustomToolOperation.list]: 'Listing custom tools',
276-
},
277-
'Custom tool action'
278-
)
279-
}
280-
281-
if (name === ManageMcpTool.id) {
282-
return resolveOperationDisplayTitle(
283-
args.operation,
284-
{
285-
[ManageMcpToolOperation.add]: 'Creating MCP server',
286-
[ManageMcpToolOperation.edit]: 'Updating MCP server',
287-
[ManageMcpToolOperation.delete]: 'Deleting MCP server',
288-
[ManageMcpToolOperation.list]: 'Listing MCP servers',
289-
},
290-
'MCP server action'
291-
)
292-
}
293-
294-
if (name === ManageSkill.id) {
295-
return resolveOperationDisplayTitle(
296-
args.operation,
297-
{
298-
[ManageSkillOperation.add]: 'Creating skill',
299-
[ManageSkillOperation.edit]: 'Updating skill',
300-
[ManageSkillOperation.delete]: 'Deleting skill',
301-
[ManageSkillOperation.list]: 'Listing skills',
302-
},
303-
'Skill action'
304-
)
305-
}
306-
307-
if (name === ManageScheduledTask.id) {
308-
return resolveOperationDisplayTitle(
309-
args.operation,
310-
{
311-
[ManageScheduledTaskOperation.create]: 'Creating scheduled task',
312-
[ManageScheduledTaskOperation.get]: 'Getting scheduled task',
313-
[ManageScheduledTaskOperation.update]: 'Updating scheduled task',
314-
[ManageScheduledTaskOperation.delete]: 'Deleting scheduled task',
315-
[ManageScheduledTaskOperation.list]: 'Listing scheduled tasks',
316-
},
317-
'Scheduled task action'
318-
)
319-
}
320-
321-
if (name === ManageCredential.id) {
322-
return resolveOperationDisplayTitle(
323-
args.operation,
324-
{
325-
[ManageCredentialOperation.rename]: 'Renaming credential',
326-
[ManageCredentialOperation.delete]: 'Deleting credential',
327-
},
328-
'Credential action'
329-
)
330-
}
331-
211+
export function resolveToolDisplayTitle(name: string, args?: Record<string, unknown>): string {
212+
// Cases that enrich the title with live workspace/block names from the client
213+
// stores. Everything else is resolved by the shared name+args resolver, which
214+
// is the single source of truth for tool-call titles.
332215
if (name === RunWorkflow.id) {
333-
const workflowName = resolveWorkflowNameForDisplay(args.workflowId)
216+
const workflowName = resolveWorkflowNameForDisplay(args?.workflowId)
334217
return workflowName ? `Running ${workflowName}` : 'Running workflow'
335218
}
336219

337220
if (name === RunFromBlock.id) {
338-
const workflowName = resolveWorkflowNameForDisplay(args.workflowId)
339-
const blockName = resolveBlockNameForDisplay(args.startBlockId)
221+
const workflowName = resolveWorkflowNameForDisplay(args?.workflowId)
222+
const blockName = resolveBlockNameForDisplay(args?.startBlockId)
340223
if (workflowName && blockName) return `Running ${workflowName} from ${blockName}`
341224
if (workflowName) return `Running ${workflowName}`
342225
if (blockName) return `Running from ${blockName}`
343226
return 'Running workflow'
344227
}
345228

346229
if (name === RunWorkflowUntilBlock.id) {
347-
const workflowName = resolveWorkflowNameForDisplay(args.workflowId)
348-
const blockName = resolveBlockNameForDisplay(args.stopAfterBlockId)
230+
const workflowName = resolveWorkflowNameForDisplay(args?.workflowId)
231+
const blockName = resolveBlockNameForDisplay(args?.stopAfterBlockId)
349232
if (workflowName && blockName) return `Running ${workflowName} until ${blockName}`
350233
if (workflowName) return `Running ${workflowName}`
351234
if (blockName) return `Running until ${blockName}`
@@ -354,11 +237,11 @@ export function resolveToolDisplayTitle(
354237

355238
if (name === QueryLogs.id) {
356239
const workflowName =
357-
resolveWorkflowNameForDisplay(args.workflowId) ?? stringParam(args.workflowName)
358-
return workflowName ? `Querying logs for ${workflowName}` : undefined
240+
resolveWorkflowNameForDisplay(args?.workflowId) ?? stringParam(args?.workflowName)
241+
if (workflowName) return `Querying logs for ${workflowName}`
359242
}
360243

361-
return undefined
244+
return getToolDisplayTitle(name, args)
362245
}
363246

364247
function decodeStreamingString(value: string): string {
@@ -480,5 +363,18 @@ export function resolveStreamingToolDisplayTitle(
480363
)
481364
}
482365

366+
if (name === ManageFolder.id) {
367+
return resolveOperationDisplayTitle(
368+
matchStreamingStringArg(streamingArgs, 'operation'),
369+
{
370+
[ManageFolderOperation.create]: 'Creating folder',
371+
[ManageFolderOperation.rename]: 'Renaming folder',
372+
[ManageFolderOperation.move]: 'Moving folder',
373+
[ManageFolderOperation.delete]: 'Deleting folder',
374+
},
375+
'Folder action'
376+
)
377+
}
378+
483379
return undefined
484380
}

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,9 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
477477
tsMs
478478
)
479479
if (isRecord(payload.arguments)) node.args = payload.arguments
480+
// Tool-call titles are derived from the tool name (+args) at serialize
481+
// time; the stream only carries behavioral flags now.
480482
const ui = isRecord(payload.ui) ? payload.ui : undefined
481-
const uiTitle = ui ? (asString(ui.title) ?? asString(ui.phaseLabel)) : undefined
482-
if (uiTitle) node.uiTitle = uiTitle
483483
if (ui?.hidden === true) node.hidden = true
484484
} else if (phase === MothershipStreamV1ToolPhase.args_delta) {
485485
const node = upsertToolNode(

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

Lines changed: 0 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,3 @@
1-
import {
2-
Agent,
3-
Auth,
4-
CreateWorkflow,
5-
Deploy,
6-
EditWorkflow,
7-
Ffmpeg,
8-
FunctionExecute,
9-
GenerateAudio,
10-
GenerateImage,
11-
GenerateVideo,
12-
GetPageContents,
13-
Glob,
14-
Grep,
15-
Knowledge,
16-
KnowledgeBase,
17-
ManageMcpTool,
18-
ManageSkill,
19-
Media,
20-
OpenResource,
21-
Read as ReadTool,
22-
Research,
23-
ScheduledTask,
24-
ScrapePage,
25-
SearchLibraryDocs,
26-
SearchOnline,
27-
Superagent,
28-
Table,
29-
UserMemory,
30-
UserTable,
31-
Workflow,
32-
WorkspaceFile,
33-
} from '@/lib/copilot/generated/tool-catalog-v1'
341
import type { ChatContext } from '@/stores/panel'
352

363
const EDIT_CONTENT_TOOL_ID = 'edit_content'
@@ -199,49 +166,3 @@ export const SUBAGENT_LABELS: Record<string, string> = {
199166
file: 'File Agent',
200167
media: 'Media Agent',
201168
} as const
202-
203-
interface ToolTitleMetadata {
204-
title: string
205-
}
206-
207-
/**
208-
* Fallback titles for tool calls when the stream did not provide one.
209-
*/
210-
export const TOOL_UI_METADATA: Record<string, ToolTitleMetadata> = {
211-
[Glob.id]: { title: 'Finding files' },
212-
[Grep.id]: { title: 'Searching' },
213-
[ReadTool.id]: { title: 'Reading file' },
214-
[SearchOnline.id]: { title: 'Searching online' },
215-
[ScrapePage.id]: { title: 'Scraping page' },
216-
[GetPageContents.id]: { title: 'Getting page contents' },
217-
[SearchLibraryDocs.id]: { title: 'Searching library docs' },
218-
[ManageMcpTool.id]: { title: 'MCP server action' },
219-
[ManageSkill.id]: { title: 'Skill action' },
220-
[UserMemory.id]: { title: 'Accessing memory' },
221-
[FunctionExecute.id]: { title: 'Running code' },
222-
[Superagent.id]: { title: 'Executing action' },
223-
[UserTable.id]: { title: 'Managing table' },
224-
[WorkspaceFile.id]: { title: 'Editing file' },
225-
[EDIT_CONTENT_TOOL_ID]: { title: 'Applying file content' },
226-
[CreateWorkflow.id]: { title: 'Creating workflow' },
227-
[EditWorkflow.id]: { title: 'Editing workflow' },
228-
[Workflow.id]: { title: 'Workflow Agent' },
229-
[RUN_SUBAGENT_ID]: { title: 'Run Agent' },
230-
[Deploy.id]: { title: 'Deploy Agent' },
231-
[Auth.id]: { title: 'Auth Agent' },
232-
[Knowledge.id]: { title: 'Knowledge Agent' },
233-
[KnowledgeBase.id]: { title: 'Managing knowledge base' },
234-
[Table.id]: { title: 'Table Agent' },
235-
[ScheduledTask.id]: { title: 'Scheduled Task Agent' },
236-
job: { title: 'Job Agent' },
237-
[Agent.id]: { title: 'Tools Agent' },
238-
custom_tool: { title: 'Creating tool' },
239-
[Research.id]: { title: 'Research Agent' },
240-
[OpenResource.id]: { title: 'Opening resource' },
241-
[Media.id]: { title: 'Media Agent' },
242-
[GenerateImage.id]: { title: 'Generating image' },
243-
[GenerateVideo.id]: { title: 'Generating video' },
244-
[GenerateAudio.id]: { title: 'Generating audio' },
245-
[Ffmpeg.id]: { title: 'Processing media' },
246-
context_compaction: { title: 'Compacted context' },
247-
}

0 commit comments

Comments
 (0)