SLOPSHOPPER

synapse-tui

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

newbandtoasttimer
v0.1.0MITupdated 2026-10-16VishiATChoudhary/synapse/claude-mod
A shopper browsing a rack in a slop shop
README

<img src="assets/promo.gif" alt="Synapse promo: agents multiply, link up, and go multiplayer" width="900">

synapse

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

heysynapse.vercel.app


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">

Quick start

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.

What you get

<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>

  • Live canvas. Working agents pulse fast with an orange tick running round them, idle ones breathe. Messages travel along the links as little packets.
  • Grouped by project. Agents of the same git repo sit together in a labelled frame, each project with its own shading (solid, hatched, dotted, ...). Worktrees count as their main repo, and a shading never changes once assigned.
  • Light and dark. One ink on paper, orange for live state. Follows your system, or flip it with the button in the top bar. Fonts are bundled, so the canvas loads nothing from the internet.
  • Links are directional. A → B means B gets A's context. Draw both directions for a two-way pair.
  • Agents talk. Any two linked agents can message each other, Claude to Codex included.
  • You see it in the terminal. Claude Code and Codex print a line whenever context or messages arrive. Claude Code also gets a live band above the prompt and a toast the moment something connects.

How it works

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
  1. Discovery. Claude sessions come from Claude Code's own session registry. Every other CLI is found by scanning processes and their working folders, which also works when they run inside Superset.
  2. Links live in ~/.synapse/edges.json. You draw them on the canvas, or an agent makes them itself with the skill or MCP tools.
  3. Context. On each prompt, a hook injects what the linked agents said since last time, capped at 8k characters per link. Transcripts are read straight from disk: Claude's JSONL, Codex rollouts and Gemini chats.
  4. Messages go through the same store. Claude and Codex get them with the next prompt. Other agents pick them up through the synapse_inbox MCP tool.
  5. Identity. An agent figures out who it is by walking up the process tree to a known agent process, so no setup is needed per session.

Supported agents

AgentShows upPeers can read its contextMessagesTerminal lineLive band + toasts
Claude Code✓✓✓✓✓
Codex✓✓✓✓
Gemini CLI✓✓✓ (MCP)
OpenCode✓✓ (MCP)
Cursor Agent✓✓ (MCP)
Amp✓✓ (MCP)
Copilot CLI✓✓ (MCP)
Superset CLI✓✓ (CLI)

In the terminal

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.

Let agents do it themselves

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".

Canvas controls

⌘F or /fuzzy search. ↑↓ to pick, Enter to jump
drag the ring around a nodecreate a link
drag a nodemove it (saved)
drag a project framemove the whole project; other frames step aside so they never overlap
click a nodedetails, 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 backgroundzoom, pan
F, or double-click the backgroundfit everything on screen
Aarrange: one block per project (also runs by itself when project frames overlap)
☾ / ☀ in the top barlight or dark

What's in the box

lib.mjsdiscovery, transcript parsers (Claude, Codex, Gemini), projects and colours, message store
ops.mjspeers, link, unlink, read, send, inbox. Everything below calls these
server.mjs + public/the canvas: one HTML file, canvas 2D, live over SSE
hook.mjsSessionStart and UserPromptSubmit hook for Claude Code and Codex
mcp.mjsdependency-free stdio MCP server
cli.mjs, skill/the synapse CLI and the /synapse skill
claude-mod/Claude Code band and toasts
install.mjsidempotent 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).

Good to know

  • Messages are pulled, not pushed: an agent sees them with its next prompt or inbox check.
  • Agents that are already running pick up the MCP server and hooks after a restart.
  • Plain codex asks you once to trust the new hook. Gemini only loads MCP servers in folders you have trusted.
  • Shared context is labelled as coming from peers, and agents are told not to treat it as instructions from you.
  • Everything stays on your machine. Synapse makes no network calls.

Roadmap

  • Multiplayer: coming soon. Link your agents with a teammate's, across machines.
  • Push delivery, so an idle agent wakes up when a message arrives.
  • Readable transcripts for OpenCode, Amp and Copilot.

License

MIT

Source 2 files
hooks/register.tsx 123 lines
1import { 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}
123
types/index.d.ts 9 lines
1export 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