Skip to content

fix(wework): isolate codex subagent stream state - #1645

Merged
qdaxb merged 1 commit into
mainfrom
support_subagent
Jun 30, 2026
Merged

qdaxb merged 1 commit into
mainfrom
support_subagent

Conversation

@qdaxb

@qdaxb qdaxb commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added a subagent status indicator in the workbench, with hover details, counts, and localized labels.
    • Subagent activity now appears in the runtime stream and is reflected in the workbench UI.
  • Bug Fixes

    • Improved transcript and turn handling so root content is shown more reliably.
    • Fixed subagent and cross-thread activity from leaking into main turn output or transcript views.
    • Updated running-state logic to better respect completed tasks.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds end-to-end subagent isolation: the Codex executor now tracks a root_thread_id and filters subagent messages, items, rollout markers, and turn completions from the root turn's output, transcript, and completion state. On the frontend, a new RuntimeSubagentActivityPayload type and response.subagent.activity stream event feed a new SubagentStatusIndicator UI component displayed in desktop and mobile workbench layouts.

Changes

Executor: Root-thread and subagent filtering

Layer / File(s) Summary
Root-turn detection helpers
executor/src/agents/codex.rs, executor/src/runtime_work/codex_notifications.rs, executor/src/runtime_work/codex_rollout.rs
Shared helpers (codex_agent_path, stream_thread_id, is_root_codex_turn_event, is_root_turn_marker, notification_item_type) extract agent path/thread ID and identify root-turn identity; codex_notification remaps item/started/item/completed for subagent and collab-agent types; wrapped_item_method gains matching arms.
CodexRunState root-thread tracking and subagent gating
executor/src/agents/codex.rs
root_thread_id is set immediately after thread creation; handle_message handles thread/started to capture root thread, routes item/* through is_subagent_message, and gates turn/completed on root-agent identity; tests cover subagent turn completion and cross-thread delta/completion ignoring.
Rollout subagent marker filtering
executor/src/runtime_work/codex_rollout.rs
apply_rollout_item_to_turns, is_turn_start_item, and is_turn_completion_item now all require is_root_turn_marker; tests verify subagent task completion, message items, and tail-based running status are isolated from root turn.
CodexNotificationEventMapper subagent tracking and emission
executor/src/runtime_work/events.rs
CodexNotificationEventMapper gains subagent_item_ids: BTreeSet<String> and root_thread_id; delta paths early-return for subagent items; new match arms for "subagent/activity" and "collab-agent/activity" emit response.subagent.activity and block events; helpers parse agent paths, thread IDs, kind/status, timestamps, and collab-agent fields.
CodexNotificationCacheMapper subagent filtering and process_item_id
executor/src/runtime_work/runtime_handle_messages.rs
CodexNotificationCacheMapper gains subagent_item_ids and root_thread_id; reasoning/agentMessage deltas ignore subagent items; append_runtime_assistant_process_delta accepts process_item_id for cross-tool-step block matching; tests cover commentary merging, subagent delta ignoring, cross-thread ignoring.
Transcript and running-state subagent filtering
executor/src/runtime_work/transcript.rs, executor/src/runtime_work/handler.rs, executor/tests/process_manager_contract.rs
transcript_messages skips non-root agent-path and cross-thread turns/items; transcript_running helper forces running=false when local task status is terminal; applied in cache-hit, non-cache, and incremental transcript paths; process manager test polls file content instead of existence.

Frontend: Subagent activity stream and status UI

Layer / File(s) Summary
RuntimeSubagentActivityPayload type and stream event routing
wework/src/types/api.ts, wework/src/types/workbench.ts, wework/src/stream/responseApiStream.ts, wework/src/stream/chatStream.ts, wework/src/features/workbench/runtimePaneMessages.ts, wework/src/stream/responseApiStream.test.ts
Defines RuntimeSubagentActivityPayload and RuntimeSubagentStatus types; adds response.subagent.activity to RESPONSE_API_STREAM_EVENTS; emitSubagentActivity normalizes and dispatches the payload; ChatStreamHandlers and RuntimeTaskStreamHandlers gain onSubagentActivity; payload union types updated.
useWorkbenchPaneSession subagent status tracking
wework/src/components/layout/useWorkbenchPaneSession.ts
Adds subagentStatuses: RuntimeSubagentStatus[] state; resets on task switch; onSubagentActivity updates entries via updateRuntimeSubagentStatuses; onAssistantSettled marks running subagents done; subagentStatuses included in returned session object.
SubagentStatusIndicator component and layout integration
wework/src/components/layout/SubagentStatusIndicator.tsx, ...SubagentStatusIndicator.test.tsx, wework/src/components/layout/DesktopWorkbenchMain.tsx, wework/src/components/layout/MobileWorkbenchLayout.tsx, wework/src/i18n/locales/*/common.json
New component renders hover-driven dropdown of up to 4 subagent statuses with lucide icons and shortened IDs; conditionally rendered in DesktopTopBar right slot and mobile conversation header; EN and ZH-CN i18n strings added.

