qwen-code/docs/users/features
Alex Yanchenko 218dec6a6f
feat(hooks): add MessageDisplay hook for mid-turn streaming (#6489)
* feat(hooks): add MessageDisplay hook for mid-turn streaming

Fires repeatedly as the assistant reply streams, before Stop (which only fires once at the end of the turn). Fire-and-forget, cumulative text payload, debounced (~200ms) except for the unconditional final firing. Fires from the single streaming loop in client.ts shared by the terminal UI and ACP paths.

Fixes #6488

* fix(hooks): address MessageDisplay review feedback

- Chain fire-and-forget MessageDisplay requests per message_id instead of
  firing them fully unbounded, so a slow hook command can't pile up
  concurrent processes.
- Gate the final flush on non-empty displayed_text and !signal.aborted,
  matching the adjacent Stop hook's guard.
- Document why the final flush intentionally re-sends the last debounced
  text (is_final itself is new information).
- Simplify the debounce constant's JSDoc to drop the competitor comparison.
- Add tests for the mid-stream debounced flush and the rejected-request
  warn path.

* test(hooks): drain microtasks before asserting on chained MessageDisplay calls

fireMessageDisplayHook now chains per-message_id through a promise (see
previous commit), so the final flush's actual messageBus.request() call
lands a few microtask ticks after the generator itself finishes — the
mid-stream-flush test needs to let that chain settle before asserting.

* fix(hooks): flush MessageDisplay is_final on every for-await exit path

The three early `return turn` paths inside the streaming loop (always-on
loop-detection safety, heuristic loop detection, and the stream Error event)
exited before the final MessageDisplay flush, which only sat after the loop
ended normally. Hook scripts relying on is_final: true to know when to flush
never received it when a turn ended via loop detection or an API error.

Extracts the flush into a shared closure and calls it from all four exits
(the three early returns plus the normal fall-through), instead of only the
one at the bottom of the loop. Adds regression tests for all three previously
missed exits, plus the two guard-coverage tests requested in review (abort
suppresses the flush, a tool-call-only turn with no Content events does not
fire a vacuous empty-text event).

Addresses the outstanding critical review comment and the follow-up test
coverage suggestion on PR #6489.

* fix(hooks): fire MessageDisplay on the ACP surface, coalesce delivery, drain is_final before turn end

Addresses the three findings from the local verification report on #6489:

- ACP/qwen serve (Finding 1): the delivery logic now lives in a shared
  MessageDisplayDispatcher (packages/core), and Session.ts wires it into
  all four raw-stream loops (main prompt, Stop-hook continuation, cron
  tick, background notification) — these surfaces consume GeminiChat's
  stream directly and never enter GeminiClient.sendMessageStream, so
  they need their own fire sites. The daemon no longer advertises an
  event it never emits.

- Slow-hook backlog (Finding 2): the per-message promise chain is
  replaced by coalescing delivery — at most one in-flight request plus
  one pending payload per message; newer flushes overwrite the pending
  slot, which is lossless because displayed_text is cumulative, and
  is_final is sticky. A slow hook now sees fewer, newer payloads instead
  of an ever-growing queue of stale ones.

- Headless is_final drop (Finding 3): finish() resolves only once every
  enqueued payload has actually been delivered, and every exit out of
  the streaming loops awaits it (early returns, normal fall-through,
  and the enclosing finally for uncaught exceptions), so a short-lived
  -p process can no longer exit with the final payload still queued.
  As a consequence, is_final delivery now strictly precedes the Stop
  hook rather than racing it.

Also: the failure log line carries the message_id, finish() is
idempotent, the review-requested tests are added (mid-stream and final
firings share one message_id; isFinal as the sole flush reason), and
hooks.md gains a delivery-semantics contract covering coalescing, the
drain guarantee, no is_final on cancellation, provisional
displayed_text, and multiple messages per tool-using turn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hooks): bound MessageDisplay drain wait, fix test gaps flagged in review

finish() now gives up waiting on drain after 5s (MESSAGE_DISPLAY_DRAIN_TIMEOUT_MS) instead of blocking turn teardown for up to the full 60s hook timeout, per the re-verification's S1 finding. Delivery keeps running in the background past the timeout; only the caller's wait is bounded.

Also: add the config.ts bridge test for MessageDisplay field extraction (S5), and add the missing MessageDisplay/InstructionsLoaded entries to acpAgent.test.ts's HookEventName mock (S6).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hooks): dispatch MessageDisplay is_final alongside stale deliveries, share one drain budget

Round-3 review findings on #6489:

- finish() no longer queues the is_final payload behind an in-flight
  mid-stream delivery: the pending slot's supersession argument applies to
  the in-flight slot too, so the final payload is dispatched immediately,
  alongside the stale delivery if one is still running. is_final is handed
  to the hook the moment the message ends — before Stop — on every surface,
  and can no longer be dropped by a short-lived process exiting with it
  still queued (Finding 1).
- The bounded drain wait is memoized: every finish() call (explicit,
  finally, or concurrent) shares one promise and one timer, so the teardown
  ceiling is MESSAGE_DISPLAY_DRAIN_TIMEOUT_MS itself, not a multiple of it
  (Finding 2).
- hooks.md delivery semantics rewritten to match the shipped behavior,
  including the headless orphaned-hook caveat and the unspecified completion
  order between an overlapped stale execution and the final one (Finding 3).
