feat(flows): improve workflow builder UX and diagnostics (#4574)

This commit is contained in:
Steven Enamakel
2026-07-05 17:09:08 -07:00
committed by GitHub
parent d8644f105d
commit cadeee5d89
75 changed files with 5958 additions and 689 deletions
+6 -1
View File
@@ -15,12 +15,16 @@ const dispatch = vi.hoisted(() => vi.fn());
const selectorState = vi.hoisted(() => ({
activeThreadIds: {} as Record<string, true>,
proposals: {} as Record<string, WorkflowProposal>,
messagesByThreadId: {} as Record<string, unknown[]>,
}));
vi.mock('../store/hooks', () => ({
useAppDispatch: () => dispatch,
useAppSelector: (sel: (s: unknown) => unknown) =>
sel({
thread: { activeThreadIds: selectorState.activeThreadIds },
thread: {
activeThreadIds: selectorState.activeThreadIds,
messagesByThreadId: selectorState.messagesByThreadId,
},
chatRuntime: { pendingWorkflowProposalsByThread: selectorState.proposals },
}),
}));
@@ -45,6 +49,7 @@ describe('useWorkflowBuilderChat', () => {
chatSend.mockReset().mockResolvedValue(undefined);
selectorState.activeThreadIds = {};
selectorState.proposals = {};
selectorState.messagesByThreadId = {};
dispatch.mockReset().mockImplementation((action: { type: string }) => {
if (action.type === 'createNewThread') {
return { unwrap: () => Promise.resolve({ id: 'builder-1' }) };
+16 -1
View File
@@ -54,6 +54,13 @@ export interface UseWorkflowBuilderChat {
sending: boolean;
/** The latest proposal the agent returned on this thread, or `null`. */
proposal: WorkflowProposal | null;
/**
* The dedicated thread's transcript (user + agent turns) so a caller can
* render the full conversation, not just the latest proposal. Empty until the
* first send. Sourced from the same `messagesByThreadId` store the main chat
* transcript reads.
*/
messages: ThreadMessage[];
/** Last send error (thread create / RPC failure), or `null`. */
error: string | null;
/** Send a builder turn, creating the dedicated thread on first use. */
@@ -62,6 +69,8 @@ export interface UseWorkflowBuilderChat {
clearProposal: () => void;
}
const EMPTY_MESSAGES: ThreadMessage[] = [];
/**
* @param seedThreadId Optional existing thread to bind to instead of creating a
* fresh one — lets a caller reuse a thread across mounts (unused today; the
@@ -78,12 +87,18 @@ export function useWorkflowBuilderChat(seedThreadId?: string | null): UseWorkflo
const proposalsByThread = useAppSelector(
state => state.chatRuntime.pendingWorkflowProposalsByThread
);
const messagesByThreadId = useAppSelector(state => state.thread.messagesByThreadId);
const proposal = useMemo(
() => (threadId ? (proposalsByThread[threadId] ?? null) : null),
[threadId, proposalsByThread]
);
const messages = useMemo(
() => (threadId ? (messagesByThreadId[threadId] ?? EMPTY_MESSAGES) : EMPTY_MESSAGES),
[threadId, messagesByThreadId]
);
// "Sending" = we're mid-dispatch OR the runtime still marks the thread active.
const runtimeActive = threadId ? Boolean(activeThreadIds[threadId]) : false;
const sending = localSending || runtimeActive;
@@ -157,5 +172,5 @@ export function useWorkflowBuilderChat(seedThreadId?: string | null): UseWorkflo
if (threadId) dispatch(clearWorkflowProposalForThread({ threadId }));
}, [dispatch, threadId]);
return { threadId, sending, proposal, error, send, clearProposal };
return { threadId, sending, proposal, messages, error, send, clearProposal };
}