Shows Synapse links into this session above the prompt, with toasts when agents connect or message

<img src="assets/promo.gif" alt="Synapse promo: agents multiply, link up, and go multiplayer" width="900">
Every coding agent on your machine, on one canvas. Draw a line between two of them and they share context.
<img alt="Node 18+" src="https://img.shields.io/badge/node-%E2%89%A518-16130F?style=flat-square&labelColor=F1EFE9"> <img alt="Zero dependencies" src="https://img.shields.io/badge/dependencies-0-16130F?style=flat-square&labelColor=F1EFE9"> <img alt="MCP" src="https://img.shields.io/badge/MCP-stdio-16130F?style=flat-square&labelColor=F1EFE9"> <img alt="MIT" src="https://img.shields.io/badge/license-MIT-FF4400?style=flat-square&labelColor=F1EFE9">
Claude Code · Codex · Gemini CLI · OpenCode · Cursor Agent · Amp · Copilot CLI · Superset
You have five agents open. One just figured out the auth bug, another is about to touch the same file, and the only way they find out about each other is you copy-pasting between terminals.
Synapse puts every live agent on a canvas as a pulsing node. Drag from one agent to another and the second one can see what the first is doing. Linked agents can also message each other, whichever CLI they run in.
<img src="assets/canvas.gif" alt="The Synapse canvas: agents pulse, context flows along the links" width="1000">
git clone https://github.com/VishiATChoudhary/synapse && cd synapse
node install.mjs # wires Synapse into every agent CLI it finds (backs up each config first)
npm start # canvas at http://localhost:4747
No npm install: Synapse is plain Node 18+ with zero dependencies.
Just want to look around? npm run demo opens a canvas full of made-up agents on http://localhost:4848 and leaves your real sessions alone.
<table> <tr> <td width="50%"><img src="assets/panel.png" alt="Agent panel with links, agent messages and recent activity"></td> <td width="50%"><img src="assets/search.png" alt="Fuzzy search dims everything that does not match"></td> </tr> <tr> <td><b>Click an agent</b> to see who it reads from, who it shares with, the messages agents sent it, and its latest conversation, rendered as markdown.</td> <td><b>⌘F</b> fuzzy-searches by name, project, agent type or folder. Each word matches on its own, so <code>cdx api</code> finds the Codex agent in <code>acme-api</code>.</td> </tr> </table>
A → B means B gets A's context. Draw both directions for a two-way pair.flowchart LR
subgraph machine[your machine]
C1[Claude Code] & C2[Codex] & C3[Gemini, OpenCode,<br/>Cursor, Amp, ...]
end
D[discovery<br/>session registry + process scan] --> S[(~/.synapse<br/>edges · messages)]
UI[canvas<br/>localhost:4747] <--> S
S --> H[prompt hook<br/>Claude Code + Codex]
S <--> M[MCP server<br/>every agent]
S <--> K[synapse CLI<br/>+ /synapse skill]
H --> C1 & C2
M --> C1 & C2 & C3
~/.synapse/edges.json. You draw them on the canvas, or an agent makes them itself with the skill or MCP tools.synapse_inbox MCP tool.| Agent | Shows up | Peers can read its context | Messages | Terminal line | Live band + toasts |
|---|---|---|---|---|---|
| Claude Code | ✓ | ✓ | ✓ | ✓ | ✓ |
| Codex | ✓ | ✓ | ✓ | ✓ | |
| Gemini CLI | ✓ | ✓ | ✓ (MCP) | ||
| OpenCode | ✓ | ✓ (MCP) | |||
| Cursor Agent | ✓ | ✓ (MCP) | |||
| Amp | ✓ | ✓ (MCP) | |||
| Copilot CLI | ✓ | ✓ (MCP) | |||
| Superset CLI | ✓ | ✓ (CLI) |
When a session starts, and whenever a prompt pulls something in, Claude Code and Codex print:
synapse ⇠ reads codex-api-rate-limit · ⇢ shares with api-review
synapse ⇢ pulled context from codex-api-rate-limit (13 msgs, 3.4k chars) · ✉ 1 message from api-review
In Claude Code, the claude-mod/ add-on keeps a band above the prompt and pops a toast as soon as an agent connects, unlinks or messages you:
◉ synapse reads ⇠ ● codex-api-rate-limit codex shares ⇢ ● api-review claude ✉ 1 from api-review
Run it with claude --plugin-dir ./claude-mod. Its test runs with claude plugin test claude-mod.
Every agent gets the same six operations, as MCP tools (synapse_peers, synapse_link, synapse_unlink, synapse_read, synapse_send, synapse_inbox) and as a CLI:
synapse peers --all # who am I linked to, and who else is running
synapse link codex-api # link both ways (--in / --out for one direction)
synapse read codex-api # their latest conversation
synapse send codex-api "Did the rate limiter land? I'm about to touch auth."
synapse inbox # what other agents sent me
synapse rename "auth refactor" # rename yourself (or: synapse rename <agent> <name>)
synapse ui # start the canvas
The installer puts the launcher at ~/.synapse/bin/synapse and adds a /synapse skill to Claude Code and Codex, so you can just say "connect to the codex agent and ask what it found".
⌘F or / | fuzzy search. ↑↓ to pick, Enter to jump |
| drag the ring around a node | create a link |
| drag a node | move it (saved) |
| drag a project frame | move the whole project; other frames step aside so they never overlap |
| click a node | details, links, messages, recent activity |
| double-click a node (or rename in its panel) | rename it. Empty name goes back to the generated one |
| hover a link, click × (or select it and press ⌫) | remove the link |
| scroll, drag the background | zoom, pan |
| F, or double-click the background | fit everything on screen |
| A | arrange: one block per project (also runs by itself when project frames overlap) |
| ☾ / ☀ in the top bar | light or dark |
lib.mjs | discovery, transcript parsers (Claude, Codex, Gemini), projects and colours, message store |
ops.mjs | peers, link, unlink, read, send, inbox. Everything below calls these |
server.mjs + public/ | the canvas: one HTML file, canvas 2D, live over SSE |
hook.mjs | SessionStart and UserPromptSubmit hook for Claude Code and Codex |
mcp.mjs | dependency-free stdio MCP server |
cli.mjs, skill/ | the synapse CLI and the /synapse skill |
claude-mod/ | Claude Code band and toasts |
install.mjs | idempotent installer. --dry-run shows what it would change |
promo/ | the Remotion source of the video above |
Settings: PORT, SYNAPSE_HOME (state folder, default ~/.synapse), SYNAPSE_BUDGET (characters per link per prompt), SYNAPSE_AGENT (force an identity).
codex asks you once to trust the new hook. Gemini only loads MCP servers in folders you have trusted.MIT
hooks/register.tsx 123 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Peer, View } from '../types'
5
6// Reads ~/.synapse (written by the Synapse canvas, CLI and MCP server) and shows,
7// for this session: who is linked in, who it shares with, and unread agent messages.
8
9const view = atom({ plugin: 'synapse-tui', key: 'view' } as const, null)
10
11type Edge = {
12 id: string
13 from: string
14 to: string
15 fromName: string
16 toName?: string
17 fromColor?: string
18 toColor?: string
19 fromKind?: string
20 toKind?: string
21}
22type Message = { id: string; from: string; to: string; fromName: string; delivered: boolean }
23
24const ACCENT = '#FF4400'
25const POLL_MS = 1500
26
27export const register: Register = on => {
28 let timer: { cancel(): void } | undefined
29 let known: Map<string, Edge> | null = null
30 let seenMessages: Set<string> | null = null
31
32 on('session.start', async ($, e, next) => {
33 const started = await next(e)
34 const home = await $.env.get('HOME')
35 const me = await $.session.id()
36 const dir = `${home}/.synapse`
37
38 const readJSON = async <T,>(file: string, fallback: T): Promise<T> => {
39 try {
40 return JSON.parse(await $.fs.read(`${dir}/${file}`)) as T
41 } catch {
42 return fallback
43 }
44 }
45
46 const poll = async () => {
47 const edges = await readJSON<Edge[]>('edges.json', [])
48 const messages = await readJSON<Message[]>('messages.json', [])
49 const mine = edges.filter(x => x.from === me || x.to === me)
50 const unread = messages.filter(m => m.to === me && !m.delivered)
51
52 // toasts for what changed since the last look (none on the first look)
53 if (known) {
54 for (const x of mine) {
55 if (known.has(x.id)) continue
56 if (x.to === me) {
57 $.ui.toast(`⇠ synapse: ${x.fromName} connected. Its context flows in with your next prompt.`, { timeoutMs: 6000 })
58 } else {
59 $.ui.toast(`⇢ synapse: now sharing this session with ${x.toName ?? 'an agent'}`, { timeoutMs: 6000 })
60 }
61 }
62 for (const [id, x] of known) {
63 if (!mine.some(y => y.id === id)) $.ui.toast(`synapse: unlinked ${x.to === me ? x.fromName : x.toName ?? 'an agent'}`)
64 }
65 }
66 if (seenMessages) {
67 for (const m of unread) {
68 if (!seenMessages.has(m.id)) $.ui.toast(`✉ synapse: message from ${m.fromName}, delivered with your next prompt`, { timeoutMs: 6000 })
69 }
70 }
71 known = new Map(mine.map(x => [x.id, x]))
72 seenMessages = new Set(unread.map(m => m.id))
73
74 const peer = (name: string | undefined, color: string | undefined, kind: string | undefined): Peer => ({
75 name: name ?? '?',
76 color: color ?? ACCENT,
77 kind: kind ?? '',
78 })
79 const fresh: View = {
80 incoming: mine.filter(x => x.to === me).map(x => peer(x.fromName, x.fromColor, x.fromKind)),
81 outgoing: mine.filter(x => x.from === me).map(x => peer(x.toName, x.toColor, x.toKind)),
82 unread: unread.length,
83 unreadFrom: [...new Set(unread.map(m => m.fromName))],
84 }
85 if (JSON.stringify(await read($, view)) !== JSON.stringify(fresh)) await update($, view, () => fresh)
86 }
87
88 timer?.cancel()
89 await poll()
90 timer = $.clock.every(POLL_MS, () => void poll())
91 return started
92 })
93
94 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
95 const v = await read($, view)
96 if (e.props.hasSurvey || !v || (!v.incoming.length && !v.outgoing.length && !v.unread)) return next(e)
97
98 const { Box, Text } = $.ui.resolve(e)
99 const list = (peers: Peer[], prefix: string) =>
100 peers.map((p, i) => (
101 <Text key={`${prefix}${i}`}>
102 <Text color={p.color}>● </Text>
103 <Text>{p.name}</Text>
104 {p.kind ? <Text dimColor> {p.kind}</Text> : null}
105 <Text>{' '}</Text>
106 </Text>
107 ))
108
109 return (
110 <Box>
111 <Text color={ACCENT} weight="bold">◉ synapse </Text>
112 {v.incoming.length ? <Text dimColor>reads ⇠ </Text> : null}
113 {list(v.incoming, 'in')}
114 {v.outgoing.length ? <Text dimColor>shares ⇢ </Text> : null}
115 {list(v.outgoing, 'out')}
116 {v.unread ? (
117 <Text color={ACCENT}>✉ {v.unread} from {v.unreadFrom.join(', ')}</Text>
118 ) : null}
119 </Box>
120 )
121 })
122}
123types/index.d.ts 9 lines1export type Peer = { name: string; color: string; kind: string }
2export type View = { incoming: Peer[]; outgoing: Peer[]; unread: number; unreadFrom: string[] }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'synapse-tui': { view: View | null }
7 }
8}
9