Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<SubBlockRowView
title={title}
displayValue={displayValue}
tooltipValue={tooltipValue}
isMonospace={isMonospaceField}
variant={variant}
icon={icon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { resolveSelectedTriggerId } from '@/lib/workflows/blocks/canvas-trigger-sentence'
import {
getDisplayValue,
getTooltipDisplayValue,
hasDisplayableRowValue,
resolveDropdownLabel,
resolveSkillsLabel,
Expand Down Expand Up @@ -556,6 +557,16 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
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. */
Expand All @@ -564,6 +575,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
<SubBlockRowView
title={subBlock.title ?? subBlock.id}
displayValue={displayValue}
tooltipValue={tooltipValue}
variant='inline-value'
/>
)
Expand Down
19 changes: 19 additions & 0 deletions apps/sim/lib/workflows/subblocks/display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ vi.mock('@/blocks', () => ({

import {
getDisplayValue,
getTooltipDisplayValue,
resolveDropdownLabel,
resolveFilterFieldLabel,
resolveSandboxLabel,
Expand Down Expand Up @@ -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)
})
})
20 changes: 20 additions & 0 deletions apps/sim/lib/workflows/subblocks/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions packages/workflow-renderer/src/lib/overflow-span.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
<>
<span className={className} {...handlers}>
{children ?? value}
</span>
<FloatingTooltip label={value} state={state} />
<FloatingTooltip label={resolvedTooltipValue} state={state} />
</>
)
}
Original file line number Diff line number Diff line change
@@ -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(
<SubBlockRowView
title='Messages'
displayValue={compactValue}
tooltipValue={fullValue}
variant='inline-value'
/>
)

expect(container.textContent).toBe(compactValue)

const trigger = container.querySelector<HTMLElement>('.truncate')
if (!trigger) throw new Error('inline tooltip trigger not found')
hover(trigger)

expect(document.body.querySelector('[data-native-surface-overlay]')?.textContent).toBe(
fullValue
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand Down Expand Up @@ -45,6 +47,7 @@ export interface SubBlockRowViewProps {
export function SubBlockRowView({
title,
displayValue,
tooltipValue,
isMonospace,
icon: Icon,
variant = 'row',
Expand All @@ -54,6 +57,7 @@ export function SubBlockRowView({
<InlineChip>
<OverflowSpan
value={displayValue ?? title}
tooltipValue={tooltipValue}
className={cn('min-w-0 truncate', isMonospace && 'font-mono')}
/>
</InlineChip>
Expand All @@ -64,6 +68,7 @@ export function SubBlockRowView({
return (
<OverflowSpan
value={displayValue ?? title}
tooltipValue={tooltipValue}
className={cn(
'min-w-0 truncate text-sm',
variant === 'statement-primary'
Expand All @@ -80,6 +85,7 @@ export function SubBlockRowView({
<Icon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<OverflowSpan
value={displayValue ?? '-'}
tooltipValue={tooltipValue}
className={cn(
'min-w-0 flex-1 truncate text-left text-[var(--text-primary)] text-sm',
isMonospace && 'font-mono'
Expand Down
Loading