From 5ae61fb0ee80321109c92fab9d26362a4b9d0bed Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <207458273+DemonGatanjieu@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:05:50 +0800 Subject: [PATCH] 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 --- CHANGELOG.md | 2 +- web/modules/ui_ai_workbench.js | 21 +++++++++++++++------ web/styles/37-ai-workbench.css | 34 ++++++++++++++++++++++++---------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30dc0dd..4e14e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/web/modules/ui_ai_workbench.js b/web/modules/ui_ai_workbench.js index 82728a4..44e78ff 100644 --- a/web/modules/ui_ai_workbench.js +++ b/web/modules/ui_ai_workbench.js @@ -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; } diff --git a/web/styles/37-ai-workbench.css b/web/styles/37-ai-workbench.css index 0c24044..097f284 100644 --- a/web/styles/37-ai-workbench.css +++ b/web/styles/37-ai-workbench.css @@ -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; }