Browser Bridge¶
Pi extensions and tools inside the container can delegate actions to the browser via the backend bridge endpoint.
How It Works¶
Extensions POST to http://host.containers.internal:<nginx_port>/api/browser-delegate with a browser ID. Each browser tab generates a UUID stored in sessionStorage (survives refresh, unique per tab). When a terminal starts, the frontend sends this ID with the terminal_start WebSocket message. The backend maps the ID to the tab's WebSocket. The container reads the current browser ID dynamically via klangk-browser-id (which reads from tmux's global environment, updated on every attach/reattach).
Flow¶
LLM calls tool → Pi extension execute()
→ shell out to klangk-browser-id to get current browser ID
→ HTTP POST to /api/browser-delegate {action, browser_id, ...}
→ Backend resolves browser_id → (workspace_id, target_connection)
→ WebSocket message to target only: {"type":"browser_request","id":"...","action":"..."}
→ Flutter BrowserDelegate handles action (fetch, celebrate, etc.)
→ WebSocket message: {"cmd":"browser_response","id":"...","data":"..."}
→ Backend verifies sender matches target, returns HTTP response to extension
→ Extension returns result to LLM
Built-in actions: fetch (HTTP request with browser cookies). All other actions are dispatched to the ToolPluginRegistry which routes to Dart plugin handlers registered by klangk/ subdirectories.
Browser ID¶
The browser ID is a UUID generated by each browser tab (crypto.randomUUID()) and stored in sessionStorage. It:
- Survives page refresh (same tab, same ID)
- Is unique per tab (new tab = new ID)
- Is registered with the backend on every
terminal_start(including after reconnect) - Is stored in the container's tmux global environment via
klangk-attach-browser - Is read dynamically per-request via
klangk-browser-id(never cached in process env)
Container-side scripts¶
klangk-attach-browser <browser-id>— Called by the backend afterterminal_start. Stores the browser ID in the tmux global environment (or/tmp/.klangk-browser-idin non-tmux mode).klangk-browser-id— Prints the current browser ID to stdout. Reads from tmux global environment first, falls back to$KLANGK_BROWSER_IDenv var, then to the file fallback. Call this per-request — do not cache the result.
Writing a Pi Extension That Uses the Bridge¶
Pi extensions that use the bridge must read the browser ID dynamically by shelling out to klangk-browser-id. Do not cache the result — it changes on browser refresh or tab switch.
import { execSync } from "child_process";
const BRIDGE_URL = process.env.KLANGK_BRIDGE_URL;
const WORKSPACE_TOKEN = process.env.KLANGK_WORKSPACE_TOKEN;
/**
* Read the current browser ID from klangk-browser-id.
*
* Call this per-request, not once at module load — the ID changes
* when the user refreshes the browser or switches tabs.
*/
function getBrowserId(): string {
try {
return execSync("klangk-browser-id", { encoding: "utf-8" }).trim();
} catch {
return "";
}
}
export default function (pi: any) {
if (!BRIDGE_URL) return;
pi.registerTool({
name: "my-tool",
description: "Does something via the browser",
parameters: {},
async execute() {
const browserId = getBrowserId();
if (!browserId) {
return { content: [{ type: "text", text: "No browser connected." }] };
}
const resp = await fetch(`${BRIDGE_URL}/api/browser-delegate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(WORKSPACE_TOKEN
? { Authorization: `Bearer ${WORKSPACE_TOKEN}` }
: {}),
},
body: JSON.stringify({
action: "my_action",
browser_id: browserId,
}),
});
const data = await resp.json();
return { content: [{ type: "text", text: JSON.stringify(data) }] };
},
});
}
Key points:
BRIDGE_URLis set at container creation time and is stable — safe to read once at module load.getBrowserId()must be called per-request (per tool execution), not at module load.- The POST body uses
browser_id(snake_case), notbrowserId. - Include
WORKSPACE_TOKENin theAuthorizationheader if set.
Writing a Bridge Client (Python)¶
Python tools (like git-credential-klangk) should call klangk-browser-id via subprocess:
import subprocess
def get_browser_id():
try:
result = subprocess.run(
["klangk-browser-id"],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
return result.stdout.strip()
except (OSError, subprocess.TimeoutExpired):
pass
return ""
Current Client-Side Plugins¶
- celebrate (
plugins/celebrate/): Triggers confetti animation in the browser - beep (
plugins/beep/): Plays a beep sound in the browser - bobdobbs (
plugins/bobdobbs/): Bob "J.R." Dobbs quote generator - browser-fetch (
plugins/browser-fetch/): HTTP fetch using the browser's cookies/session - boingball (
plugins/boingball/): Bouncing Boing Ball animation overlay - git-credential (
plugins/git-credential/): Git credential helper that prompts for PAT in the browser