Sequence Diagram

sequenceDiagram
  participant Codex as Codex RPC
  participant HM as CodexRunState.handle_message
  participant EM as CodexNotificationEventMapper
  participant RS as responseApiStream
  participant Hook as useWorkbenchPaneSession
  participant UI as SubagentStatusIndicator

  Codex->>HM: thread/started {threadId}
  HM->>HM: set root_thread_id

  Codex->>HM: item/started {subagent item}
  HM->>HM: is_subagent_message? → ignore

  Codex->>EM: subagent/activity notification
  EM->>RS: response.subagent.activity event
  RS->>Hook: onSubagentActivity(payload)
  Hook->>Hook: updateRuntimeSubagentStatuses
  Hook->>UI: subagentStatuses updated → re-render

  Codex->>HM: turn/completed {root agent}
  HM->>HM: is_root_codex_turn_event? → emit ExecutionOutcome
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • wecode-ai/Wegent#1601: Both PRs modify the runtime transcript assembly path in executor/src/runtime_work/handler.rs, with the referenced PR updating thread-ID derivation for transcript reads.
  • wecode-ai/Wegent#1614: Both PRs add thread/started handling to extract and cache a thread ID (root_thread_id here vs provider session ID there) used by downstream message/event processing.

Poem

🐇 Hop hop, the root thread is found,
Subagents filtered, no noise to confound.
A spinner spins in the top bar so neat,
Four agents listed — the display's complete.
The transcript is clean, the turn is precise,
This rabbit declares subagent filtering nice! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.25% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: isolating Codex subagent stream state across the workbench and runtime pipeline.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 support_subagent

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.

@qdaxb
qdaxb merged commit fa73be7 into main Jun 30, 2026
24 of 25 checks passed
@qdaxb
qdaxb deleted the support_subagent branch June 30, 2026 12:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (2)
executor/src/agents/codex.rs (1)

781-813: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider consolidating the duplicated root-detection helpers into a shared module.

codex_agent_path and is_root_codex_turn_event are now defined independently in agents/codex.rs, runtime_work/codex_notifications.rs, and runtime_work/codex_rollout.rs (with stream_thread_id/is_root_turn_marker related). They already diverge subtly — e.g. is_root_codex_turn_event checks the turn object first here but the top-level params first in codex_notifications.rs. Functionally equivalent today, but the copies can drift if the "root" definition changes. Extracting them into one pub(crate) helper would keep the subagent contract single-sourced.

🤖 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 `@executor/src/agents/codex.rs` around lines 781 - 813, Consolidate the
duplicated root-detection logic so the “root” contract is defined once and
reused across codex-related modules. Move the shared behavior behind a single
pub(crate) helper for symbols like codex_agent_path and is_root_codex_turn_event
(and the related stream_thread_id/is_root_turn_marker path checks) and update
agents/codex.rs, runtime_work/codex_notifications.rs, and
runtime_work/codex_rollout.rs to call that helper instead of maintaining
separate copies. Keep the helper’s precedence/field-normalization rules
consistent so the turn-vs-params lookup cannot drift between implementations.
wework/src/stream/responseApiStream.test.ts (1)

5-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the non-happy-path normalization branches.

This only asserts the snake_case happy path. emitSubagentActivity also accepts camelCase aliases and drops payloads without agent_path; those are the branches most likely to regress.

