VicroCode
Make Code Create Value
VicroCode is a lightweight online platform for publishing, running, sharing, and monetizing code projects. Launch HTML, Python, SQLite, AI agents, management tools, games, and more without server setup.
Please wait while VicroCode loads. You can also explore the AI programming guide
Loading...

AI MARKET GUIDE

Making an Agent's Wait Stop Being a Black Box: A Live Tool-Call Status Board

A dev wished he could see which tool his model was calling during long waits. Here's a reproducible Python + hosted HTML board that streams thinking vs tool state.

The wait nobody can see inside

The thing that stuck with me from a recent open-source share was small but honest. Someone built a plugin for DeepSeek Harness called dsh-thinkbar that turns the model-select button into a status indicator: while the model is thinking, the button slowly "heats up" through blue, red, orange, gold; when it runs a tool, it flips to a different state and shows the tool name; once real output starts streaming, the effect retreats. The whole point was to separate two things that normally feel identical from the outside — the model is reasoning versus the model is running tool X.

What I liked most wasn't the animation. It was the caveat the author left in plain sight: the "temperature" only tracks how long you've been waiting, hits full at roughly 20 seconds, and a full bar does not mean the model finished 100% of its reasoning. That's the correct instinct. A progress-looking thing that doesn't actually know progress is a trap, and calling that out up front is the difference between a useful gauge and a lie.

That share, plus the surrounding noise of people griping about usage limits and long waits during agent runs, is a clear enough signal on its own. When you're staring at a spinner for 30 seconds, the anxiety isn't really the wait. It's not knowing whether the thing is stuck, thinking, or halfway through calling a tool that might fail. So I built my own version of that visibility, but as a standalone status board instead of a plugin, because I wanted it to work regardless of which harness I'm using that day.

What I actually built

The shape is simple: the agent emits a state event whenever something changes, a small backend records it, and a hosted page shows the current state live. Three states, nothing fancy — `reasoning`, `tool:<name>`, and `output`. Same separation the thinkbar made, minus any pretense of a completion percentage.

I kept the whole thing inside one platform to avoid the usual glue. The agent side is straightforward AI agent development: the agent already knows when it's about to call a tool and which one, so I just have it announce that before and after each call. The backend is a tiny endpoint that takes those announcements. Being able to run Python online meant I didn't stand up a server anywhere — the ingest endpoint and the state store both live in the same place as the agent logic.

Here's the ingest side, trimmed to the essentials:

import json, time, sqlite3

DB = "agent_state.db"

def init():
    con = sqlite3.connect(DB)
    con.execute("""
        CREATE TABLE IF NOT EXISTS events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            session TEXT NOT NULL,
            state TEXT NOT NULL,      -- 'reasoning' | 'tool' | 'output'
            tool_name TEXT,           -- set only when state = 'tool'
            ts REAL NOT NULL
        )
    """)
    con.commit()
    con.close()

def record(session, state, tool_name=None):
    con = sqlite3.connect(DB)
    con.execute(
        "INSERT INTO events (session, state, tool_name, ts) VALUES (?, ?, ?, ?)",
        (session, state, tool_name, time.time()),
    )
    con.commit()
    con.close()

def current(session):
    con = sqlite3.connect(DB)
    row = con.execute(
        "SELECT state, tool_name, ts FROM events WHERE session = ? ORDER BY id DESC LIMIT 1",
        (session,),
    ).fetchone()
    con.close()
    if not row:
        return {"state": "idle", "tool_name": None, "since": None}
    return {"state": row[0], "tool_name": row[1], "since": row[2]}

A SQLite table is honestly enough here. Each transition is one row, so you get a free timeline of what the agent did in what order, which turned out to be more useful than the live view when I went back to figure out why a run felt slow. The database is small, append-only, and easy to edit or prune when a session's over.

The agent wraps its tool calls with the two announcements:

def run_tool(session, name, fn, *args, **kwargs):
    record(session, "tool", tool_name=name)
    try:
        return fn(*args, **kwargs)
    finally:
        record(session, "reasoning")   # back to thinking after the tool returns

Before the model starts drafting the actual answer, it flips to `output`. That's the cue on the board that the wait is basically over.

The board itself

The front is a single HTML page that polls the `current` endpoint every second and paints the state. I went with polling instead of anything fancier because it's boring and it works, and the board is a read-only status view — no login gate, nothing sensitive behind it, so I didn't need to build auth for it. If you ever put session content or controls on this page, that changes and you'd want access control, but a state label and a tool name aren't that.

The display logic mirrors the three states directly:

async function tick() {
  const r = await fetch(`/current?session=${SESSION}`);
  const s = await r.json();
  const el = document.getElementById("status");

  if (s.state === "tool") {
    el.textContent = `Running tool: ${s.tool_name}`;
    el.className = "tool";
  } else if (s.state === "reasoning") {
    const secs = s.since ? Math.floor(Date.now() / 1000 - s.since) : 0;
    el.textContent = `Reasoning · waited ${secs}s`;
    el.className = "reasoning";
  } else if (s.state === "output") {
    el.textContent = "Writing the answer";
    el.className = "output";
  } else {
    el.textContent = "Idle";
    el.className = "idle";
  }
}
setInterval(tick, 1000);
tick();

Since the page just needs to be reachable while the agent runs, web app hosting on the same platform meant I published it next to the endpoint and shared the link with the two people who care. That's the payoff of web app hosting here — the board is a URL, not a local dev server someone has to spin up.

Where I drew the unverified line

This is the part I want to be explicit about, because it's easy to oversell. The board shows what state the agent reports and how many seconds it's been in that state. That's it. It does not know how much reasoning is left, it can't tell you a tool call is about to finish, and the elapsed-seconds counter is not a progress bar. Same trap the thinkbar author flagged: a rising number feels like progress, but nothing here measures completion. Any claim like "it's 80% done" or "tool calls average N seconds" would be unverified — I have the timeline data to compute durations after the fact, but I have not measured that this board makes anything faster, and it obviously doesn't. It only makes the wait legible.

Worth naming the boundary honestly: the original inspiration hooks into a specific harness's plugin interface and runs entirely in the browser with no data collection. My version is a different trade — it does send state events to a backend, so it's not zero-telemetry, and it doesn't plug into anyone else's tool. It's a separate board you point your own agent at. If your setup depends on that harness's plugin API, npm packaging, or its exact model-select button, none of that carries over; you'd rebuild the announce-and-display loop the way I did above rather than porting the plugin.

Why this is worth an afternoon

The recurring frustration across what I'm seeing from developers is less about raw capability and more about not being able to see what the machine is doing while you pay for the wait. A status board doesn't fix latency or usage limits. What it fixes is the specific discomfort of a silent 40-second gap where you can't tell reasoning from a hung tool call. Separating those two states, showing the tool name, and refusing to fake a completion signal — that's the whole value, and it's a small enough build that you can have it running the same day you decide you want it.