← back to Agent Abrams Kanban
Add interactive controls (pause/speed/spawn/click-advance) + virtual clock + README
3a5fb88bf25437ec59eb2ca43f6b189d485bd5a7 · 2026-07-25 00:41:21 -0700 · Steve
Files touched
Diff
commit 3a5fb88bf25437ec59eb2ca43f6b189d485bd5a7
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jul 25 00:41:21 2026 -0700
Add interactive controls (pause/speed/spawn/click-advance) + virtual clock + README
---
README.md | 40 ++++++++++++++++++++++++++++++
index.html | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 108 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ca34081
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# Agent Abrams — Task Pipeline Kanban
+
+A single self-contained `index.html` — a self-animating agent task-pipeline board.
+Task cards flow **Queued → Running → Done → Shipped** with smooth column-to-column
+slides, then recycle back to Queued so the board never empties. Pure CSS/JS, no build,
+no dependencies. Just open `index.html`.
+
+## Features
+
+- **FLIP slide animation** — cards live in natural flex flow inside scrollable columns;
+ cross-column moves animate via First-Last-Invert-Play, so they never overflow or overlap.
+- **Timed auto-advance** — each card has a randomized dwell window per stage, so work
+ flows steadily instead of clumping.
+- **Agent avatars** — colored dot with initials + agent name per card (Orion, Vega, Nova…).
+- **Live per-card timer** and an animated progress bar on Running cards.
+- **Per-column counts** that bump on change, plus header telemetry:
+ uptime, total shipped, throughput/min.
+- **Dark neon UI** — glassmorphism, animated grid backdrop, per-column accent glow.
+- Honors `prefers-reduced-motion`.
+
+## Interactive controls (top-right)
+
+| Control | Action |
+|---------|--------|
+| **+ Task** | Queue a new task immediately |
+| **1× / 2× / 4×** | Cycle simulation speed (rescales pending hops live) |
+| **⏸ Pause** | Freeze the pipeline — a virtual clock keeps timers & throughput honest |
+| **Click a card** | Manually advance it to the next column |
+
+## How it works
+
+A virtual clock (`now()`) subtracts accumulated paused time, so pausing freezes every
+timer and the throughput stat rather than letting wall-clock drift skew the numbers.
+Speed changes rescale each card's remaining time in place. Cards that reach Shipped are
+re-themed with a fresh agent + title and slid back to Queued, keeping a constant
+population without churning DOM nodes.
+
+Started from Kimi K2.5's Model Arena artifact; rebuilt the positioning engine
+(absolute-stride → FLIP-in-flow), replaced random-roll advancement with timed dwell,
+and added the control surface + telemetry.
diff --git a/index.html b/index.html
index 164cb33..e4c0a0f 100644
--- a/index.html
+++ b/index.html
@@ -53,6 +53,18 @@ body::before{
.stats{display:flex;gap:18px;font-size:.72rem;color:var(--muted);letter-spacing:1px;text-transform:uppercase}
.stats b{color:var(--ink);font-variant-numeric:tabular-nums}
+.controls{display:flex;gap:8px;align-items:center}
+.controls button{
+ font:inherit;font-size:.68rem;letter-spacing:1px;text-transform:uppercase;font-weight:700;
+ color:var(--muted);background:rgba(255,255,255,0.05);
+ border:1px solid rgba(255,255,255,0.12);border-radius:8px;
+ padding:6px 12px;cursor:pointer;transition:all .18s;
+}
+.controls button:hover{color:var(--ink);border-color:rgba(255,255,255,0.28);background:rgba(255,255,255,0.09)}
+.controls button.active{color:#06060b;background:var(--running);border-color:var(--running);box-shadow:0 0 12px rgba(0,229,255,.4)}
+.controls button.paused{color:#06060b;background:var(--shipped);border-color:var(--shipped);box-shadow:0 0 12px rgba(255,176,32,.4)}
+.card{cursor:pointer}
+
.board{
flex:1;min-height:0;
display:grid;grid-template-columns:repeat(4,1fr);gap:16px;
@@ -151,6 +163,11 @@ body::before{
<div>Shipped <b id="shipCount">0</b></div>
<div>Throughput <b id="tput">0.0</b>/min</div>
</div>
+ <div class="controls">
+ <button id="btnSpawn" title="Queue a new task">+ Task</button>
+ <button id="btnSpeed" title="Cycle simulation speed">1×</button>
+ <button id="btnPause" title="Pause / resume the pipeline">⏸ Pause</button>
+ </div>
</div>
<div class="board" id="board">
@@ -207,11 +224,16 @@ body::before{
const rint = (a,b)=>a+Math.floor(Math.random()*(b-a+1));
const pick = a=>a[Math.floor(Math.random()*a.length)];
- const now = ()=>performance.now();
+
+ // --- virtual clock: freezes on pause so timers + throughput stay honest ---
+ let paused = false, speed = 1, pausedAccum = 0, pauseStart = 0;
+ const now = ()=>performance.now() - pausedAccum - (paused ? performance.now()-pauseStart : 0);
+ // dwell scaled by sim speed
+ const dwell = stage => rint(...DWELL[stage]) / speed;
let uid = 0, shippedTotal = 0;
- const startWall = Date.now();
const tasks = [];
+ const startV = now();
function makeTask(seedStage){
const agent = pick(AGENTS);
@@ -221,7 +243,7 @@ body::before{
agent,
status: seedStage || 'queued',
createdAt: now(),
- nextAt: now() + rint(...DWELL[seedStage||'queued']),
+ nextAt: now() + dwell(seedStage||'queued'),
el: null,
};
t.el = buildEl(t);
@@ -229,6 +251,7 @@ body::before{
bodies[t.status].appendChild(t.el);
t.el.classList.add('enter');
t.el.addEventListener('animationend',()=>t.el.classList.remove('enter'),{once:true});
+ t.el.addEventListener('click',()=>{ advance(t); refreshCounts(); });
return t;
}
@@ -278,7 +301,14 @@ body::before{
moveWithFlip(t, 'queued');
}
// status was set inside moveWithFlip; schedule its next hop
- t.nextAt = now() + rint(...DWELL[t.status]);
+ t.nextAt = now() + dwell(t.status);
+ }
+
+ function refreshCounts(){
+ STAGES.forEach(s=>{
+ const n = tasks.filter(t=>t.status===s).length;
+ if(counts[s].textContent != n) counts[s].textContent = n;
+ });
}
function moveWithFlip(t, toStage){
@@ -318,6 +348,7 @@ body::before{
// ---- ticks ----
function tickAdvance(){
+ if(paused) return;
const t0 = now();
// advance at most a couple per tick so slides don't collide visually
let moved = 0;
@@ -326,11 +357,7 @@ body::before{
advance(t); moved++;
}
}
- // refresh column counts
- STAGES.forEach(s=>{
- const n = tasks.filter(t=>t.status===s).length;
- if(counts[s].textContent != n) counts[s].textContent = n;
- });
+ refreshCounts();
}
function tickSecond(){
@@ -342,25 +369,50 @@ body::before{
const bar = t.el.querySelector('.bar>i');
if(bar){
// progress = elapsed since entering running / its dwell window (approx via nextAt)
- const total = DWELL.running[1];
+ const total = DWELL.running[1]/speed;
const remain = Math.max(0, t.nextAt - t0);
const pct = Math.min(100, Math.max(4, 100*(1 - remain/total)));
bar.style.width = pct+'%';
}
}
}
- // header stats
- document.getElementById('uptime').textContent = fmt(Date.now()-startWall);
+ // header stats (virtual clock => frozen while paused)
+ const up = now() - startV;
+ document.getElementById('uptime').textContent = fmt(up);
document.getElementById('shipCount').textContent = shippedTotal;
- const mins = (Date.now()-startWall)/60000;
+ const mins = up/60000;
document.getElementById('tput').textContent = mins>0 ? (shippedTotal/mins).toFixed(1) : '0.0';
}
+ // ---- controls ----
+ const btnPause = document.getElementById('btnPause');
+ const btnSpeed = document.getElementById('btnSpeed');
+ const btnSpawn = document.getElementById('btnSpawn');
+ const SPEEDS = [1,2,4];
+
+ btnPause.addEventListener('click',()=>{
+ paused = !paused;
+ if(paused){ pauseStart = performance.now(); }
+ else { pausedAccum += performance.now()-pauseStart; }
+ btnPause.textContent = paused ? '▶ Resume' : '⏸ Pause';
+ btnPause.classList.toggle('paused', paused);
+ });
+
+ btnSpeed.addEventListener('click',()=>{
+ const i = (SPEEDS.indexOf(speed)+1) % SPEEDS.length;
+ // rescale every pending hop so the change takes effect immediately
+ const t0 = now(), prev = speed;
+ speed = SPEEDS[i];
+ tasks.forEach(t=>{ t.nextAt = t0 + (t.nextAt - t0) * (prev/speed); });
+ btnSpeed.textContent = speed+'×';
+ btnSpeed.classList.toggle('active', speed!==1);
+ });
+
+ btnSpawn.addEventListener('click',()=>{ makeTask('queued'); refreshCounts(); });
+
// ---- seed ~10 cards spread across the pipeline ----
const seed = ['queued','queued','queued','running','running','running','done','done','shipped','shipped'];
- seed.forEach((s,i)=>{
- setTimeout(()=>{ makeTask(s); STAGES.forEach(st=>counts[st].textContent = tasks.filter(t=>t.status===st).length); }, i*90);
- });
+ seed.forEach((s,i)=>{ setTimeout(()=>{ makeTask(s); refreshCounts(); }, i*90); });
setInterval(tickAdvance, 700);
setInterval(tickSecond, 500);
← 212c8d3 Agent Abrams task-pipeline kanban: FLIP slides, timed auto-a
·
back to Agent Abrams Kanban
·
chore: lint (strict-eq, dedup .card rule) + refactor (cache 3dfa365 →