Suggested test additions
+  test('emits camelCase subagent activity payloads', () => {
+    const onSubagentActivity = vi.fn()
+
+    emitResponseApiEvent(
+      { onSubagentActivity },
+      'response.subagent.activity',
+      {
+        task_id: 1,
+        subtask_id: 2,
+        device_id: 'device-1',
+        local_task_id: 'local-1',
+        data: {
+          agentPath: '/root/worker',
+          agentName: 'worker',
+          agentThreadId: 'thread-1',
+          kind: 'started',
+          status: 'running',
+          occurredAtMs: 12345,
+        },
+      },
+      createResponseApiStreamState()
+    )
+
+    expect(onSubagentActivity).toHaveBeenCalledWith(
+      expect.objectContaining({
+        agent_path: '/root/worker',
+        agent_name: 'worker',
+        agent_thread_id: 'thread-1',
+        occurred_at_ms: 12345,
+      })
+    )
+  })
+
+  test('ignores subagent activity payloads without agent_path', () => {
+    const onSubagentActivity = vi.fn()
+
+    emitResponseApiEvent(
+      { onSubagentActivity },
+      'response.subagent.activity',
+      {
+        task_id: 1,
+        subtask_id: 2,
+        device_id: 'device-1',
+        local_task_id: 'local-1',
+        data: { kind: 'started', status: 'running' },
+      },
+      createResponseApiStreamState()
+    )
+
+    expect(onSubagentActivity).not.toHaveBeenCalled()
+  })

As per coding guidelines, "Run tests before committing and follow the project’s testing principles: AAA, mock external services, test edge cases, and keep tests isolated."

🤖 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 `@wework/src/stream/responseApiStream.test.ts` around lines 5 - 40, Expand the
emitSubagentActivity coverage in responseApiStream.test.ts to include the
normalization edge cases: add a test for camelCase input aliases being mapped
correctly by emitResponseApiEvent, and add a test that verifies payloads missing
agent_path are dropped and do not call onSubagentActivity. Use the existing
emitResponseApiEvent and createResponseApiStreamState helpers so the new tests
stay isolated and focused on the normalization branches.

Source: Coding guidelines

🤖 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 `@executor/src/runtime_work/handler.rs`:
- Around line 1844-1852: The local_task_finished helper in handler.rs does not
treat archived tasks as finished, so archived transcripts can still appear
running. Update local_task_finished to recognize the archived status alongside
the existing done/complete/failed/error/cancelled/canceled values, while keeping
the running flag check intact. Use the local_task_finished function and the
status normalization logic in RuntimeTaskLink to locate and adjust the matching
list.

In `@executor/src/runtime_work/runtime_handle_messages.rs`:
- Around line 176-179: The turn completion handling in handle_messages should
also be gated by thread identity, not just agent_path, because a child
turn/completed event with only threadId can still complete the root cached
assistant message once root_thread_id has been seen. Update the matching logic
around is_root_codex_turn_event and the turn/completed branch to require the
event’s threadId to match the root thread before calling
complete_runtime_assistant_message, and keep the missing-path behavior in the
root-detection logic consistent with that same rule.
- Around line 898-913: The item-specific delta check in
process_block_accepts_delta_for_item bypasses the terminal-state guard when
process_item_id is Some, which can allow late updates to done/failed blocks.
Update the Some(item_id) branch to still validate the block with
process_block_accepts_delta after matching type, process_kind, and
process_item_id, so item-matched deltas are rejected for terminal blocks just
like the None path.

In `@wework/src/components/layout/DesktopWorkbenchMain.tsx`:
- Around line 770-775: The right-side summary in DesktopWorkbenchMain is
receiving chatColumnWidth, which can be a percentage string when the panel is
closed and prevents SubagentStatusIndicator from auto-expanding. Update the
right prop logic to pass a numeric width in the full-width desktop layout, using
the existing chatColumnWidth computation or a derived numeric value from
DesktopWorkbenchMain/paneSession so SubagentStatusIndicator gets a number
instead of '100%'. Keep the fix localized to the SubagentStatusIndicator usage
and preserve the existing statuses prop wiring.

