Documentation
Introduction
Terminal Graph is a native macOS app from INTDEV — an infinite canvas where nodes are real terminals, browsers, notes, editors, file watchers, and small utility nodes.
Drop them wherever, wire them up, and keep the whole mess of a project in one spatial view instead of cycling through tabs and tmux panes. The connections pipe data between nodes, which opens up workflows you can’t get from tabs alone. Curious to see what you come up with.
Download & install
Open the DMG, drag TerminalGraphBeta.app into your Applications folder, then eject the DMG.
Auto-updates
The app is signed with an Apple Developer ID and notarized by Apple, and Sparkle keeps it up to date automatically. Updates install in place, and the macOS permissions you grant (folder access, microphone, notifications) carry across updates.
Getting started
Adding nodes
Right-click anywhere on the canvas to open the node menu. Select the type of node you want to add. Nodes can be repositioned by dragging their title bar and resized by dragging any corner or edge. Double-click a node’s title bar to fit it to its content: browser, note, editor, image, and video nodes snap to their natural size (clamped to the visible area, with images and video keeping their aspect ratio). Double-click again to restore the previous size.
Getting around
Scroll or two-finger swipe to pan the canvas, and pinch or ⌘-scroll to zoom. Scrolling over an unfocused node pans the canvas — click the node to focus it first if you want to scroll its own content. Press ⌘⌥0 to fit everything on screen, ⌘⌥F to fit the selected node, and ⌘⌥M to toggle the minimap for jumping around large boards.
Drag and drop
Drop a file, image, or video onto empty canvas to create a node for it: images become image nodes and videos open in a video player. Drop an image onto a terminal instead and its path is typed into the shell, which is handy for handing a screenshot to a CLI agent like Claude Code. Images dragged from a browser, Slack, or the macOS screenshot thumbnail work even when there’s no file on disk; they are saved to a temporary file first. A highlighted border shows which node a drop will land in, versus spawning a new node on the canvas.
Wiring nodes
Drag from one port to another to create a connection. Ports live on the edges of each node; output ports are on the right side, input ports on the left. The port hitbox is a half-circle on the outside of the node edge — hover slowly near the edge to reveal it. Right-click a port to disconnect it.
Connections render as smooth Bezier curves. In Wiring Mode you can add waypoints to route a curve around other nodes: double-click a wire to add a waypoint, then drag it into position. Double-click a waypoint to remove it. Right-click a wire or waypoint for more options.
Press ⌘⌥L to toggle Wiring Mode, which makes all ports visible simultaneously and is easier to use when wiring many connections at once. Wiring works without Wiring Mode too, but waypoint interactions require it.
Command palette
Press ⌘K to open the command palette. It is the fastest way to reach any action — adding nodes, changing settings, running commands — without navigating menus.
Sidebar
Press ⌘⌥S to toggle the sidebar, which contains a file tree and workspace switcher. The tree spans every open project (see Projects) and is populated once at least one project folder is open.
Projects
A project is a folder on disk; a workspace is the window and canvas that holds it. A workspace can hold more than one project at once, so you can keep several codebases side by side on one canvas and wire dataflow between them.
Each project occupies its own tinted region on the canvas, and every node carries a small badge for the project it belongs to. The sidebar file tree spans all open projects.
Adding a project
- File → Add Project to Workspace…
- Right-click the canvas and choose Add Project…
- Drag a folder onto the canvas.
Opening a single folder still works as before; the window becomes a multi-project workspace the moment you add a second one. Use File → Save Workspace As… to save a multi-project layout you want to reopen later.
Keyboard shortcuts
All shortcuts are also listed next to their corresponding menu items inside the app.
| Shortcut | Action | Category |
|---|---|---|
| ⌘, | Settings | App |
| ⌘Q | Quit Application | App |
| ⌘O | Open Folder | File |
| ⌘C | Copy | Edit |
| ⌘V | Paste | Edit |
| ⌘A | Select All | Edit |
| Esc | Deselect All | Edit |
| ⌘N | New Terminal | Nodes |
| ⌘⇧N | New Note | Nodes |
| ⌘⇧B | New Browser | Nodes |
| ⌘⇧E | New File Editor | Nodes |
| ⌘D | Duplicate Selection | Nodes |
| ⌘⇧D | Duplicate Selection Vertically | Nodes |
| ⌘G | New Freeform Group | Nodes |
| ⌘⇧G | New Split Group | Nodes |
| ⌘W | Close Node | Nodes |
| ⌘⇧T | Reopen Last Closed | Nodes |
| ⌘K | Command Palette | Palette |
| ⌘⌥L | Toggle Wiring Mode | View |
| ⌘R | Reload Page | View |
| ⌘⇧R | Reload Page from Origin | View |
| ⌘⏎ | Enter / Exit Focus View | View |
| ⌘⇧⏎ | Push / Pop Nested Focus | View |
| ⌘⌥⏎ | Toggle Group / Child Focus | View |
| ⌘⌥0 | Zoom to Fit All | View |
| ⌘⌥F | Zoom to Fit Node | View |
| ⌘⌥T | Tidy Selection | View |
| ⌘⌥S | Toggle Sidebar | View |
| ⌘⌥M | Toggle Minimap | View |
| ⌘= | Zoom In / Increase Font Size | View |
| ⌘− | Zoom Out / Decrease Font Size | View |
| ⌘0 | Reset Zoom / Font Size | View |
| ⌘⌥← | Navigate Left | Navigation |
| ⌘⌥→ | Navigate Right | Navigation |
| ⌘⌥↑ | Navigate Up | Navigation |
| ⌘⌥↓ | Navigate Down | Navigation |
| ⌘M | Minimize Window | Window |
Notes: Esc clears multi-selection when two or more nodes are selected. ⌘= / ⌘− / ⌘0zoom the canvas when no node is focused; with a note, editor, or browser focused they adjust that node’s font size instead, and terminal nodes handle font size directly via Ghostty. Focus View presents a single node or group on its own; inside it, ⌘⇧⏎ steps in and out of a nested group and ⌘⌥⏎ switches focus between a group and its contents. The navigation arrow shortcuts are canvas-level keyboard monitor shortcuts, not menu items.
Node types
Each node type exposes a set of typed ports. Connections are validated before wiring; incompatible port types are rejected. Port types are described in the Dataflow section.
Terminal
An interactive shell backed by libghostty with full stdio streams and process lifecycle signals. Right-click on terminal content for a context menu with Copy, Paste, Clear, Reset Terminal, Close Node, and group/split actions. The menu respects mouse capture, so it won’t interfere with vim or tmux.
| Port | Type | Direction | Description |
|---|---|---|---|
stdout | stream | output | Terminal stdout |
stderr | stream | output | Terminal stderr |
stdin | stream | input | Inject into process stdin |
text | signal | input | Inject text into the Ghostty surface and submit with Return, bypassing the stdin FIFO |
exit | signal | output | Process exit event; payload is the exit code |
cwd | state | output | Current working directory, tracked via OSC 7 |
Browser
An embedded web browser backed by WKWebView with navigation, console logging, and DOM inspection. Reload with ⌘R, or hard-reload with ⌘⇧R (also ⇧-click the reload button, or right-click the page and choose Hard Reload) to bypass the cache and re-fetch CSS and JS, which helps when a local dev server keeps serving stale assets. To make a node skip the cache on every reload, right-click the page and turn on Disable Cache. Type a search term rather than a URL into the address bar and it runs a web search; choose the engine in Settings → General → Browser.
| Port | Type | Direction | Description |
|---|---|---|---|
url | state | output | Current page URL |
console | stream | output | JavaScript console logs — not yet functional |
dom | state | output | DOM serialized as HTML, evaluated only when wired |
navigate | signal | input | Load a URL string |
reload | signal | input | Reload the current page |
Note
A free-form Markdown editor with optional file-backing, powered by Monaco.
| Port | Type | Direction | Description |
|---|---|---|---|
content-changed | state | output | Current note text, emitted on every edit |
write | signal | input | Replace content with incoming text |
append | signal | input | Append text, preserving cursor position and undo history |
save-content | signal | output | Emitted on save; payload is the note content |
save-path | signal | output | Emitted on save; payload is the file path (empty for ephemeral notes) |
Editor
A file-backed code editor powered by Monaco with language detection and external file watching. Auto-reloads on external change when not dirty.
| Port | Type | Direction | Description |
|---|---|---|---|
path | state | output | Absolute path of the open file |
content | state | output | Current file content |
save | signal | output | Save attempt; optional payload is the file path |
write | signal | input | Replace content |
append | signal | input | Append to content |
open | signal | input | Load a new file by path |
Image
Displays images from files or raw binary data. File-backed images auto-reload on change. No output ports. You can also create one by dragging an image onto the canvas (see Drag and drop).
| Port | Type | Direction | Description |
|---|---|---|---|
path | signal | input | Load image from a file path |
data | signal | input | Load image from raw bytes |
refresh | signal | input | Reload from the current file |
Video
Plays video files natively via AVKit, with standard playback controls. Like image nodes, it plays any format your Mac supports (MP4, MOV, and more). No output ports. Create one by dragging a video file onto the canvas (see Drag and drop), or from the right-click menu.
| Port | Type | Direction | Description |
|---|---|---|---|
path | signal | input | Load a video from a file path |
refresh | signal | input | Reload from the current file |
File Watcher
Watches files matching a glob pattern and emits a signal when one changes. Relative patterns resolve against the workspace root. Exact file paths are watched directly; glob patterns scan the relevant directory and match absolute, directory-relative, and workspace-relative paths. Changes are debounced approximately 0.5 seconds to avoid event stampedes. By default, one signal is emitted per debounce window with the first matching file path. Enable Report all matches to emit one signal per matching file.
| Port | Type | Direction | Description |
|---|---|---|---|
changed | signal | output | Fires when a matching file changes; payload is the full file path |
Trigger
Emits a payload-free signal manually or on an interval. Use it to kick off scheduled flows, such as Trigger → Run for a periodic command.
| Port | Type | Direction | Description |
|---|---|---|---|
trigger | signal | output | Manual or interval trigger event |
Run
Runs a non-interactive shell command once for each input signal. The signal payload is written to the command’s stdin. Completed stdout is emitted as both output and latest; stderr and exit status have separate signal ports. Inputs queue while a command is running.
Run defaults to the workspace root as its working directory. A custom working directory can be absolute or relative to that root. Built-in command templates include Custom Command (zsh), Filter Lines (grep), Query JSON (jq), Find & Replace (sed), Sort Lines (sort), and Word Count (wc).
| Port | Type | Direction | Description |
|---|---|---|---|
input | signal | input | Command invocation payload, written to stdin |
latest | state | output | Most recent stdout |
output | signal | output | Stdout from each completed command |
error | signal | output | Stderr from each completed command |
exit | signal | output | Process exit status |
Collect
Frames a continuous stream into discrete signal payloads. Use it between stream-producing nodes and signal-based utility nodes, such as Terminal stdout → Collect → Run.
Delimiters include newline, double newline, null byte, space, and a custom string. A timeout can flush the current buffer when no delimiter arrives.
| Port | Type | Direction | Description |
|---|---|---|---|
input | stream | input | Continuous bytes to frame |
reset | signal | input | Clear the buffer and counters |
latest | state | output | Most recent framed message |
output | signal | output | Framed message payload |
count | state | output | Number of messages emitted |
Gate
Passes or blocks signal payloads based on open/closed state. Toggle it manually or drive it from other signals.
| Port | Type | Direction | Description |
|---|---|---|---|
input | signal | input | Payload to pass when open |
open | signal | input | Set the gate open |
close | signal | input | Set the gate closed |
toggle | signal | input | Flip open/closed state |
output | signal | output | Passed payloads |
Switch
Routes signal payloads by ordered regular-expression rules. The first matching rule wins. Payloads that match no rule go to default.
| Port | Type | Direction | Description |
|---|---|---|---|
input | signal | input | Payload to route |
output-1 | signal | output | Rule 1 matches |
output-2 | signal | output | Rule 2 matches |
output-3 | signal | output | Rule 3 matches |
default | signal | output | No rule matched |
Delay
Changes when signal payloads are forwarded. Queue delays every payload independently. Debounce emits the latest payload only after quiet time. Throttle emits immediately, then keeps the latest trailing payload for the next window.
| Port | Type | Direction | Description |
|---|---|---|---|
input | signal | input | Payload to delay, debounce, or throttle |
output | signal | output | Forwarded payload |
Template
Renders text from retained placeholder values. Add placeholders like {{name}} to create matching state input ports. When the trigger port fires, Template renders the latest values and emits the result as a signal.
| Port | Type | Direction | Description |
|---|---|---|---|
trigger | signal | input | Render the template |
{{name}} | state | input | Dynamic input port for each placeholder |
output | signal | output | Rendered text |
Webhook
Turns localhost HTTP requests into signal payloads. Each node owns a path on the shared webhook server (bound to 127.0.0.1, port configurable in Settings) and emits the request body as a signal. Webhook is inbound-only; use Run with curl or another script for outbound HTTP.
By default the output payload is the raw request body text. Enable Include envelope to wrap the body in a JSON object with id, receivedAt, method, path, query, headers, and body. When the request has a JSON content type, the envelope’s body field contains the parsed JSON object (so downstream jq can access nested fields directly).
Enable Reject invalid JSON to return HTTP 400 for requests with a JSON content type whose body fails to parse. When off, invalid JSON is accepted and the raw text is used as the body.
| Port | Type | Direction | Description |
|---|---|---|---|
output | signal | output | Request body (or full envelope when enabled) |
Groups
A group is a container that bundles nodes together so you can move and resize them as a unit, optionally bound to an isolated git worktree. Groups have two layout strategies:
- Freeform — arrange member nodes anywhere inside the group’s interior. The group resizes to hold them.
- Split-tree — tile member nodes as panes, like a terminal multiplexer. Drag dividers to adjust ratios; right-click a pane to split it horizontally or vertically.
A group’s layout strategy is fixed at creation time. Groups cannot be nested.
Right-click anywhere on a group’s chrome for a single menu with its actions: add a node, split a pane, manage its worktree, Copy Group ID (for agent/MCP prompts), Rename (unbound groups), or delete it.
Creating a group
- ⌘G creates an empty freeform group at the viewport center.
- ⌘⇧G creates an empty split-tree group at the viewport center.
- Right-click empty canvas and choose New Freeform Group or New Split Group to drop one where you clicked.
- The command palette (⌘K) exposes the same actions, plus New Freeform Group with Worktree and New Split Group with Worktree variants that create a group already bound to a fresh git worktree (see below).
Adding and removing members
- Right-click empty space inside a freeform group and choose Add node… to drop a new node directly into the group.
- Right-click any pane in a split-tree group and choose Split right… or Split below… to split the pane and add a new node alongside.
- Spawning a new terminal (⌘N) while a group is focused places the terminal inside the group automatically.
- Drag a member out past the dashed-red detach border and release to eject it back to the canvas. In split-tree groups, detaching a pane shows a ghost snapshot during the drag so you can see what you’re pulling out. Closing a node normally (⌘W) removes it from the group too.
- Drag an external node over a split-tree group to see a live green preview of where it will land. Drop to attach it at that position.
Worktree binding
Bind a group to a git branch and Terminal Graph manages a dedicated git worktree for it. Every terminal spawned inside the group inherits the worktree path as its working directory, so the whole group operates against an isolated checkout of the branch.
- Bind an existing group: right-click the group’s title bar and choose Bind to Worktree…. Enter a worktree name (pre-filled from a themed name pack) and an optional branch name. The base branch defaults to
main. If the branch doesn’t exist yet, it’s created from the base. - Detached HEAD: leave the branch name empty in the bind dialog to create a worktree at a detached HEAD. The title bar shows the short SHA instead of a branch name.
- Detach without deleting: right-click the title bar and choose Detach Worktree…. The group and its members stay on the canvas; the worktree directory is removed. Optionally tick “Also delete branch” to run
git branch -D. - Delete the whole group: the standard delete flow on a worktree-bound group prompts you to confirm and optionally delete the branch as well.
- The group’s title bar shows the worktree name and branch (e.g. “europa · feat/auth”) while bound. If a branch is created or commits land on a detached HEAD, the title updates automatically.
- If the worktree directory disappears on disk, the group displays a banner with Recreate, Detach, and Delete actions so you can reconcile state without leaving the app.
Worktree groups require the workspace to be a git repository with at least one commit. Terminal Graph offers to create an initial commit if the repo has none yet.
Blueprints
A blueprint is a saved snapshot of a selection of nodes — or an entire group with its layout — that you can stamp out again later. Use them to capture frequently used setups: a debugger triple, a shell + editor + browser triad, a worktree group’s full layout.
Capturing a blueprint
- Select one or more nodes (click, then ⌘-click to add to the selection). A whole group can be selected as a single unit.
- Open the command palette (⌘K) and run Create Blueprint from Selection…, or right-click empty canvas and choose the same action.
- Give the blueprint a name and optional description, then choose a scope:
- Workspace — saved under
{PROJECT}/.terminalgraph/blueprints/; visible only inside this project. - Global — saved under
~/.config/terminalgraph/blueprints/; available in every workspace.
- Workspace — saved under
Inserting a blueprint
- Run Insert Blueprint… from the command palette to place at viewport center, or from the canvas right-click menu to place where you clicked.
- The picker lists every workspace and global blueprint. Type to filter by name; click to insert.
Sharing blueprints
Blueprints are portable: export one from your library to hand off to someone else, or import one they sent you.
- Export — in the sidebar Blueprints section, right-click a blueprint and choose Export… to save it as a
.blueprintfile, or drag it straight out to Finder. - Import from a file — double-click a
.blueprintfile in Finder to open it in Terminal Graph. - Import from a URL — run File → Import Blueprint from URL… and paste a link to a
.blueprintfile, such as a GitHub gist or raw URL.
Every import opens a preview first: the blueprint’s name, where it came from, a summary of its contents, and any terminal commands it would run. Nothing happens automatically; you choose a scope (This workspace or Global library) and Save to Library. Importing only adds the blueprint to your library; it never runs a command or drops nodes onto the canvas on its own. Insert it when you’re ready, like any other blueprint.
What gets captured
- Each selected node’s config and live runtime state (terminal CWD, browser URL, editor file path).
- Connections between captured nodes.
- Whole groups with their layout (freeform pane positions or split-tree structure) and all members.
- Worktree-bound groups remember they were worktree-bound. When you insert the blueprint, Terminal Graph prompts you to bind it to a new worktree or insert without one.
Blueprint files are plain JSON with a .blueprint extension, the same format your library stores and what you export to share, so a file copied straight from your library folder imports anywhere. Older <name>.blueprint.json files still load and migrate to .blueprint automatically.
Dataflow
Connections in Terminal Graph carry typed data between node ports. They render as smooth S-curves between output and input ports. Optionally, add waypoints in Wiring Mode to route curves around obstacles. The runtime validates type compatibility before a connection is established and rejects wiring between incompatible port types. Utility nodes use signal payloads by default; continuous streams stay streams until a node such as Collect frames them.
Port types
| Type | Description |
|---|---|
| stream | Continuous byte flow backed by named FIFO pipes. Used for terminal stdio and other long-running flows where chunk boundaries are not semantic. Stream ports connect to stream ports; use Collect to turn a stream into signal payloads. |
| state | Persistent key/value backed by a StateStore. Emits on change. New subscribers receive the current value immediately (e.g., current URL, current file path, latest Run output). |
| signal | Discrete fire-and-forget events with optional string payloads. Used for process exit codes, save attempts, file changes, webhooks, and most utility-node routing. Not buffered. |
Port compatibility
| Output → Input | Allowed? |
|---|---|
| stream → stream | Yes |
| signal → stream, signal, state | Yes |
| state → stream, signal, state | Yes (replays current value on connect) |
| stream → signal or state | No — use Collect to frame a stream into signals |
Signal delivery order
When an output port fans out to multiple connections, subscribers fire synchronously in connection creation order. This is deterministic but invisible — there is no UI indicator for which connection was created first.
Practical consequence: if you wire a single Trigger to both a Gate’s toggle and input ports, the delivery order depends on which connection you drew first. If you need a guaranteed sequence (e.g., open the gate before sending data through it), insert a Delay node on the data path — even a minimal delay defers delivery to the next event-loop tick.
Utility-node patterns
| Flow | Use it for |
|---|---|
Trigger → Run | Run a command manually or on an interval. |
File Watcher → Run | Run a command when matching files change. |
Terminal stdout → Collect → Run | Invoke a command once per framed stream message. |
Webhook → Switch → Run | Route local HTTP requests to different command handlers. |
Template → Run | Render command input from retained state values. |
Environment variables
Terminal nodes expose their streams through environment variables that Terminal Graph injects automatically into each shell session:
| Variable | Description |
|---|---|
$TG_STDIN | FIFO path for data flowing into this terminal from the graph |
$TG_STDOUT | FIFO path for data flowing out to whatever is wired to the stdout port |
$TG_STDERR | FIFO path for data flowing out to whatever is wired to the stderr port |
$TG_NODE_ID | This node’s unique identifier |
$TG_WRITE_TIMEOUT_MS | Timeout for write retries (default 2000 ms) |
The FIFO files are backed by Unix named pipes, so cat, tee, and shell redirection work on them like any normal file. The tg CLI wraps these operations for common patterns.
Hooks
Hooks are executable scripts that run automatically in response to workspace events. Place them in .terminalgraph/hooks/ inside your project directory, named after the hook point they handle.
Available hooks
| Hook | When it fires |
|---|---|
post-worktree-create | After a worktree is created and attached to a group |
How hooks run
When a hook fires, Terminal Graph opens a small ephemeral terminal next to the group and runs the script inside it. The terminal border shows the hook’s state:
- Yellow — running
- Green — succeeded (auto-closes after a short delay)
- Red — failed (stays open so you can read the output)
Ephemeral hook terminals are excluded from state persistence and don’t appear in the recently-closed list.
Environment
Hook scripts receive environment variables describing the context that triggered them, such as TG_WORKTREE_PATH, TG_BRANCH_NAME, and TG_BASE_BRANCH_NAME.
Getting started
Terminal Graph creates sample templates in .terminalgraph/hooks/ when a workspace is opened. Copy and rename one to get started — they’re already marked executable.
CLI reference
The terminalgraph / tg CLI is automatically injected into terminal-node shells. No PATH setup is required.
Subcommands
| Command | Description |
|---|---|
tg run [flags] <cmd> [args…] | Pipe data through a command. Default: bidirectional stdin/stdout; stderr merged. |
tg send [flags] [text…] | Write text to $TG_STDOUT (or $TG_STDERR with --err). |
tg recv | Drain $TG_STDIN to terminal stdout. |
tg notify [-t TITLE] [message…] | Send a macOS notification from the terminal node, delivered by Terminal Graph. Requires notification permission in System Settings; otherwise it warns on stderr but still sends. |
tg ports | List the FIFO paths for all ports on the current node. |
tg env | Print all TG_* environment variables. |
tg help [--agent|--human] | Show usage. Auto-detects agent mode via CLAUDECODE, CURSOR_AGENT, or CI=true. |
tg version | Print the app version. |
Flags: tg run
| Flag | Description |
|---|---|
-i, --in | Read $TG_STDIN only; command output goes to terminal. |
-o, --out | Write to $TG_STDOUT only; read from terminal stdin. |
-e, --capture-err | Split stderr to $TG_STDERR (default: merge into stdout). |
-b N, --batch N | Re-invoke the command per N input lines. |
-t D, --batch-time D | Re-invoke when the current batch is D old. Accepts 500ms, 2s, 1m, 1h, or bare seconds. |
Flags: tg send
| Flag | Description |
|---|---|
-e, --err | Write to $TG_STDERR instead of stdout. |
-n, --no-newline | Suppress the trailing newline. |
Examples
001# Filter inbound stream and push matches onward002tg run grep ERROR003004# One-off message to the graph005tg send "deployment complete"006007# Notify when a long task finishes008tg notify --title "Deploy" "production is live"009010# Generate output from a command that ignores stdin011tg run --out date012013# Watch what upstream is pushing (debug)014tg recv015016# Batch an infinite stream for a turn-based tool017tg run --batch 50 --batch-time 10s claude -p "Summarize"
Run terminalgraph help for the complete reference.
Themes
Terminal Graph ships with 14 bundled themes and supports custom themes via TOML files. Themes control colors across the entire app — window chrome, canvas, nodes, title bars, sidebar, ports, and Monaco editors all follow the active theme.
Bundled themes
INTDEV Dark (default), INTDEV Light, Daybreak Dark, Daybreak Light, Nord, Nord Light, Catppuccin Mocha, Catppuccin Latte, Tokyo Night, Tokyo Night Day, Gruvbox Dark, Gruvbox Light, Girly Pop Dark, and Girly Pop Light.
Appearance mode
Choose between System, Light, or Dark in Settings → Appearance or via the command palette. System mode follows your macOS appearance and automatically switches between your chosen light and dark themes.
Custom themes
Create a .toml file in ~/.config/terminalgraph/themes/. Each theme defines color tokens for window, canvas, nodes, text, accent, sidebar, and ports. Use a bundled theme as a starting point — click Open Themes Folder in Settings to find the directory. Custom themes with the same name as a bundled theme override the built-in version.
Edits to theme colors in the Settings appearance tab persist immediately to the theme’s TOML file for custom themes. Edits to bundled themes are preview-only — save as a new theme to keep your changes.
MCP server
Terminal Graph includes a localhost MCP server that lets AI agents control the canvas programmatically. Enable it in Settings → MCP Server.
Capabilities
25 tools covering node lifecycle (create, move, resize, duplicate, focus, delete), port wiring (connect, disconnect), group management (create, add/remove members, delete, layout), blueprints (capture, instantiate, delete), reads (context, nodes, connections, blueprints, workspaces), canvas screenshots, terminal execution, and composite workflows.
Targeting windows
Most tools accept an optional workspace_id to target a specific open window. Omit it to hit the focused window. Run list_workspaces to enumerate every open window — each returns a stable id plus its project root path, and the focused window is flagged. A tool can target a window by either its id or its path, so agents can drive several windows in one session.
Referencing nodes and groups
MCP tools reference a node or group by its ID. To grab one, right-click a node and choose Copy Node ID, or right-click a group and choose Copy Group ID. The ID lands on the clipboard, ready to paste into an agent prompt. Selecting several nodes first copies all their IDs, newline-separated.
Screenshots
capture_canvas returns a PNG of the visible canvas by default. Pass an optional x, y, width, and heightbounding box (in canvas coordinates, the same space as node frames) to capture an explicit region — including off-screen areas — without panning. Regions are capped at 4000 pt per side.
Configuration
- Default: off. Enable in Settings → MCP Server.
- Port: 4930 (configurable, must be 1024–65535).
- The settings tab shows copyable host config snippets for Claude Desktop, Claude Code, and Codex.
Settings
Press ⌘, to open the settings window.
General
- Worktree name pack — choose a themed name pack for worktree display names. Available packs: moons (default), islands, constellations, stations, ports, and signals.
- Grid spacing — controls snap-to-grid distance and grid dot density on the canvas.
- Default search engine— when you type something that isn’t a URL into a browser node’s address bar, it runs a web search with this engine: Google (default), Bing, DuckDuckGo, or a custom URL template (
%sis replaced with your query).
Appearance
- Theme selection — pick from bundled or custom themes for both dark and light modes.
- Appearance mode — System, Light, or Dark.
- Token editing — adjust individual color tokens for window, canvas, nodes, text, sidebar, and ports.
- Canvas texture — set a backdrop for the canvas from the Canvas controls: None, a soft Vignette, or Top Lit.
- Fonts — choose the Terminal Font (from installed monospaced families) and the Interface Font (any installed family). Changes apply live to open terminals and the app chrome.
- Text size — scale Interface Text (menus, sidebar, and node chrome) and Node Content(what’s inside notes, editors, and other nodes) independently. Both apply live.
MCP Server
- Enable/disable toggle, port configuration, and copyable host config snippets. See MCP server for details.
Webhook
- Webhook port — the local port that webhook nodes listen on (default 4932). Change and click Restart to apply. All webhook nodes re-register automatically.
Account
Click the support button in the window chrome to sign in with your internet.dev account. You can also sign in or out via the command palette.
Supporters see a badge in the window chrome, which can be hidden in Settings. Terminal Graph works fully without an account — signing in is optional and only needed for support features.
Data storage
Terminal Graph persists data in three places:
- Global configuration —
~/.config/terminalgraph/. Holds app-level preferences, keybindings, global blueprints underblueprints/, and custom themes underthemes/. - Per-project data —
{PROJECT_DIR}/.terminalgraph/inside each project folder. Holds that project’s own canvas state (state.json, used when the folder is opened on its own), notes, editor buffers, project-scoped blueprints underblueprints/, hook scripts underhooks/, and git worktrees. - Workspace files —
.terminalgraph-workspacefiles capture a whole multi-project layout: the list of projects, their canvas regions, and every node and connection across them (see Projects). The home window persists toglobal.terminalgraph-workspace, and workspaces you save live underworkspaces/, both inside~/.config/terminalgraph/. A multi-project canvas lives in its workspace file, not in the per-projectstate.json.
Canvas state saves automatically as you work, so your layout survives crashes and force-quits, not just a clean quit. Back up these directories if you care about preserving your layouts. Deleting them, or uninstalling the app, removes all locally persisted state.
Feedback
The app is still in beta, so expect things to break. There are two ways to report issues from inside the app:
- Help → Report a Bug… — for something that is broken.
- Help → Send Feedback… — for ideas, UX gripes, or anything else.
Both menu items open a pre-filled email in your default mail client, addressed to [email protected], with your app version, macOS version, and a few other details pre-populated. Remove anything you’re not comfortable sharing before sending.
You can also email [email protected] directly.
What makes a useful bug report
- What you did
- What you expected to happen
- What actually happened
- Screenshots are useful. A short screen recording is even better.
- A half-written “hey this felt weird” is better than nothing.
Stuck on something? Come talk to us.