Skip to content

Commit 0fdb2be

Browse files
committed
improvement(mothership): tool display titles, html sanitization, and ui fixes
- Use TOOL_UI_METADATA as fallback for tool display titles (fast_edit shows "Editing workflow" instead of "Fast Edit") - Harden HTML-to-text extraction with replaceUntilStable to prevent nested tag injection - Decode HTML entities in a single pass to avoid double-unescaping - Fix Google Drive/Docs query escaping for backslashes in folder IDs - Replace regex with indexOf for email sender/display name parsing - Update embedded workflow run tooltip to "Run workflow"
1 parent 3e3c160 commit 0fdb2be

File tree

7 files changed

+73
-20
lines changed

7 files changed

+73
-20
lines changed

apps/sim/app/api/webhooks/agentmail/route.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,24 @@ async function createRejectedTask(
267267
* Format: "username@domain.com" or "Display Name <username@domain.com>"
268268
*/
269269
function extractSenderEmail(from: string): string {
270-
const match = from.match(/<([^>]+)>/)
271-
return (match?.[1] || from).toLowerCase().trim()
270+
const openBracket = from.indexOf('<')
271+
const closeBracket = from.indexOf('>', openBracket + 1)
272+
if (openBracket !== -1 && closeBracket !== -1) {
273+
return from
274+
.substring(openBracket + 1, closeBracket)
275+
.toLowerCase()
276+
.trim()
277+
}
278+
return from.toLowerCase().trim()
272279
}
273280

274281
function extractDisplayName(from: string): string | null {
275-
const match = from.match(/^(.+?)\s*</)
276-
return match?.[1]?.trim().replace(/^"|"$/g, '') || null
282+
const openBracket = from.indexOf('<')
283+
if (openBracket <= 0) return null
284+
const name = from.substring(0, openBracket).trim()
285+
if (!name) return null
286+
if (name.startsWith('"') && name.endsWith('"')) {
287+
return name.slice(1, -1) || null
288+
}
289+
return name
277290
}

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

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

33
import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types'
4-
import { SUBAGENT_LABELS } from '../../types'
4+
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types'
55
import type { AgentGroupItem } from './components'
66
import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components'
77

@@ -47,7 +47,10 @@ function toToolData(tc: NonNullable<ContentBlock['toolCall']>): ToolCallData {
4747
return {
4848
id: tc.id,
4949
toolName: tc.name,
50-
displayTitle: tc.displayTitle || formatToolName(tc.name),
50+
displayTitle:
51+
tc.displayTitle ||
52+
TOOL_UI_METADATA[tc.name as keyof typeof TOOL_UI_METADATA]?.title ||
53+
formatToolName(tc.name),
5154
status: tc.status,
5255
result: tc.result,
5356
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWor
197197
</Button>
198198
</Tooltip.Trigger>
199199
<Tooltip.Content side='bottom'>
200-
<p>{isExecuting ? 'Stop' : 'Run'}</p>
200+
<p>{isExecuting ? 'Stop' : 'Run workflow'}</p>
201201
</Tooltip.Content>
202202
</Tooltip.Root>
203203
</>

apps/sim/connectors/google-docs/google-docs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ function buildQuery(sourceConfig: Record<string, unknown>): string {
162162

163163
const folderId = sourceConfig.folderId as string | undefined
164164
if (folderId?.trim()) {
165-
parts.push(`'${folderId.trim().replace(/'/g, "\\'")}' in parents`)
165+
parts.push(`'${folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' in parents`)
166166
}
167167

168168
return parts.join(' and ')

apps/sim/connectors/google-drive/google-drive.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ function buildQuery(sourceConfig: Record<string, unknown>): string {
112112

113113
const folderId = sourceConfig.folderId as string | undefined
114114
if (folderId?.trim()) {
115-
parts.push(`'${folderId.trim().replace(/'/g, "\\'")}' in parents`)
115+
parts.push(`'${folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' in parents`)
116116
}
117117

118118
const fileType = (sourceConfig.fileType as string) || 'all'

apps/sim/lib/mothership/inbox/format.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,26 +94,60 @@ function isForwardedEmail(subject: string | null, body: string | null): boolean
9494
return false
9595
}
9696

97+
/**
98+
* Repeatedly applies a regex replacement until the string stabilises.
99+
* Prevents incomplete sanitization from nested/overlapping patterns
100+
* like `<scr<script>ipt>`.
101+
*/
102+
export function replaceUntilStable(input: string, pattern: RegExp, replacement: string): string {
103+
let prev = input
104+
let next = prev.replace(pattern, replacement)
105+
while (next !== prev) {
106+
prev = next
107+
next = prev.replace(pattern, replacement)
108+
}
109+
return next
110+
}
111+
112+
const HTML_ENTITY_MAP: Record<string, string> = {
113+
'&nbsp;': ' ',
114+
'&amp;': '&',
115+
'&lt;': '<',
116+
'&gt;': '>',
117+
'&quot;': '"',
118+
'&#39;': "'",
119+
}
120+
121+
/**
122+
* Decodes known HTML entities in a single pass to avoid double-unescaping.
123+
* A two-step decode (e.g. `&amp;` -> `&` then `&lt;` -> `<`) would turn
124+
* `&amp;lt;` into `<`, which is incorrect.
125+
*/
126+
function decodeHtmlEntities(text: string): string {
127+
return text.replace(/&(?:nbsp|amp|lt|gt|quot|#39);/g, (match) => HTML_ENTITY_MAP[match] ?? match)
128+
}
129+
97130
/**
98131
* Basic HTML to text extraction.
99132
*/
100133
function extractTextFromHtml(html: string | null): string | null {
101134
if (!html) return null
102135

103-
return html
104-
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
105-
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
136+
let text = html
137+
text = replaceUntilStable(text, /<style[^>]*>[\s\S]*?<\/style\s*>/gi, '')
138+
text = replaceUntilStable(text, /<script[^>]*>[\s\S]*?<\/script\s*>/gi, '')
139+
140+
text = text
106141
.replace(/<br\s*\/?>/gi, '\n')
107142
.replace(/<\/p>/gi, '\n\n')
108143
.replace(/<\/div>/gi, '\n')
109144
.replace(/<\/li>/gi, '\n')
110-
.replace(/<[^>]+>/g, '')
111-
.replace(/&nbsp;/g, ' ')
112-
.replace(/&amp;/g, '&')
113-
.replace(/&lt;/g, '<')
114-
.replace(/&gt;/g, '>')
115-
.replace(/&quot;/g, '"')
116-
.replace(/&#39;/g, "'")
145+
146+
text = replaceUntilStable(text, /<[^>]+>/g, '')
147+
148+
text = decodeHtmlEntities(text)
117149
.replace(/\n{3,}/g, '\n\n')
118150
.trim()
151+
152+
return text
119153
}

apps/sim/lib/mothership/inbox/response.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
22
import { marked } from 'marked'
33
import { getBaseUrl } from '@/lib/core/utils/urls'
44
import * as agentmail from '@/lib/mothership/inbox/agentmail-client'
5+
import { replaceUntilStable } from '@/lib/mothership/inbox/format'
56
import type { InboxTask } from '@/lib/mothership/inbox/types'
67

78
const logger = createLogger('InboxResponse')
@@ -82,7 +83,9 @@ const EMAIL_STYLES = `
8283
function stripRawHtml(text: string): string {
8384
return text
8485
.split(/(```[\s\S]*?```)/g)
85-
.map((segment, i) => (i % 2 === 0 ? segment.replace(/<\/?[a-z][^>]*>/gi, '') : segment))
86+
.map((segment, i) =>
87+
i % 2 === 0 ? replaceUntilStable(segment, /<\/?[a-z][^>]*>/gi, '') : segment
88+
)
8689
.join('')
8790
}
8891

0 commit comments

Comments
 (0)