[aidan] feat/workflow-edit: add draft testing save flow

This commit is contained in:
abccodes
2026-06-17 00:33:27 -07:00
parent d3532f7a26
commit 5aeef89206
14 changed files with 825 additions and 218 deletions
+12
View File
@@ -68,6 +68,9 @@ export interface AgentSession {
id: string;
name: string;
status: 'draft' | 'running' | 'waiting_approval' | 'completed' | 'error' | 'stopped';
/** For workflow Test Agent sessions: drives the test card's footer
* (running -> red Force Stop; complete/error -> green close). */
workflow_test_state?: 'running' | 'complete' | 'error' | null;
provider: string;
model: string;
mode: string;
@@ -703,6 +706,14 @@ const agentsSlice = createSlice({
}
},
setSessionTestState(
state,
action: PayloadAction<{ sessionId: string; state: 'running' | 'complete' | 'error' }>
) {
const session = state.sessions[action.payload.sessionId];
if (session) session.workflow_test_state = action.payload.state;
},
setSessionConnState(
state,
action: PayloadAction<{ sessionId: string; state: 'live' | 'reconnecting' }>
@@ -1408,6 +1419,7 @@ export const {
updateSession,
updateSessionStatus,
setSessionConnState,
setSessionTestState,
addMessage,
addOptimisticMessage,
markOptimisticFailed,
@@ -81,6 +81,12 @@ export interface Workflow {
edit_agent_session_id?: string | null;
/** Sticky session id for the embedded scheduling agent (cadence -> gated tool call). */
schedule_agent_session_id?: string | null;
/** Pending Edit-Agent draft of the steps; present only while editing. */
draft_steps?: WorkflowStep[] | null;
/** True when a draft is staged (server-computed convenience flag). */
has_draft?: boolean;
/** Most recent Test Agent session for this workflow. */
last_test_session_id?: string | null;
/** Tool permissions the user answered once and we reuse on later runs so an
* unattended scheduled fire doesn't stall on a prompt. tool name -> answer. */
remembered_approvals?: Record<string, 'allow' | 'deny'>;
@@ -133,6 +139,9 @@ export interface OpenCard {
sidecarKind?: 'watching' | 'viewing-completed' | 'viewing-error' | 'testing' | null;
/** Per-step expand state for ExpandedView. Stores step ids. */
expandedStepIds?: string[];
/** One-shot "Schedule this workflow?" prompt shown right after a convert.
* Transient: lives only on the just-created card, never on hub-opened ones. */
showScheduleNudge?: boolean;
/** Pre-seed message for the Fix-with-Agent flow so the EditAgent composer
* knows which failure context to lead with. Cleared once consumed. */
fixSeed?: { runId: string; stepIdx: number; stepLabel: string; error: string } | null;
@@ -210,6 +219,18 @@ export const updateWorkflow = createAsyncThunk<
},
);
export const commitDraft = createAsyncThunk('workflows/commitDraft', async (id: string) => {
const res = await fetch(`${API}/${id}/draft/commit`, { method: 'POST' });
if (!res.ok) throw new Error(`commit failed ${res.status}`);
return (await res.json()) as Workflow;
});
export const discardDraft = createAsyncThunk('workflows/discardDraft', async (id: string) => {
const res = await fetch(`${API}/${id}/draft/discard`, { method: 'POST' });
if (!res.ok) throw new Error(`discard failed ${res.status}`);
return (await res.json()) as Workflow;
});
export const deleteWorkflow = createAsyncThunk('workflows/delete', async (id: string) => {
await fetch(`${API}/${id}`, { method: 'DELETE' });
return id;
@@ -364,6 +385,8 @@ const slice = createSlice({
.addCase(fetchWorkflows.rejected, (state) => { state.loading = false; state.loaded = true; })
.addCase(createWorkflow.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(updateWorkflow.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(commitDraft.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(discardDraft.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(deleteWorkflow.fulfilled, (state, action) => {
delete state.items[action.payload];
delete state.runs[action.payload];
@@ -8,6 +8,7 @@ import {
addApprovalRequest,
removeApprovalRequest,
updateSessionStatus,
setSessionTestState,
updateSessionCost,
updateSessionContext,
setContextOverflow,
@@ -444,6 +445,13 @@ class WebSocketManager {
}
switch (event) {
case 'agent:test_state':
// broadcast_global puts everything under data (no top-level session_id).
if (data.session_id && data.state) {
store.dispatch(setSessionTestState({ sessionId: data.session_id, state: data.state }));
}
break;
case 'agent:status':
// Capture pre-transition status so we only fire a system notification
// on a real running→terminal transition. Otherwise a session that