Skip to content

feat(web): forward SDK lifecycle events to Web UI (tasks + hooks) (#975)#1939

Open
Wirasm wants to merge 1 commit into
devfrom
feat/web-sdk-lifecycle-events
Open

feat(web): forward SDK lifecycle events to Web UI (tasks + hooks) (#975)#1939
Wirasm wants to merge 1 commit into
devfrom
feat/web-sdk-lifecycle-events

Conversation

@Wirasm

@Wirasm Wirasm commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #975

Summary

Implements all 4 phases of #975: forwards Claude SDK subagent task lifecycle
and hook callback events to the Web UI through the existing SSE pipeline.
Until now the SDK fired task_started / task_progress / task_notification
and hook_started / hook_response system events and they hit the
claude.system_message_unhandled debug log. After this PR, DAG nodes
spawning subagents or invoking hooks show live activity under the parent
node in the run detail view and as at-a-glance counts on the graph card.

Phase 1 — provider (packages/providers)

  • 5 new MessageChunk variants: task_started, task_progress,
    task_notification, hook_started, hook_response.
  • streamClaudeMessages in claude/provider.ts switches on the SDK
    system subtype and yields the new chunks. Housekeeping tasks flagged
    with skip_transcript are suppressed at the provider boundary so
    they never reach the UI.
  • 13 new provider tests in provider.test.ts (chunk shape, ordering,
    housekeeping suppression, agentProgressSummaries opt-in).

Phase 2 — workflow engine (packages/workflows + server)

  • 2 new WorkflowEmitterEvent variants: task_activity (carries
    activity: started|progress|completed|failed|stopped, plus
    summary, usage, lastToolName, taskType) and
    hook_activity (carries hookEvent, hookName, outcome,
    exitCode).
  • dag-executor.ts aggregates the 5 provider chunks into the 2
    emitter events; both are persisted to workflow_events for timeline
    replay. WORKFLOW_EVENT_TYPES extended with task_activity and
    hook_activity.
  • server/src/adapters/web/workflow-bridge.ts maps them to SSE
    workflow_task_activity and workflow_hook_activity.
  • Slack bridge's exhaustive-check no-op list updated.
  • New workflow-bridge.test.ts covers the mapper.

Phase 3 — Web UI (packages/web)

  • DagTaskInfo / DagHookInfo arrays under each DagNodeState.
  • useSSE and useDashboardSSE route the new event types to two
    new store handlers (handleTaskActivity, handleHookActivity).
    The store mutates the same row across the started → progress →
    completed/failed lifecycle so the user sees one entry that updates
    in place; the same row preserves accumulated fields
    (description from started survives a progress event that
    omits it).
  • DagNodeProgress renders tasks as a collapsible Subagent tasks
    sub-list and hooks as inline indicators (e.g.
    PreToolUse(Bash) → approved) under the parent node.
  • ExecutionDagNode + WorkflowDagViewer surface active/total
    task and hook counts on the graph card so activity is visible
    without opening the run detail panel.
  • 12 new store reducer tests in workflow-store.test.ts.

Phase 4 — agentProgressSummaries (packages/providers)

  • agentProgressSummaries: true is set for workflow nodes by
    default. Direct chat calls (no nodeConfig) keep the SDK default
    (false) so the chat surface is unchanged. Authors can opt out
    per-node with agentProgressSummaries: false in nodeConfig.
  • New NodeConfig.agentProgressSummaries field documents the
    override.

Test plan

bun run validate (type-check + lint + format:check + bundled checks

  • all 1300+ tests) passes locally.

Summary by CodeRabbit

Release Notes

  • New Features

    • Workflows now display subagent task activity and hook callbacks on DAG nodes, including task status, descriptions, summaries, and tool usage information.
    • Added configurable AI-generated progress summaries for workflow nodes.
    • Enhanced DAG visualization with active/total task counts and hook indicators.
  • Bug Fixes

    • Fixed improper event handling for task and hook activities that caused unintended side-effects.
  • Tests

    • Added comprehensive test coverage for task and hook activity event handling, including out-of-order delivery scenarios.

Implements all 4 phases of issue #975: forward Claude SDK subagent task
lifecycle and hook callback events to the Web UI through the existing
SSE pipeline.

Phase 1 — packages/providers/src/{types.ts,claude/provider.ts}
  5 new MessageChunk variants (task_started / task_progress /
  task_notification / hook_started / hook_response) carrying the same
  payload the SDK emits. Housekeeping tasks flagged with
  skip_transcript are suppressed at the provider boundary so they never
  reach the UI.

Phase 2 — packages/workflows/src/{event-emitter.ts,dag-executor.ts}
  + packages/server/src/adapters/web/workflow-bridge.ts
  Two new WorkflowEmitterEvent variants (task_activity / hook_activity)
  aggregate the 5 provider chunks into a node-scoped stream; the SSE
  bridge maps them to workflow_task_activity and
  workflow_hook_activity. Both are persisted to workflow_events for
  timeline replay. Slack bridge's exhaustive-check no-op list updated
  to keep the never-narrow intact.

Phase 3 — packages/web/{lib/types.ts,hooks/{useSSE,useDashboardSSE}.ts,
stores/workflow-store.ts,components/workflows/{DagNodeProgress,
ExecutionDagNode,WorkflowDagViewer}.tsx}
  New DagTaskInfo / DagHookInfo arrays under each DagNodeState. SSE
  handlers added; store mutates the same row across the started →
  progress → completed/failed lifecycle so the user sees one entry that
  updates in place. DagNodeProgress renders tasks as a collapsible
  Subagent tasks sub-list and hooks as inline indicators
  (e.g. 'PreToolUse(Bash) → approved'). ExecutionDagNode surfaces
  active/total task and hook counts on the graph card so activity is
  visible without opening the run detail panel.

Phase 4 — packages/providers/src/{types.ts,claude/provider.ts}
  agentProgressSummaries defaults to true for workflow nodes
  (the agentProgressSummaries field is forwarded to Claude SDK
  options). Direct chat calls keep the SDK default (false) so the chat
  surface is unchanged. Authors can opt out per-node with
  agentProgressSummaries: false in nodeConfig.

Tests: 13 new provider tests (chunks + agentProgressSummaries), 6
new bridge mapper tests, 12 new store reducer tests, and new event
variants covered in event-emitter. full `bun run validate` passes
(type-check + lint + format:check + bundled checks + all 1300+ tests).
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request implements end-to-end forwarding of Claude SDK subagent task and hook lifecycle events to the Web UI. The feature spans nine files across provider, workflow, server, and frontend layers, adding task progress visibility and hook outcome tracking during DAG node execution.

Changes

Task and Hook Activity Lifecycle Support

Layer / File(s) Summary
Type contracts across system
packages/providers/src/types.ts, packages/workflows/src/event-emitter.ts, packages/workflows/src/store.ts, packages/web/src/lib/types.ts
Introduces new discriminated-union MessageChunk variants for task/hook lifecycle (task_started, task_progress, task_notification, hook_started, hook_response). Defines WorkflowEmitterEvent union members TaskActivityEvent and HookActivityEvent. Adds WORKFLOW_EVENT_TYPES entries. Defines frontend SSE event interfaces WorkflowTaskActivityEvent and WorkflowHookActivityEvent, and UI state types DagTaskInfo/DagHookInfo within DagNodeState.
Claude provider task and hook handling
packages/providers/src/claude/provider.ts, packages/providers/src/claude/provider.test.ts
streamClaudeMessages recognizes Claude SDK system events (task_started, task_progress, task_notification, hook_started, hook_response) and yields typed MessageChunks with normalized field mappings. applyNodeConfig enables agentProgressSummaries: true by default for workflow nodes (with per-node override support). Tests validate field translation, transcript suppression, and default behavior.
Workflow executor event emission
packages/workflows/src/dag-executor.ts, packages/workflows/src/event-emitter.ts, packages/workflows/src/event-emitter.test.ts
DAG executor stream handler recognizes task/hook MessageChunk variants and emits corresponding TaskActivityEvent and HookActivityEvent through the workflow event emitter, persisting to the store as task_activity and hook_activity workflow events. Tests verify emitter accepts all new event shapes.
Server-side SSE bridge
packages/server/src/adapters/web/workflow-bridge.ts, packages/server/src/adapters/web/workflow-bridge.test.ts, packages/server/package.json
mapWorkflowEvent maps task_activity and hook_activity workflow events to new SSE payload types (workflow_task_activity, workflow_hook_activity) with event-specific fields and timestamps. Test coverage validates payload structure for task started/progress/completed and hook started/response, including conditional presence of optional fields.
Frontend SSE routing and store aggregation
packages/web/src/hooks/useSSE.ts, packages/web/src/hooks/useDashboardSSE.ts, packages/web/src/stores/workflow-store.ts, packages/web/src/stores/workflow-store.test.ts
useSSE and useDashboardSSE route workflow_task_activity and workflow_hook_activity SSE events to store handlers. Workflow store implements handleTaskActivity and handleHookActivity to accumulate per-node task and hook state on DagNodeState, tolerating out-of-order event delivery and merging partial payloads while preserving first-set descriptive fields and updating completion/outcome state. Tests verify seeding behavior, in-place update semantics, and node overwrite resilience.
Frontend UI rendering
packages/web/src/components/workflows/WorkflowDagViewer.tsx, packages/web/src/components/workflows/ExecutionDagNode.tsx, packages/web/src/components/workflows/DagNodeProgress.tsx
WorkflowDagViewer derives task and hook counts from node live status; ExecutionDagNode displays counts in a conditional metadata row. DagNodeProgress adds interactive task and hook sections with helper renderers for labels, status badges, outcome indicators, and exit codes.
Slack adapter noise filtering
packages/adapters/src/chat/slack/workflow-bridge.ts
SlackWorkflowBridge.handleEvent explicitly routes task_activity and hook_activity to the noise case, preventing spurious Slack status/approval side-effects.

Sequence Diagram(s)

sequenceDiagram
  participant SDK as Claude SDK
  participant Provider as Provider<br/>(streamClaudeMessages)
  participant Executor as DAG Executor
  participant Emitter as WorkflowEventEmitter
  participant Store as WorkflowEventStore
  participant SSEBridge as SSE Bridge<br/>(mapWorkflowEvent)
  participant Frontend as Frontend Store<br/>(useDashboardSSE)
  
  SDK->>Provider: system event<br/>(task_started, task_progress, hook_started, hook_response)
  Provider->>Provider: normalize fields, apply<br/>agentProgressSummaries flag
  Provider->>Executor: yield MessageChunk<br/>(task_started, task_progress, hook_started, hook_response)
  Executor->>Emitter: emit TaskActivityEvent<br/>or HookActivityEvent
  Executor->>Store: persist task_activity<br/>or hook_activity workflow event
  Store->>SSEBridge: WorkflowEventEmitter<br/>broadcasts stored event
  SSEBridge->>SSEBridge: map to workflow_task_activity<br/>or workflow_hook_activity SSE payload
  SSEBridge->>Frontend: send SSE event<br/>with type, taskId/hookId, activity, metadata
  Frontend->>Frontend: handleTaskActivity or<br/>handleHookActivity: upsert DagTaskInfo/DagHookInfo on node<br/>tolerate out-of-order, merge partial fields
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • coleam00/Archon#1757: Modifies SlackWorkflowBridge event handling; this PR adds explicit task_activity/hook_activity noise routing that complements the broader Slack adapter UX upgrade.

Poem

🐰 Hark! The subagents now reveal their dance,
Each task and hook in real-time's gentle glance,
No more the blindfold—progress blooms and flows,
While DAG nodes shimmer as the workflow grows!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(web): forward SDK lifecycle events to Web UI (tasks + hooks) (#975)' clearly and concisely describes the main change: forwarding SDK lifecycle events to the Web UI.
Description check ✅ Passed The PR description comprehensively covers all four phases of implementation with detailed explanations of changes, test plan, and validation evidence.
Linked Issues check ✅ Passed All key requirements from #975 are met: task lifecycle events (started/progress/notification) forwarded via SSE, hook lifecycle events (started/response) forwarded via SSE, Web UI renders subagent tasks and hook activity, agentProgressSummaries enabled for workflow nodes.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #975 objectives: provider phase adds MessageChunk variants, workflows phase adds WorkflowEmitterEvent variants, Web UI phase adds DagTaskInfo/DagHookInfo rendering, and agentProgressSummaries configuration is controlled per-node.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-sdk-lifecycle-events

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/web/src/stores/workflow-store.test.ts (1)

659-680: ⚡ Quick win

Add regression tests for “terminal first, then late started” ordering.

Current tests assert seeding on out-of-order terminal events, but not the follow-up started event. Add cases that send completed/response first, then started, and assert state does not regress from terminal to started.

Also applies to: 780-802

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/stores/workflow-store.test.ts` around lines 659 - 680, Add
regression tests that send a terminal task notification before a later "started"
notification and assert the state does not regress: use the same pattern as the
existing test (calling useWorkflowStore.getState().handleWorkflowStatus,
.handleDagNode, and .handleTaskActivity with taskEvent for completed/response
first), then dispatch a subsequent handleTaskActivity with activity 'started'
for the same runId/nodeId/taskId and assert the stored task activity remains
'completed' (or terminal) and not 'started'; add analogous tests for
response->started ordering as well, reusing identifiers like run-t5, nodeId
'plan', taskId 't-1' and referencing handleTaskActivity, taskEvent, statusEvent,
dagNodeEvent and useWorkflowStore so they’re easy to find, and apply the same
additional test case pattern mentioned for the other block around lines 780-802.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/src/components/workflows/DagNodeProgress.tsx`:
- Around line 100-108: The toggle button in DagNodeProgress should expose its
expanded state and reference the collapsible container for assistive tech: add
aria-expanded={expanded} and aria-controls pointing to the collapsible section
id on the button (e.g., aria-controls={`details-${id}`}), and add a matching id
on the collapsible container element (use a stable unique id via useId or the
node id), then apply the same changes to the second toggle instance around lines
136-195 so both buttons include aria-expanded and aria-controls with matching
container ids.

In `@packages/web/src/stores/workflow-store.ts`:
- Around line 348-364: The started-event handler currently replaces an existing
task row (using tasks[taskIdx] = newTask), which causes late 'started' events to
regress state and drop fields like summary/usage/lastToolName; instead, when
taskIdx >= 0 merge the new started fields into the existing task object (update
activity/startedAt/updatedAt and only set description/taskType if present)
preserving any existing fields (e.g.,
completed/response/summary/usage/lastToolName); apply the same merge approach to
the other similar blocks referenced (lines handling other activities around the
same file, e.g., the blocks at ~365-394 and ~423-437) so out-of-order events
update fields incrementally rather than overwriting the entire object.

---

Nitpick comments:
In `@packages/web/src/stores/workflow-store.test.ts`:
- Around line 659-680: Add regression tests that send a terminal task
notification before a later "started" notification and assert the state does not
regress: use the same pattern as the existing test (calling
useWorkflowStore.getState().handleWorkflowStatus, .handleDagNode, and
.handleTaskActivity with taskEvent for completed/response first), then dispatch
a subsequent handleTaskActivity with activity 'started' for the same
runId/nodeId/taskId and assert the stored task activity remains 'completed' (or
terminal) and not 'started'; add analogous tests for response->started ordering
as well, reusing identifiers like run-t5, nodeId 'plan', taskId 't-1' and
referencing handleTaskActivity, taskEvent, statusEvent, dagNodeEvent and
useWorkflowStore so they’re easy to find, and apply the same additional test
case pattern mentioned for the other block around lines 780-802.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1bcb2711-5767-4043-8806-760f9e750653

📥 Commits

Reviewing files that changed from the base of the PR and between d72c20c and 9389c41.

📒 Files selected for processing (19)
  • packages/adapters/src/chat/slack/workflow-bridge.ts
  • packages/providers/src/claude/provider.test.ts
  • packages/providers/src/claude/provider.ts
  • packages/providers/src/types.ts
  • packages/server/package.json
  • packages/server/src/adapters/web/workflow-bridge.test.ts
  • packages/server/src/adapters/web/workflow-bridge.ts
  • packages/web/src/components/workflows/DagNodeProgress.tsx
  • packages/web/src/components/workflows/ExecutionDagNode.tsx
  • packages/web/src/components/workflows/WorkflowDagViewer.tsx
  • packages/web/src/hooks/useDashboardSSE.ts
  • packages/web/src/hooks/useSSE.ts
  • packages/web/src/lib/types.ts
  • packages/web/src/stores/workflow-store.test.ts
  • packages/web/src/stores/workflow-store.ts
  • packages/workflows/src/dag-executor.ts
  • packages/workflows/src/event-emitter.test.ts
  • packages/workflows/src/event-emitter.ts
  • packages/workflows/src/store.ts
👮 Files not reviewed due to content moderation or server errors (7)
  • packages/providers/src/types.ts
  • packages/workflows/src/event-emitter.ts
  • packages/providers/src/claude/provider.ts
  • packages/workflows/src/dag-executor.ts
  • packages/workflows/src/event-emitter.test.ts
  • packages/server/src/adapters/web/workflow-bridge.ts
  • packages/server/src/adapters/web/workflow-bridge.test.ts

Comment on lines 100 to 108
<button
type="button"
onClick={(e): void => {
e.stopPropagation();
setExpanded(prev => !prev);
}}
className="text-text-tertiary hover:text-text-secondary shrink-0 text-xs cursor-pointer"
aria-label={expanded ? 'Collapse iterations' : 'Expand iterations'}
aria-label={expanded ? 'Collapse details' : 'Expand details'}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Expose collapse state to assistive tech on the details toggle.

At Line [100], the toggle button has an aria-label but does not expose expanded state. Add aria-expanded + aria-controls on the button, and an id on the collapsible container so screen readers can track state changes.

Suggested patch
 function DagNodeItem({
@@
   const hasTasks = (node.tasks?.length ?? 0) > 0;
   const hasHooks = (node.hooks?.length ?? 0) > 0;
   const hasSubItems = hasIterations || hasTasks || hasHooks;
+  const detailsId = `dag-node-details-${node.nodeId}`;
@@
           {hasSubItems && (
             <button
               type="button"
               onClick={(e): void => {
                 e.stopPropagation();
                 setExpanded(prev => !prev);
               }}
               className="text-text-tertiary hover:text-text-secondary shrink-0 text-xs cursor-pointer"
               aria-label={expanded ? 'Collapse details' : 'Expand details'}
+              aria-expanded={expanded}
+              aria-controls={detailsId}
             >
               {expanded ? '\u25BC' : '\u25B6'}
             </button>
           )}
@@
       {expanded && hasSubItems && (
-        <div className="ml-6 mt-0.5 space-y-1">
+        <div id={detailsId} className="ml-6 mt-0.5 space-y-1">

Also applies to: 136-195

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/components/workflows/DagNodeProgress.tsx` around lines 100 -
108, The toggle button in DagNodeProgress should expose its expanded state and
reference the collapsible container for assistive tech: add
aria-expanded={expanded} and aria-controls pointing to the collapsible section
id on the button (e.g., aria-controls={`details-${id}`}), and add a matching id
on the collapsible container element (use a stable unique id via useId or the
node id), then apply the same changes to the second toggle instance around lines
136-195 so both buttons include aria-expanded and aria-controls with matching
container ids.

Comment on lines +348 to +364
if (event.activity === 'started') {
// Replace any prior started row for the same taskId (e.g. a
// re-spawn) — keeps the list flat rather than duplicating.
const newTask: DagTaskInfo = {
taskId: event.taskId,
activity: 'started',
startedAt: now,
updatedAt: now,
...(event.description !== undefined ? { description: event.description } : {}),
...(event.taskType !== undefined ? { taskType: event.taskType } : {}),
};
if (taskIdx >= 0) {
tasks[taskIdx] = newTask;
} else {
tasks.push(newTask);
}
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Late started events currently overwrite newer task/hook state.

Out-of-order streams are partially handled, but if completed/response arrives first and started arrives later, the started branch replaces the existing row and regresses state (completed/responsestarted) while dropping metadata. Task seeding also drops fields like summary/usage/lastToolName on first non-started event.

Suggested fix (merge instead of replace when row already exists)
-              if (event.activity === 'started') {
+              if (event.activity === 'started') {
                 const newTask: DagTaskInfo = {
                   taskId: event.taskId,
                   activity: 'started',
                   startedAt: now,
                   updatedAt: now,
                   ...(event.description !== undefined ? { description: event.description } : {}),
                   ...(event.taskType !== undefined ? { taskType: event.taskType } : {}),
                 };
                 if (taskIdx >= 0) {
-                  tasks[taskIdx] = newTask;
+                  const prev = tasks[taskIdx];
+                  tasks[taskIdx] = {
+                    ...prev,
+                    // never regress terminal/progress info on late "started"
+                    activity: prev.activity === 'started' ? 'started' : prev.activity,
+                    startedAt: Math.min(prev.startedAt, now),
+                    updatedAt: Math.max(prev.updatedAt, now),
+                    ...(prev.description === undefined && event.description !== undefined
+                      ? { description: event.description }
+                      : {}),
+                    ...(prev.taskType === undefined && event.taskType !== undefined
+                      ? { taskType: event.taskType }
+                      : {}),
+                  };
                 } else {
                   tasks.push(newTask);
                 }
               } else {
                 if (taskIdx < 0) {
                   tasks.push({
                     taskId: event.taskId,
                     activity: event.activity,
                     startedAt: now,
                     updatedAt: now,
+                    ...(event.description !== undefined ? { description: event.description } : {}),
+                    ...(event.taskType !== undefined ? { taskType: event.taskType } : {}),
+                    ...(event.summary !== undefined ? { summary: event.summary } : {}),
+                    ...(event.lastToolName !== undefined ? { lastToolName: event.lastToolName } : {}),
+                    ...(event.usage !== undefined ? { usage: event.usage } : {}),
                   });
                 } else {
-              if (event.activity === 'started') {
+              if (event.activity === 'started') {
                 const newHook: DagHookInfo = {
                   hookId: event.hookId,
                   hookName: event.hookName,
                   hookEvent: event.hookEvent,
                   activity: 'started',
                   startedAt: now,
                   updatedAt: now,
                 };
                 if (hookIdx >= 0) {
-                  hooks[hookIdx] = newHook;
+                  const prev = hooks[hookIdx];
+                  hooks[hookIdx] = {
+                    ...prev,
+                    hookName: prev.hookName || event.hookName,
+                    hookEvent: prev.hookEvent || event.hookEvent,
+                    // keep response outcome if it already arrived
+                    activity: prev.activity === 'response' ? 'response' : 'started',
+                    startedAt: Math.min(prev.startedAt, now),
+                    updatedAt: Math.max(prev.updatedAt, now),
+                  };
                 } else {
                   hooks.push(newHook);
                 }
               } else {

Also applies to: 365-394, 423-437

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/stores/workflow-store.ts` around lines 348 - 364, The
started-event handler currently replaces an existing task row (using
tasks[taskIdx] = newTask), which causes late 'started' events to regress state
and drop fields like summary/usage/lastToolName; instead, when taskIdx >= 0
merge the new started fields into the existing task object (update
activity/startedAt/updatedAt and only set description/taskType if present)
preserving any existing fields (e.g.,
completed/response/summary/usage/lastToolName); apply the same merge approach to
the other similar blocks referenced (lines handling other activities around the
same file, e.g., the blocks at ~365-394 and ~423-437) so out-of-order events
update fields incrementally rather than overwriting the entire object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): forward SDK lifecycle events to Web UI (tasks + hooks)

1 participant