fix: put the recall chip in the header utilities, not the cell the sidebar's expand button owns
Release / release (push) Successful in 6s

The chip registered into `conversation.session.header.corner`, which is a
SINGLE cell the product already occupies: the client catalog lists its
occupants as `client-ui-sidebar-right ExpandButton` and its replaceRisk as
`shadows-shipped-ui`. Two registrations at priority 0 in a single cell are a
hard error inside the slot registry, and the error lands in the OTHER
plugin's `slots.inject()` callback: the renderer's `changed` handler catches
it, calls `stop()` on that inject controller — killing that contribution for
the rest of the fiber's life — and rethrows it from a microtask, which is the
uncaught `single slot ... already has a registration at priority 0 (registered
by dsh-recall)` in the console. So the chip was not merely racing the shipped
right-sidebar expand control; it was taking it down.

It now registers into `conversation.session.header.utilities`, a list cell
(`replaceRisk: none`, right-aligned, same Session scope and therefore the same
`useProjection` prop) as `{ id: 'dsh-recall', order: 20 }`, so it stays at the
right-hand end without competing for a seat.

The suite could not have caught this: the slot stub accepts any registration
and knows nothing about cardinality, keys or collisions. It now asserts the
chip carries a list id and that this plugin registers nothing into the corner.
This commit is contained in:
NotExpectedYet committed 2026-09-27 06:36:49 +01:00
1 parent 811871e978
commit 28df7a0e56
3 files changed
+61 -26

No files matched your search

