Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
602c715cbf | ||
|
|
28df7a0e56 | ||
|
|
811871e978 | ||
|
|
fbf1da7452 |
No files matched your search
@@ -2,6 +2,20 @@
|
||||
|
||||
Every release, newest first, generated from the commits in each release.
|
||||
|
||||
## v0.1.5 — 2026-09-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- put the recall chip in the header utilities, not the cell the sidebar's expand button owns
|
||||
|
||||
|
||||
## v0.1.4 — 2026-09-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- register the settings card instead of throwing while building it
|
||||
|
||||
|
||||
## v0.1.3 — 2026-09-26
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -146,18 +146,20 @@ Parameters: `query`, `action`, `limit`, `path` (restrict to a subdirectory),
|
||||
|
||||
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
|
||||
on it, deliberately: at the measured rate a whole conversation's avoided
|
||||
context is a fraction of a penny, and a chip advertising that reads as the tool
|
||||
being useless rather than as the arithmetic being honest. The tooltip carries
|
||||
the full comparison.
|
||||
- a **chip in the session header's utilities** — `Recall 3 · 40× · 457 KB kept
|
||||
out`. No price on it, deliberately: at the measured rate a whole conversation's
|
||||
avoided context is a fraction of a penny, and a chip advertising that reads as
|
||||
the tool being useless rather than as the arithmetic being honest. The tooltip
|
||||
carries the full comparison.
|
||||
- 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
|
||||
read each time. Token counts are bytes ÷ 4."*
|
||||
|
||||
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
|
||||
framework lays it out only while its occupant renders something.
|
||||
recall costs no layout at all. The chip is an entry in the header's `utilities`
|
||||
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
|
||||
projection unit (`recallUsage`) with a wire view the browser reads through
|
||||
|
||||
+35
-9
@@ -4,7 +4,7 @@
|
||||
* Three contributions, each into a seat the Harness 0.1.7 composition declares:
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* **Where plugin settings live now.** 0.1.5 drew plugin configuration as cards
|
||||
@@ -385,6 +385,16 @@ window.__ModuleLoader__.load({
|
||||
*/
|
||||
constructor(ctx) {
|
||||
this.ctx = ctx
|
||||
// Every field the projection reads must exist BEFORE anything projects.
|
||||
//
|
||||
// `SettingsFormModel.bind()` is not a subscription setter: it builds the
|
||||
// snapshot store from one immediate `project()` call, so the projection
|
||||
// runs once, right here, inside this constructor. Assigning `describe`
|
||||
// after `bind` made that first projection read `undefined.getSnapshot()`
|
||||
// — a TypeError thrown while the inject callback was registering the
|
||||
// card, which aborted the registration and left the plugin with no
|
||||
// settings section at all and nothing in the UI to say so.
|
||||
this.describe = ctx.configForms.describe()
|
||||
this.form = ctx.configForms.get(SETTINGS_ENTRY)
|
||||
this.model = new SettingsFormModel(
|
||||
this.form,
|
||||
@@ -399,8 +409,6 @@ window.__ModuleLoader__.load({
|
||||
})),
|
||||
)
|
||||
this.store = this.model.bind(() => this.projection())
|
||||
/** The describe mirror that knows whether a secret is configured. */
|
||||
this.describe = ctx.configForms.describe()
|
||||
}
|
||||
|
||||
/** Everything the page renders, rebuilt whenever the form or a draft moves. */
|
||||
@@ -612,8 +620,8 @@ window.__ModuleLoader__.load({
|
||||
|
||||
function RecallChip(props) {
|
||||
const usage = useUsage(props)
|
||||
// The corner is laid out only while its occupant renders something, so a
|
||||
// conversation that never called the tool costs no layout at all.
|
||||
// The utilities list draws nothing for an entry that renders nothing, so
|
||||
// a conversation that never called the tool costs no layout at all.
|
||||
if (usage === undefined || !usage.searches) return null
|
||||
return h(
|
||||
'span',
|
||||
@@ -727,10 +735,28 @@ window.__ModuleLoader__.load({
|
||||
)
|
||||
})
|
||||
|
||||
// The chip: the header's far-right corner, which the framework lays out
|
||||
// only while its occupant renders something.
|
||||
ctx.slots.inject('conversation.session.header.corner', () =>
|
||||
ctx.slots.register({ name: 'conversation.session.header.corner' }, (props) => h(RecallChip, props)),
|
||||
// The chip: a right-aligned session utility, next to the header's
|
||||
// far-right corner.
|
||||
//
|
||||
// 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
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dsh-recall",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Local-embedding code recall for DeepSeek Harness — returns ranked excerpts with line ranges instead of whole files, so the context window stops growing with every read.",
|
||||
|
||||
+32
-12
@@ -109,6 +109,11 @@ function makePrimitives(record) {
|
||||
|
||||
bind(project) {
|
||||
this.project = project
|
||||
// Faithful to the framework: `bind` is not a subscription setter, it
|
||||
// builds the snapshot store from ONE immediate projection. A stub that
|
||||
// only stored the projection would let a controller read a field it has
|
||||
// not assigned yet — which is exactly the bug this reproduces.
|
||||
project()
|
||||
return { getSnapshot: () => project(), subscribe: () => () => {} }
|
||||
}
|
||||
|
||||
@@ -400,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.
|
||||
const { registered, tabTypes } = mount({ withConfigForms: false })
|
||||
assert.deepEqual(registered.map((item) => item.meta.name).sort(), [
|
||||
'conversation.session.header.corner',
|
||||
'conversation.session.header.utilities',
|
||||
'sidebar.right.pane.tab',
|
||||
])
|
||||
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 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.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', () => {
|
||||
@@ -435,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', () => {
|
||||
const { registered } = mount()
|
||||
assert.deepEqual(registered.map((item) => item.meta.name).sort(), [
|
||||
'conversation.session.header.corner',
|
||||
'conversation.session.header.utilities',
|
||||
'plugins.bundle.config',
|
||||
'sidebar.right.pane.tab',
|
||||
])
|
||||
@@ -585,17 +605,17 @@ function usageView(overrides = {}) {
|
||||
|
||||
test('the header chip renders nothing when the conversation never used it', () => {
|
||||
const { renderSlot } = mount()
|
||||
// The corner is laid out only while its occupant renders something, so 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.corner', { useProjection: () => usageView({ searches: 0, excerpts: 0 }) }), null)
|
||||
// The utilities list draws nothing for an entry that renders nothing, so
|
||||
// this is the state that must cost no space at all.
|
||||
assert.equal(renderSlot('conversation.session.header.utilities', { useProjection: () => undefined }), null)
|
||||
assert.equal(renderSlot('conversation.session.header.utilities', { useProjection: () => usageView({ searches: 0, excerpts: 0 }) }), null)
|
||||
// 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', () => {
|
||||
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)
|
||||
const text = String(chip.props.children)
|
||||
assert.match(text, /Recall 3/)
|
||||
@@ -608,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', () => {
|
||||
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
|
||||
assert.match(detail, /3 searches/)
|
||||
assert.match(detail, /9 excerpts from 5 files/)
|
||||
|
||||
Reference in new issue
Block a user