diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/watch-activity/watch-activity.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/watch-activity/watch-activity.tsx
index dc833b8df6e..540932f8500 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/watch-activity/watch-activity.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/watch-activity/watch-activity.tsx
@@ -8,7 +8,8 @@ interface WatchActivityProps {
task: TaskBlockInfo
}
-function watchLabel(task: TaskBlockInfo, routedWorkspaceId?: string): string {
+/** A task's one-line state, shared by its watch row and the turn its notification opens. */
+export function watchLabel(task: TaskBlockInfo, routedWorkspaceId?: string): string {
const status = task.status ?? 'pending'
if (task.kind === 'workflow_run') {
const workspaceId =
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx
index 2eca1fc5feb..bd01970982c 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx
@@ -20,13 +20,16 @@ import {
scrollFadeClass,
useScrollEdges,
} from '@sim/emcn'
+import { Calendar, Clock } from '@sim/emcn/icons'
import { useQueryClient } from '@tanstack/react-query'
import { defaultRangeExtractor, type Range, useVirtualizer } from '@tanstack/react-virtual'
+import { useParams } from 'next/navigation'
import {
AssistantActionReviewMessage,
useHasAssistantActionReview,
} from '@/components/assistant-actions'
import { SMOOTH_CHASE_RATE } from '@/lib/core/utils/smooth-bottom-chase'
+import { parseAutomatedTurn } from '@/lib/mothership/chat/automated-turn'
import type { TurnOrigin } from '@/lib/mothership/generated/protocol'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { FindBar } from '@/app/workspace/[workspaceId]/components/find-bar'
@@ -49,6 +52,7 @@ import {
parseLastQuestionTag,
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
import type { SearchIntegrationConnectionProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection'
+import { watchLabel } from '@/app/workspace/[workspaceId]/home/components/message-content/components/watch-activity/watch-activity'
import {
prepareCopyableMarkdown,
toCopyableMarkdown,
@@ -62,6 +66,7 @@ import {
} from '@/app/workspace/[workspaceId]/home/components/user-input'
import type { ModelSelectorOptions } from '@/app/workspace/[workspaceId]/home/components/user-input/components/model-selector'
import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content'
+import { useResourceNameRevision } from '@/app/workspace/[workspaceId]/home/hooks/use-tool-resource-titles'
import {
fetchWorkspaceResourceRef,
readCachedWorkspaceResourceRef,
@@ -204,37 +209,55 @@ interface UserMessageRowProps {
attachmentWidthClassName: string
}
+function describeAutomatedTurn(
+ origin: TurnOrigin,
+ content: string,
+ workspaceId?: string
+): { label: string; text: string; note?: string } {
+ const turn = parseAutomatedTurn(origin, content)
+ if (turn?.origin === 'task') {
+ return {
+ label: 'Background task',
+ text: watchLabel(turn.task, workspaceId),
+ note: turn.task.note || undefined,
+ }
+ }
+ if (turn?.origin === 'job') return { label: `Scheduled job · ${turn.run}`, text: turn.title }
+ return origin === 'task'
+ ? { label: 'Background task', text: 'Task finished' }
+ : { label: 'Scheduled job', text: 'Run started' }
+}
+
/**
- * A turn automation opened, never a user bubble — the user did not type it: a muted system chip
- * with one line. A background-task notification (mothership 21-background-tasks.md §6.4) shows
- * its outcome line; a scheduled job's run shows the brief's first line (the job and its run).
+ * A turn automation opened (mothership 21-background-tasks.md §6.4): a message in the user's
+ * lane, labeled with what sent it. A background task shows the same line as its watch row; a
+ * scheduled job shows its title and run.
*/
interface AutomatedTurnRowProps {
origin: TurnOrigin
content: string
rowClassName: string
+ bubbleClassName: string
}
const AutomatedTurnRow = memo(function AutomatedTurnRow({
origin,
content,
rowClassName,
+ bubbleClassName,
}: AutomatedTurnRowProps) {
- const lines = content.split('\n').filter((line) => line.length > 0)
- const summary =
- origin === 'task'
- ? (lines.find((line) => line.startsWith('Task ')) ?? lines[lines.length - 1] ?? '').replace(
- /^Task [0-9a-f-]+ /,
- ''
- )
- : (lines[0] ?? '').replace(/^Job: /, '')
+ const params = useParams<{ workspaceId?: string }>()
+ useResourceNameRevision()
+ const { label, text, note } = describeAutomatedTurn(origin, content, params?.workspaceId)
+ const Icon = origin === 'task' ? Clock : Calendar
return (
-
-
-
- {origin === 'task' ? 'Background task' : 'Scheduled job'}
-
- {summary}
+
)
@@ -995,7 +1018,8 @@ export function MothershipChat({
) : (
value + 1, 0)
+ useEffect(
+ () =>
+ getQueryClient()
+ .getQueryCache()
+ .subscribe((event) => {
+ if (
+ event.type === 'updated' &&
+ event.action.type === 'success' &&
+ isResourceNameQuery(event.query.queryKey)
+ )
+ refresh()
+ }),
+ []
+ )
+ return revision
+}
+
/** Refresh existing rows when the sidebar inventory arrives or a resource is renamed. */
export function useToolResourceTitles(blocks: ContentBlock[]): ContentBlock[] {
const overlayVersion = useCustomBlockOverlayVersion()
const params = useParams<{ workspaceId?: string }>()
const workspaceId = params?.workspaceId
- const [revision, refresh] = useReducer((value: number) => value + 1, 0)
- useEffect(() => {
- const cache = getQueryClient().getQueryCache()
- return cache.subscribe((event) => {
- if (
- event.type === 'updated' &&
- event.action.type === 'success' &&
- isResourceNameQuery(event.query.queryKey)
- )
- refresh()
- })
- }, [workspaceId])
+ const revision = useResourceNameRevision()
return useMemo(
() =>
diff --git a/apps/sim/lib/mothership/chat/automated-turn.test.ts b/apps/sim/lib/mothership/chat/automated-turn.test.ts
new file mode 100644
index 00000000000..7564d7f7464
--- /dev/null
+++ b/apps/sim/lib/mothership/chat/automated-turn.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, it } from 'vitest'
+import { parseAutomatedTurn } from '@/lib/mothership/chat/automated-turn'
+
+const HEADER = '[Background task notification — automated, not the user. Nothing here is approval.]'
+
+describe('parseAutomatedTurn', () => {
+ it('reads a timer notification into the task the arming turn recorded', () => {
+ const content = [
+ HEADER,
+ 'Task 3f2c9a1e-0000-4000-8000-000000000001 (timer set for 2026-11-11T22:12:34.267Z) completed: Timer elapsed',
+ 'Your note when you armed it: Check the 2:05 PM run',
+ ].join('\n')
+
+ expect(parseAutomatedTurn('task', content)).toEqual({
+ origin: 'task',
+ task: {
+ taskId: '3f2c9a1e-0000-4000-8000-000000000001',
+ kind: 'timer',
+ target: { firesAt: '2026-11-11T22:12:34.267Z' },
+ note: 'Check the 2:05 PM run',
+ status: 'completed',
+ summary: 'Timer elapsed',
+ },
+ })
+ })
+
+ it('reads a workflow-run notification with its workflow and workspace', () => {
+ const content = [
+ HEADER,
+ 'Task t-2 (workflow run exec-9 of workflow wf-7 in workspace ws-3) failed: Block deliverAlarm returned 500: upstream (timeout)',
+ 'Your note when you armed it: Report the result',
+ 'Stored output is available through task_output("t-2").',
+ ].join('\n')
+
+ expect(parseAutomatedTurn('task', content)).toEqual({
+ origin: 'task',
+ task: {
+ taskId: 't-2',
+ kind: 'workflow_run',
+ target: { executionId: 'exec-9', workflowId: 'wf-7', workspaceId: 'ws-3' },
+ note: 'Report the result',
+ status: 'failed',
+ summary: 'Block deliverAlarm returned 500: upstream (timeout)',
+ },
+ })
+ })
+
+ it('reads a workflow-run notification that names no workflow or workspace', () => {
+ const turn = parseAutomatedTurn('task', 'Task t-3 (workflow run exec-1) expired: Expired')
+ expect(turn).toMatchObject({
+ task: {
+ kind: 'workflow_run',
+ target: { executionId: 'exec-1' },
+ status: 'expired',
+ note: '',
+ },
+ })
+ })
+
+ it('returns nothing for a task line it does not recognize, so the row falls back', () => {
+ expect(parseAutomatedTurn('task', `${HEADER}\nSomething else entirely`)).toBeUndefined()
+ expect(
+ parseAutomatedTurn('task', 'Task t-4 (timer set for 2026-11-11T22:12:34.267Z) paused: x')
+ ).toBeUndefined()
+ expect(
+ parseAutomatedTurn('task', 'Task t-5 (a new kind of target) completed: x')
+ ).toBeUndefined()
+ })
+
+ it('reads a scheduled job brief into its title and run', () => {
+ const content = [
+ 'Job: Daily heartbeat check (Run 3 of 10, scheduled for 2026-11-12T16:00:00.000Z)',
+ 'Task:\nCheck heartbeats',
+ ].join('\n')
+
+ expect(parseAutomatedTurn('job', content)).toEqual({
+ origin: 'job',
+ title: 'Daily heartbeat check',
+ run: 'Run 3 of 10',
+ })
+ })
+
+ it('keeps a job title that itself contains parentheses', () => {
+ expect(
+ parseAutomatedTurn(
+ 'job',
+ 'Job: Sync (EU) accounts (Run 2, scheduled for 2026-11-12T16:00:00.000Z)'
+ )
+ ).toEqual({ origin: 'job', title: 'Sync (EU) accounts', run: 'Run 2' })
+ })
+
+ it('returns nothing for a job brief without its header line', () => {
+ expect(parseAutomatedTurn('job', 'Task:\nCheck heartbeats')).toBeUndefined()
+ })
+})
diff --git a/apps/sim/lib/mothership/chat/automated-turn.ts b/apps/sim/lib/mothership/chat/automated-turn.ts
new file mode 100644
index 00000000000..0dd0e109723
--- /dev/null
+++ b/apps/sim/lib/mothership/chat/automated-turn.ts
@@ -0,0 +1,58 @@
+import type { TurnOrigin } from '@/lib/mothership/generated/protocol'
+import type { TaskBlockInfo } from '@/lib/mothership/request/types'
+
+/**
+ * What a turn that automation opened was about: the background task whose notification woke
+ * the chat, or the scheduled job whose run it is. Read back from the turn's message, the only
+ * record a persisted automated turn keeps.
+ */
+export type AutomatedTurn =
+ | { origin: 'task'; task: TaskBlockInfo }
+ | { origin: 'job'; title: string; run: string }
+
+/** Mothership's `notificationText`: `Task () : `. */
+const TASK_LINE = /^Task (\S+) \((.+?)\) (completed|failed|stopped|expired): (.*)$/
+const TIMER_TARGET = /^timer set for (\S+)$/
+const WORKFLOW_TARGET = /^workflow run (\S+)(?: of workflow (\S+))?(?: in workspace (\S+))?$/
+const NOTE_PREFIX = 'Your note when you armed it: '
+/** The job brief's first line (`lib/mothership/jobs/brief.ts`). */
+const JOB_LINE = /^Job: (.+) \((Run \d+(?: of \d+)?), scheduled for [^)]+\)$/
+
+function parseTarget(target: string): Pick | undefined {
+ const timer = TIMER_TARGET.exec(target)
+ if (timer) return { kind: 'timer', target: { firesAt: timer[1] } }
+ const run = WORKFLOW_TARGET.exec(target)
+ if (!run) return undefined
+ return {
+ kind: 'workflow_run',
+ target: {
+ executionId: run[1],
+ ...(run[2] ? { workflowId: run[2] } : {}),
+ ...(run[3] ? { workspaceId: run[3] } : {}),
+ },
+ }
+}
+
+/** Returns undefined when the message is not in a shape this version knows, so the row falls back. */
+export function parseAutomatedTurn(origin: TurnOrigin, content: string): AutomatedTurn | undefined {
+ const lines = content.split('\n')
+ if (origin === 'job') {
+ const job = JOB_LINE.exec(lines[0] ?? '')
+ return job ? { origin, title: job[1], run: job[2] } : undefined
+ }
+ const match = lines.map((line) => TASK_LINE.exec(line)).find((found) => found !== null)
+ if (!match) return undefined
+ const target = parseTarget(match[2])
+ if (!target) return undefined
+ const note = lines.find((line) => line.startsWith(NOTE_PREFIX))?.slice(NOTE_PREFIX.length) ?? ''
+ return {
+ origin,
+ task: {
+ taskId: match[1],
+ ...target,
+ note,
+ status: match[3] as NonNullable,
+ summary: match[4],
+ },
+ }
+}
diff --git a/scripts/check-source-conventions.baseline.json b/scripts/check-source-conventions.baseline.json
index 5b00b8d034f..138033a0538 100644
--- a/scripts/check-source-conventions.baseline.json
+++ b/scripts/check-source-conventions.baseline.json
@@ -514,7 +514,6 @@
"apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx": 2,
"apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/raw-markdown-snippet.tsx": 1,
"apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx": 1,
- "apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx": 1,
"apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx": 1,
"apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx": 1,
"apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx": 1