feat(ui): show the turn/step a context event contributed to

This commit is contained in:
bowenliang123 committed 2026-08-15 01:23:48 +08:00
1 parent d8f9324693
commit 888bb23078
2 files changed
+39 -2

No files matched your search

+27 -2
View File
@@ -153,7 +153,7 @@ const requireStateful = (spec) => {
const m2 = { exports: {} }
const pluginExports2 = factory(requireStateful, m2, globalThis.window, fakeDoc)
const DICT_FOR_TEST = { 'tab': 'Context', 'loading': '…', 'error': 'x', 'detail.step': 'Turn {t} · Step {s}', 'gran.step': 'Step', 'gran.turn': 'Turn', 'detail.turn': 'Turn {t} · {n} steps', 'detail.lastStep': 'last step', 'overview.actual': '· last actual prompt {n}' }
const DICT_FOR_TEST = { 'tab': 'Context', 'loading': '…', 'error': 'x', 'detail.step': 'Turn {t} · Step {s}', 'gran.step': 'Step', 'gran.turn': 'Turn', 'detail.turn': 'Turn {t} · {n} steps', 'detail.lastStep': 'last step', 'overview.actual': '· last actual prompt {n}', 'events.at': 'Turn {t} · Step {s}' }
let viewComponent = null
const fakeCtx2 = {
get: () => undefined,
@@ -493,9 +493,34 @@ const fmtTimeLocal = (t) => {
assert.equal(nodeTimes[0].args[2], fmtTimeLocal(65000), 'formatted time (65000ms)')
assert.equal(nodeTimes[1].args[2], fmtTimeLocal(1000), 'formatted time (1000ms)')
// ---- context events show the turn/step they contributed to ----
// The host sends events oldest->newest; the list reverses them. Each event
// carries the request that follows it (host-stamped); the label renders
// only when both turn and step are present (in-flight events stay bare).
ctxSlots[0][1]({
...snapshot,
events: [
{ seq: 7, kind: 'prune', time: 2000, tokens: 100, turn: 1, step: 0 },
{ seq: 8, kind: 'model', time: 3000, from: 'a', to: 'b', turn: 1, step: 1 },
{ seq: 9, kind: 'inject', time: 4000, tokens: 5, form: 'notice', turn: 2, step: 0 },
{ seq: 10, kind: 'compaction', time: 5000, tokens: 5000, count: 4 }, // no request after -> unlabeled
],
})
tr = renderView()
const evRows = byClass(tr, 'lc-event')
assert.equal(evRows.length, 4, 'event rows rendered newest first')
const atLabels = byClass(tr, 'lc-event-at')
assert.equal(atLabels.length, 3, 'only stamped events carry a turn/step label')
assert.equal(atLabels[0].args[2], 'Turn 2 · Step 0', 'inject shows the request it fed')
assert.equal(atLabels[1].args[2], 'Turn 1 · Step 1', 'model switch shows its request')
assert.equal(atLabels[2].args[2], 'Turn 1 · Step 0', 'prune shows its request')
ctxSlots[0][1](snapshot) // restore
tr = renderView()
assert.equal(byClass(tr, 'lc-event').length, 0, 'event list restored to the empty state')
// ---- overview shows the provider-reported actual prompt of the last request ----
const overviewNum = byClass(tr, 'lc-overview-num')[0]
assert.ok(overviewNum, 'overview number row present')
assert.match(textOf(overviewNum), /· last actual prompt 83\.0k/, 'overview shows the last actual prompt alongside the estimate')
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, right-anchored default, message times, overview actual)')
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, right-anchored default, message times, event turn/step labels, overview actual)')
+12
View File
@@ -134,6 +134,9 @@ interface ContextEventRecord {
name?: string
from?: string
to?: string
/** Turn/step of the request this event contributed to (host-stamped). */
turn?: number
step?: number
}
// ---- dictionaries ------------------------------------------------------------
@@ -160,6 +163,7 @@ const DICT_ZH: Record<string, string> = {
'detail.output': '输出 {n}',
'events.title': '上下文事件',
'events.empty': '暂无上下文事件(压缩、注入、模型切换会出现在这里)',
'events.at': '第 {t} 轮 · 第 {s} 步',
'nodes.title': '消息构成',
'nodes.hint': '当前模型可见的消息,最新在前',
'nodes.more': '… 更早的 {n} 条消息已省略',
@@ -206,6 +210,7 @@ const DICT_EN: Record<string, string> = {
'detail.output': 'output {n}',
'events.title': 'Context events',
'events.empty': 'No context events yet (compaction, injections, model switches appear here)',
'events.at': 'Turn {t} · Step {s}',
'nodes.title': 'Messages',
'nodes.hint': 'currently model-visible, newest first',
'nodes.more': '… {n} earlier messages omitted',
@@ -622,6 +627,12 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
return h('div', { key: ev.seq + '-' + i, className: 'lc-event' },
h('span', { className: 'lc-event-icon lc-event-' + ev.kind }, EVENT_ICONS[ev.kind] || '•'),
h('span', { className: 'lc-event-label', title: label }, label),
// Which request this event contributed to (host-stamped: the first
// request logged after the event). In-flight or trimmed-away
// events have no turn/step and stay unlabeled.
typeof ev.turn === 'number' && typeof ev.step === 'number'
? h('span', { className: 'lc-event-at' }, t('events.at', { t: ev.turn, s: ev.step }))
: null,
ev.tokens ? h('span', { className: 'lc-event-tokens' + (ev.kind === 'inject' ? ' lc-up' : ' lc-down') },
(ev.kind === 'inject' ? '+' : '−') + fmt(ev.tokens)) : null,
h('span', { className: 'lc-event-time' }, fmtTime(ev.time)))
@@ -881,6 +892,7 @@ const STYLES = [
'.lc-event-icon.lc-event-inject { color: #a855f7; }',
'.lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }',
'.lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
'.lc-event-at { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; }',
'.lc-event-tokens { color: var(--dsw-alias-state-success-primary); }',
'.lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }',
'.lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }',