[BUG] Desktop app: plugin Spinner tree (ui.render) is replaced by the default row once the row names a step
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [ ] I am using the latest version of Claude Code (the desktop app bundles 2.1.286; the CLI is at 2.1.289, but the app has not shipped it yet)
What's Wrong?
A plugin hooks on('ui.render', { component: 'Spinner' }, …) and returns its own tree on the desktop surface (a row holding an Svg and the next(e) result). The tree validates, and claude plugin test mounts it on desktop without refusal.
In the Claude desktop app (Code tab):
- While the spinner row's
wordisWorking, the plugin's tree is drawn. - As soon as the row names a step (
Running a command, or a tool call's description), the app draws its default spinner row instead, although it keeps raisingui.renderfor the Spinner and the plugin keeps returning its tree. - The plugin's tree comes back briefly whenever the row returns to
Workingbetween steps.
No ui.render (Spinner) refused line appears in the transcript.
What Should Happen?
The Spinner contract in the plugin types says that on the desktop word is "what the row says its step is doing (Creating notes.md), or Working while the row shows no words". A plugin's tree should be drawn in both cases. If step rows are not meant to be replaceable, the types and docs should say so (and the engine could stop raising ui.render for them).
Error Messages/Logs
Each line is one ui.render Spinner dispatch, logged from inside the hook:
time, surface, requestId, e.props.mode, what the hook returned, hook duration, e.props.word
13:35:03 desktop main requesting svg 3ms 'Working'
13:35:05 desktop main responding svg 1ms 'Working'
13:35:05 desktop main tool-use svg 1ms 'Running a command'
13:35:07 desktop main tool-use svg 1ms 'Note engine start time and wait'
13:35:13 desktop main requesting svg 0ms 'Working'
13:35:14 desktop main tool-use svg 2ms 'Running a command'
13:35:16 desktop main tool-use svg 1ms 'Reading the spinner render trace'
All 15 dispatches in a 13-second turn returned the plugin's tree; none errored; the slowest took 23ms.
On screen the plugin's tree showed only around the 'Working' entries.
Steps to Reproduce
- Write a hooks module with:
``ts``
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
const { Box, Text } = $.ui.resolve(e)
const theirs = await next(e)
return Box({ flexDirection: 'row', columnGap: 1, children: [Text({ color: '#d97757', children: 'PLUGIN' }), theirs] })
})
- Load it in the desktop app (for example
CLAUDE_CODE_PLUGIN_DIRS=<folder>) and restart the app. - Send a prompt that makes Claude run a few tool calls.
PLUGINshows while the row saysWorking, then disappears once the row names the running step, and reappears briefly between steps.
Claude Model
Opus
Is this a regression?
I don't know
Claude Code Version
2.1.286 (bundled with the Claude desktop app)
Platform
Anthropic API (Claude subscription)
Operating System
macOS
Terminal/Shell
Claude desktop app (Code tab)