feat(client): focus the trend chart bars on the browser's open category
Expanding a category in the Context browser now plots every trend bar on
that category alone: the axis rescales to the category's own max (provider
anchor preserved) in both total and delta modes, the hover tip names the
category, and the card subtitle reads "Showing {cat} only" until the
category collapses. The /context modal keeps its unfocused chart.
This commit is contained in:
1 parent
1eb10e64e2
commit
bcbbc3367f
7 files changed
+203
-13
No files matched your search
@@ -51,6 +51,11 @@ export interface ContextBrowserProps {
|
||||
onNodeFocusHandled?: () => void
|
||||
hoverKey?: string | null
|
||||
onHoverKey?: (key: string | null) => void
|
||||
/**
|
||||
* Reports the open category (null once none opens): the Context tab focuses the trend chart's bars on it.
|
||||
* Absent (the /context modal) — the accordion stays purely internal.
|
||||
*/
|
||||
onOpenCat?: (cat: string | null) => void
|
||||
loadImage?: ImageLoader
|
||||
/**
|
||||
* The timeline source's detail state (split generation): while the first
|
||||
@@ -584,6 +589,13 @@ export function makeContextBrowser(
|
||||
// compares epochs.
|
||||
const [rowQuery, setRowQuery] = useState('')
|
||||
const [toolSort, setToolSort] = useState<'size' | 'name'>('size')
|
||||
// Every open-category change (toggle, step pick, pin, brief reveal) reports outward so the Context tab
|
||||
// can focus the trend chart on the open category.
|
||||
const onOpenCat = props.onOpenCat
|
||||
const setCat = (c: string | null): void => {
|
||||
setOpenCat(c)
|
||||
if (onOpenCat !== undefined) onOpenCat(c)
|
||||
}
|
||||
|
||||
// Full message content: the conversation-window join first (zero cost),
|
||||
// plus nodes fetched on demand for seqs outside the window (node arrays
|
||||
@@ -620,9 +632,9 @@ export function makeContextBrowser(
|
||||
const pinSeq = props.pinSeq
|
||||
useEffect(() => {
|
||||
setSel(pinSeq === null || pinSeq === undefined ? 'live' : pinSeq)
|
||||
setOpenCat(null)
|
||||
setCat(null)
|
||||
setOpenElem(null)
|
||||
}, [pinSeq])
|
||||
}, [pinSeq, onOpenCat])
|
||||
// Step-brief reveal: select the owning step, open the node's category + element (the pagination effect above already pulls older
|
||||
// history for a missing join), then arm a one-shot scroll consumed by the layout effect once the row renders.
|
||||
const rootRef = useRef<HTMLDivElement | null>(null)
|
||||
@@ -631,11 +643,11 @@ export function makeContextBrowser(
|
||||
useEffect(() => {
|
||||
if (nodeFocus === null || nodeFocus === undefined) return
|
||||
setSel(nodeFocus.step)
|
||||
setOpenCat(nodeFocus.cat)
|
||||
setCat(nodeFocus.cat)
|
||||
setOpenElem('n' + String(nodeFocus.seq))
|
||||
focusScrollRef.current = true
|
||||
if (props.onNodeFocusHandled !== undefined) props.onNodeFocusHandled()
|
||||
}, [nodeFocus, props.onNodeFocusHandled])
|
||||
}, [nodeFocus, props.onNodeFocusHandled, onOpenCat])
|
||||
useLayoutEffect(() => {
|
||||
if (!focusScrollRef.current) return
|
||||
focusScrollRef.current = false
|
||||
@@ -681,7 +693,7 @@ export function makeContextBrowser(
|
||||
const total = breakdown.total
|
||||
const pick = (v: string) => {
|
||||
setSel(v === 'live' ? 'live' : Number(v))
|
||||
setOpenCat(null)
|
||||
setCat(null)
|
||||
setOpenElem(null)
|
||||
}
|
||||
|
||||
@@ -718,11 +730,11 @@ export function makeContextBrowser(
|
||||
|| ((c === 'system' || c === 'tools') && view.header === null)
|
||||
if (!openable) return
|
||||
if (openCat === c) {
|
||||
setOpenCat(null)
|
||||
setCat(null)
|
||||
setOpenElem(null)
|
||||
return
|
||||
}
|
||||
setOpenCat(c)
|
||||
setCat(c)
|
||||
// A different category opens unfiltered — the lens belongs to the open one.
|
||||
setRowQuery('')
|
||||
setOpenElem(singleKeyOf(c))
|
||||
|
||||
@@ -105,6 +105,8 @@ export function makeContextView(
|
||||
// turn-level pin below (mirrors the strip-click's consume-once focus flow).
|
||||
const [jumpSeq, setJumpSeq] = useState<number | null>(null)
|
||||
const [hoverCat, setHoverCat] = useState<string | null>(null)
|
||||
// The browser's open category: the trend card focuses its bars on it (collapsing the category restores all).
|
||||
const [focusCat, setFocusCat] = useState<string | null>(null)
|
||||
const [pickedKinds, setPickedKinds] = useState<string[]>([...EVENT_KINDS])
|
||||
const toggleKind = (k: string) => {
|
||||
setPickedKinds((p) => {
|
||||
@@ -368,7 +370,7 @@ export function makeContextView(
|
||||
<div className="lc-card">
|
||||
<div className="lc-card-title">
|
||||
<span className="lc-card-title-text">{t('trend.title')}</span>
|
||||
<span className="lc-card-sub">{t('trend.hint')}</span>
|
||||
<span className="lc-card-sub">{focusCat !== null ? t('trend.focus', { cat: kit.catLabel(focusCat) }) : t('trend.hint')}</span>
|
||||
<div className="lc-trend-ctl">
|
||||
<div className="lc-gran">
|
||||
<button
|
||||
@@ -415,6 +417,7 @@ export function makeContextView(
|
||||
mode={trendMode}
|
||||
focusTurn={focusTurn}
|
||||
hoverCat={trendHoverCat}
|
||||
focusCat={focusCat}
|
||||
onSelect={setSelectedSeq}
|
||||
onHover={setHoveredSeq}
|
||||
onHoverTurn={setHoverTurn}
|
||||
@@ -453,6 +456,7 @@ export function makeContextView(
|
||||
pinSeq={pinnedReq !== null ? pinnedReq.seq : null}
|
||||
hoverKey={hoverCat}
|
||||
onHoverKey={setHoverCat}
|
||||
onOpenCat={setFocusCat}
|
||||
nodeFocus={nodeFocus}
|
||||
onNodeFocusHandled={clearNodeFocus}
|
||||
loadImage={loadImage}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { memo, useLayoutEffect, useMemo, useRef, type ReactElement, type UIEvent } from 'react'
|
||||
import type { ContextEventRecord, RequestRecord } from '../../shared/types'
|
||||
import type { Category, ContextEventRecord, RequestRecord } from '../../shared/types'
|
||||
import { CATS } from '../categories'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
@@ -19,6 +19,12 @@ export interface TrendChartProps {
|
||||
focusTurn: number | null
|
||||
/** Mirrored category hover (shared with the overview and the browser): lights that category's segment in every bar. */
|
||||
hoverCat: string | null
|
||||
/**
|
||||
* The browser's open category: every bar plots only that category (provider-anchored), with the axis rescaled
|
||||
* to its own max. Null plots every category; an unrecognized key (a stale or hostile state) degrades to the
|
||||
* unfocused chart.
|
||||
*/
|
||||
focusCat?: string | null
|
||||
onSelect: (seq: number | null) => void
|
||||
onHover: (seq: number | null) => void
|
||||
onHoverTurn: (turn: number | null) => void
|
||||
@@ -75,7 +81,7 @@ export function jumpTargetOf(requests: RequestRecord[], seq: number): RequestRec
|
||||
}
|
||||
|
||||
export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactElement {
|
||||
const { t, fmt, eventLabel, eventAt } = kit
|
||||
const { t, fmt, eventLabel, eventAt, catLabel } = kit
|
||||
|
||||
const CHART_H = 112
|
||||
// Quarter-mark label tops for the axis (mirrored to .lc-axis-q1/.lc-axis-q3 in trendChart.css): chart top 18
|
||||
@@ -112,6 +118,20 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactE
|
||||
const barTotalOf = (req: RequestRecord): number =>
|
||||
typeof req.prompt === 'number' && req.prompt > 0 ? req.prompt : req.total
|
||||
|
||||
/**
|
||||
* Focus a bar on one category (the browser's open category): the kept bucket carries its provider-anchored
|
||||
* value, the other buckets zero, and the provider prompt drops — the downstream stack/anchor/tooltip math
|
||||
* reads the derived record unchanged (`total` IS the plotted figure; the anchor becomes a no-op).
|
||||
*/
|
||||
const focusOf = (req: RequestRecord, cat: string): RequestRecord => {
|
||||
const key = cat as Category | 'system' | 'tools'
|
||||
const { prompt: _prompt, output: _output, ...out } = req
|
||||
const v = Math.round((req[key] || 0) * anchorOf(req))
|
||||
out.total = v
|
||||
for (const c of CATS) out[c.key] = c.key === key ? v : 0
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Delta mode: each category keeps the SIGNED change vs the previous record so bars can diverge
|
||||
* above/below the zero line; `total` is the churn (summed magnitude), `net` the signed change
|
||||
@@ -212,9 +232,16 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactE
|
||||
|
||||
return function TrendChart(props: TrendChartProps): ReactElement {
|
||||
const delta = props.mode === 'delta'
|
||||
// An unrecognized focus key degrades to the unfocused chart instead of plotting an empty axis.
|
||||
const focus = props.focusCat !== null && props.focusCat !== undefined && CATS.some(c => c.key === props.focusCat)
|
||||
? props.focusCat
|
||||
: null
|
||||
const requests = useMemo(
|
||||
() => (delta ? props.requests.map((req, i) => deltaOf(req, i > 0 ? props.requests[i - 1] : null)) : props.requests),
|
||||
[props.requests, delta],
|
||||
() => {
|
||||
const base = focus !== null ? props.requests.map(req => focusOf(req, focus)) : props.requests
|
||||
return delta ? base.map((req, i) => deltaOf(req, i > 0 ? base[i - 1] : null)) : base
|
||||
},
|
||||
[props.requests, delta, focus],
|
||||
)
|
||||
const markers = props.markers
|
||||
let maxTotal = 1
|
||||
@@ -400,7 +427,10 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactE
|
||||
const n = req.net ?? 0
|
||||
return [head, t('tip.delta', { n: (n > 0 ? '+' : '') + fmt(n) })]
|
||||
}
|
||||
return [head, t('tip.total', { n: fmt(barTotalOf(req)) })]
|
||||
// Focused: the metric row IS the focused category's figure, so the tip names it instead of claiming a total.
|
||||
return [head, focus !== null
|
||||
? t('tip.cat', { cat: catLabel(focus), n: fmt(barTotalOf(req)) })
|
||||
: t('tip.total', { n: fmt(barTotalOf(req)) })]
|
||||
}
|
||||
const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex(r => r.seq === props.hoveredSeq) : -1
|
||||
const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null
|
||||
|
||||
@@ -58,6 +58,7 @@ export const DICT_ZH: Record<string, string> = {
|
||||
'gran.total': '全量', 'gran.delta': '增量',
|
||||
'gran.modeHint': '全量:累计构成;增量:相对上一条请求的变化量',
|
||||
'trend.hint': '✂ 表示压缩/剪枝,步骤/轮次 切换粒度',
|
||||
'trend.focus': '仅展示 {cat}',
|
||||
'trend.empty': '发起一轮对话后,这里会展示每次模型请求的上下文构成',
|
||||
'detail.step': '第 {t} 轮 · 第 {s} 步',
|
||||
'detail.turn': '第 {t} 轮 · 共 {n} 步',
|
||||
@@ -149,6 +150,7 @@ export const DICT_ZH: Record<string, string> = {
|
||||
'tip.turn': '第 {t} 轮 · 共 {n} 步',
|
||||
'tip.turn1': '第 {t} 轮 · 共 1 步',
|
||||
'tip.total': '合计 ≈ {n}',
|
||||
'tip.cat': '{cat} ≈ {n}',
|
||||
'tip.delta': 'Δ {n}',
|
||||
'ev.compaction': '压缩上下文(摘要替换 {n} 条消息)',
|
||||
'ev.prune': '剪枝工具输出',
|
||||
@@ -279,6 +281,7 @@ export const DICT_EN: Record<string, string> = {
|
||||
'gran.total': 'Total', 'gran.delta': 'Delta',
|
||||
'gran.modeHint': 'Total: cumulative makeup; Delta: change vs the previous request',
|
||||
'trend.hint': '✂ marks compaction/prune, Step/Turn switches granularity',
|
||||
'trend.focus': 'Showing {cat} only',
|
||||
'trend.empty': 'Send a message and each model request’s context makeup shows up here',
|
||||
'detail.step': 'Turn {t} · Step {s}',
|
||||
'detail.turn': 'Turn {t} · {n} steps',
|
||||
@@ -370,6 +373,7 @@ export const DICT_EN: Record<string, string> = {
|
||||
'tip.turn': 'Turn {t} · {n} steps',
|
||||
'tip.turn1': 'Turn {t} · 1 step',
|
||||
'tip.total': 'Total ≈ {n}',
|
||||
'tip.cat': '{cat} ≈ {n}',
|
||||
'tip.delta': 'Δ {n}',
|
||||
'ev.compaction': 'Context compacted (summary replaced {n} messages)',
|
||||
'ev.prune': 'Tool output pruned',
|
||||
|
||||
@@ -1504,3 +1504,45 @@ describe('ContextBrowser — the split generation detail states', () => {
|
||||
await m.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ContextBrowser open-category reporting', () => {
|
||||
test('toggling categories and step picks report the open category outward', async () => {
|
||||
const data = tl({
|
||||
requests: [req({ seq: 10, turn: 1, step: 0 }), req({ seq: 20, turn: 2, step: 0 })],
|
||||
nodes: [
|
||||
node({ seq: 1, cat: 'user', text: 'hi' }),
|
||||
node({ seq: 2, cat: 'user', text: 'again' }),
|
||||
node({ seq: 3, cat: 'tool', tokens: 30, tool: 'bash', text: 'out' }),
|
||||
],
|
||||
})
|
||||
const opens: (string | null)[] = []
|
||||
const onOpenCat = (c: string | null): void => { opens.push(c) }
|
||||
const m = await mount(h(Browser, props({ data, onOpenCat })))
|
||||
assert.deepEqual(opens, [null], 'the mount-time pin pass reports the closed accordion')
|
||||
await click(catRow(m, 'user'))
|
||||
assert.deepEqual(opens, [null, 'user'])
|
||||
await click(catRow(m, 'user'))
|
||||
assert.deepEqual(opens, [null, 'user', null])
|
||||
await click(catRow(m, 'tool'))
|
||||
assert.deepEqual(opens, [null, 'user', null, 'tool'])
|
||||
await pickStep(m, '20')
|
||||
assert.deepEqual(opens, [null, 'user', null, 'tool', null], 'a step pick closes the open category')
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('a pin change and a brief reveal report their reset / target category', async () => {
|
||||
const data = tl({ requests: [req({ seq: 10, turn: 1, step: 0 })], nodes: [node({ seq: 1, cat: 'user', text: 'hi' })] })
|
||||
const opens: (string | null)[] = []
|
||||
const onOpenCat = (c: string | null): void => { opens.push(c) }
|
||||
const m = await mount(h(Browser, props({ data, onOpenCat })))
|
||||
await m.update(h(Browser, props({ data, onOpenCat, pinSeq: 10 })))
|
||||
assert.deepEqual(opens, [null, null], 'pinning resets the accordion')
|
||||
await m.update(h(Browser, props({
|
||||
data, onOpenCat, pinSeq: 10,
|
||||
nodeFocus: { step: 'live', seq: 1, cat: 'tool' },
|
||||
onNodeFocusHandled: () => {},
|
||||
})))
|
||||
assert.deepEqual(opens, [null, null, 'tool'], 'the reveal opens the node category')
|
||||
await m.unmount()
|
||||
})
|
||||
})
|
||||
@@ -385,6 +385,30 @@ describe('ContextView — interactions', () => {
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('expanding a browser category focuses the trend bars on it; collapsing restores all', async () => {
|
||||
const m = await mountRich('sv-focus')
|
||||
assert.equal(text(query(m.container, '.lc-axis-top')), '420', 'unfocused: the axis tops at the largest whole-bar total')
|
||||
// Five segments on the bar: richTimeline carries no injects, so inject renders none.
|
||||
assert.equal(queryAll(m.container, '.lc-bar[data-seq="4"] .lc-bar-stack > .lc-cat-seg').length, 5)
|
||||
|
||||
// Expanding the browser's assistant category focuses every bar on it — one segment per bar, the axis
|
||||
// rescaled to the category's own max (20/60/80; the first rides its provider-prompt anchor to 21).
|
||||
await click(queryAll(m.container, '.lc-br-cat-row')[4])
|
||||
assert.equal(text(query(m.container, '.lc-axis-top')), '80')
|
||||
const segs = queryAll(m.container, '.lc-bar .lc-bar-stack > .lc-cat-seg')
|
||||
assert.equal(segs.length, 3)
|
||||
for (const s of segs) assert.equal(s.getAttribute('data-cat'), 'assistant')
|
||||
assert.ok(text(m.container).includes(DICT_EN['trend.focus'].replace('{cat}', DICT_EN['cat.assistant'])),
|
||||
'the card subtitle names the focused category')
|
||||
|
||||
// Collapsing the category restores the whole composition and the usage hint.
|
||||
await click(queryAll(m.container, '.lc-br-cat-row')[4])
|
||||
assert.equal(text(query(m.container, '.lc-axis-top')), '420')
|
||||
assert.equal(queryAll(m.container, '.lc-bar[data-seq="4"] .lc-bar-stack > .lc-cat-seg').length, 5)
|
||||
assert.ok(text(m.container).includes(DICT_EN['trend.hint']))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('delta mode pairs the detail with the previous record; first bar has none', async () => {
|
||||
const m = await mountRich('sv-delta')
|
||||
const chart = query(m.container, '.lc-chart')
|
||||
|
||||
@@ -466,6 +466,80 @@ describe('TrendChart category hover-link', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('TrendChart category focus (the browser open category)', () => {
|
||||
const r1 = req(1, { turn: 1, step: 0 })
|
||||
const r2 = req(2, { turn: 1, step: 1, system: 200, tools: 100, user: 60, inject: 40, assistant: 80, tool: 120, total: 600 })
|
||||
|
||||
test('total mode plots only the focused category, rescaled to its own max, and the tip names it', async () => {
|
||||
const m = await mount(h(TrendChart, propsOf([r1, r2], { focusCat: 'tool' })))
|
||||
// maxTotal follows the focused figure (60 / 120), not the bars' whole compositions (300 / 600).
|
||||
assert.equal(query(m.container, '.lc-axis-top').textContent, '120')
|
||||
const bs = bars(m.container)
|
||||
const segs1 = queryAll(bs[0], '.lc-bar-stack > div')
|
||||
assert.equal(segs1.length, 1)
|
||||
assert.equal(segs1[0].getAttribute('data-cat'), 'tool')
|
||||
assertColor(segs1[0].style.background, CATS[5].color)
|
||||
assert.equal(segs1[0].style.height, `${Math.round(60 / 120 * CHART_H)}px`)
|
||||
assert.equal(queryAll(bs[1], '.lc-bar-stack > div')[0].style.height, `${CHART_H}px`)
|
||||
|
||||
// The tooltip's metric row names the focused category and carries its own figure.
|
||||
await m.update(h(TrendChart, propsOf([r1, r2], { focusCat: 'tool', hoveredSeq: 1 })))
|
||||
assert.deepEqual(
|
||||
queryAll(query(m.container, '.lc-chart-tip'), 'span').map(r => r.textContent),
|
||||
[kit.t('tip.step', { t: 1, s: 0 }), kit.t('tip.cat', { cat: kit.catLabel('tool'), n: '60' })],
|
||||
)
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('the provider anchor rides the focused figure; a category at zero everywhere keeps the unit axis', async () => {
|
||||
// r1 anchors 600/300 = 2 → its focused tool value rides 120 (unanchored it would stay 60).
|
||||
const r1a = req(1, { turn: 1, step: 0, prompt: 600 })
|
||||
const m = await mount(h(TrendChart, propsOf([r1a, r2], { focusCat: 'tool' })))
|
||||
assert.equal(query(m.container, '.lc-axis-top').textContent, '120')
|
||||
assert.equal(queryAll(bars(m.container)[0], '.lc-bar-stack > div')[0].style.height, `${CHART_H}px`)
|
||||
await m.unmount()
|
||||
|
||||
const zero = req(9, { turn: 1, step: 0, system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0 })
|
||||
const mz = await mount(h(TrendChart, propsOf([zero], { focusCat: 'user' })))
|
||||
assert.equal(query(mz.container, '.lc-axis-top').textContent, '1')
|
||||
assert.equal(queryAll(bars(mz.container)[0], '.lc-bar-stack > div').length, 0)
|
||||
await mz.unmount()
|
||||
})
|
||||
|
||||
test('delta mode diffs the focused category only', async () => {
|
||||
const base = req(1, { turn: 1, step: 0 })
|
||||
const grown = req(2, { turn: 1, step: 1, system: 130, tools: 50, user: 30, inject: 20, assistant: 40, tool: 60, total: 330 })
|
||||
const shrunk = req(3, { turn: 2, step: 0, system: 90, tools: 50, user: 30, inject: 20, assistant: 40, tool: 60, total: 290 })
|
||||
const m = await mount(h(TrendChart, propsOf([base, grown, shrunk], { mode: 'delta', focusCat: 'system' })))
|
||||
// Only the system deltas (+30 / -40) drive the scale: maxUp 30, maxDown 40.
|
||||
assert.equal(query(m.container, '.lc-axis-top').textContent, '+30')
|
||||
assert.equal(query(m.container, '.lc-axis-bot').textContent, '-40')
|
||||
const bs = bars(m.container)
|
||||
assert.equal(queryAll(bs[0], '.lc-bar-up > div').length, 0)
|
||||
const up = queryAll(bs[1], '.lc-bar-up > div')
|
||||
assert.equal(up.length, 1)
|
||||
assert.equal(up[0].getAttribute('data-cat'), 'system')
|
||||
assert.equal(up[0].style.height, `${Math.round(30 * CHART_H / 70)}px`)
|
||||
const down = queryAll(bs[2], '.lc-bar-down > div')
|
||||
assert.equal(down.length, 1)
|
||||
assert.equal(down[0].style.height, `${Math.round(40 * CHART_H / 70)}px`)
|
||||
|
||||
// The delta tip reports the focused category's signed change.
|
||||
await m.update(h(TrendChart, propsOf([base, grown, shrunk], { mode: 'delta', focusCat: 'system', hoveredSeq: 2 })))
|
||||
assert.ok(query(m.container, '.lc-chart-tip').textContent!.includes(kit.t('tip.delta', { n: '+30' })))
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('an unknown focus key and an explicit null degrade to the unfocused chart', async () => {
|
||||
for (const focusCat of ['nope', null]) {
|
||||
const m = await mount(h(TrendChart, propsOf([r1, r2], { focusCat })))
|
||||
assert.equal(queryAll(bars(m.container)[0], '.lc-bar-stack > div').length, 6)
|
||||
assert.equal(query(m.container, '.lc-axis-top').textContent, '600')
|
||||
await m.unmount()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('TrendChart markers', () => {
|
||||
test('compaction/prune markers render the ✂ glyph with a positioned or bare title', async () => {
|
||||
const reqs = [req(1, { turn: 1, step: 0 }), req(2, { turn: 1, step: 1 }), req(3, { turn: 2, step: 0 })]
|
||||
|
||||
Reference in new issue
Block a user