d25d482dc2
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
318 lines
8.0 KiB
TypeScript
318 lines
8.0 KiB
TypeScript
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)
|
|
}
|