AI News HubLIVE
In-site rewrite5 min read

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.

SourceHacker News AIAuthor: sathvikchinnu

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]