+9 -7
View File
@@ -146,18 +146,20 @@ Parameters: `query`, `action`, `limit`, `path` (restrict to a subdirectory),
Two surfaces, both reading a host projection rather than polling: Two surfaces, both reading a host projection rather than polling:
- a **chip in the session header** — `Recall 3 · 40× · 457 KB kept out`. No price - a **chip in the session header's utilities** — `Recall 3 · 40× · 457 KB kept
on it, deliberately: at the measured rate a whole conversation's avoided out`. No price on it, deliberately: at the measured rate a whole conversation's
context is a fraction of a penny, and a chip advertising that reads as the tool avoided context is a fraction of a penny, and a chip advertising that reads as
being useless rather than as the arithmetic being honest. The tooltip carries the tool being useless rather than as the arithmetic being honest. The tooltip
the full comparison. carries the full comparison.
- a **right-sidebar Recall tab** — the same figures with room for what they are - a **right-sidebar Recall tab** — the same figures with room for what they are
not: *"An estimate, and an upper bound: it assumes a whole file would have been not: *"An estimate, and an upper bound: it assumes a whole file would have been
read each time. Token counts are bytes ÷ 4."* read each time. Token counts are bytes ÷ 4."*
Both render nothing when there is nothing to say — a conversation that never used Both render nothing when there is nothing to say — a conversation that never used
recall costs no layout at all, which matters for the header corner because the recall costs no layout at all. The chip is an entry in the header's `utilities`
framework lays it out only while its occupant renders something. list, carrying its own id. The far-right corner is deliberately left alone: it is
a single cell that the product's own right-sidebar expand control occupies, so a
registration there shadows shipped UI rather than sitting beside it.
The data path is `presentationMeta` → the `tool/result` event's `meta` → a The data path is `presentationMeta` → the `tool/result` event's `meta` → a
projection unit (`recallUsage`) with a wire view the browser reads through projection unit (`recallUsage`) with a wire view the browser reads through
+25 -7
View File
@@ -4,7 +4,7 @@
* Three contributions, each into a seat the Harness 0.1.7 composition declares: * Three contributions, each into a seat the Harness 0.1.7 composition declares:
* *
* 1. the plugin's configuration page, in the **Plugins** panel; * 1. the plugin's configuration page, in the **Plugins** panel;
* 2. a one-line usage chip in a conversation's header corner; * 2. a one-line usage chip in a conversation's header utilities;
* 3. a right-sidebar tab with the same figures and room for their caveats. * 3. a right-sidebar tab with the same figures and room for their caveats.
* *
* **Where plugin settings live now.** 0.1.5 drew plugin configuration as cards * **Where plugin settings live now.** 0.1.5 drew plugin configuration as cards
@@ -620,8 +620,8 @@ window.__ModuleLoader__.load({
function RecallChip(props) { function RecallChip(props) {
const usage = useUsage(props) const usage = useUsage(props)
// The corner is laid out only while its occupant renders something, so a // The utilities list draws nothing for an entry that renders nothing, so
// conversation that never called the tool costs no layout at all. // a conversation that never called the tool costs no layout at all.
if (usage === undefined || !usage.searches) return null if (usage === undefined || !usage.searches) return null
return h( return h(
'span', 'span',
@@ -735,10 +735,28 @@ window.__ModuleLoader__.load({
) )
}) })
// The chip: the header's far-right corner, which the framework lays out // The chip: a right-aligned session utility, next to the header's
// only while its occupant renders something. // far-right corner.
ctx.slots.inject('conversation.session.header.corner', () => //
ctx.slots.register({ name: 'conversation.session.header.corner' }, (props) => h(RecallChip, props)), // It is in the `utilities` LIST, not the corner, and that is not a
// cosmetic choice. The corner is a `single` cell whose occupant the
// product already declares — `client-ui-sidebar-right ExpandButton`, the
// right sidebar's expand control — and the client catalog reports it as
// `replaceRisk: "shadows-shipped-ui"`. A second registration at the same
// priority is a hard error inside the slot registry, thrown while the
// other plugin's `slots.inject()` callback runs: the renderer catches it,
// STOPS that inject controller for the rest of the fiber's life, and
// rethrows it uncaught. So squatting there did not merely race — it took
// the shipped expand control down and logged an uncaught error. A list
// entry carrying its own id cannot collide with anyone, and the framework
// draws nothing for an entry that renders nothing.
ctx.slots.inject('conversation.session.header.utilities', () =>
ctx.slots.register(
// `order` ascends left to right, so a high value keeps the chip at
// the right-hand end of the utilities, where it was before.
{ name: 'conversation.session.header.utilities', id: 'dsh-recall', order: 20 },
(props) => h(RecallChip, props),
),
) )
// The right-sidebar tab is registered in two stages, and the second is // The right-sidebar tab is registered in two stages, and the second is
+27 -12
View File
@@ -405,17 +405,32 @@ test('the chip and the sidebar tab survive a deployment with no settings provide
// profile without a settings provider loses the page and keeps the rest. // profile without a settings provider loses the page and keeps the rest.
const { registered, tabTypes } = mount({ withConfigForms: false }) const { registered, tabTypes } = mount({ withConfigForms: false })
assert.deepEqual(registered.map((item) => item.meta.name).sort(), [ assert.deepEqual(registered.map((item) => item.meta.name).sort(), [
'conversation.session.header.corner', 'conversation.session.header.utilities',
'sidebar.right.pane.tab', 'sidebar.right.pane.tab',
]) ])
assert.equal(tabTypes.length, 1, 'the tab type is independent of settings') assert.equal(tabTypes.length, 1, 'the tab type is independent of settings')
}) })
test('the header chip is a single-seat contribution with no key', () => { test('the header chip is an additive list entry carrying its own id', () => {
// The header's far-right corner is a SINGLE cell, and the product already
// occupies it with its right-sidebar expand control — the client catalog
// reports that key as `replaceRisk: "shadows-shipped-ui"`. A second
// registration at the same priority is a hard error inside the registry,
// thrown while the OTHER plugin's `slots.inject()` callback runs: the
// renderer catches it, stops that controller for the rest of the fiber's
// life, and rethrows it uncaught. So the chip belongs in a list cell, where
// a unique id makes it additive.
const { registered } = mount() const { registered } = mount()
const chip = registered.find((item) => item.meta.name === 'conversation.session.header.corner') const chip = registered.find((item) => item.meta.name === 'conversation.session.header.utilities')
assert.ok(chip !== undefined) assert.ok(chip !== undefined)
assert.equal(chip.meta.key, undefined, 'the corner is a single seat, so it takes no key') assert.equal(chip.meta.id, 'dsh-recall', 'a list cell is addressed by its id')
assert.equal(typeof chip.meta.order, 'number', 'the order places the cell within the list')
assert.equal(chip.meta.key, undefined, 'a list cell takes an id, not a key')
assert.deepEqual(
registered.filter((item) => item.meta.name === 'conversation.session.header.corner'),
[],
'the corner is a single cell the product owns; a registration there shadows shipped UI',
)
}) })
test('the sidebar tab registers a type AND a body under the same id', () => { test('the sidebar tab registers a type AND a body under the same id', () => {
@@ -440,7 +455,7 @@ test('the sidebar tab registers a type AND a body under the same id', () => {
test('the browser half contributes three seats, none of them the retired 0.1.5 one', () => { test('the browser half contributes three seats, none of them the retired 0.1.5 one', () => {
const { registered } = mount() const { registered } = mount()
assert.deepEqual(registered.map((item) => item.meta.name).sort(), [ assert.deepEqual(registered.map((item) => item.meta.name).sort(), [
'conversation.session.header.corner', 'conversation.session.header.utilities',
'plugins.bundle.config', 'plugins.bundle.config',
'sidebar.right.pane.tab', 'sidebar.right.pane.tab',
]) ])
@@ -590,17 +605,17 @@ function usageView(overrides = {}) {
test('the header chip renders nothing when the conversation never used it', () => { test('the header chip renders nothing when the conversation never used it', () => {
const { renderSlot } = mount() const { renderSlot } = mount()
// The corner is laid out only while its occupant renders something, so this // The utilities list draws nothing for an entry that renders nothing, so
// is the state that must cost no space at all. // this is the state that must cost no space at all.
assert.equal(renderSlot('conversation.session.header.corner', { useProjection: () => undefined }), null) assert.equal(renderSlot('conversation.session.header.utilities', { useProjection: () => undefined }), null)
assert.equal(renderSlot('conversation.session.header.corner', { useProjection: () => usageView({ searches: 0, excerpts: 0 }) }), null) assert.equal(renderSlot('conversation.session.header.utilities', { useProjection: () => usageView({ searches: 0, excerpts: 0 }) }), null)
// And a host without the projection service at all. // And a host without the projection service at all.
assert.equal(renderSlot('conversation.session.header.corner', {}), null) assert.equal(renderSlot('conversation.session.header.utilities', {}), null)
}) })
test('the header chip leads with what was kept out, not what it cost', () => { test('the header chip leads with what was kept out, not what it cost', () => {
const { renderSlot } = mount() const { renderSlot } = mount()
const chip = renderSlot('conversation.session.header.corner', { useProjection: () => usageView() }) const chip = renderSlot('conversation.session.header.utilities', { useProjection: () => usageView() })
assert.ok(chip !== null) assert.ok(chip !== null)
const text = String(chip.props.children) const text = String(chip.props.children)
assert.match(text, /Recall 3/) assert.match(text, /Recall 3/)
@@ -613,7 +628,7 @@ test('the header chip leads with what was kept out, not what it cost', () => {
test('the chip tooltip carries the figures the chip has no room for', () => { test('the chip tooltip carries the figures the chip has no room for', () => {
const { renderSlot } = mount() const { renderSlot } = mount()
const chip = renderSlot('conversation.session.header.corner', { useProjection: () => usageView({ rereadTokens: 500_000 }) }) const chip = renderSlot('conversation.session.header.utilities', { useProjection: () => usageView({ rereadTokens: 500_000 }) })
const detail = chip.props.title const detail = chip.props.title
assert.match(detail, /3 searches/) assert.match(detail, /3 searches/)
assert.match(detail, /9 excerpts from 5 files/) assert.match(detail, /9 excerpts from 5 files/)