Terminal Graph
Contents

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.

Unsigned-app workaround

The Apple Developer account for Internet Development Studio Company is still being processed, so macOS will block the app on first launch with an “Apple could not verify this app is free of malware” dialog. Two ways past it:

  1. System Settings path: Attempt to open the app and let it fail. Then open System Settings → Privacy & Security, scroll to the bottom, and click Open Anyway. Confirm in the next dialog.
  2. Terminal path: Run the following command, then open the app normally.
    xattr -dr com.apple.quarantine /Applications/TerminalGraphBeta.app

Auto-updates

Sparkle automatic updates work, and the update archives are signed with a separate key that does not need Apple’s notarization blessing. Builds are also self-signed with a stable identity, so the macOS permissions you grant — folder access, microphone, notifications — now persist across updates instead of resetting each time. The app still isn’t notarized (that waits on the Developer account), so a fresh download may show the first-launch verification dialog above.

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.

Drag and drop

Drop a file or image onto empty canvas to create a node for it — images become image nodes. 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 ⌘⌥D 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 file tree is only populated when the app is opened with a project folder.

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 Node Nodes
⌘⇧D Duplicate Node Vertically Nodes
⌘G New Freeform Group Nodes
⌘⇧G New Split Group Nodes
⌘W Close Node Nodes
⌘⇧T Reopen Last Closed Node Nodes
⌘K Command Palette Palette
⌘⌥D Toggle Wiring Mode View
⌘⏎ Toggle Focus Mode 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
⌘= Increase Font Size View
⌘− Decrease Font Size View
⌘0 Reset 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. Font size shortcuts apply to adjustable nodes; terminal nodes handle font size directly via Ghostty. 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.

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

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.

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

  1. Select one or more nodes (click, then -click to add to the selection). A whole group can be selected as a single unit.
  2. Open the command palette (⌘K) and run Create Blueprint from Selection…, or right-click empty canvas and choose the same action.
  3. 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.

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.

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 (<name>.blueprint.json) and can be copied between machines or shared with other workspaces.

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

# Filter inbound stream and push matches onward
tg run grep ERROR

# One-off message to the graph
tg send "deployment complete"

# Notify when a long task finishes
tg notify --title "Deploy" "production is live"

# Generate output from a command that ignores stdin
tg run --out date

# Watch what upstream is pushing (debug)
tg recv

# Batch an infinite stream for a turn-based tool
tg 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.

Screenshots

capture_canvas returns a PNG of the visible canvas by default. Pass an optional x, y, width, and height bounding 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.

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.

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 two locations:

  • Global configuration~/.config/terminalgraph/. Holds app-level preferences, keybindings, canvas state for freestanding (no-workspace) windows, global blueprints under blueprints/, and custom themes under themes/.
  • Per-workspace data{PROJECT_DIR}/.terminalgraph/ inside each project directory opened as a workspace. Holds canvas state, node layouts, notes, editor buffers, workspace-scoped blueprints under blueprints/, and hook scripts under hooks/.

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.

Sign-off

That’s it. Go break things.

From the bottom of my heart, thank you.
— Caidan

[email protected] · @caidanwilliams on X · caidan.dev on Bluesky