fix(ui): render the full history — earlier turns/steps were unreachable

The chart sliced to the newest 80 requests, so anything older was never
rendered. All requests the host provides (capped at 160) are now rendered
and reachable by horizontal scrolling; the scrollbar is thicker and
higher-contrast, edge fades appear whenever more history sits beyond the
viewport, and the hint mentions the sideways scroll.
This commit is contained in:
bowenliang123 committed 2026-08-14 22:36:19 +08:00
1 parent 0d4ce1fbc3
commit 8f7e77d233
2 files changed
+65 -10

No files matched your search

+34 -1
View File
@@ -354,4 +354,37 @@ tr = renderView()
assert.equal(byClass(tr, 'lc-bar').length, 4, 'back to one bar per step')
assert.equal(byClass(tr, 'lc-gran-on')[0].args[2], 'Step', 'step button active again')
console.log('✔ chart render test passed (fixed-width bars, scroll container, turn ranges, hover linking, overview tooltip, turn strip, granularity toggle)')
// ---- edge fades signal reachable history beyond the viewport ----
let scroller = byClass(tr, 'lc-chart-scroll')[0]
const fakeScroller = { scrollLeft: 200, clientWidth: 120, scrollWidth: 800 }
scroller.args[1].onScroll({ currentTarget: fakeScroller })
tr = renderView()
assert.equal(byClass(tr, 'lc-chart-fade-l').length, 1, 'left fade shown while scrolled into history')
assert.equal(byClass(tr, 'lc-chart-fade-r').length, 1, 'right fade shown while more bars follow')
fakeScroller.scrollLeft = 680
scroller.args[1].onScroll({ currentTarget: fakeScroller })
tr = renderView()
assert.equal(byClass(tr, 'lc-chart-fade-r').length, 0, 'right fade gone at the right end')
assert.equal(byClass(tr, 'lc-chart-fade-l').length, 1, 'left fade stays at the right end')
fakeScroller.scrollLeft = 0
scroller.args[1].onScroll({ currentTarget: fakeScroller })
tr = renderView()
assert.equal(byClass(tr, 'lc-chart-fade-l').length, 0, 'left fade gone at the start')
// ---- no 80-bar cap: every request the host sends is rendered ----
const bigRequests = []
for (let i = 0; i < 120; i++) {
bigRequests.push({
seq: 1000 + i, turn: 1 + Math.floor(i / 4), step: i % 4, time: 1000 * i,
system: 10, tools: 20, user: 30, inject: 5, assistant: 15, tool: 20, total: 100,
})
}
ctxSlots[0][1]({ ...snapshot, requests: bigRequests })
tr = renderView()
assert.equal(byClass(tr, 'lc-bar').length, 120, 'all requests render — earlier turns/steps stay reachable')
assert.equal(byClass(tr, 'lc-turn').length, 30, 'turn strip follows the full history')
ctxSlots[0][1](snapshot) // restore
tr = renderView()
assert.equal(byClass(tr, 'lc-bar').length, 4, 'snapshot restored')
console.log('✔ chart render test passed (fixed-width bars, scroll container, turn ranges, hover linking, overview tooltip, turn strip, granularity toggle, edge fades, full history)')
+31 -9
View File
@@ -142,7 +142,7 @@ const DICT_ZH: Record<string, string> = {
'tools.more': '等 {n} 个',
'trend.title': '历史趋势',
'gran.step': '步骤', 'gran.turn': '轮次',
'trend.hint': '每次模型请求一段;点击柱子查看详情,✂ 表示压缩/剪枝',
'trend.hint': '每次模型请求一段;点击柱子查看详情,✂ 表示压缩/剪枝;左右滑动可回看更早',
'trend.empty': '发起一轮对话后,这里会展示每次模型请求的上下文构成',
'detail.step': 'T{t} · 第 {s} 步',
'detail.estTotal': '估算合计 ≈ {n}',
@@ -184,7 +184,7 @@ const DICT_EN: Record<string, string> = {
'tools.more': 'of {n}',
'trend.title': 'History',
'gran.step': 'Step', 'gran.turn': 'Turn',
'trend.hint': 'one bar per model request; click a bar for details, ✂ marks compaction/prune',
'trend.hint': 'one bar per model request; click a bar for details, ✂ marks compaction/prune; scroll sideways to see earlier',
'trend.empty': 'Send a message and each model request’s context makeup shows up here',
'detail.step': 'T{t} · step {s}',
'detail.estTotal': 'estimated ≈ {n}',
@@ -408,11 +408,19 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
grp.count++
}
// Stick to the newest bars unless the user scrolled away from the end.
// Stick to the newest bars unless the user scrolled away from the end,
// and keep the edge fades in sync with the scroll position.
const scrollRef = React.useRef<HTMLDivElement | null>(null)
const [edges, setEdges] = React.useState<{ left: boolean; right: boolean }>({ left: false, right: false })
const updateEdges = (el: HTMLDivElement): void => {
const left = el.scrollLeft > 4
const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4
setEdges(prev => (prev.left === left && prev.right === right) ? prev : { left, right })
}
React.useEffect(() => {
const el = scrollRef.current
if (el === null) return
updateEdges(el)
if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) el.scrollLeft = el.scrollWidth
})
@@ -421,7 +429,14 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
h('span', { className: 'lc-axis-top' }, fmt(maxTotal)),
h('span', { className: 'lc-axis-mid' }, fmt(Math.round(maxTotal / 2))),
h('span', { className: 'lc-axis-bot' }, '0')),
h('div', { className: 'lc-chart-scroll', ref: scrollRef },
h('div', {
className: 'lc-chart-scroll',
ref: scrollRef,
onScroll: (e: ReactNS.UIEvent<HTMLDivElement>) => { updateEdges(e.currentTarget) },
},
// Edge fades: visible whenever more history sits beyond the viewport,
// so the horizontal scroll affordance is obvious.
edges.left ? h('div', { className: 'lc-chart-fade lc-chart-fade-l' }) : null,
// Hovering a bar previews it in the detail below; leaving the plot
// clears the preview (a pinned selection, if any, takes over again).
h('div', { className: 'lc-chart', onMouseLeave: () => { props.onHover(null) } },
@@ -476,7 +491,8 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
title: 'T' + grp.turn,
onMouseEnter: () => { props.onHoverTurn(grp.turn) },
}, 'T' + grp.turn)
}))))
})),
edges.right ? h('div', { className: 'lc-chart-fade lc-chart-fade-r' }) : null))
}
function RequestDetail(props: RequestDetailProps): ReactNS.ReactElement | null {
@@ -653,7 +669,9 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
? h('div', { className: 'lc-empty' }, t('trend.empty'))
: h('div', null,
h(TrendChart, {
requests: displayRequests.slice(-80),
// The host caps the log at 160 requests; render them ALL so
// earlier turns/steps stay reachable via horizontal scroll.
requests: displayRequests,
events,
selectedSeq: pinnedReq ? pinnedReq.seq : null,
hoveredSeq,
@@ -709,10 +727,14 @@ const STYLES = [
'.lc-axis-top { top: 13px; }',
'.lc-axis-mid { top: 69px; }',
'.lc-axis-bot { top: 125px; }',
'.lc-chart-scroll { flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }',
'.lc-chart-scroll::-webkit-scrollbar { height: 6px; }',
'.lc-chart-scroll::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l1); border-radius: 3px; }',
'.lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }',
'.lc-chart-scroll::-webkit-scrollbar { height: 8px; }',
'.lc-chart-scroll::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 45%, transparent); border-radius: 4px; }',
'.lc-chart-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 70%, transparent); }',
'.lc-chart-scroll::-webkit-scrollbar-track { background: transparent; }',
'.lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }',
'.lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }',
'.lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }',
'.lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }',
'.lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }',
'.lc-grid-top { top: 18px; }',