Skip to content

Commit 44bbc92

Browse files
fix(streaming): reset stale output drilldowns
1 parent 199dc56 commit 44bbc92

3 files changed

Lines changed: 105 additions & 29 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.test.tsx

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
*/
44
import { act, type ReactNode } from 'react'
55
import { createRoot, type Root } from 'react-dom/client'
6-
import { afterEach, describe, expect, it, vi } from 'vitest'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const { outputMenuState } = vi.hoisted(() => ({
9+
outputMenuState: { includeNestedWorkflow: true },
10+
}))
711

812
vi.mock('@sim/emcn', () => ({
913
cn: (...values: unknown[]) => values.flat().filter(Boolean).join(' '),
@@ -134,31 +138,37 @@ vi.mock('@/lib/workflows/streaming/nested-output-options', () => {
134138

135139
return {
136140
collectReferencedWorkflowIds: () => [],
137-
buildWorkflowOutputOptions: () => [rootOutput, nestedOutput],
138-
buildWorkflowOutputMenu: () => [
139-
{
141+
buildWorkflowOutputOptions: () =>
142+
outputMenuState.includeNestedWorkflow ? [rootOutput, nestedOutput] : [rootOutput],
143+
buildWorkflowOutputMenu: () => {
144+
const rootNode = {
140145
blockId: 'summary',
141146
blockName: 'Summarizer',
142147
blockType: 'agent',
143148
outputs: [rootOutput],
144149
children: [],
145-
},
146-
{
147-
blockId: 'workflow',
148-
blockName: 'Research',
149-
blockType: 'workflow_input',
150-
outputs: [],
151-
children: [
152-
{
153-
blockId: 'workflow/agent',
154-
blockName: 'Writer',
155-
blockType: 'agent',
156-
outputs: [nestedOutput],
157-
children: [],
158-
},
159-
],
160-
},
161-
],
150+
}
151+
return outputMenuState.includeNestedWorkflow
152+
? [
153+
rootNode,
154+
{
155+
blockId: 'workflow',
156+
blockName: 'Research',
157+
blockType: 'workflow_input',
158+
outputs: [],
159+
children: [
160+
{
161+
blockId: 'workflow/agent',
162+
blockName: 'Writer',
163+
blockType: 'agent',
164+
outputs: [nestedOutput],
165+
children: [],
166+
},
167+
],
168+
},
169+
]
170+
: [rootNode]
171+
},
162172
}
163173
})
164174

@@ -167,6 +177,10 @@ import { OutputSelect } from '@/app/workspace/[workspaceId]/w/[workflowId]/compo
167177
let root: Root | null = null
168178
let container: HTMLDivElement | null = null
169179

180+
beforeEach(() => {
181+
outputMenuState.includeNestedWorkflow = true
182+
})
183+
170184
function outputSelect(
171185
workflowId: string,
172186
selectedOutputs: string[],
@@ -251,4 +265,15 @@ describe('OutputSelect nested workflow menu', () => {
251265
expect(document.body.textContent).toContain('Summarizer')
252266
expect(document.body.textContent).not.toContain('Back')
253267
})
268+
269+
it('returns to the root menu when a workflow edit invalidates the active path', () => {
270+
const onOutputSelect = renderOutputSelect([])
271+
clickOption('Outputs')
272+
273+
outputMenuState.includeNestedWorkflow = false
274+
rerenderOutputSelect('root', [], onOutputSelect)
275+
276+
expect(document.body.textContent).toContain('Summarizer')
277+
expect(document.body.textContent).not.toContain('Back')
278+
})
254279
})

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ interface OutputSelectProps {
5757
className?: string
5858
}
5959

60+
interface OutputSelectMenuProps {
61+
outputMenu: readonly WorkflowOutputMenuNode[]
62+
workflowOutputs: readonly WorkflowOutputOption[]
63+
selectedOutputs: string[]
64+
onOutputSelect: (outputIds: string[]) => void
65+
disabled: boolean
66+
placeholder: string
67+
valueMode: 'id' | 'label'
68+
align: 'start' | 'end' | 'center'
69+
maxHeight: number
70+
size: 'sm' | 'md'
71+
className?: string
72+
}
73+
6074
function getOutputValue(output: WorkflowOutputOption, valueMode: 'id' | 'label'): string {
6175
return valueMode === 'label' && !output.blockId.includes('/') ? output.label : output.id
6276
}
@@ -88,7 +102,7 @@ function resolveOutputMenuNode(
88102
* @returns The OutputSelect component
89103
*/
90104
export function OutputSelect(props: OutputSelectProps) {
91-
return <OutputSelectContent key={props.workflowId ?? 'no-workflow'} {...props} />
105+
return <OutputSelectContent {...props} />
92106
}
93107

94108
function OutputSelectContent({
@@ -103,7 +117,6 @@ function OutputSelectContent({
103117
size = 'sm',
104118
className,
105119
}: OutputSelectProps) {
106-
const [menuPath, setMenuPath] = useState<string[]>([])
107120
const blocks = useWorkflowStore((state) => state.blocks)
108121
const edges = useWorkflowStore((state) => state.edges)
109122
const { isShowingDiff, isDiffReady, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore(
@@ -184,6 +197,43 @@ function OutputSelectContent({
184197
})
185198
: []
186199
const outputMenu = buildWorkflowOutputMenu(workflowOutputs)
200+
const outputMenuRevision = JSON.stringify([
201+
workflowId,
202+
...workflowOutputs.map((output) => output.id),
203+
])
204+
205+
return (
206+
<OutputSelectMenu
207+
key={outputMenuRevision}
208+
outputMenu={outputMenu}
209+
workflowOutputs={workflowOutputs}
210+
selectedOutputs={selectedOutputs}
211+
onOutputSelect={onOutputSelect}
212+
disabled={disabled}
213+
placeholder={placeholder}
214+
valueMode={valueMode}
215+
align={align}
216+
maxHeight={maxHeight}
217+
size={size}
218+
className={className}
219+
/>
220+
)
221+
}
222+
223+
function OutputSelectMenu({
224+
outputMenu,
225+
workflowOutputs,
226+
selectedOutputs,
227+
onOutputSelect,
228+
disabled,
229+
placeholder,
230+
valueMode,
231+
align,
232+
maxHeight,
233+
size,
234+
className,
235+
}: OutputSelectMenuProps) {
236+
const [menuPath, setMenuPath] = useState<string[]>([])
187237
const activeMenuNode = resolveOutputMenuNode(outputMenu, menuPath)
188238

189239
const validOutputCount = selectedOutputs.filter((val) =>

apps/sim/lib/workflows/streaming/streaming.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
agentStreamProtocolResponseHeaders,
1010
createStreamingResponse,
1111
} from '@/lib/workflows/streaming/streaming'
12+
import type { AgentStreamSink } from '@/providers/stream-events'
1213

1314
const workflowStreamingLoggerCallIndex = loggerMock.createLogger.mock.calls.findIndex(
1415
([name]) => name === 'WorkflowStreaming'
@@ -1207,7 +1208,7 @@ describe('createStreamingResponse agent-events-v1', () => {
12071208
},
12081209
executeFn: async ({ onStream }) => {
12091210
let textController!: ReadableStreamDefaultController<Uint8Array>
1210-
let sink: { onEvent: (event: unknown) => void | Promise<void> } | undefined
1211+
let sink: AgentStreamSink | undefined
12111212
const textStream = new ReadableStream<Uint8Array>({
12121213
start(controller) {
12131214
textController = controller
@@ -1218,7 +1219,7 @@ describe('createStreamingResponse agent-events-v1', () => {
12181219
blockId: 'agent-1',
12191220
stream: textStream,
12201221
streamFormat: 'text',
1221-
subscribe: (nextSink: { onEvent: (event: unknown) => void | Promise<void> }) => {
1222+
subscribe: (nextSink: AgentStreamSink) => {
12221223
sink = nextSink
12231224
return () => {}
12241225
},
@@ -1297,7 +1298,7 @@ describe('createStreamingResponse agent-events-v1', () => {
12971298
},
12981299
executeFn: async ({ onStream }) => {
12991300
let textController!: ReadableStreamDefaultController<Uint8Array>
1300-
let sink: { onEvent: (event: unknown) => void | Promise<void> } | undefined
1301+
let sink: AgentStreamSink | undefined
13011302
const textStream = new ReadableStream<Uint8Array>({
13021303
start(controller) {
13031304
textController = controller
@@ -1308,7 +1309,7 @@ describe('createStreamingResponse agent-events-v1', () => {
13081309
blockId: 'agent-1',
13091310
stream: textStream,
13101311
streamFormat: 'text',
1311-
subscribe: (nextSink: { onEvent: (event: unknown) => void | Promise<void> }) => {
1312+
subscribe: (nextSink: AgentStreamSink) => {
13121313
sink = nextSink
13131314
return () => {}
13141315
},
@@ -1543,7 +1544,7 @@ describe('createStreamingResponse agent-events-v1', () => {
15431544
},
15441545
executeFn: async ({ onStream }) => {
15451546
let textController!: ReadableStreamDefaultController<Uint8Array>
1546-
let sink: { onEvent: (event: unknown) => void | Promise<void> } | undefined
1547+
let sink: AgentStreamSink | undefined
15471548
const textStream = new ReadableStream<Uint8Array>({
15481549
start(controller) {
15491550
textController = controller
@@ -1554,7 +1555,7 @@ describe('createStreamingResponse agent-events-v1', () => {
15541555
blockId: 'agent-1',
15551556
stream: textStream,
15561557
streamFormat: 'text',
1557-
subscribe: (nextSink: any) => {
1558+
subscribe: (nextSink: AgentStreamSink) => {
15581559
sink = nextSink
15591560
return () => {
15601561
sink = undefined

0 commit comments

Comments
 (0)