In `@wework/src/components/layout/SubagentStatusIndicator.test.tsx`:
- Around line 17-39: The SubagentStatusIndicator test suite only covers the
desktop auto-expand and hover flow, so it misses the compact/tap behavior in the
component. Add a dedicated test for the compact variant of
SubagentStatusIndicator that renders it in compact mode and verifies the panel
opens and closes via click/tap interactions, using the existing test ids like
subagent-status-hover-region and subagent-status-panel to keep the coverage
aligned with the component behavior.

In `@wework/src/components/layout/SubagentStatusIndicator.tsx`:
- Around line 42-66: The SubagentStatusIndicator trigger is hover/focus-only, so
touch users can’t open or close the panel; add a real click toggle to the button
in SubagentStatusIndicator so panelVisible can change on tap as well as
hover/focus. Update the button handler alongside the existing hover/focus state
logic, and keep aria-expanded in sync with the same open/close state used by
MobileWorkbenchLayout.

In `@wework/src/i18n/locales/en/common.json`:
- Around line 75-76: The `subagents_running` and `subagents_count` strings in
the `common.json` locale currently use a single plural form, which makes
`SubagentStatusIndicator` render incorrect English when `count === 1`. Update
the locale entries to support proper singular/plural messaging for the summary
text, and make sure the templates used by `SubagentStatusIndicator` select the
singular form for 1 and the plural form otherwise.

In `@wework/src/stream/responseApiStream.ts`:
- Around line 385-393: Forward the missing agent_id through the subagent
activity mapping so the primary identifier is preserved. Update the
onSubagentActivity payload construction in responseApiStream.ts to read agent_id
from the incoming data, alongside agent_name, agent_thread_id, kind, status, and
occurred_at_ms, and include it in the object passed through base so
RuntimeSubagentActivityPayload reaches the session/UI layer intact.

---

Nitpick comments:
In `@executor/src/agents/codex.rs`:
- Around line 781-813: Consolidate the duplicated root-detection logic so the
“root” contract is defined once and reused across codex-related modules. Move
the shared behavior behind a single pub(crate) helper for symbols like
codex_agent_path and is_root_codex_turn_event (and the related
stream_thread_id/is_root_turn_marker path checks) and update agents/codex.rs,
runtime_work/codex_notifications.rs, and runtime_work/codex_rollout.rs to call
that helper instead of maintaining separate copies. Keep the helper’s
precedence/field-normalization rules consistent so the turn-vs-params lookup
cannot drift between implementations.

In `@wework/src/stream/responseApiStream.test.ts`:
- Around line 5-40: Expand the emitSubagentActivity coverage in
responseApiStream.test.ts to include the normalization edge cases: add a test
for camelCase input aliases being mapped correctly by emitResponseApiEvent, and
add a test that verifies payloads missing agent_path are dropped and do not call
onSubagentActivity. Use the existing emitResponseApiEvent and
createResponseApiStreamState helpers so the new tests stay isolated and focused
on the normalization branches.
🪄 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: 6970ce5b-c133-42b4-a89c-34b2629ee8d2

📥 Commits

Reviewing files that changed from the base of the PR and between 4cda3af and 9d3d3b6.

📒 Files selected for processing (21)
  • executor/src/agents/codex.rs
  • executor/src/runtime_work/codex_notifications.rs
  • executor/src/runtime_work/codex_rollout.rs
  • executor/src/runtime_work/events.rs
  • executor/src/runtime_work/handler.rs
  • executor/src/runtime_work/runtime_handle_messages.rs
  • executor/src/runtime_work/transcript.rs
  • executor/tests/process_manager_contract.rs
  • wework/src/components/layout/DesktopWorkbenchMain.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.tsx
  • wework/src/components/layout/SubagentStatusIndicator.test.tsx
  • wework/src/components/layout/SubagentStatusIndicator.tsx
  • wework/src/components/layout/useWorkbenchPaneSession.ts
  • wework/src/features/workbench/runtimePaneMessages.ts
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
  • wework/src/stream/chatStream.ts
  • wework/src/stream/responseApiStream.test.ts
  • wework/src/stream/responseApiStream.ts
  • wework/src/types/api.ts
  • wework/src/types/workbench.ts