- The dispatcher mirrors its warnings to console.warn itself (stderr on
  headless/ACP, ink patchConsole in the TUI) in addition to the injected
  debug-file sink, so hitting the drain timeout is visible by default
  (Finding 4).
- A superseded mid-stream delivery that fails after the final was
  dispatched no longer warns; failures during streaming still do.
- New tests: finish() twice while delivery is in flight (the exact
  client.ts sequence), concurrent finish() calls sharing one budget,
  is_final overtaking a held mid-stream delivery, and drain resolving on
  the final delivery alone.

* refactor(core): consolidate MessageDisplay finish() calls, dedupe test spy setup

client.ts: wrap the turn.run() streaming loop in try/finally so messageDisplay.finish() fires once instead of at each of the three early-return sites plus the post-loop path -- matching the pattern the four raw-stream loops in Session.ts already use for the same dispatcher.

message-display-dispatcher.test.ts: centralize the console.warn spy setup/teardown in beforeEach/afterEach instead of five repeated per-test try/finally blocks.

No behavior change: full client.test.ts (246/246) and the message-display-buffer/dispatcher suites (24/24) pass unchanged.

* docs(hooks): clarify MessageDisplay cancellation timing (round-4 nit)

* test(hooks): cover the 3 untested MessageDisplay dispatch sites, fix cancellation doc wording

Adds MessageDisplay is_final coverage for the Stop-hook continuation loop, the in-session cron fire, and the background-notification loop, each with a normal-completion and an abort case. Adds three MessageDisplayDispatcher edge-case tests: a delivery settling just before the drain timeout, an abort arriving after a drain wait has already started, and addChunk called after abort but before finish(). Rewords the cancellation-timing doc bullet to state the actual criterion (abort signal state when finish() runs) rather than an approximation of it.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-11 13:24:32 +00:00
..
channels feat(dingtalk): mention response senders (#6679) 2026-07-11 00:08:13 +00:00
_meta.ts docs: fix model-provider config shape and refresh feature/setting drift (#6552) 2026-07-08 23:04:47 +00:00
approval-mode.md feat(auto-mode): add classifyAllShell setting to route all shell commands through classifier (#6040) 2026-07-01 16:23:24 +08:00
arena.md fix(core): Align MCP OAuth guidance and docs (#5589) 2026-06-24 07:09:53 +08:00
auto-mode.md feat(auto-mode): add classifyAllShell setting to route all shell commands through classifier (#6040) 2026-07-01 16:23:24 +08:00
code-review.md feat(review): give every line of a large diff an accountable reviewer (#6612) 2026-07-10 15:53:09 +00:00
commands.md Fix long session timeline scrolling (#6526) 2026-07-09 11:43:21 +00:00
computer-use.md Fix long session timeline scrolling (#6526) 2026-07-09 11:43:21 +00:00
dual-output.md fix(dual-output): prevent FIFO blocking on startup when no reader connected (#4894) 2026-06-15 06:04:06 +08:00
followup-suggestions.md feat(cli): show follow-up suggestion in input placeholder (#5145) 2026-06-19 13:39:12 +08:00
headless.md docs: correct stale CLI flags/keybinding and document model.reasoningEffort (#6219) 2026-07-03 01:55:45 +00:00
hooks.md feat(hooks): add MessageDisplay hook for mid-turn streaming (#6489) 2026-07-11 13:24:32 +00:00
language.md refactor(cli): revert dynamic slash command LLM translation (#4145) 2026-05-15 16:01:16 +08:00
lsp.md fix(lsp): expose status and startup diagnostics (#3649) 2026-05-17 01:42:28 +08:00
markdown-rendering.md feat(cli): support /copy N to copy Nth-last AI message (#4761) 2026-06-08 10:08:45 +08:00
mcp.md docs: refresh settings, MCP glob, auth alias, and autonomous loop docs (#6090) 2026-07-01 06:43:29 +00:00
memory.md fix(memory): give each linked git worktree its own auto-memory root (#6462) 2026-07-08 14:34:03 +00:00
sandbox.md fix(core): Align MCP OAuth guidance and docs (#5589) 2026-06-24 07:09:53 +08:00
scheduled-tasks.md feat(scheduler): make recurring cron/loop job expiration configurable (#6173) 2026-07-03 06:34:37 +00:00
skills.md docs: fix skill invocation syntax and include Feishu in channel lists (#6320) 2026-07-05 09:49:52 +00:00
status-line.md fix(cli): replace all emoji with Unicode text symbols in TUI rendering (#5999) 2026-06-30 15:18:01 +00:00
structured-output.md docs: user + design docs for --json-schema structured output (#4051) 2026-05-17 23:10:34 +08:00
sub-agents.md fix(agent): stop forking result-bearing work; keep omitted subagent_type awaitable (#5155) 2026-06-16 09:38:10 +08:00
tips.md feat: add contextual tips system with post-response context awareness (#2904) 2026-04-13 17:40:27 +08:00
token-caching.md feat: update docs 2025-12-12 19:37:36 +08:00
tool-use-summaries.md feat(tui): Ctrl+O frozen transcript view and unified tool output rendering (#5666) 2026-07-09 23:40:29 +00:00
worktree.md feat(worktree): Phase D — startup --worktree flag + symlinkDirectories + PR refs (#4381) 2026-05-27 17:04:51 +08:00