chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
import { type ComponentType, type CSSProperties, createElement, type ReactNode } from 'react'
|
||||
import { Body, Head, Html, Link, Markdown, Section, Text } from '@react-email/components'
|
||||
import { render } from '@react-email/render'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import * as agentmail from '@/lib/mothership/inbox/agentmail-client'
|
||||
import { replaceUntilStable } from '@/lib/mothership/inbox/format'
|
||||
import type { InboxTask } from '@/lib/mothership/inbox/types'
|
||||
|
||||
const logger = createLogger('InboxResponse')
|
||||
|
||||
interface InboxResponseContext {
|
||||
inboxProviderId: string | null
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the mothership execution result as an email reply via AgentMail.
|
||||
* Returns the AgentMail response message ID for thread stitching, or null on failure.
|
||||
*/
|
||||
export async function sendInboxResponse(
|
||||
inboxTask: InboxTask,
|
||||
result: { success: boolean; content: string; error?: string },
|
||||
ctx: InboxResponseContext
|
||||
): Promise<string | null> {
|
||||
if (!ctx.inboxProviderId || !inboxTask.agentmailMessageId) {
|
||||
logger.warn('Cannot send response: missing inbox provider or message ID', {
|
||||
taskId: inboxTask.id,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const chatUrl = inboxTask.chatId
|
||||
? `${getBaseUrl()}/workspace/${ctx.workspaceId}/chat/${inboxTask.chatId}`
|
||||
: `${getBaseUrl()}/workspace/${ctx.workspaceId}/home`
|
||||
|
||||
const text = result.success
|
||||
? `${result.content}\n\n[View full conversation](${chatUrl})\n\nBest,\nMothership`
|
||||
: `I wasn't able to complete this task.\n\nError: ${result.error || 'Unknown error'}\n\n[View details](${chatUrl})\n\nBest,\nMothership`
|
||||
|
||||
const html = result.success
|
||||
? await renderEmailHtml(result.content, chatUrl)
|
||||
: await renderErrorHtml(result.error || 'Unknown error', chatUrl)
|
||||
|
||||
try {
|
||||
const response = await agentmail.replyToMessage(
|
||||
ctx.inboxProviderId,
|
||||
inboxTask.agentmailMessageId,
|
||||
{ text, html }
|
||||
)
|
||||
|
||||
logger.info('Inbox response sent', { taskId: inboxTask.id, responseId: response.message_id })
|
||||
return response.message_id
|
||||
} catch (error) {
|
||||
logger.error('Failed to send inbox response email', {
|
||||
taskId: inboxTask.id,
|
||||
error: getErrorMessage(error, 'Unknown error'),
|
||||
})
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const FONT_FAMILY = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif"
|
||||
const CODE_FONT_FAMILY = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace"
|
||||
|
||||
const emailStyles = {
|
||||
body: {
|
||||
fontFamily: FONT_FAMILY,
|
||||
fontSize: '15px',
|
||||
lineHeight: '25px',
|
||||
color: '#1a1a1a',
|
||||
fontWeight: 430,
|
||||
},
|
||||
content: {
|
||||
margin: 0,
|
||||
},
|
||||
markdownContainer: {
|
||||
margin: 0,
|
||||
},
|
||||
signature: {
|
||||
color: '#525252',
|
||||
marginTop: '32px',
|
||||
fontSize: '14px',
|
||||
},
|
||||
signatureText: {
|
||||
color: '#525252',
|
||||
margin: '0 0 16px 0',
|
||||
fontSize: '14px',
|
||||
lineHeight: '25px',
|
||||
fontFamily: FONT_FAMILY,
|
||||
},
|
||||
signatureLink: {
|
||||
color: '#1a1a1a',
|
||||
textDecoration: 'underline',
|
||||
textDecorationStyle: 'dashed',
|
||||
textUnderlineOffset: '2px',
|
||||
},
|
||||
} satisfies Record<string, CSSProperties>
|
||||
|
||||
const markdownStyles = {
|
||||
p: {
|
||||
margin: '0 0 16px 0',
|
||||
fontSize: '15px',
|
||||
lineHeight: '25px',
|
||||
color: '#1a1a1a',
|
||||
fontFamily: FONT_FAMILY,
|
||||
fontWeight: 430,
|
||||
},
|
||||
h1: {
|
||||
fontWeight: 600,
|
||||
color: '#1a1a1a',
|
||||
margin: '24px 0 12px 0',
|
||||
fontSize: '24px',
|
||||
lineHeight: '32px',
|
||||
fontFamily: FONT_FAMILY,
|
||||
},
|
||||
h2: {
|
||||
fontWeight: 600,
|
||||
color: '#1a1a1a',
|
||||
margin: '24px 0 12px 0',
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontFamily: FONT_FAMILY,
|
||||
},
|
||||
h3: {
|
||||
fontWeight: 600,
|
||||
color: '#1a1a1a',
|
||||
margin: '24px 0 12px 0',
|
||||
fontSize: '16px',
|
||||
lineHeight: '24px',
|
||||
fontFamily: FONT_FAMILY,
|
||||
},
|
||||
h4: {
|
||||
fontWeight: 600,
|
||||
color: '#1a1a1a',
|
||||
margin: '24px 0 12px 0',
|
||||
fontSize: '15px',
|
||||
lineHeight: '25px',
|
||||
fontFamily: FONT_FAMILY,
|
||||
},
|
||||
strong: {
|
||||
fontWeight: 600,
|
||||
color: '#1a1a1a',
|
||||
},
|
||||
codeInline: {
|
||||
backgroundColor: '#f3f3f3',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: CODE_FONT_FAMILY,
|
||||
fontSize: '13px',
|
||||
color: '#1a1a1a',
|
||||
},
|
||||
codeBlock: {
|
||||
backgroundColor: '#f3f3f3',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ededed',
|
||||
overflowX: 'auto',
|
||||
margin: '24px 0',
|
||||
fontFamily: CODE_FONT_FAMILY,
|
||||
fontSize: '13px',
|
||||
lineHeight: '21px',
|
||||
color: '#1a1a1a',
|
||||
},
|
||||
table: {
|
||||
borderCollapse: 'collapse',
|
||||
margin: '16px 0',
|
||||
},
|
||||
th: {
|
||||
border: '1px solid #ededed',
|
||||
padding: '8px 12px',
|
||||
textAlign: 'left',
|
||||
fontSize: '14px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
fontWeight: 600,
|
||||
},
|
||||
td: {
|
||||
border: '1px solid #ededed',
|
||||
padding: '8px 12px',
|
||||
textAlign: 'left',
|
||||
fontSize: '14px',
|
||||
},
|
||||
blockQuote: {
|
||||
borderLeft: '4px solid #e0e0e0',
|
||||
margin: '16px 0',
|
||||
padding: '4px 16px',
|
||||
color: '#525252',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
a: {
|
||||
color: '#2563eb',
|
||||
textDecoration: 'underline',
|
||||
textDecorationStyle: 'dashed',
|
||||
textUnderlineOffset: '2px',
|
||||
},
|
||||
ul: {
|
||||
margin: '16px 0',
|
||||
paddingLeft: '24px',
|
||||
},
|
||||
ol: {
|
||||
margin: '16px 0',
|
||||
paddingLeft: '24px',
|
||||
},
|
||||
li: {
|
||||
margin: '4px 0',
|
||||
},
|
||||
hr: {
|
||||
border: 'none',
|
||||
borderTop: '1px solid #ededed',
|
||||
margin: '24px 0',
|
||||
},
|
||||
} satisfies Record<string, CSSProperties>
|
||||
|
||||
interface InboxResponseEmailProps {
|
||||
children?: ReactNode
|
||||
chatUrl: string
|
||||
linkLabel: string
|
||||
}
|
||||
|
||||
interface EmailMarkdownProps {
|
||||
children?: string
|
||||
markdownContainerStyles?: CSSProperties
|
||||
markdownCustomStyles?: Record<string, CSSProperties>
|
||||
}
|
||||
|
||||
const EmailMarkdown = Markdown as ComponentType<EmailMarkdownProps>
|
||||
|
||||
function InboxResponseEmail({ children, chatUrl, linkLabel }: InboxResponseEmailProps) {
|
||||
return createElement(
|
||||
Html,
|
||||
{ lang: 'en', dir: 'ltr' },
|
||||
createElement(Head),
|
||||
createElement(
|
||||
Body,
|
||||
{ style: emailStyles.body },
|
||||
createElement(Section, { style: emailStyles.content }, children),
|
||||
createElement(
|
||||
Section,
|
||||
{ style: emailStyles.signature },
|
||||
createElement(
|
||||
Text,
|
||||
{ style: emailStyles.signatureText },
|
||||
createElement(Link, { href: chatUrl, style: emailStyles.signatureLink }, linkLabel)
|
||||
),
|
||||
createElement(
|
||||
Text,
|
||||
{ style: emailStyles.signatureText },
|
||||
'Best,',
|
||||
createElement('br'),
|
||||
'Sim'
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function stripRawHtml(text: string): string {
|
||||
return text
|
||||
.split(/(```[\s\S]*?```)/g)
|
||||
.map((segment, i) =>
|
||||
i % 2 === 0 ? replaceUntilStable(segment, /<\/?[a-z][^>]*>/gi, '') : segment
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
function preserveSoftBreaks(text: string): string {
|
||||
return text
|
||||
.split(/(```[\s\S]*?```)/g)
|
||||
.map((segment, i) => (i % 2 === 0 ? segment.replace(/([^\n])\n(?=[^\n])/g, '$1 \n') : segment))
|
||||
.join('')
|
||||
}
|
||||
|
||||
function stripUnsafeUrls(html: string): string {
|
||||
return html.replace(/href\s*=\s*(['"])(?:javascript|vbscript|data):.*?\1/gi, 'href="#"')
|
||||
}
|
||||
|
||||
async function renderEmailHtml(markdown: string, chatUrl: string): Promise<string> {
|
||||
const safeMarkdown = preserveSoftBreaks(stripRawHtml(markdown))
|
||||
const html = await render(
|
||||
createElement(
|
||||
InboxResponseEmail,
|
||||
{ chatUrl, linkLabel: 'View full conversation' },
|
||||
createElement(
|
||||
EmailMarkdown,
|
||||
{
|
||||
markdownContainerStyles: emailStyles.markdownContainer,
|
||||
markdownCustomStyles: markdownStyles,
|
||||
},
|
||||
safeMarkdown
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return stripUnsafeUrls(html)
|
||||
}
|
||||
|
||||
async function renderErrorHtml(error: string, chatUrl: string): Promise<string> {
|
||||
const html = await render(
|
||||
createElement(
|
||||
InboxResponseEmail,
|
||||
{ chatUrl, linkLabel: 'View details' },
|
||||
createElement(
|
||||
Text,
|
||||
{ key: 'message', style: markdownStyles.p },
|
||||
"I wasn't able to complete this task."
|
||||
),
|
||||
createElement(
|
||||
Text,
|
||||
{ key: 'error', style: { ...markdownStyles.p, color: '#6b7280' } },
|
||||
`Error: ${error}`
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return stripUnsafeUrls(html)
|
||||
}
|
||||
Reference in New Issue
Block a user