feat(client): echo a DNA band hover onto its feed row
Hovering a band on the DNA bar now lights the row it names below — a message band its exact feed row, a tool-schema band the tools group row, the system band the system row — the reverse of the row-hover-to-band link, so the two surfaces read as one surface. The echoed row also scrolls into view, confined to the nearest scrollable ancestor's own scrollport (the capped column): the row only has to reach the scrollport's visible slice, since a capped column usually extends below the page fold and a page scroll would drag the pointer off the pinned band mid-scrub. Clicking a band still lands on the row and expands it.
This commit is contained in:
1 parent
ef478dee24
commit
9217fca879
3 files changed
+52
-2
No files matched your search
@@ -325,6 +325,32 @@ export function makeContextBrowser(
|
||||
&& (rowDnaKey === 'tools' ? view.header !== null
|
||||
: rowDnaKey === 'system' ? view.system !== null
|
||||
: dnaByKey.has(rowDnaKey)) ? rowDnaKey : null
|
||||
// A band hover brings its echoed row into view — within the nearest scrollable ancestor's own scrollport
|
||||
// (the capped column) and nothing above it: the pointer rests on the pinned bar, and a page scroll would
|
||||
// drag it off the band mid-scrub.
|
||||
useLayoutEffect(() => {
|
||||
if (liveDnaKey === null) return
|
||||
/* v8 ignore next 1 -- liveDnaKey non-null implies a mounted card: the ref always resolves. */
|
||||
const row = rootRef.current?.querySelector('.lc-br-elem-echo') ?? null
|
||||
/* v8 ignore next 1 -- a live band names a rendered item, so its echoed row always exists. */
|
||||
if (row === null) return
|
||||
let scroller: HTMLElement | null = row.parentElement
|
||||
while (scroller !== null && scroller.scrollHeight <= scroller.clientHeight) {
|
||||
scroller = scroller.parentElement
|
||||
}
|
||||
/* v8 ignore next 1 -- jsdom lays out nothing: no ancestor ever overflows. */
|
||||
if (scroller === null) return
|
||||
/* v8 ignore start -- real layout only: jsdom measures no boxes. */
|
||||
const rb = row.getBoundingClientRect()
|
||||
const sb = scroller.getBoundingClientRect()
|
||||
// The row only has to reach the scrollport's VISIBLE slice: a capped column usually extends below the
|
||||
// page fold, so its own box bottom is no visibility boundary.
|
||||
const visTop = Math.max(sb.top, 0)
|
||||
const visBottom = Math.min(sb.bottom, window.innerHeight)
|
||||
if (rb.top < visTop) scroller.scrollTop -= visTop - rb.top
|
||||
else if (rb.bottom > visBottom) scroller.scrollTop += rb.bottom - visBottom
|
||||
/* v8 ignore stop */
|
||||
}, [liveDnaKey])
|
||||
const pickDna = (key: string): void => {
|
||||
const it = dnaByKey.get(key)
|
||||
/* v8 ignore next 1 -- the bar only reports keys of the parts it was handed; defensive. */
|
||||
@@ -407,7 +433,9 @@ export function makeContextBrowser(
|
||||
|
||||
/** Expandable element row; `err` carries the red run-state dot right after the chevron so a failed result scans while collapsed.
|
||||
* `opts.open` overrides the `openElem === key` derivation for rows whose key set is wider (the flat feed's tools group);
|
||||
* `opts.onHover` lets a DNA feed row light its own band on the composition bar. */
|
||||
* `opts.onHover` lets a DNA feed row light its own band on the composition bar. The reverse echo — a DNA band
|
||||
* hover lighting its row — derives straight from the band key: a node or the system prompt matches its own row,
|
||||
* a tool-schema band matches the tools group row. */
|
||||
const elemRow = (
|
||||
key: string, tag: ReactNode | null, preview: string,
|
||||
tokens: number, time: number | undefined, body: ReactNode,
|
||||
@@ -417,8 +445,10 @@ export function makeContextBrowser(
|
||||
) => {
|
||||
const open = opts?.open ?? openElem === key
|
||||
const hover = opts?.onHover
|
||||
const echo = dna && liveDnaKey !== null
|
||||
&& (liveDnaKey === key || (key === 'tools' && liveDnaKey.startsWith('tool:')))
|
||||
return (
|
||||
<div key={key} className={'lc-br-elem' + (open ? ' lc-br-elem-on' : '')}>
|
||||
<div key={key} className={'lc-br-elem' + (open ? ' lc-br-elem-on' : '') + (echo ? ' lc-br-elem-echo' : '')}>
|
||||
<button
|
||||
type="button"
|
||||
className="lc-br-elem-row hover:bg-(--dsw-alias-interactive-bg-hover)"
|
||||
|
||||
@@ -88,6 +88,8 @@
|
||||
.lc-br-time { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; }
|
||||
/* A PENDING stamp (a live-tail item no request carries yet) is an estimate; its tooltip names the not-yet-sent step. */
|
||||
.lc-br-time-pending { text-decoration: underline dotted; text-underline-offset: 2px; }
|
||||
/* A DNA band hover echoes onto its feed row: the row's button wears the hover wash while the pointer is on the bar. */
|
||||
.lc-br-elem-echo > .lc-br-elem-row { background-color: var(--dsw-alias-interactive-bg-hover); }
|
||||
/* Call-hit tally on a tool-definition row. */
|
||||
.lc-br-hits { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
||||
.lc-br-content { padding: 2px 6px 8px 26px; display: flex; flex-direction: column; gap: 6px; }
|
||||
|
||||
@@ -326,6 +326,24 @@ describe('ContextBrowser DNA mode and the open-category bar pin', () => {
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('hovering a DNA band echoes onto its feed row; a tool-schema band lights the tools group row', async () => {
|
||||
const m = await mount(h(Browser, props({ data: dnaData, headers: dnaHeaders })))
|
||||
await click(dnaButton(m))
|
||||
// Band n3 (index 4) is feed row index 2 (the feed runs newest-first).
|
||||
await hover(bands(m)[4])
|
||||
assert.ok(flatRows(m)[2].className.includes('lc-br-elem-echo'))
|
||||
assert.ok(flatRows(m).every((r, i) => i === 2 || !r.className.includes('lc-br-elem-echo')))
|
||||
await unhover(bands(m)[4])
|
||||
assert.ok(flatRows(m).every(r => !r.className.includes('lc-br-elem-echo')))
|
||||
// A tool-schema band echoes onto the tools group row; the system band onto the system row.
|
||||
await hover(bands(m)[1])
|
||||
assert.ok(flatRow(m, 'Tool Schemas').closest('.lc-br-elem')?.className.includes('lc-br-elem-echo'))
|
||||
await unhover(bands(m)[1])
|
||||
await hover(bands(m)[0])
|
||||
assert.ok(flatRow(m, 'System Prompt').closest('.lc-br-elem')?.className.includes('lc-br-elem-echo'))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('a tiny item stays a hoverable filament (the minBand floor) while the tooltip reports the true share', async () => {
|
||||
const data = tl({
|
||||
current: { system: 0, tools: 0, user: 2, inject: 0, skill: 0, assistant: 998, tool: 0, total: 1000 },
|
||||
|
||||
Reference in new issue
Block a user