AI workbench: steps numbered in order down the timeline

Each step's dot carries its number (1, 2, 3...) in its kind's colour, reads
folded into one numbered row, and while the AI works the next number
waits at the foot, so the order is plain at a glance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-10-20 23:05:50 +08:00
co-authored by Claude Opus 5.5
parent f57a78e08c
commit 5ae61fb0ee
3 changed files with 40 additions and 17 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
### ✨ AI workbench: see everything the AI did, open it, undo it (AI 工作台)
- **A new page on the rail, *AI workbench***: every step an AI app (Claude Code, Cursor, Cherry Studio… connected through Anomalous's MCP) takes is kept, grouped into **tasks**. The AI names a task when it starts (a new `begin_task` tool); otherwise one app's calls that follow each other make one task.
- **A task is a timeline**: what it looked up (folded into one line, a click shows each), what it changed on the canvas, the workflow runs, downloads, scans and spoken lines, and the AI's own notes (a new `report` tool, also for pictures it made another way). Each step says when, how long it took, and why it failed when it did.
- **A task is a timeline**: what it looked up (folded into one line, a click shows each), what it changed on the canvas, the workflow runs, downloads, scans and spoken lines, and the AI's own notes (a new `report` tool, also for pictures it made another way). Each step says when, how long it took, and why it failed when it did. Steps are **numbered in order** (1, 2, 3… down the line), each dot in its kind's colour; while the AI works, the next number waits at the foot.
- **What it made stays, and leads there**: the pictures a run made are kept with the task (watched until the run ends) and shown on top as *What it made*; a click opens the picture, a model opens its page, a combo opens the combo, a spoken line plays, a canvas change is found on the canvas or **undone** (also all of a task's canvas changes at once, stopping at anything you changed since).
- **Live**: a task the AI is still working on grows by itself and says *The AI is working…*; the page follows the newest task unless you picked another. While you are elsewhere, the rail's *AI workbench* gets a dot. Home gets *What the AI did lately*.
- Tasks can be renamed or removed from the list (only the record goes: pictures and the canvas stay). The newest 300 are kept, in ComfyUI's user folder. Without any AI connected, the page shows the address to add in the AI app.
+15 -6
View File
@@ -221,9 +221,10 @@ function resultLinks(step) {
// ---------------------------------------------------------------- one task
function stepRow(step) {
/** One step of the timeline; `number` is its place in the task (the dot shows it). */
function stepRow(step, number) {
const row = el('div', `anomalous-ai-step is-${step.kind}${step.ok ? '' : ' is-failed'}`);
row.append(el('span', 'anomalous-ai-step-dot'));
row.append(el('span', 'anomalous-ai-step-dot', number));
const body = el('div', 'anomalous-ai-step-body');
const line = el('div', 'anomalous-ai-step-line');
line.append(el('span', 'anomalous-ai-kind', t(`aiKind_${step.kind}`)), el('span', 'anomalous-ai-step-title', t(toolKey(step.tool))));
@@ -257,9 +258,9 @@ function stepRow(step) {
return row;
}
function looksRow(steps) {
function looksRow(steps, number) {
const row = el('div', 'anomalous-ai-step is-look is-folded');
row.append(el('span', 'anomalous-ai-step-dot'));
row.append(el('span', 'anomalous-ai-step-dot', number));
const body = el('div', 'anomalous-ai-step-body');
const tools = lookTools(steps).map(({ tool, count }) => (count > 1 ? `${t(toolKey(tool))} ×${count}` : t(toolKey(tool))));
const head = button('anomalous-ai-looks', `${t('aiLooks', { count: steps.length })}:${tools.join('、')}`, () => {
@@ -360,13 +361,21 @@ function renderTask() {
header.append(titleLine, el('div', 'anomalous-ai-task-meta', meta.join(' · ')));
const timeline = el('div', 'anomalous-ai-timeline');
for (const item of groupSteps(task.steps)) timeline.append(item.type === 'looks' ? looksRow(item.steps) : stepRow(item.step));
// Numbered in the order they happened, so the timeline reads 1, 2, 3 from the top.
const items = groupSteps(task.steps);
items.forEach((item, at) => timeline.append(item.type === 'looks' ? looksRow(item.steps, at + 1) : stepRow(item.step, at + 1)));
if (!(task.steps || []).length) timeline.append(el('div', 'anomalous-ai-empty-task', t('aiNoSteps')));
const parts = [header];
const made = strip(task);
if (made) parts.push(made);
parts.push(timeline);
if (status === 'live') parts.push(el('div', 'anomalous-ai-working', (task.steps || []).some(step => step.state === 'running') ? t('aiRunRunning') : t('aiWorking')));
if (status === 'live') {
// What comes next: the following number, waiting.
const next = el('div', 'anomalous-ai-step is-next');
next.append(el('span', 'anomalous-ai-step-dot', items.length + 1),
el('div', 'anomalous-ai-working', (task.steps || []).some(step => step.state === 'running') ? t('aiRunRunning') : t('aiWorking')));
timeline.append(next);
}
box.replaceChildren(...parts);
box.scrollTop = keepBottom && status === 'live' ? box.scrollHeight : keepTop;
}
+24 -10
View File
@@ -295,9 +295,9 @@
.anomalous-ai-timeline::before {
content: '';
position: absolute;
top: 8px;
bottom: 8px;
left: 6px;
top: 14px;
bottom: 14px;
left: 11px;
width: 2px;
border-radius: 1px;
background: var(--amb-border);
@@ -307,7 +307,7 @@
--kind: var(--ai-look);
position: relative;
display: grid;
grid-template-columns: 14px minmax(0, 1fr) auto;
grid-template-columns: 24px minmax(0, 1fr) auto;
gap: 12px;
padding: 9px 0;
}
@@ -322,20 +322,35 @@
.anomalous-ai-step.is-act { --kind: var(--amb-text-soft); }
.anomalous-ai-step.is-failed { --kind: var(--amb-danger); }
/* The step's number in a dot of its kind's colour; the line through the dots is the order. */
.anomalous-ai-step-dot {
position: relative;
z-index: 1;
width: 14px;
height: 14px;
margin-top: 2px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-top: -3px;
box-sizing: border-box;
border: 3px solid var(--amb-bg-panel, #16161a);
border-radius: 50%;
background: var(--kind);
color: #fff;
font-size: 11px;
font-weight: 700;
box-shadow: 0 0 0 3px var(--amb-bg-panel, #16161a);
}
.anomalous-ai-step.is-folded .anomalous-ai-step-dot {
transform: scale(0.72);
border: 1.5px solid var(--kind);
background: var(--amb-bg-panel, #16161a);
color: var(--amb-text-soft);
}
.anomalous-ai-step.is-next .anomalous-ai-step-dot {
border: 1.5px dashed var(--amb-ok);
background: var(--amb-bg-panel, #16161a);
color: var(--amb-ok-text);
}
.anomalous-ai-step-body {
@@ -407,7 +422,6 @@
}
.anomalous-ai-working {
margin: 10px 0 0 26px;
animation: anomalous-ai-breathe 1.8s ease-in-out infinite;
}