Repository navigation
fix(ui-web): let a direct sub-agent chat be stopped from its own surface - #1140
Conversation
A turn sent to a sub-agent instance could not be stopped. The instance surface exposed no cancel, and the global stop cannot reach one: it sends turn.cancel with no target, and the server resolves the lane from that target, so the lookup misses every direct turn -- they are registered under direct_lane(...), never under the bare session key. The backend has accepted a target all along; the page just never wired it. Add instanceCancel to the subagents source, calling turn.cancel with the (agent, handle) pair that names the lane, and draw a stop button on the instance composer while that instance is answering -- beside the send button, which keeps its own job of queueing while a turn runs. The busy state clears on the cancelled turn's own error frame, which arrives tagged with the target and settles like a completed turn's, so a refused cancel needs no local undo. A stop whose call fails surfaces its reason in the same line a refused send does. Co-authored-by: GLM (GLM-5.3-Flash) <noreply@z.ai>
|
Windows compatibility review — PR #1140 @ Scope: full base Result: no Windows-specific issue. The change is DOM/state wiring plus an RPC parameter shape; no path, filesystem, subprocess, signal, encoding, or platform primitive. Verification (native Windows 11, isolated worktree Limitations: no real-browser walkthrough and no live gateway; the cancel path is covered through the mocked source. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the full github/main...HEAD diff and the surrounding direct-chat lifecycle. The new source method sends turn.cancel with the same (agent, handle) target used to resolve direct-turn lanes, and the backend tags the cancellation event back to that target, so the existing direct event path clears the busy state and continues any queued work. The optional source capability preserves compatibility with hosts that cannot cancel.
Coverage included AGENTS.md and the Web UI context/architecture rules, the changed code and callers, relevant backend cancellation and event-routing code, nearby history, backward compatibility, test integrity, and a dry merge against the current target branch. No tests were weakened.
Verification:
npm test -- --run src/features/subagents/source.test.ts src/features/subagents/SubagentsPage.test.tsx: 2 files, 101 tests passednpm run type-check: passedgit diff --check github/main...HEAD: passedgit merge-tree --write-tree HEAD github/main: clean
|
No blockers in the reviewed scope. Scope: I reviewed only the What I examined: the base-to-head diff, the direct-lane cancel path this depends on ( Confirmed findings: none in the reviewed scope. Disclosed observations (outside the reviewed scope, none blocking):
Verification (run at 03c5fec in a detached worktree of this head, node 26, Linux):
Checked and deliberately not reported:
|
Summary
A turn sent to a sub-agent instance could not be stopped. The instance surface exposed no cancel, and the global stop cannot reach one: it sends
turn.cancelwith notarget, and the server resolves the lane from that target, so the lookup misses every direct turn - they are registered underdirect_lane(...), never under the bare session key (#459). The backend has accepted a target all along; the page just never wired it.This adds
instanceCancelto the subagents source, callingturn.cancelwith the(agent, handle)pair that names the lane, and draws a stop button on the instance composer while that instance is answering - beside the send button, which keeps its own job of queueing while a turn runs. The busy state clears on the cancelled turn's own error frame, which arrives tagged with the target and settles like a completed turn's, so a refused cancel needs no local undo. A stop whose call fails surfaces its reason in the same line a refused send does. Queued messages stay queued and run after the stop, matching the page composer's own stop semantics.The stop button reuses the shared
.gosend-button class withdata-state="stop"as the marker, the way the page composer's go button names its own state - no new class in the page's namespace.Type
Verification
Commands run (node 24 from /opt/homebrew/opt/node@24, in ui-web/ unless noted):
npx vitest run src/features/subagents/- 136 passed (5 files)npx vitest run- 260 files, 4432 tests passed (4428 before; the 4 new regression tests are 3 in SubagentsPage.test.tsx, 1 in source.test.ts)npx vitest run src/features/subagents/SubagentsPage.test.tsx src/features/subagents/source.test.ts -t "stop"- 3 failed; fix restored - all passnpx tsc --noEmit -p tsconfig.json- exit 0npx eslint src/features/subagents/- 0 errors (2 pre-existing react-hooks warnings, verified present on the unmodified file)node scripts/check-class-namespace.mjs- OKuv run python scripts/check_source_language.py origin/main- exit 0 (this run compares the working tree against origin/main, which is what caught and rejected an earlier CJK test fixture; the fixture text is English now)uv run pytest tests/test_rpc_turn_cancel.py -q(repo root) - 34 passed, includingtest_a_direct_turn_is_cancelled_through_its_target, the backend contract this wires upNo docs update needed: the stop is the same control the page composer already teaches.
Risk
turn.cancelthe page composer already sends, now addressed; no new surfaceRelated Issues
Fixes #459