diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index bcda9aeb776..d6f137a01a3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -53,6 +53,7 @@ import { getConditionRows, getRouterRows } from '@/lib/workflows/dynamic-handle- import { getDependsOnFields } from '@/lib/workflows/subblocks/dependencies' import { getDisplayValue, + getTooltipDisplayValue, hasDisplayableRowValue, resolveDropdownLabel, resolveFilterFieldLabel, @@ -595,11 +596,16 @@ const SubBlockRow = memo(function SubBlockRow({ webhookUrlDisplayValue || selectorDisplayName const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value) + const tooltipValue = + subBlock?.type === 'messages-input' && !maskedValue && !hydratedName + ? getTooltipDisplayValue(rawValue) + : displayValue return ( workflowMap, workflowLabelsReady ) + const tooltipValue = + subBlock.type === 'messages-input' + ? resolvePreviewDisplayValue( + getTooltipDisplayValue(rawValue), + subBlock, + rawValue, + workflowMap, + workflowLabelsReady + ) + : displayValue /* The preview has no hooks, so a selector it cannot hydrate comes back as the `-` sentinel. That reads as noise mid-sentence, so hand the slot back and let its noun stand in instead. */ @@ -564,6 +575,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps ) diff --git a/apps/sim/lib/workflows/subblocks/display.test.ts b/apps/sim/lib/workflows/subblocks/display.test.ts index 6933eeac44a..4b1c3871280 100644 --- a/apps/sim/lib/workflows/subblocks/display.test.ts +++ b/apps/sim/lib/workflows/subblocks/display.test.ts @@ -13,6 +13,7 @@ vi.mock('@/blocks', () => ({ import { getDisplayValue, + getTooltipDisplayValue, resolveDropdownLabel, resolveFilterFieldLabel, resolveSandboxLabel, @@ -287,4 +288,22 @@ describe('getDisplayValue', () => { ).toBe('one, two +1') expect(getDisplayValue(['a', 'b'])).toBe('a, b') }) + + it('keeps message previews compact while tooltips retain the full first message', () => { + const content = `You are a research assistant. ${'Keep every instruction. '.repeat(4)}`.trim() + const messages = [{ role: 'system', content }] + const serializedMessages = JSON.stringify(messages) + + expect(getDisplayValue(messages)).toBe(`${content.slice(0, 50)}...`) + expect(getTooltipDisplayValue(messages)).toBe(content) + expect(getDisplayValue(serializedMessages)).toBe(`${content.slice(0, 50)}...`) + expect(getTooltipDisplayValue(serializedMessages)).toBe(content) + }) + + it('keeps long plain strings complete for both display and tooltip use', () => { + const code = `const result = ${'computeValue() + '.repeat(6)}0; return result;` + + expect(getDisplayValue(code)).toBe(code) + expect(getTooltipDisplayValue(code)).toBe(code) + }) }) diff --git a/apps/sim/lib/workflows/subblocks/display.ts b/apps/sim/lib/workflows/subblocks/display.ts index 37f3f1f9785..532ae4b4ab0 100644 --- a/apps/sim/lib/workflows/subblocks/display.ts +++ b/apps/sim/lib/workflows/subblocks/display.ts @@ -311,6 +311,26 @@ export const getDisplayValue = (value: unknown): string => { return stringValue.trim().length > 0 ? stringValue : '-' } +/** + * Formats the full value shown by a collapsed-row tooltip. + * + * Message arrays keep a compact first-message preview in {@link getDisplayValue}, + * but their tooltip needs the complete first-message content. Other values keep + * the same resolved display text so selector labels and structured summaries do + * not change semantics. + */ +export const getTooltipDisplayValue = (value: unknown): string => { + const parsedValue = tryParseJson(value) + + if (isMessagesArray(parsedValue)) { + const firstMessage = parsedValue[0] + if (!firstMessage?.content || firstMessage.content.trim() === '') return '-' + return firstMessage.content.trim() + } + + return getDisplayValue(value) +} + /** * Whether a collapsed-node row has a meaningful value to display. * Rows whose value renders as the empty placeholder are hidden from the diff --git a/packages/workflow-renderer/src/lib/overflow-span.tsx b/packages/workflow-renderer/src/lib/overflow-span.tsx index b86caeff67d..bfad7d4cbf4 100644 --- a/packages/workflow-renderer/src/lib/overflow-span.tsx +++ b/packages/workflow-renderer/src/lib/overflow-span.tsx @@ -3,6 +3,8 @@ import { FloatingTooltip, isTextClipped, useFloatingTooltip } from '@sim/emcn' interface OverflowSpanProps { value: string + /** Full tooltip label when the visible value was shortened before rendering. */ + tooltipValue?: string className: string /** * Decorated rendering of `value` — the same characters, wrapped. Used to mark @@ -19,15 +21,18 @@ interface OverflowSpanProps { * attribute here: on the canvas it pops the browser's raw, unstyled tooltip * with the full untruncated value (including raw code/JSON) over the graph. */ -export function OverflowSpan({ value, className, children }: OverflowSpanProps) { - const { state, handlers } = useFloatingTooltip(isTextClipped) +export function OverflowSpan({ value, tooltipValue, className, children }: OverflowSpanProps) { + const resolvedTooltipValue = tooltipValue ?? value + const { state, handlers } = useFloatingTooltip( + (target) => resolvedTooltipValue !== value || isTextClipped(target) + ) return ( <> {children ?? value} - + ) } diff --git a/packages/workflow-renderer/src/workflow-block/sub-block-row-view.test.tsx b/packages/workflow-renderer/src/workflow-block/sub-block-row-view.test.tsx new file mode 100644 index 00000000000..935023f5351 --- /dev/null +++ b/packages/workflow-renderer/src/workflow-block/sub-block-row-view.test.tsx @@ -0,0 +1,60 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it } from 'vitest' +import { SubBlockRowView } from './sub-block-row-view' + +let host: HTMLDivElement | null = null +let root: Root | null = null + +function mount(element: React.ReactElement): HTMLDivElement { + host = document.createElement('div') + document.body.appendChild(host) + root = createRoot(host) + act(() => root?.render(element)) + return host +} + +function hover(element: HTMLElement): void { + act(() => { + element.dispatchEvent( + new MouseEvent('pointerover', { bubbles: true, clientX: 100, clientY: 100 }) + ) + }) +} + +afterEach(() => { + act(() => root?.unmount()) + host?.remove() + document.body.querySelectorAll('[data-native-surface-overlay]').forEach((node) => node.remove()) + host = null + root = null +}) + +describe('SubBlockRowView tooltip values', () => { + it('shows a full tooltip for an upstream-truncated inline value', () => { + const compactValue = 'You are a research assistant. Keep every instruction...' + const fullValue = + 'You are a research assistant. Keep every instruction, constraint, and output requirement.' + const container = mount( + + ) + + expect(container.textContent).toBe(compactValue) + + const trigger = container.querySelector('.truncate') + if (!trigger) throw new Error('inline tooltip trigger not found') + hover(trigger) + + expect(document.body.querySelector('[data-native-surface-overlay]')?.textContent).toBe( + fullValue + ) + }) +}) diff --git a/packages/workflow-renderer/src/workflow-block/sub-block-row-view.tsx b/packages/workflow-renderer/src/workflow-block/sub-block-row-view.tsx index c74d21bac3c..e005454aa55 100644 --- a/packages/workflow-renderer/src/workflow-block/sub-block-row-view.tsx +++ b/packages/workflow-renderer/src/workflow-block/sub-block-row-view.tsx @@ -14,6 +14,8 @@ export interface SubBlockRowViewProps { title: string /** Resolved display value on the right; `undefined` hides the value span. */ displayValue?: string + /** Full value for tooltip-only disclosure when `displayValue` is compact. */ + tooltipValue?: string /** Render the value in a monospace font (e.g. filter expressions). */ isMonospace?: boolean /** @@ -45,6 +47,7 @@ export interface SubBlockRowViewProps { export function SubBlockRowView({ title, displayValue, + tooltipValue, isMonospace, icon: Icon, variant = 'row', @@ -54,6 +57,7 @@ export function SubBlockRowView({ @@ -64,6 +68,7 @@ export function SubBlockRowView({ return (