AgentStack
SKILL verified MIT Self-run

Acp Thinking Spinner

skill-humanerd-drew-opencode-drewgent-acp-thinking-spinner · by humanerd-drew

Add a "Thinking..." spinner card to an ACP server's tool-call stack so ACP clients (Zed Agent Panel, openCode TUI, JetBrains/VS Code ACP UI) show a working indicator during the LLM API call phase. Triggers when working on an Agent Client Protocol (ACP) server and the user wants LLM-thinking visibility in the client's tool-call stack.

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-humanerd-drew-opencode-drewgent-acp-thinking-spinner

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Acp Thinking Spinner? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

ACP Thinking Spinner — Tool-Card Pattern

When an ACP server ({{AGENT_NAME}}'s acp_adapter/, Hermes, or any Agent Client Protocol implementation) needs to show a working indicator during the LLM API call phase, the standard approach is to synthesize a virtual tool call with kind="think". This places a "Thinking..." card in the client's tool-call stack — the same place real tool calls appear, which is the most reliable UI surface.

Why this pattern (root-cause: ACP spec gap)

ACP has no dedicated channel for "agent is generating a response." It only has:

| Channel | Where it renders | Problem | |---------|------------------|---------| | update_agent_thought_text | Client's "Thinking" area | Most clients render this as static text — JetBrains got stuck on "Thinking" with rawInput updates, others don't blink. Unreliable for "is it stuck?" UX. | | update_agent_message_text | Answer streaming area | Only fires once streaming actually starts; doesn't cover the 0-30s gap before first delta. | | ToolCallStart / ToolCallUpdate | Tool-call stack (openCode-style) | The most reliable UI surface — clients render an animated spinner here for any tool call. Synthesizing a virtual think-kind call works. | | Status bar | Client-specific | Not standardized in ACP — see agentclientprotocol.com/rfds/session-usage (RFD, not yet spec). Cannot trigger from the server. |

Conclusion: For a "is it stuck or working?" signal in ACP, the only reliable channel is the tool-call stack. Use a virtual kind="think" card.

State machine

start     → ToolCallStart(kind="think", title="Thinking...")
heartbeat → ToolCallUpdate(status="in_progress", elapsed text)   # at 30s, 60s, ...
complete  → ToolCallUpdate(status="completed")

Hook sites in the LLM call lifecycle:

  1. Before _interruptible_api_call() / _interruptible_streaming_api_call()start
  2. First streamed delta (on_first_delta callback) → complete
  3. Long-running timer (e.g. threading.Timer(30.0, ...)) → heartbeat; cancel on complete

If a real tool call is dispatched before the LLM finishes, the thinking card is implicitly replaced by the real tool's card (ACP client stack semantics). No need to manually close.

{{AGENT_NAME}} reference implementation

Files modified ({{AGENT_NAME}} ACP server at ~/.{{AGENT_NAME_LOWER}}/source/{{AGENT_NAME_LOWER}}-agent/):

acp_adapter/tools.py — three helpers

  • build_thinking_start(tool_call_id) -> ToolCallStart
  • build_thinking_progress(tool_call_id, elapsed) -> ToolCallProgress
  • build_thinking_complete(tool_call_id) -> ToolCallProgress

All use kind="think" and acp.start_tool_call / acp.update_tool_call.

acp_adapter/events.py — callback factory

make_agent_progress_cb(conn, session_id, loop) returns a callable matching the signature:

agent_progress_callback(state: str, name: str, args: dict = None)

Stores name → tool_call_id in a closure dict. State machine: start/heartbeat/complete.

acp_adapter/server.py — wire

Alongside existing thinking_cb, add:

agent_progress_cb = make_agent_progress_cb(conn, session_id, loop)
agent.agent_progress_callback = agent_progress_cb

run_agent.py — three hook sites + callback attr

  1. Add agent_progress_callback: callable = None to AIAgent.__init__ signature
  2. self.agent_progress_callback = agent_progress_callback in the body
  3. Hook A (LLM call start, right before _interruptible_*_api_call):

``python if self.agent_progress_callback: try: self.agent_progress_callback("start", "llm_response", {}) except Exception: pass ``

  1. Hook B (first delta, inside the on_first_delta closure):

``python if self.agent_progress_callback: try: self.agent_progress_callback("complete", "llm_response", {}) except Exception: pass ``

  1. Hook C (30s+ timer callback):

``python def _llm_heartbeat(): elapsed = time.time() - api_start_time if self.agent_progress_callback: try: self.agent_progress_callback("heartbeat", "llm_response", {"elapsed": elapsed}) except Exception: pass # fallback: thinking_callback (text), _vprint (raw stdout) _llm_heartbeat_timer = threading.Timer(30.0, _llm_heartbeat) ``

Channel routing rule (stdout breaks ACP)

ACP is JSON-RPC over stdio. Writing raw text to stdout (e.g. print() or _vprint) breaks the protocol. Three channels, in priority order:

| Channel | Path | When | |---------|------|------| | agent_progress_callback | ToolCallStart/Update (tool card) | ACP client available — preferred | | thinking_callback | update_agent_thought_text | Fallback for non-ACP paths; some clients (JetBrains) render this as static "Thinking..." | | _vprint (raw stdout) | Plain text | Last-resort for non-ACP environments (gateway streaming, plain CLI) |

Always check agent_progress_callback first, then thinking_callback, then _vprint. Never print() directly in agent code — it leaks through the JSON-RPC pipe.

Env var

DREW_LLM_HEARTBEAT_DISABLED=1 disables the 30s+ timer. Use for tests / CI to avoid timing flakiness.

Verification

After patching:

  1. python3 -c "import ast; ast.parse(open('').read())" for each modified file
  2. AST check that the 3 hook sites are present:

``python hook_count = body_src.count('self.agent_progress_callback(') assert hook_count == 3, f"expected 3 hook sites, got {hook_count}" ``

  1. Restart the ACP server (e.g. exit the CLI process; new {{AGENT_NAME_LOWER}} invocation picks up the patch)
  2. Trigger a long-running LLM call and confirm the client shows the spinner card in the tool-call stack

Pitfalls

  • Don't put text content in update_agent_thought_text thinking channel if the user said "I don't want text — I want an indicator." Use the tool-card channel instead.
  • Don't write to sys.stdout directly in ACP mode — breaks JSON-RPC. Always go through a registered callback or the tool-card helper.
  • Don't fire heartbeat without a prior start — the closure dict won't have a toolcallid, the call is a no-op. This is intentional safety.
  • Mock client skips streamingif isinstance(client, Mock): _use_streaming = False already exists; heartbeat only fires on streaming, so mock tests are unaffected.

Related

  • ACP spec: https://agentclientprotocol.com/protocol/v1/tool-calls
  • ACP status/usage RFD (in progress, not yet spec): https://agentclientprotocol.com/rfds/session-usage
  • openCode ACP: https://opencode.ai/docs/acp/
  • Zed Agent Panel: https://zed.dev/docs/ai/agent-panel
  • JetBrains ACP client (stuck on "Thinking" issue): https://youtrack.jetbrains.com/projects/WI/issues/WI-83745
  • {{AGENT_NAME}}'s own ACP source: acp_adapter/{server,events,tools,session}.py

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.