Comment on lines +1844 to +1852
fn local_task_finished(link: &RuntimeTaskLink) -> bool {
!link.running
&& matches!(
link.status
.replace(['_', '-'], "")
.to_ascii_lowercase()
.as_str(),
"done" | "complete" | "completed" | "failed" | "error" | "cancelled" | "canceled"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat archived local tasks as finished.

archive_task sets status = "archived" and running = false, but this helper excludes "archived", so stale streaming messages can still make archived transcripts report running: true.

Suggested fix
-            "done" | "complete" | "completed" | "failed" | "error" | "cancelled" | "canceled"
+            "done" | "complete" | "completed" | "failed" | "error" | "cancelled" | "canceled"
+            | "archived"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn local_task_finished(link: &RuntimeTaskLink) -> bool {
!link.running
&& matches!(
link.status
.replace(['_', '-'], "")
.to_ascii_lowercase()
.as_str(),
"done" | "complete" | "completed" | "failed" | "error" | "cancelled" | "canceled"
)
fn local_task_finished(link: &RuntimeTaskLink) -> bool {
!link.running
&& matches!(
link.status
.replace(['_', '-'], "")
.to_ascii_lowercase()
.as_str(),
"done" | "complete" | "completed" | "failed" | "error" | "cancelled" | "canceled"
| "archived"
)
🤖 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 `@executor/src/runtime_work/handler.rs` around lines 1844 - 1852, The
local_task_finished helper in handler.rs does not treat archived tasks as
finished, so archived transcripts can still appear running. Update
local_task_finished to recognize the archived status alongside the existing
done/complete/failed/error/cancelled/canceled values, while keeping the running
flag check intact. Use the local_task_finished function and the status
normalization logic in RuntimeTaskLink to locate and adjust the matching list.

Comment on lines +176 to +179
"turn/completed" if is_root_codex_turn_event(params) => {
complete_runtime_assistant_message(store, local_task_id, request)
}
"turn/completed" => {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Also gate turn completion by thread id.

Line 176 only checks agent_path, while Line 286 treats missing paths as root. After root_thread_id is observed, a child turn/completed carrying only threadId will still mark the root cached assistant message done.

Suggested fix
-            "turn/completed" if is_root_codex_turn_event(params) => {
+            "turn/completed" if is_root_codex_turn_event(params) && !self.is_subagent_thread(params) => {
                 complete_runtime_assistant_message(store, local_task_id, request)
             }

Also applies to: 280-286

🤖 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 `@executor/src/runtime_work/runtime_handle_messages.rs` around lines 176 - 179,
The turn completion handling in handle_messages should also be gated by thread
identity, not just agent_path, because a child turn/completed event with only
threadId can still complete the root cached assistant message once
root_thread_id has been seen. Update the matching logic around
is_root_codex_turn_event and the turn/completed branch to require the event’s
threadId to match the root thread before calling
complete_runtime_assistant_message, and keep the missing-path behavior in the
root-detection logic consistent with that same rule.

Comment on lines +898 to +913
fn process_block_accepts_delta_for_item(
block: &Value,
block_type: &str,
process_kind: &str,
process_item_id: Option<&str>,
) -> bool {
process_block_accepts_delta(block, block_type)
&& string_field(block, "process_kind").as_deref() == Some(process_kind)
if string_field(block, "type").as_deref() != Some(block_type)
|| string_field(block, "process_kind").as_deref() != Some(process_kind)
{
return false;
}

match process_item_id {
Some(item_id) => string_field(block, "process_item_id").as_deref() == Some(item_id),
None => process_block_accepts_delta(block, block_type),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve the terminal-block guard for item-matched deltas.

When process_item_id is present, Line 911 bypasses process_block_accepts_delta, so a late delta can mutate a done/failed process block.

Suggested fix
-    if string_field(block, "type").as_deref() != Some(block_type)
+    if !process_block_accepts_delta(block, block_type)
         || string_field(block, "process_kind").as_deref() != Some(process_kind)
     {
         return false;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn process_block_accepts_delta_for_item(
block: &Value,
block_type: &str,
process_kind: &str,
process_item_id: Option<&str>,
) -> bool {
process_block_accepts_delta(block, block_type)
&& string_field(block, "process_kind").as_deref() == Some(process_kind)
if string_field(block, "type").as_deref() != Some(block_type)
|| string_field(block, "process_kind").as_deref() != Some(process_kind)
{
return false;
}
match process_item_id {
Some(item_id) => string_field(block, "process_item_id").as_deref() == Some(item_id),
None => process_block_accepts_delta(block, block_type),
}
fn process_block_accepts_delta_for_item(
block: &Value,
block_type: &str,
process_kind: &str,
process_item_id: Option<&str>,
) -> bool {
if !process_block_accepts_delta(block, block_type)
|| string_field(block, "process_kind").as_deref() != Some(process_kind)
{
return false;
}
🤖 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 `@executor/src/runtime_work/runtime_handle_messages.rs` around lines 898 - 913,
The item-specific delta check in process_block_accepts_delta_for_item bypasses
the terminal-state guard when process_item_id is Some, which can allow late
updates to done/failed blocks. Update the Some(item_id) branch to still validate
the block with process_block_accepts_delta after matching type, process_kind,
and process_item_id, so item-matched deltas are rejected for terminal blocks
just like the None path.

Comment on lines +770 to +775
right={
(paneSession.subagentStatuses?.length ?? 0) > 0 ? (
<SubagentStatusIndicator
statuses={paneSession.subagentStatuses}
availableWidth={chatColumnWidth}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pass a numeric width here.

When the right panel is closed, chatColumnWidth is '100%', but SubagentStatusIndicator only auto-expands for numeric widths. In the full-width desktop layout the summary therefore stays collapsed unless the user hovers.

🤖 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 `@wework/src/components/layout/DesktopWorkbenchMain.tsx` around lines 770 -
775, The right-side summary in DesktopWorkbenchMain is receiving
chatColumnWidth, which can be a percentage string when the panel is closed and
prevents SubagentStatusIndicator from auto-expanding. Update the right prop
logic to pass a numeric width in the full-width desktop layout, using the
existing chatColumnWidth computation or a derived numeric value from
DesktopWorkbenchMain/paneSession so SubagentStatusIndicator gets a number
instead of '100%'. Keep the fix localized to the SubagentStatusIndicator usage
and preserve the existing statuses prop wiring.

Comment on lines +17 to +39
describe('SubagentStatusIndicator', () => {
test('expands automatically when title space is available', () => {
render(<SubagentStatusIndicator statuses={statuses} availableWidth={900} />)

expect(screen.getByTestId('subagent-status-panel')).toBeInTheDocument()
expect(screen.getByText('worker')).toBeInTheDocument()
expect(screen.getByText('0ffa96e5')).toBeInTheDocument()
})

test('collapses when title space is constrained and opens on hover', () => {
render(<SubagentStatusIndicator statuses={statuses} availableWidth={360} />)

expect(screen.queryByTestId('subagent-status-panel')).not.toBeInTheDocument()

fireEvent.mouseEnter(screen.getByTestId('subagent-status-hover-region'))

expect(screen.getByTestId('subagent-status-panel')).toBeInTheDocument()

fireEvent.mouseLeave(screen.getByTestId('subagent-status-hover-region'))

expect(screen.queryByTestId('subagent-status-panel')).not.toBeInTheDocument()
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Cover the compact/tap path too.

This suite only exercises the desktop auto-expand/hover behavior, so it would not catch the mobile regression path introduced by rendering the indicator in compact mode. Add a click/tap test for the compact variant.

As per coding guidelines, **/*.{py,ts,tsx,js,jsx} should “test edge cases” and keep tests isolated.

Suggested test
 describe('SubagentStatusIndicator', () => {
@@
   test('collapses when title space is constrained and opens on hover', () => {
     render(<SubagentStatusIndicator statuses={statuses} availableWidth={360} />)
@@
     expect(screen.queryByTestId('subagent-status-panel')).not.toBeInTheDocument()
   })
+
+  test('opens in compact mode on tap', () => {
+    render(<SubagentStatusIndicator statuses={statuses} availableWidth={0} compact />)
+
+    fireEvent.click(screen.getByTestId('subagent-status-toggle-button'))
+
+    expect(screen.getByTestId('subagent-status-panel')).toBeInTheDocument()
+  })
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('SubagentStatusIndicator', () => {
test('expands automatically when title space is available', () => {
render(<SubagentStatusIndicator statuses={statuses} availableWidth={900} />)
expect(screen.getByTestId('subagent-status-panel')).toBeInTheDocument()
expect(screen.getByText('worker')).toBeInTheDocument()
expect(screen.getByText('0ffa96e5')).toBeInTheDocument()
})
test('collapses when title space is constrained and opens on hover', () => {
render(<SubagentStatusIndicator statuses={statuses} availableWidth={360} />)
expect(screen.queryByTestId('subagent-status-panel')).not.toBeInTheDocument()
fireEvent.mouseEnter(screen.getByTestId('subagent-status-hover-region'))
expect(screen.getByTestId('subagent-status-panel')).toBeInTheDocument()
fireEvent.mouseLeave(screen.getByTestId('subagent-status-hover-region'))
expect(screen.queryByTestId('subagent-status-panel')).not.toBeInTheDocument()
})
})
describe('SubagentStatusIndicator', () => {
test('expands automatically when title space is available', () => {
render(<SubagentStatusIndicator statuses={statuses} availableWidth={900} />)
expect(screen.getByTestId('subagent-status-panel')).toBeInTheDocument()
expect(screen.getByText('worker')).toBeInTheDocument()
expect(screen.getByText('0ffa96e5')).toBeInTheDocument()
})
test('collapses when title space is constrained and opens on hover', () => {
render(<SubagentStatusIndicator statuses={statuses} availableWidth={360} />)
expect(screen.queryByTestId('subagent-status-panel')).not.toBeInTheDocument()
fireEvent.mouseEnter(screen.getByTestId('subagent-status-hover-region'))
expect(screen.getByTestId('subagent-status-panel')).toBeInTheDocument()
fireEvent.mouseLeave(screen.getByTestId('subagent-status-hover-region'))
expect(screen.queryByTestId('subagent-status-panel')).not.toBeInTheDocument()
})
test('opens in compact mode on tap', () => {
render(<SubagentStatusIndicator statuses={statuses} availableWidth={0} compact />)
fireEvent.click(screen.getByTestId('subagent-status-toggle-button'))
expect(screen.getByTestId('subagent-status-panel')).toBeInTheDocument()
})
})
🤖 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 `@wework/src/components/layout/SubagentStatusIndicator.test.tsx` around lines
17 - 39, The SubagentStatusIndicator test suite only covers the desktop
auto-expand and hover flow, so it misses the compact/tap behavior in the
component. Add a dedicated test for the compact variant of
SubagentStatusIndicator that renders it in compact mode and verifies the panel
opens and closes via click/tap interactions, using the existing test ids like
subagent-status-hover-region and subagent-status-panel to keep the coverage
aligned with the component behavior.

Source: Coding guidelines

Comment on lines +42 to +66
return (
<div
data-testid="subagent-status-hover-region"
className={cn('relative flex shrink-0 items-center', className)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setHovered(true)}
onBlur={() => setHovered(false)}
>
<button
type="button"
data-testid="subagent-status-toggle-button"
className={cn(
'flex h-8 max-w-[11rem] items-center gap-1.5 rounded-full border border-border/70 bg-surface px-2.5 text-xs text-text-secondary shadow-sm hover:bg-background',
compact && 'h-9 min-w-[44px] justify-center px-2'
)}
aria-label={t('workbench.subagents_status')}
aria-expanded={panelVisible}
>
<Bot className="h-4 w-4 shrink-0 text-primary" />
{!compact && <span className="truncate">{summary}</span>}
<ChevronDown
className={cn('h-3.5 w-3.5 shrink-0 transition-transform', panelVisible && 'rotate-180')}
/>
</button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add a real toggle for touch devices.

panelVisible only changes via hover/focus, but the trigger button never toggles anything on click. That makes the compact mobile usage in MobileWorkbenchLayout.tsx effectively non-interactive on touch-only devices.

Proposed fix
-  const [hovered, setHovered] = useState(false)
+  const [expanded, setExpanded] = useState(false)
@@
-  const panelVisible = autoExpanded || hovered
+  const panelVisible = autoExpanded || expanded
@@
     <div
       data-testid="subagent-status-hover-region"
       className={cn('relative flex shrink-0 items-center', className)}
-      onMouseEnter={() => setHovered(true)}
-      onMouseLeave={() => setHovered(false)}
-      onFocus={() => setHovered(true)}
-      onBlur={() => setHovered(false)}
+      onMouseEnter={() => setExpanded(true)}
+      onMouseLeave={() => !compact && setExpanded(false)}
+      onFocus={() => setExpanded(true)}
+      onBlur={event => {
+        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
+          setExpanded(false)
+        }
+      }}
     >
       <button
         type="button"
         data-testid="subagent-status-toggle-button"
+        onClick={() => setExpanded(value => !value)}
         className={cn(
           'flex h-8 max-w-[11rem] items-center gap-1.5 rounded-full border border-border/70 bg-surface px-2.5 text-xs text-text-secondary shadow-sm hover:bg-background',
           compact && 'h-9 min-w-[44px] justify-center px-2'
         )}
         aria-label={t('workbench.subagents_status')}
         aria-expanded={panelVisible}
+        aria-controls="subagent-status-panel"
       >
@@
         <div
+          id="subagent-status-panel"
           data-testid="subagent-status-panel"
           className="absolute right-0 top-[calc(100%+0.5rem)] z-popover w-64 rounded-lg border border-border/80 bg-background p-2 shadow-lg"
         >
🤖 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 `@wework/src/components/layout/SubagentStatusIndicator.tsx` around lines 42 -
66, The SubagentStatusIndicator trigger is hover/focus-only, so touch users
can’t open or close the panel; add a real click toggle to the button in
SubagentStatusIndicator so panelVisible can change on tap as well as
hover/focus. Update the button handler alongside the existing hover/focus state
logic, and keep aria-expanded in sync with the same open/close state used by
MobileWorkbenchLayout.

Comment on lines +75 to +76
"subagents_running": "{{count}} running",
"subagents_count": "{{count}} subagents",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid singular-count copy like 1 subagents.

SubagentStatusIndicator uses these templates directly for the summary text, so count === 1 currently renders incorrect English in the button label.

🤖 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 `@wework/src/i18n/locales/en/common.json` around lines 75 - 76, The
`subagents_running` and `subagents_count` strings in the `common.json` locale
currently use a single plural form, which makes `SubagentStatusIndicator` render
incorrect English when `count === 1`. Update the locale entries to support
proper singular/plural messaging for the summary text, and make sure the
templates used by `SubagentStatusIndicator` select the singular form for 1 and
the plural form otherwise.

Comment on lines +385 to +393
handlers.onSubagentActivity?.({
...base,
agent_path: agentPath,
agent_name: stringField(data, 'agent_name') ?? stringField(data, 'agentName'),
agent_thread_id: stringField(data, 'agent_thread_id') ?? stringField(data, 'agentThreadId'),
kind: stringField(data, 'kind'),
status: stringField(data, 'status'),
occurred_at_ms:
optionalNumberField(data, 'occurred_at_ms') ?? optionalNumberField(data, 'occurredAtMs'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 --glob 'wework/src/**' --glob 'executor/src/**' '\bagent_id\b|\bagentId\b|response\.subagent\.activity' \
  wework/src/types/api.ts \
  wework/src/stream/responseApiStream.ts \
  wework/src/components/layout/useWorkbenchPaneSession.ts \
  wework/src/components/layout/SubagentStatusIndicator.tsx \
  executor/src/runtime_work/events.rs

Repository: wecode-ai/Wegent

Length of output: 13983


Forward agent_id through this mapper. RuntimeSubagentActivityPayload already includes it, and the executor emits it on response.subagent.activity; omitting it here drops the primary subagent identifier before the session/UI layer can use it.

🤖 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 `@wework/src/stream/responseApiStream.ts` around lines 385 - 393, Forward the
missing agent_id through the subagent activity mapping so the primary identifier
is preserved. Update the onSubagentActivity payload construction in
responseApiStream.ts to read agent_id from the incoming data, alongside
agent_name, agent_thread_id, kind, status, and occurred_at_ms, and include it in
the object passed through base so RuntimeSubagentActivityPayload reaches the
session/UI layer intact.

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.

1 participant