acp-thinking-spinner
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…
Skill by humanerd-drew
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/humanerd-drew-acp-thinking-spinner
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: design, backend, ml, agent-building
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
Network hosts it talks to 1
codeload.github.com· found in install command
Files it touches
~/.claude/skills/acp-thinking-spinner
What's inside
- Skill name
acp-thinking-spinner- Description
- 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.
- License
- MIT
- Instructions
- 129 lines in SKILL.md · view on GitHub
Files (1)
- SKILL.md
SKILL.md (first lines)
# 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:
```python
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:
```python
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
```
4. **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
```
5. **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('<file>').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}"
```
3. Restart the ACP server (e.g. exit the CLI process; new `{{AGENT_NAME_LOWER}}` invocation picks up the patch)
4. 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 tool_call_id, the call is a no-op. This is intentional safety.
- **Mock client skips streaming** — `if isinstance(client, Mock): _use_streaming = False` already exists; heartbeat only fires on streaming, so mock tests are unaffected.From humanerd-drew/opencode-drewgent, MIT.
More from humanerd-drew/opencode-drewgent
- 0.515score
content-management
humanerd-drew
CMO-style autonomous content agent.
SkillCFreshness: C★ 3Claude Code +9 - 0.511score
goose-acp-integration
humanerd-drew
Integrate {{AGENT_NAME}} with ACP (Agent Client Protocol) agents.
SkillCFreshness: C★ 3Claude Code +9 - 0.511score
vault-health
humanerd-drew
Maintain .{{AGENT_NAME_LOWER}} as an Obsidian vault — graph connectivity overhaul (P-layer mesh, skill clusters, SEO article web), broken link detection…
SkillCFreshness: C★ 3Claude Code +9 - 0.511score
self-replicating-agent-tdd
humanerd-drew
TDD-PDCA plan for building a self-replicating branching agent in {{AGENT_NAME}}.
SkillCFreshness: C★ 3Claude Code +9 - 0.511score
trend-harvester
humanerd-drew
AI Trend Collection & Filtering — collect GitHub trending repos, score via 5-axis philosophy filter, evaluate, apply, and retire.
SkillCFreshness: C★ 3Claude Code +9 - 0.507score
qa-cycle
humanerd-drew
Contract-First QA pipeline - implements 3-phase QA (contract → micro-QA → full-QA) using P2-hippocampus qa-evidence system
SkillCFreshness: C★ 3Claude Code +9 - 0.504score
qa-scenario-gen
humanerd-drew
Generate test scenarios and acceptance criteria from user requirements
SkillCFreshness: C★ 3Claude Code +9 - 0.277score
session-pattern-archiver
humanerd-drew
No description from the publisher yet.
SkillCFreshness: C★ 3Claude Code +9 - 0.277score
brain-signal-system
humanerd-drew
No description from the publisher yet.
SkillCFreshness: C★ 3Claude Code +9 - 0.277score
skill-gym
humanerd-drew
No description from the publisher yet.
SkillCFreshness: C★ 3Claude Code +9 - 0.277score
tool-integration-protocol
humanerd-drew
No description from the publisher yet.
SkillCFreshness: C★ 3Claude Code +9 - 0.277score
harsh-critic
humanerd-drew
No description from the publisher yet.
SkillCFreshness: C★ 3Claude Code +9
Signals
- Stars
- 3
- Forks
- 8
- Open issues
- 0
- Last commit (this folder)
- Jul 9, 2026 (3 months ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from humanerd-drew
- 0.515score
content-management
humanerd-drew
CMO-style autonomous content agent.
SkillCFreshness: C★ 3Claude Code +9 - 0.511score
trend-harvester
humanerd-drew
AI Trend Collection & Filtering — collect GitHub trending repos, score via 5-axis philosophy filter, evaluate, apply, and retire.
SkillCFreshness: C★ 3Claude Code +9 - 0.511score
goose-acp-integration
humanerd-drew
Integrate {{AGENT_NAME}} with ACP (Agent Client Protocol) agents.
SkillCFreshness: C★ 3Claude Code +9 - 0.511score
vault-health
humanerd-drew
Maintain .{{AGENT_NAME_LOWER}} as an Obsidian vault — graph connectivity overhaul (P-layer mesh, skill clusters, SEO article web), broken link detection…
SkillCFreshness: C★ 3Claude Code +9 - 0.511score
self-replicating-agent-tdd
humanerd-drew
TDD-PDCA plan for building a self-replicating branching agent in {{AGENT_NAME}}.
SkillCFreshness: C★ 3Claude Code +9 - 0.507score
qa-cycle
humanerd-drew
Contract-First QA pipeline - implements 3-phase QA (contract → micro-QA → full-QA) using P2-hippocampus qa-evidence system
SkillCFreshness: C★ 3Claude Code +9
Cite this page
acp-thinking-spinner skill. Agent Mart. https://agentmart.dev/skills/humanerd-drew-acp-thinking-spinner (last updated 2026-10-24)