From b7b6e0266f22d6473b1f99555d0ffe626bbe7338 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 16:18:14 -0700 Subject: [PATCH 1/8] feat(logs): add Troubleshoot in Chat button for errored runs Errored log runs now surface a "Troubleshoot in Chat" action in the log details panel. It tags the failed run as a logs context (executionId) and auto-sends a message to Chat asking Sim to investigate and fix the error, porting the old copilot "Fix in Chat" behavior to mothership and adding run-ID tagging. Cross-route handoff rides a one-shot MothershipHandoffStorage consumed once on the home surface mount, so the tagged run + prompt survive the navigation from Logs to Chat and the agent receives the full run error via the resolved logs context. --- .../app/workspace/[workspaceId]/home/home.tsx | 13 ++++ .../components/log-details/log-details.tsx | 44 +++++++++++- .../lib/core/utils/browser-storage.test.ts | 46 +++++++++++++ apps/sim/lib/core/utils/browser-storage.ts | 69 +++++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 apps/sim/lib/core/utils/browser-storage.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index c206ffd9bf0..5357bc6839c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -29,6 +29,7 @@ import { LandingPromptStorage, type LandingWorkflowSeed, LandingWorkflowSeedStorage, + MothershipHandoffStorage, } from '@/lib/core/utils/browser-storage' import { MOTHERSHIP_SEND_MESSAGE_EVENT, @@ -322,6 +323,18 @@ export function Home({ chatId, userName, userId }: HomeProps) { return () => window.removeEventListener(MOTHERSHIP_SEND_MESSAGE_EVENT, handler) }, [sendMessage]) + /** + * Consumes a one-shot handoff left by another surface (e.g. "Troubleshoot in + * Chat" on an errored log) and auto-sends it into this fresh chat, tagging the + * run so Sim can inspect the failure. `consume` clears the entry atomically, + * so it fires at most once — a re-run (StrictMode, reload, `sendMessage` + * identity change) reads nothing and no-ops. + */ + useEffect(() => { + const handoff = MothershipHandoffStorage.consume() + if (handoff) sendMessage(handoff.message, undefined, handoff.contexts) + }, [sendMessage]) + function resolveResourceFromContext( context: ChatContext ): { type: MothershipResourceType; id: string } | null { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index e28a8036242..585674ce61c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -20,14 +20,16 @@ import { Tooltip, useCopyToClipboard, } from '@sim/emcn' -import { Workflow } from '@sim/emcn/icons' +import { Workflow, Wrench } from '@sim/emcn/icons' import { formatDuration } from '@sim/utils/formatting' import { ArrowDown, ArrowUp, Check, ChevronUp, Clipboard, Search, X } from 'lucide-react' +import { useParams, useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' import { createPortal } from 'react-dom' import type { WorkflowLogRow } from '@/lib/api/contracts/logs' import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' import { apportionCredits, dollarsToCredits } from '@/lib/billing/credits/conversion' +import { MothershipHandoffStorage } from '@/lib/core/utils/browser-storage' import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans' import type { TraceSpan } from '@/lib/logs/types' import { @@ -52,6 +54,7 @@ import { usePermissionConfig } from '@/hooks/use-permission-config' import { formatCost } from '@/providers/utils' import { useLogDetailsUIStore } from '@/stores/logs/store' import { MAX_LOG_DETAILS_WIDTH_RATIO, MIN_LOG_DETAILS_WIDTH } from '@/stores/logs/utils' +import type { ChatContext } from '@/stores/panel' /** * Renders an already-apportioned integer credit value. `dollars` is only used @@ -275,6 +278,9 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP const scrollAreaRef = useRef(null) + const router = useRouter() + const { workspaceId } = useParams<{ workspaceId: string }>() + const { config: permissionConfig } = usePermissionConfig() const isInitialTabMountRef = useRef(true) @@ -382,6 +388,29 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP const formattedTimestamp = formatDate(log.createdAt) const logStatus = getDisplayStatus(log.status) + /** + * Troubleshooting hands the failed run off to Chat, tagging it by + * `executionId`. A real Chat run can't be debugged from inside itself, so + * mothership-triggered logs are excluded — `isLikelyExecution` already encodes + * "has an executionId and isn't a mothership run". + */ + const canTroubleshoot = log.status === 'failed' && isLikelyExecution + + const handleTroubleshoot = useCallback(() => { + if (!log.executionId) return + const workflowName = log.workflow?.name?.trim() || null + const context: ChatContext = { + kind: 'logs', + executionId: log.executionId, + label: workflowName ?? 'this run', + } + const message = workflowName + ? `The "${workflowName}" workflow run failed. Investigate the error in this run and help me fix it.` + : 'This workflow run failed. Investigate the error in this run and help me fix it.' + MothershipHandoffStorage.store({ message, contexts: [context] }) + router.push(`/workspace/${workspaceId}/home`) + }, [log.executionId, log.workflow?.name, workspaceId, router]) + return ( <>
@@ -541,6 +570,19 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
)} + {/* Troubleshoot — hand the failed run off to Chat to diagnose */} + {canTroubleshoot && ( + + )} + {/* Files */} {log.files && log.files.length > 0 && } diff --git a/apps/sim/lib/core/utils/browser-storage.test.ts b/apps/sim/lib/core/utils/browser-storage.test.ts new file mode 100644 index 00000000000..9eab2f5cc08 --- /dev/null +++ b/apps/sim/lib/core/utils/browser-storage.test.ts @@ -0,0 +1,46 @@ +/** + * @vitest-environment jsdom + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { MothershipHandoffStorage, STORAGE_KEYS } from '@/lib/core/utils/browser-storage' +import type { ChatContext } from '@/stores/panel' + +describe('MothershipHandoffStorage', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('round-trips a handoff and trims the message, preserving contexts', () => { + const contexts: ChatContext[] = [{ kind: 'logs', executionId: 'run-1', label: 'My Flow' }] + expect(MothershipHandoffStorage.store({ message: ' fix it ', contexts })).toBe(true) + + expect(MothershipHandoffStorage.consume()).toEqual({ message: 'fix it', contexts }) + }) + + it('is one-shot — a second consume returns null', () => { + MothershipHandoffStorage.store({ message: 'fix it' }) + + expect(MothershipHandoffStorage.consume()).not.toBeNull() + expect(MothershipHandoffStorage.consume()).toBeNull() + }) + + it('refuses to store an empty message', () => { + expect(MothershipHandoffStorage.store({ message: ' ' })).toBe(false) + expect(MothershipHandoffStorage.consume()).toBeNull() + }) + + it('drops and clears a handoff older than maxAge', () => { + vi.useFakeTimers() + try { + vi.setSystemTime(new Date('2026-01-01T00:00:00Z')) + MothershipHandoffStorage.store({ message: 'fix it' }) + + vi.advanceTimersByTime(61 * 1000) + + expect(MothershipHandoffStorage.consume()).toBeNull() + expect(localStorage.getItem(STORAGE_KEYS.MOTHERSHIP_HANDOFF)).toBeNull() + } finally { + vi.useRealTimers() + } + }) +}) diff --git a/apps/sim/lib/core/utils/browser-storage.ts b/apps/sim/lib/core/utils/browser-storage.ts index d22702f4c57..18c68405614 100644 --- a/apps/sim/lib/core/utils/browser-storage.ts +++ b/apps/sim/lib/core/utils/browser-storage.ts @@ -4,6 +4,7 @@ */ import { createLogger } from '@sim/logger' +import type { ChatContext } from '@/stores/panel' const logger = createLogger('BrowserStorage') @@ -103,6 +104,7 @@ export const STORAGE_KEYS = { LANDING_PAGE_PROMPT: 'sim_landing_page_prompt', LANDING_PAGE_WORKFLOW_SEED: 'sim_landing_page_workflow_seed', WORKSPACE_RECENCY: 'sim_workspace_recency', + MOTHERSHIP_HANDOFF: 'sim_mothership_handoff', } as const export class WorkspaceRecencyStorage { @@ -296,3 +298,70 @@ export class LandingWorkflowSeedStorage { return BrowserStorage.removeItem(LandingWorkflowSeedStorage.KEY) } } + +export interface MothershipHandoff { + /** The message to auto-send to Chat once the home surface mounts. */ + message: string + /** Structured contexts to attach — e.g. a `logs` mention tagging a run. */ + contexts?: ChatContext[] +} + +/** + * One-shot handoff that seeds an auto-sent Chat (mothership) message when the + * user is routed to the workspace home from elsewhere in the app — e.g. the + * "Troubleshoot in Chat" action on an errored log, which tags the failed run + * and asks Sim to fix it. + * + * The home surface consumes this exactly once on mount. A short max-age guards + * against a stale handoff firing on a later, unrelated visit, and `consume` + * always clears the entry so it can never be replayed. + */ +export class MothershipHandoffStorage { + private static readonly KEY = STORAGE_KEYS.MOTHERSHIP_HANDOFF + + /** + * Store a handoff to be auto-sent on the next home-surface mount. + * @returns True if stored, false when the message is empty. + */ + static store(handoff: MothershipHandoff): boolean { + const message = handoff.message.trim() + if (!message) { + return false + } + + return BrowserStorage.setItem(MothershipHandoffStorage.KEY, { + message, + contexts: handoff.contexts, + timestamp: Date.now(), + }) + } + + /** + * Retrieve and consume the stored handoff. Always clears the entry, so a + * handoff fires at most once even when it has expired. + * @param maxAge - Maximum age in milliseconds (default: 60 seconds) + */ + static consume(maxAge: number = 60 * 1000): MothershipHandoff | null { + const data = BrowserStorage.getItem<{ + message?: string + contexts?: ChatContext[] + timestamp?: number + } | null>(MothershipHandoffStorage.KEY, null) + + if (!data?.message || !data.timestamp) { + return null + } + + MothershipHandoffStorage.clear() + + if (Date.now() - data.timestamp > maxAge) { + return null + } + + return { message: data.message, contexts: data.contexts } + } + + static clear(): boolean { + return BrowserStorage.removeItem(MothershipHandoffStorage.KEY) + } +} From 9a9a11ad0b242db11b3e1dcbe692ac4ee740b7d5 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 16:32:00 -0700 Subject: [PATCH 2/8] fix(logs): deliver troubleshoot to a mounted chat + harden handoff Review follow-ups: - Same-route case (Cursor): LogDetailsContent is also embedded in the Chat resource panel, where router.push('/home') doesn't remount Home, so the mount-only handoff consume never fired. Generalize the existing sendMothershipMessage event to carry contexts and be cancelable: deliver straight to a mounted chat when one claims it, and only persist + navigate when none is listening. - Corrupted-entry tombstone (Greptile): consume now clears whenever any entry exists, so a malformed/expired handoff can't linger across future mounts. - Silent store failure (Greptile): only navigate when the handoff actually stored, so a failed write never strands the user on an empty chat. --- .../app/workspace/[workspaceId]/home/home.tsx | 18 +++++++---- .../components/log-details/log-details.tsx | 12 ++++++-- .../lib/core/utils/browser-storage.test.ts | 7 +++++ apps/sim/lib/core/utils/browser-storage.ts | 6 ++-- apps/sim/lib/mothership/events.ts | 30 ++++++++++++++----- 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 5357bc6839c..19adfe6caa1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -316,8 +316,13 @@ export function Home({ chatId, userName, userId }: HomeProps) { useEffect(() => { const handler = (e: Event) => { - const message = (e as CustomEvent).detail?.message - if (message) sendMessage(message) + const detail = (e as CustomEvent).detail + if (!detail?.message) return + // Claim the event so a cross-surface producer (e.g. "Troubleshoot in + // Chat", fired while this chat is already mounted) knows a live chat + // consumed it and skips its navigate-and-persist fallback. + e.preventDefault() + sendMessage(detail.message, undefined, detail.contexts) } window.addEventListener(MOTHERSHIP_SEND_MESSAGE_EVENT, handler) return () => window.removeEventListener(MOTHERSHIP_SEND_MESSAGE_EVENT, handler) @@ -325,10 +330,11 @@ export function Home({ chatId, userName, userId }: HomeProps) { /** * Consumes a one-shot handoff left by another surface (e.g. "Troubleshoot in - * Chat" on an errored log) and auto-sends it into this fresh chat, tagging the - * run so Sim can inspect the failure. `consume` clears the entry atomically, - * so it fires at most once — a re-run (StrictMode, reload, `sendMessage` - * identity change) reads nothing and no-ops. + * Chat" on an errored log viewed from a different route) and auto-sends it + * into this fresh chat, tagging the run so Sim can inspect the failure. Only + * the cross-route path lands here — when a chat is already mounted the event + * above delivers directly. `consume` clears the entry atomically, so it fires + * at most once even across a StrictMode remount or reload. */ useEffect(() => { const handoff = MothershipHandoffStorage.consume() diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 585674ce61c..46f6166aa2b 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -32,6 +32,7 @@ import { apportionCredits, dollarsToCredits } from '@/lib/billing/credits/conver import { MothershipHandoffStorage } from '@/lib/core/utils/browser-storage' import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans' import type { TraceSpan } from '@/lib/logs/types' +import { sendMothershipMessage } from '@/lib/mothership/events' import { ExecutionSnapshot, FileCards, @@ -407,8 +408,15 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP const message = workflowName ? `The "${workflowName}" workflow run failed. Investigate the error in this run and help me fix it.` : 'This workflow run failed. Investigate the error in this run and help me fix it.' - MothershipHandoffStorage.store({ message, contexts: [context] }) - router.push(`/workspace/${workspaceId}/home`) + // If a chat is already mounted (e.g. the run is being viewed inside Chat's + // resource panel) it consumes the message directly. Otherwise persist a + // one-shot handoff and navigate so the fresh home mount picks it up — only + // navigating when the handoff actually stored, so a failed write never + // strands the user on an empty chat. + if (sendMothershipMessage(message, [context])) return + if (MothershipHandoffStorage.store({ message, contexts: [context] })) { + router.push(`/workspace/${workspaceId}/home`) + } }, [log.executionId, log.workflow?.name, workspaceId, router]) return ( diff --git a/apps/sim/lib/core/utils/browser-storage.test.ts b/apps/sim/lib/core/utils/browser-storage.test.ts index 9eab2f5cc08..30b3fc1ffef 100644 --- a/apps/sim/lib/core/utils/browser-storage.test.ts +++ b/apps/sim/lib/core/utils/browser-storage.test.ts @@ -29,6 +29,13 @@ describe('MothershipHandoffStorage', () => { expect(MothershipHandoffStorage.consume()).toBeNull() }) + it('tombstones a corrupted entry (missing timestamp) instead of leaving it forever', () => { + localStorage.setItem(STORAGE_KEYS.MOTHERSHIP_HANDOFF, JSON.stringify({ message: 'fix it' })) + + expect(MothershipHandoffStorage.consume()).toBeNull() + expect(localStorage.getItem(STORAGE_KEYS.MOTHERSHIP_HANDOFF)).toBeNull() + }) + it('drops and clears a handoff older than maxAge', () => { vi.useFakeTimers() try { diff --git a/apps/sim/lib/core/utils/browser-storage.ts b/apps/sim/lib/core/utils/browser-storage.ts index 18c68405614..6cbdab8b47a 100644 --- a/apps/sim/lib/core/utils/browser-storage.ts +++ b/apps/sim/lib/core/utils/browser-storage.ts @@ -348,13 +348,15 @@ export class MothershipHandoffStorage { timestamp?: number } | null>(MothershipHandoffStorage.KEY, null) - if (!data?.message || !data.timestamp) { + if (!data) { return null } + // Clear unconditionally once any entry exists, so a malformed or expired + // handoff is tombstoned rather than lingering across future mounts. MothershipHandoffStorage.clear() - if (Date.now() - data.timestamp > maxAge) { + if (!data.message || !data.timestamp || Date.now() - data.timestamp > maxAge) { return null } diff --git a/apps/sim/lib/mothership/events.ts b/apps/sim/lib/mothership/events.ts index c1c9e40ff59..9143a73c5b5 100644 --- a/apps/sim/lib/mothership/events.ts +++ b/apps/sim/lib/mothership/events.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import type { ChatContext } from '@/stores/panel' const logger = createLogger('MothershipEvents') @@ -11,23 +12,36 @@ export const MOTHERSHIP_SEND_MESSAGE_EVENT = 'mothership-send-message' export interface MothershipSendMessageDetail { message: string + /** Structured contexts to attach — e.g. a `logs` mention tagging a run. */ + contexts?: ChatContext[] } /** - * Dispatches a message to the Mothership chat. Producers (terminal block - * errors, console copilot actions, toast actions) call this; consumers - * listen for {@link MOTHERSHIP_SEND_MESSAGE_EVENT} on `window`. + * Dispatches a message to a mounted Mothership chat. Producers (terminal block + * errors, console copilot actions, toast actions, the log "Troubleshoot in + * Chat" action) call this; consumers listen for + * {@link MOTHERSHIP_SEND_MESSAGE_EVENT} on `window` and `preventDefault` to + * claim it. + * + * @returns `true` when a mounted host consumed the message, `false` when none + * was listening — callers that can fall back (e.g. cross-route navigation) use + * this to decide whether to persist a handoff instead. */ -export function sendMothershipMessage(message: string): void { +export function sendMothershipMessage(message: string, contexts?: ChatContext[]): boolean { const trimmed = message.trim() if (!trimmed) { logger.warn('sendMothershipMessage called with empty message') - return + return false } - window.dispatchEvent( + const consumed = !window.dispatchEvent( new CustomEvent(MOTHERSHIP_SEND_MESSAGE_EVENT, { - detail: { message: trimmed }, + detail: { message: trimmed, contexts }, + cancelable: true, }) ) - logger.info('Dispatched mothership message event', { messageLength: trimmed.length }) + logger.info('Dispatched mothership message event', { + messageLength: trimmed.length, + consumed, + }) + return consumed } From a9b0812db0d4a4b81b62b539dc84f44827b833a6 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 16:35:25 -0700 Subject: [PATCH 3/8] docs(logs): convert inline comments to TSDoc on declarations --- apps/sim/app/workspace/[workspaceId]/home/home.tsx | 9 ++++++--- .../logs/components/log-details/log-details.tsx | 14 ++++++++------ apps/sim/lib/core/utils/browser-storage.ts | 8 ++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 19adfe6caa1..20d4a8a89c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -314,13 +314,16 @@ export function Home({ chatId, userName, userId }: HomeProps) { [workspaceId, chatId, sendMessage] ) + /** + * Handles cross-surface send requests (terminal/console "Fix in Chat", the + * log "Troubleshoot in Chat" action). `preventDefault` claims the event so a + * producer that dispatched it while this chat is mounted knows a live chat + * consumed the message and skips its navigate-and-persist fallback. + */ useEffect(() => { const handler = (e: Event) => { const detail = (e as CustomEvent).detail if (!detail?.message) return - // Claim the event so a cross-surface producer (e.g. "Troubleshoot in - // Chat", fired while this chat is already mounted) knows a live chat - // consumed it and skips its navigate-and-persist fallback. e.preventDefault() sendMessage(detail.message, undefined, detail.contexts) } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 46f6166aa2b..485247fff0c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -397,6 +397,13 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP */ const canTroubleshoot = log.status === 'failed' && isLikelyExecution + /** + * Hands the failed run to Chat. When a chat is already mounted (e.g. the run + * is being viewed inside Chat's resource panel) it consumes the tagged + * message directly; otherwise a one-shot handoff is persisted and we navigate + * to a fresh chat that picks it up on mount. Navigation is gated on a + * successful store, so a failed write never strands the user on an empty chat. + */ const handleTroubleshoot = useCallback(() => { if (!log.executionId) return const workflowName = log.workflow?.name?.trim() || null @@ -408,11 +415,6 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP const message = workflowName ? `The "${workflowName}" workflow run failed. Investigate the error in this run and help me fix it.` : 'This workflow run failed. Investigate the error in this run and help me fix it.' - // If a chat is already mounted (e.g. the run is being viewed inside Chat's - // resource panel) it consumes the message directly. Otherwise persist a - // one-shot handoff and navigate so the fresh home mount picks it up — only - // navigating when the handoff actually stored, so a failed write never - // strands the user on an empty chat. if (sendMothershipMessage(message, [context])) return if (MothershipHandoffStorage.store({ message, contexts: [context] })) { router.push(`/workspace/${workspaceId}/home`) @@ -578,7 +580,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP )} - {/* Troubleshoot — hand the failed run off to Chat to diagnose */} + {/* Troubleshoot */} {canTroubleshoot && ( + )} @@ -582,15 +582,15 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP {/* Troubleshoot */} {canTroubleshoot && ( - + )} {/* Files */} From 42c53d01df487c7adad48875b322646490452dc5 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 17:05:30 -0700 Subject: [PATCH 6/8] fix(chat): only consume troubleshoot handoff on the new-chat surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate the mount-time handoff consume on `!chatId` so an existing `/chat/[chatId]` mount can't claim a pending handoff if navigation races — a handoff always targets a fresh chat. --- apps/sim/app/workspace/[workspaceId]/home/home.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 20d4a8a89c4..d0f96f9c5f8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -336,13 +336,16 @@ export function Home({ chatId, userName, userId }: HomeProps) { * Chat" on an errored log viewed from a different route) and auto-sends it * into this fresh chat, tagging the run so Sim can inspect the failure. Only * the cross-route path lands here — when a chat is already mounted the event - * above delivers directly. `consume` clears the entry atomically, so it fires - * at most once even across a StrictMode remount or reload. + * above delivers directly. Gated to the new-chat surface (`!chatId`): a + * handoff always targets a fresh chat, so an existing `/chat/[chatId]` mount + * must never claim it if navigation races. `consume` clears the entry + * atomically, so it fires at most once even across a StrictMode remount. */ useEffect(() => { + if (chatId) return const handoff = MothershipHandoffStorage.consume() if (handoff) sendMessage(handoff.message, undefined, handoff.contexts) - }, [sendMessage]) + }, [chatId, sendMessage]) function resolveResourceFromContext( context: ChatContext From 398d954511bee9037cfc272c21b3040846bffd77 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 17:07:59 -0700 Subject: [PATCH 7/8] fix(logs): hover only the interactive Run ID row in log details The detail-card rows all hovered to --surface-2, but the card itself is --surface-2, so the hover was a no-op in light mode and only showed in dark. It also implied clickability on static readout rows. Now only the clickable Run ID row hovers, using the canonical --surface-active token; static rows carry no hover. --- .../components/log-details/log-details.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 898e6b95923..7d25218574d 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -474,7 +474,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP role='button' tabIndex={0} aria-label='Copy run ID' - className='flex h-10 min-w-0 cursor-pointer items-center justify-between gap-4 px-3 transition-colors hover-hover:bg-[var(--surface-2)]' + className='flex h-10 min-w-0 cursor-pointer items-center justify-between gap-4 px-3 transition-colors hover-hover:bg-[var(--surface-active)]' onClick={() => copyRunId(log.executionId!)} onKeyDown={(event) => handleKeyboardActivation(event, () => copyRunId(log.executionId!)) @@ -490,7 +490,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP )} {/* Level */} -
+
Level @@ -498,7 +498,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
{/* Trigger */} -
+
Trigger @@ -512,7 +512,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
{/* Duration */} -
+
Duration @@ -523,7 +523,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP {/* Version */} {log.deploymentVersion && ( -
+
Version @@ -537,7 +537,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP {/* Snapshot */} {showWorkflowState && ( -
+
Snapshot @@ -600,10 +600,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP {hasCostInfo && costBreakdown && (
{costBreakdown.rows.map((row) => ( -
+
{row.label} @@ -612,7 +609,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
))} -
+
Total @@ -621,7 +618,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
{(costBreakdown.tokens.input > 0 || costBreakdown.tokens.output > 0) && ( -
+
Tokens From 462f4cd2d516de74e099d493e463de8d31072cef Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 17:13:16 -0700 Subject: [PATCH 8/8] improvement(logs): use emcn Badge for the version pill Replaces the hand-rolled version span with the canonical Badge (variant='green' size='md', pixel-identical tokens), so all three detail badges (Level, Trigger, Version) render through the same component. --- .../logs/components/log-details/log-details.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 7d25218574d..3c302c41dee 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { + Badge, Button, Chip, ChipInput, @@ -528,9 +529,9 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP Version
- + {log.deploymentVersionName || `v${log.deploymentVersion}`} - +
)}