Show live and historical turn activity trace (model, pack, steps) (0.15.17).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 22:23:26 +03:00
co-authored by Cursor
parent 649c64a587
commit baa75f2858
7 changed files with 731 additions and 118 deletions
+286 -45
View File
@@ -1,6 +1,6 @@
/**
* Turn activity timeline — shows model commands and pipeline steps in chat.
* Cursor-like, but compact and chat-native.
* Cursor-like, but compact and chat-native. Persists per assistant turn in history.
*/
const STEP_ICONS = {
@@ -16,11 +16,207 @@ const STEP_ICONS = {
park: '▼',
inventory: '▤',
compress: '▤',
ctx: '⚙',
model: '◉',
done: '✓',
skip: '',
error: '!',
};
function trimDetail(s, max = 220) {
const t = String(s || '').trim();
if (t.length <= max) {
return t;
}
return `${t.slice(0, max)}`;
}
function shortModelName(model) {
const s = String(model || '').trim();
if (!s) {
return '';
}
const slash = s.lastIndexOf('/');
return slash >= 0 ? s.slice(slash + 1) : s;
}
function buildContextMeta(context = {}) {
const parts = [];
const model = shortModelName(context.model);
if (model) {
parts.push(model);
}
if (context.pack) {
parts.push(String(context.pack).replace(/_/g, ' '));
}
if (context.persona && context.persona !== 'neutral') {
parts.push(String(context.persona));
}
if (Array.isArray(context.skills) && context.skills.length) {
parts.push(`skills: ${context.skills.slice(0, 4).join(', ')}`);
}
if (context.hop) {
parts.push(String(context.hop));
}
return parts.join(' · ');
}
function renderMetaRow(context) {
const meta = buildContextMeta(context);
if (!meta) {
return null;
}
const row = document.createElement('div');
row.className = 'sa-activity-meta';
row.textContent = meta;
row.title = meta;
return row;
}
function renderSteps(listEl, steps) {
if (!listEl) {
return;
}
listEl.replaceChildren(...steps.map(renderStep));
}
function renderStep(step) {
const row = document.createElement('div');
row.className = `sa-activity-step sa-activity-${step.status || 'done'}`;
row.dataset.id = step.id;
const icon = document.createElement('span');
icon.className = 'sa-activity-icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = STEP_ICONS[step.kind] || STEP_ICONS.think;
const body = document.createElement('div');
body.className = 'sa-activity-body';
const label = document.createElement('div');
label.className = 'sa-activity-label';
label.textContent = step.label || step.id;
body.appendChild(label);
if (step.detail) {
const detail = document.createElement('div');
detail.className = 'sa-activity-detail';
detail.textContent = step.detail;
body.appendChild(detail);
}
row.appendChild(icon);
row.appendChild(body);
return row;
}
function buildActivityCard(trace, { live = false, collapsed = false } = {}) {
const card = document.createElement('div');
card.className = 'sa-activity'
+ (live ? ' sa-activity-live' : ' sa-activity-done sa-activity-historical');
const steps = Array.isArray(trace?.steps) ? trace.steps : [];
const running = steps.some((s) => s.status === 'running');
if (!live && !running) {
card.classList.add('sa-activity-done');
}
if (running) {
card.classList.add('sa-activity-live');
}
card.setAttribute('role', 'status');
card.setAttribute('aria-live', live ? 'polite' : 'off');
const head = document.createElement('button');
head.type = 'button';
head.className = 'sa-activity-head';
head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
const spin = document.createElement('span');
spin.className = 'sa-activity-spin';
spin.setAttribute('aria-hidden', 'true');
const titleEl = document.createElement('span');
titleEl.className = 'sa-activity-title';
const runningStep = steps.find((s) => s.status === 'running');
const last = steps[steps.length - 1];
titleEl.textContent = trace?.title
|| runningStep?.label
|| last?.label
|| 'Assistent';
const chev = document.createElement('span');
chev.className = 'sa-activity-chev';
chev.setAttribute('aria-hidden', 'true');
chev.textContent = '▾';
head.appendChild(spin);
head.appendChild(titleEl);
head.appendChild(chev);
let open = !collapsed;
head.addEventListener('click', () => {
open = !open;
card.classList.toggle('sa-activity-collapsed', !open);
head.setAttribute('aria-expanded', open ? 'true' : 'false');
});
if (collapsed) {
card.classList.add('sa-activity-collapsed');
}
const meta = renderMetaRow(trace?.context || {});
const listEl = document.createElement('div');
listEl.className = 'sa-activity-steps';
renderSteps(listEl, steps);
card.appendChild(head);
if (meta) {
card.appendChild(meta);
}
card.appendChild(listEl);
return card;
}
/** Static activity card for chat history replay. */
export function mountActivityTrace(trace, { box, insertBefore = null, collapsed = true } = {}) {
const host = box || null;
if (!host || !trace || !Array.isArray(trace.steps) || !trace.steps.length) {
return null;
}
const card = buildActivityCard(trace, { live: false, collapsed });
if (insertBefore && insertBefore.parentNode === host) {
host.insertBefore(card, insertBefore);
} else {
host.appendChild(card);
}
return card;
}
export function slimActivityTrace(trace) {
if (!trace || typeof trace !== 'object') {
return undefined;
}
const steps = (trace.steps || []).slice(0, 32).map((s) => ({
id: String(s.id || '').slice(0, 40),
kind: s.kind || 'think',
label: String(s.label || s.id || '').slice(0, 120),
detail: trimDetail(s.detail, 180),
status: s.status || 'done',
}));
if (!steps.length) {
return undefined;
}
return {
title: String(trace.title || 'Assistent').slice(0, 80),
context: trace.context ? {
model: trace.context.model || undefined,
pack: trace.context.pack || undefined,
persona: trace.context.persona || undefined,
skills: Array.isArray(trace.context.skills) ? trace.context.skills.slice(0, 8) : undefined,
hop: trace.context.hop || undefined,
} : undefined,
steps,
};
}
export function createActivityController(opts = {}) {
const {
getMessagesEl,
@@ -31,9 +227,29 @@ export function createActivityController(opts = {}) {
let card = null;
let listEl = null;
let titleEl = null;
let metaEl = null;
let steps = [];
let context = {};
let title = 'Assistent';
let open = true;
function insertCard(el) {
const box = typeof getMessagesEl === 'function' ? getMessagesEl() : null;
if (!box || !el) {
return;
}
const lastUser = box.querySelector('.sa-msg.user:last-of-type');
if (lastUser && lastUser.parentNode === box) {
if (lastUser.nextSibling) {
box.insertBefore(el, lastUser.nextSibling);
} else {
box.appendChild(el);
}
return;
}
box.appendChild(el);
}
function ensureCard() {
const box = typeof getMessagesEl === 'function' ? getMessagesEl() : null;
if (!box) {
@@ -61,7 +277,7 @@ export function createActivityController(opts = {}) {
titleEl = document.createElement('span');
titleEl.className = 'sa-activity-title';
titleEl.textContent = 'Assistent';
titleEl.textContent = title || 'Assistent';
const chev = document.createElement('span');
chev.className = 'sa-activity-chev';
@@ -81,55 +297,35 @@ export function createActivityController(opts = {}) {
listEl.className = 'sa-activity-steps';
card.appendChild(head);
metaEl = renderMetaRow(context);
if (metaEl) {
card.appendChild(metaEl);
}
card.appendChild(listEl);
box.appendChild(card);
insertCard(card);
if (typeof scrollToBottom === 'function') {
scrollToBottom();
}
return card;
}
function renderStep(step) {
const row = document.createElement('div');
row.className = `sa-activity-step sa-activity-${step.status || 'running'}`;
row.dataset.id = step.id;
const icon = document.createElement('span');
icon.className = 'sa-activity-icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = STEP_ICONS[step.kind] || STEP_ICONS.think;
const body = document.createElement('div');
body.className = 'sa-activity-body';
const label = document.createElement('div');
label.className = 'sa-activity-label';
label.textContent = step.label || step.id;
body.appendChild(label);
if (step.detail) {
const detail = document.createElement('div');
detail.className = 'sa-activity-detail';
detail.textContent = step.detail;
body.appendChild(detail);
}
row.appendChild(icon);
row.appendChild(body);
return row;
}
function paint() {
if (!ensureCard() || !listEl) {
return;
}
listEl.replaceChildren(...steps.map(renderStep));
if (metaEl) {
const nextMeta = buildContextMeta(context);
metaEl.textContent = nextMeta;
metaEl.title = nextMeta;
metaEl.hidden = !nextMeta;
}
renderSteps(listEl, steps);
const running = steps.find((s) => s.status === 'running');
const last = steps[steps.length - 1];
if (titleEl) {
titleEl.textContent = running
? running.label
: (last?.label || 'Assistent');
: (last?.label || title || 'Assistent');
}
card.classList.toggle('sa-activity-live', steps.some((s) => s.status === 'running'));
card.classList.toggle('sa-activity-done', steps.length > 0 && steps.every((s) => s.status === 'done' || s.status === 'skip'));
@@ -138,15 +334,45 @@ export function createActivityController(opts = {}) {
}
}
function begin(title) {
function setContext(patch = {}) {
context = { ...context, ...patch };
if (card && !metaEl) {
metaEl = renderMetaRow(context);
if (metaEl && card.firstChild) {
card.insertBefore(metaEl, card.firstChild);
} else if (metaEl) {
card.prepend(metaEl);
}
}
paint();
}
function begin(nextTitle, nextContext = {}) {
steps = [];
card = null;
listEl = null;
titleEl = null;
metaEl = null;
open = true;
title = nextTitle || 'Assistent';
context = nextContext && typeof nextContext === 'object' ? { ...nextContext } : {};
ensureCard();
if (titleEl && title) {
titleEl.textContent = title;
const meta = buildContextMeta(context);
if (meta) {
upsert('ctx', {
kind: 'ctx',
label: 'Контекст хода',
detail: meta,
status: 'done',
});
}
if (context.model) {
upsert('model', {
kind: 'model',
label: 'Модель',
detail: String(context.model),
status: 'done',
});
}
paint();
}
@@ -184,6 +410,9 @@ export function createActivityController(opts = {}) {
s.status = 'done';
}
});
if (summary) {
title = summary;
}
if (summary && titleEl) {
titleEl.textContent = summary;
}
@@ -194,6 +423,14 @@ export function createActivityController(opts = {}) {
}
}
function snapshot() {
return slimActivityTrace({
title: titleEl?.textContent || title || 'Assistent',
context,
steps: steps.slice(),
});
}
/** Describe a sparse model patch as human-readable activity steps. */
function noteModelCommands(patch) {
if (!patch || typeof patch !== 'object') {
@@ -204,16 +441,16 @@ export function createActivityController(opts = {}) {
if (keys.length) {
done('delta', {
kind: 'delta',
label: 'Обновил сессию',
detail: keys.slice(0, 10).join(', '),
label: 'Патч сессии',
detail: keys.slice(0, 12).join(', '),
});
}
const ask = Array.isArray(patch.ask) ? patch.ask.map(String) : (patch.ask ? [String(patch.ask)] : []);
if (ask.length) {
upsert('ask', {
kind: 'ask',
label: `Запросил ${ask.join(', ')}`,
detail: 'подгружаю детали…',
label: `ask: ${ask.join(', ')}`,
detail: 'запрос деталей у SwarmUI',
status: 'running',
});
}
@@ -221,7 +458,7 @@ export function createActivityController(opts = {}) {
const slots = [].concat(patch.look_at || patch.vision_from || patch.vision_slots || []);
upsert('look', {
kind: 'look',
label: 'Смотрит на кадр',
label: 'look_at',
detail: slots.map(String).slice(0, 4).join(', '),
status: 'running',
});
@@ -231,14 +468,14 @@ export function createActivityController(opts = {}) {
upsert('generate', {
kind: 'generate',
label: 'Generate',
detail: 'ждёт пайплайн…',
detail: 'клиент → SwarmUI',
status: 'running',
});
}
if (Array.isArray(patch.variants) && patch.variants.length) {
upsert('variants', {
kind: 'generate',
label: `Варианты ×${patch.variants.length}`,
label: `variants ×${patch.variants.length}`,
status: 'running',
});
}
@@ -246,11 +483,13 @@ export function createActivityController(opts = {}) {
return {
begin,
setContext,
upsert,
done,
skip,
fail,
finish,
snapshot,
noteModelCommands,
get steps() {
return steps.slice();
@@ -260,4 +499,6 @@ export function createActivityController(opts = {}) {
export function attachActivity(SA) {
SA.createActivityController = createActivityController;
SA.mountActivityTrace = mountActivityTrace;
SA.slimActivityTrace = slimActivityTrace;
}
+78 -15
View File
@@ -183,6 +183,7 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
ollamaHealth: 'unknown',
trainingLock: false,
activity: null,
pendingActivityTrace: null,
};
function getActivity() {
@@ -199,13 +200,26 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
return state.activity;
}
function activityBegin(title) {
function activityBegin(title, context) {
const a = getActivity();
if (a) {
a.begin(title || 'Assistent');
a.begin(title || 'Assistent', context || {});
}
}
function captureActivityTrace() {
const a = getActivity();
if (!a || typeof a.snapshot !== 'function') {
return null;
}
const snap = a.snapshot();
if (snap && snap.steps && snap.steps.length) {
state.pendingActivityTrace = snap;
return snap;
}
return null;
}
function activityStep(id, patch) {
const a = getActivity();
if (a) {
@@ -224,6 +238,7 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
const a = getActivity();
if (a) {
a.finish(summary);
captureActivityTrace();
}
}
@@ -3255,6 +3270,9 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
content: content.slice(0, 4000),
persona: m.persona || undefined,
pack: m.pack || undefined,
activity: (window.SA && typeof SA.slimActivityTrace === 'function')
? SA.slimActivityTrace(m.activity)
: (m.activity || undefined),
};
});
}
@@ -3644,6 +3662,7 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
persona: m.persona ? { id: m.persona, title: m.persona } : null,
pack: m.pack,
historical: true,
activity: m.activity,
});
}
}
@@ -5216,7 +5235,14 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
} else {
appendMessage('assistant', prose);
}
state.history.push({ role: 'assistant', content: prose, persona, pack });
state.history.push({
role: 'assistant',
content: prose,
persona,
pack,
activity: captureActivityTrace() || state.pendingActivityTrace || undefined,
});
state.pendingActivityTrace = null;
persistHistory();
state.turnSettled = true;
}
@@ -5820,6 +5846,10 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
if (role === 'assistant' && !(meta && meta.historical)) {
mountCurateButtons(div, meta);
}
if (role === 'assistant' && meta?.historical && meta?.activity
&& window.SA && typeof SA.mountActivityTrace === 'function') {
SA.mountActivityTrace(meta.activity, { box, insertBefore: div, collapsed: true });
}
box.appendChild(div);
scrollMessagesToBottom({ force: true });
return div;
@@ -8978,16 +9008,6 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
// If Krea just ran, expectColdLoad stays true until warm / first token — don't clear it here.
state.llmParked = false;
setInterruptVisible(true);
if (!isContinuationTurn(opts) && !opts.fromAskHop) {
activityBegin(opts.fromDebug ? 'Debug' : 'Ход Assistent');
activityStep('think', { kind: 'think', label: 'Думаю…', status: 'running' });
} else if (opts.fromAskHop) {
activityStep('think', { kind: 'think', label: 'Отвечает с деталями…', status: 'running' });
} else if (opts.fromVisionHop) {
activityStep('look', { kind: 'look', label: 'Смотрит изображение…', status: 'running' });
} else if (opts.fromPromptEnRetry) {
activityStep('prep', { kind: 'prep', label: 'Дописываю EN-промпт…', status: 'running' });
}
if (state.expectColdLoad && !isContinuationTurn(opts)) {
startBusyUi('warming');
setStatus('Возвращаю LLM в GPU…');
@@ -9073,6 +9093,31 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
}
}
if (!isContinuationTurn(opts) && !opts.fromAskHop) {
state.pendingActivityTrace = null;
const hopLabel = opts.fromVisionHop ? 'vision hop'
: (opts.fromPromptEnRetry ? 'krea prep'
: (opts.fromAutoCritique ? 'critique'
: (opts.fromDebug ? 'debug' : '')));
activityBegin(opts.fromDebug ? 'Debug' : 'Ход Assistent', {
model,
pack,
persona,
skills: opts.fromDebug ? [] : (state.enabledSkills || []),
hop: hopLabel || undefined,
});
activityStep('think', { kind: 'think', label: 'Думаю…', status: 'running' });
} else if (opts.fromAskHop) {
getActivity()?.setContext?.({ hop: 'ask hop', pack, model });
activityStep('think', { kind: 'think', label: 'Отвечает с деталями…', status: 'running' });
} else if (opts.fromVisionHop) {
getActivity()?.setContext?.({ hop: 'vision hop' });
activityStep('look', { kind: 'look', label: 'Смотрит изображение…', status: 'running' });
} else if (opts.fromPromptEnRetry) {
getActivity()?.setContext?.({ hop: 'krea prep' });
activityStep('prep', { kind: 'prep', label: 'Дописываю EN-промпт…', status: 'running' });
}
// Auto-compress after the new user turn is in history (budget includes it).
if (!opts.fromDebug && !opts.fromCompress && !opts.fromAskHop && !opts.fromVisionHop
&& !opts.fromAutoCritique && !isContinuationTurn(opts)) {
@@ -9157,8 +9202,6 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
}
updateCtxChip();
const prose = visibleAssistantProse(reply);
state.history.push({ role: 'assistant', content: prose, persona, pack });
persistHistory();
setBusyPhase(state.pendingSilentGen ? 'silent_gen' : 'thinking');
try {
await handleReplySideEffects(reply, civitaiResults, {
@@ -9169,6 +9212,26 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
|| (!isMachineTurn(opts) && state.pendingSilentGen),
attachedSlotIds: visionSlots.map((s) => s.id),
});
if (meta.system_layers && typeof meta.system_layers === 'object') {
const layers = Object.keys(meta.system_layers).filter((k) => meta.system_layers[k]);
if (layers.length) {
activityDone('layers', {
kind: 'ctx',
label: 'system_layers',
detail: layers.slice(0, 12).join(', '),
});
}
}
captureActivityTrace();
state.history.push({
role: 'assistant',
content: prose,
persona,
pack,
activity: state.pendingActivityTrace || undefined,
});
state.pendingActivityTrace = null;
persistHistory();
} finally {
if (chatEpoch !== state.chatEpoch) {
return;