Show HN: Agent Chat Bridge – give AI IDE agents an async callback
Agent Chat Bridge is an open-source tool that enables asynchronous callbacks for AI chat sessions in IDEs like VS Code and Windsurf. Developers can register timers, shell commands, or webhooks that automatically resume a session with a prompt when triggered, with optional polling for the agent's reply. It offers a simple REST API and dashboard for managing jobs.
Notifications You must be signed in to change notification settings
Fork 0
Star 0
BranchesTags
Open more actions menu
Folders and files
NameName
Last commit message
Last commit date
Latest commit
History
78 Commits
78 Commits
.github/workflows
.github/workflows
assets
assets
build
build
docs
docs
graphify-out
graphify-out
scripts
scripts
src
src
tests
tests
.gitignore
.gitignore
.vscodeignore
.vscodeignore
LICENSE
LICENSE
README.md
README.md
eslint.config.js
eslint.config.js
package-lock.json
package-lock.json
package.json
package.json
tsconfig.json
tsconfig.json
vitest.config.ts
vitest.config.ts
Repository files navigation
Turn any AI agent chat session into an async agent. Register a timer, shell command, or webhook — the bridge automatically resumes the session with your prompt when the trigger fires.
Core flow: agent does work → POST /jobs (one call) → ends session normally → bridge fires prompt back to that session when the trigger fires. Poll GET /jobs/:id to read the agent's reply.
Supported IDEs: VS Code (GitHub Copilot Chat), Windsurf (Cascade)
Quick Start
1. Verify the bridge is running (after IDE reload)
curl --noproxy localhost http://localhost:9801/health
2. Register a 10-second timer (simplest test)
curl --noproxy localhost -X POST http://localhost:9801/jobs \ -H 'Content-Type: application/json' \ -d '{"session_id":"YOUR_SESSION_ID","prompt":"Timer fired — I am back.","seconds":10}'
→ { "job_id": "uuid", ... }
3. Agent ends its turn. 10 seconds later the prompt appears in that session.
4. Poll for the agent's reply (optional — see "Reading the reply" below)
curl --noproxy localhost http://localhost:9801/jobs/JOB_ID
→ { "status": "responded", "response_text": "..." }
⚠️ Always use --noproxy localhost in IDE terminals if a corporate proxy is configured. Without it, curl routes through the proxy (a remote server) which cannot reach localhost on your machine — you get binary garbage or a proxy error instead of JSON. --noproxy localhost,127.0.0.1 tells curl to connect directly for loopback addresses.
⚠️ Default port is 9801. Double-check the port in your curl commands.
Installation
One VSIX works in all supported IDEs:
VS Code
code --install-extension agent-chat-bridge-*.vsix --force
Windsurf
windsurf --install-extension agent-chat-bridge-*.vsix --force
Reload the IDE after install (Cmd+Shift+P → Developer: Reload Window).
Compatibility
IDE Minimum version Notes
VS Code 1.80.0 Session listing requires VS Code ≥ 1.96 (new chatSessions format)
Windsurf Latest stable Tested on macOS; CSRF token capture via language server spawn
Dashboard
Open the dashboard from the IDE command palette (Cmd+Shift+P → Agent Chat Bridge: Open Dashboard), or click the ⚡ Bridge :PORT status bar item at the bottom-right of the IDE window.
You can also open it in any browser while the bridge is running:
open http://localhost:9801/ui
The browser version uses regular fetch against the bridge server; the webview version uses VS Code's postMessage bridge. Both show the same UI.
The status bar item shows the port the bridge is bound to and opens the dashboard on click:
Windsurf — macOS Permission Prompts
On macOS, the extension may occasionally show a Keychain password dialog:
"windsurf" wants to use your confidential information stored in "Windsurf Safe Storage" in your keychain.
This is a fallback — not the normal path. The extension primarily captures the API key from the Windsurf language server's stdin metadata when the language server spawns (no Keychain access needed). The captured key is saved to the extension's globalStorage and restored automatically on subsequent IDE restarts.
The Keychain is only accessed when:
The language server was already running before the bridge extension activated (e.g. first install without a full Windsurf restart), and
No key was previously saved in globalStorage.
After the first successful capture, the prompt should not reappear.
The key is never stored outside your local machine. On Windows, the equivalent (DPAPI via PowerShell) runs silently with no prompt. On Linux, safe storage is not supported — the spawn interceptor is the only source; a full Windsurf restart is required if the key is missing.
If you click "Deny": Cascade RPC calls fail for this session. Reload the window (Cmd+Shift+P → Developer: Reload Window) to re-trigger the prompt, or do a full Windsurf restart to let the spawn interceptor capture the key without Keychain access.
API
The bridge listens on http://localhost:9801 (configurable in IDE Settings → "Agent Chat Bridge").
POST /jobs — register a job
curl --noproxy localhost -X POST http://localhost:9801/jobs \ -H 'Content-Type: application/json' \ -d '{ "session_id": "SESSION_ID", "prompt": "The task finished. Continue.", "seconds": 30 }'
Fields:
Field Type Required Description
prompt string ✅ Message submitted to the session when the trigger fires
session_id string — Target chat session ID. Omit to open a new chat
workspace string — Absolute path to workspace. Omit to use the current window's workspace
seconds number — Fire after N seconds. Must be > 0 (use 1 to fire near-immediately)
command string — Shell command (command or poll mode — fires on exit 0)
poll_interval number — Combine with command: run every N seconds until exit 0
model string or object — Model to use. String = family name (e.g. "gpt-4o"). Object = { family?, vendor?, id?, version? }
fallback "none" | "new_chat" — What to do if the session can't be focused. Default: "new_chat"
Mode is auto-detected from the fields you provide:
Fields Mode Behaviour
seconds (> 0) timer Fires after N seconds
command + poll_interval poll Runs command every N seconds, fires on first exit 0
command (no interval) command Runs command once, fires on exit 0
none of the above webhook Waits for POST /jobs/:id/fire — fires nothing on its own
No trigger fields = webhook mode. The job sits idle until you call POST /jobs/:id/fire. Use this when an external system (CI, Teams bot) fires the callback itself.
Response:
{ "ok": true, "job_id": "uuid", "mode": "timer", "fires_in": "30s", "session_id": "...", "fallback": "new_chat", "model": null, "workspace": "/path/to/ws" }
For webhook mode the response includes fire_url instead:
{ "ok": true, "job_id": "uuid", "mode": "webhook", "fire_url": "/jobs/JOB_ID/fire", ... }
GET /jobs/:id — poll job status and read the reply
curl --noproxy localhost http://localhost:9801/jobs/JOB_ID
Poll this endpoint to follow a job through its lifecycle and read the agent's reply once it arrives.
Status progression:
Status Meaning
waiting Trigger hasn't fired yet
fired Message is being sent to the session
delivered Message confirmed delivered; bridge is watching for the agent's reply
responded Agent replied — response_text is populated
submitted VS Code best-effort delivery (no confirmation); no reply watching
failed Delivery failed
{ "job_id": "uuid", "session_id": "aaaabbbb-...", "status": "responded", "mode": "timer", "prompt": "Timer fired — I am back.", "response_text": "I've reviewed the output. The build passed and...", "model": null, "fallback": "new_chat", "started_at": "2026-05-15T21:00:00.000Z", "running_for": "47s", "instance": "local" }
response_text is null while the agent is still running (delivered status) and populated once the model finishes (responded). After responded the job moves to history where response_text is also preserved.
Typical polling loop:
while true; do RESP=$(curl -s --noproxy localhost http://localhost:9801/jobs/JOB_ID) STATUS=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))") case "$STATUS" in waiting|fired|delivered) sleep 2 ;; responded) echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_text',''))"; break ;; *) echo "done: $STATUS"; break ;; esac done
POST /jobs/:id/fire — manually trigger a job
curl --noproxy localhost -X POST http://localhost:9801/jobs/JOB_ID/fire
Immediately fires any job regardless of trigger type. Useful for testing and external webhooks (CI, GitHub Actions, Teams bots, deployment pipelines, etc.).
DELETE /jobs/:id — cancel a job
curl --noproxy localhost -X DELETE http://localhost:9801/jobs/JOB_ID
GET /jobs — list active jobs
curl --noproxy localhost http://localhost:9801/jobs
Returns both local (this bridge instance) and remote (other bridge instances) jobs:
{ "jobs": [ { "job_id": "uuid", "session_id": "...", "mode": "timer", "status": "waiting", "prompt": "...", "model": null, "fallback": "new_chat", "workspace": "/path/to/ws", "command": null, "poll_interval": null, "poll_attempts": null, "seconds": 30, "fires_at": "2026-05-15T21:00:00.000Z", "started_at": "2026-05-15T20:59:30.000Z", "running_for": "30s", "instance": "local" } ], "count": 1, "local": 1, "remote": 0 }
GET /sessions — list all known chat sessions
All workspaces, most recent first
curl --noproxy localhost 'http://localhost:9801/sessions'
Filter to a specific workspace
curl --noproxy localhost 'http://localhost:9801/sessions/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'
Single session detail
curl --noproxy localhost 'http://localhost:9801/sessions/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/SESSION_ID'
Paginate
curl --noproxy localhost 'http://localhost:9801/sessions?limit=10&offset=20'
Scans session storage across all workspaces and returns sessions sorted by last activity (most recent first). Sessions with no messages sent are excluded.
Response:
{ "sessions": [ { "session_id": "aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb", "workspace_hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "workspace_path": "/home/user/workspace/my-project", "last_modified": "2026-05-08T12:34:56.000Z", "created_at": "2026-05-08T10:00:00.000Z", "first_prompt": "Help me build the async callback system", "title": "Async callback system", "source": "local" } ], "total": 47, "limit": 20, "offset": 0 }
Query params: ?limit=N (max 200, default 20), ?offset=N
Get the current window's workspace hash from GET /health → current_workspace_hash.
GET /health
curl --noproxy localhost http://localhost:9801/health
→ { "status": "ok", "ide": "vscode", "port": 9801, "configured_port": 9801, "jobs": 0,
"current_workspace": "/path/to/workspace",
"current_workspace_hash": "a1b2c3d..." }
The ide field is "vscode" or "windsurf". Extra fields are IDE-specific:
VS Code: current_workspace_hash
Windsurf: token, api_key (redacted to "***"), cascade_port, model_uid
GET /registry — see all running bridge instances
curl --noproxy localhost http://localhost:9801/registry
→ {
"registry": {
"/path/to/workspace-a": { "port": 9801, "ide": "vscode" },
"/path/to/workspace-b": { "port": 9802, "ide": "windsurf" }
},
"current_workspace": "/path/to/workspace-a"
}
Shows all running bridge instances from the shared registry file. Each entry includes the port and the IDE that registered it. Cross-window job forwarding only targets bridges running the same IDE — a VS Code bridge will never forward to a Windsurf bridge even if they share the same workspace path.
GET /history — completed/cancelled/fired jobs
curl --noproxy localhost http://localhost:9801/history
→ { "history": [ { "job_id": "...", "status": "fired", "prompt": "...", ... } ], "count": 12 }
Persistent history of all jobs that have finished (fired, delivered, responded, cancelled, or failed). Trims to the last 500 entries on disk; in-memory cache serves the last 100. Includes response_text when the agent replied.
POST /session/identify — find sessions by message content
Fuzzy-match sessions that contain specific messages. Useful for agents that need to find the session a previous exchange happened in.
curl --noproxy localhost -X POST http://localhost:9801/session/identify \ -H '
[truncated for AI cost control]