Expose active goal in stream JSON#4314
Conversation
📋 Review SummaryThis PR adds support for emitting active goal updates as stream JSON events in headless/non-interactive mode, along with improved 🔍 General Feedback
🎯 Specific Feedback🔴 CriticalNo critical issues identified. 🟡 HighNo high priority issues identified. 🟢 Medium
🔵 Low
✅ Highlights
|
pomelo-nwu
left a comment
There was a problem hiding this comment.
Scope is tight (+149/-1, focused on protocol surface + a small UX fix on top of #4273). Reading the code statically — did not run the test suite locally.
What looks good
- Type definition is consistent.
ActiveGoalStreamEvent { type: 'active_goal', active_goal: ActiveGoal | null }intypes.tsmirrors the snake_case convention used bytool_progress/content_block_*, andActiveGoal | nullaccurately reflectsServerGeminiActiveGoalEvent.valueincore/turn.ts. processEventoverride reuses the existing guard. CallingemitStreamEventIfEnabledautomatically respectsincludePartialMessages, so there is no duplicated condition. Other events go throughsuper.processEvent(event)unchanged.- Nice UX side-fix in
goalCommand.ts. Before this PR, non-interactive/goal clearreturnedvoid, whichhandleSlashCommandfell back to as"Command executed successfully."(becausecreateNonInteractiveUI().addItemis a no-op). Returning an explicitGoal cleared: <condition>is a clear improvement. - Tests cover the meaningful axes. The stream-json test asserts both the populated
ActiveGoaland thenull(cleared) emission; the CLI tests cover empty/goal, set-then-status, and clear, and properly isolate the module-scoped store via__resetActiveGoalStoreForTests.
Suggestions
-
Consider ACP mode in
goalCommand.tsas well. The new branch is gated onexecutionMode === 'non_interactive', but ACP mode also goes throughhandleSlashCommandwith the same no-opcreateNonInteractiveUI(), andSession.ts#processSlashCommandResultalready handlesmessageType: 'info'by emitting an agent message chunk to Zed. So ACP users currently get the same"Command executed successfully."fallback. Loosening the check would unify the behavior:if (context.executionMode !== 'interactive') { return infoMessage(`Goal cleared: ${cleared.condition}`); }
-
Optional: add a negative test for the disabled path. The "with partial messages disabled" describe block in
StreamJsonOutputAdapter.test.tsdoesn't assert thatprocessEvent({ type: ActiveGoal, ... })is suppressed.emitStreamEventIfEnabledalready guards onincludePartialMessages, but a single assertion would lock the behavior against future regressions. -
Minor architectural note (non-blocking).
BaseJsonOutputAdapterexposes hooks likeonTextBlockCreated/onToolUseBlockCreatedand keeps state mutation in the baseswitch. Overriding the top-levelprocessEventhere is fine becauseActiveGoaldoesn't touch message state — it's a pure side-channel event. If more side-channel events land later (e.g. hook status), it might be worth introducing anonSideChannelEventhook in the base class for symmetry. Not needed for this PR.
Risks
- Protocol-level: purely additive change to the
stream_eventdiscriminated union. Consumers doing exhaustive type-narrowing will need to handle the new variant, but that is the intended forward-compatible shape. state.finalizedguard: the baseprocessEventshort-circuits whenstate.finalizedis true. The override doesn't replicate that check, but per the emit sites incore/client.tsallActiveGoalyields happen inside the turn beforeFinished, so this is safe today. Worth keeping in mind if the lifecycle ordering ever changes.
Overall looks good — I'd suggest folding in the ACP branch unification before merge, the rest is optional polish.
wenshao
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI failing: Test (ubuntu-latest, Node 22.x). — gpt-5.5 via Qwen Code /review
Summary
Validation
Notes