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:
- 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.
-
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
- 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.
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 underblueprints/, and custom themes underthemes/. -
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 underblueprints/, and hook scripts underhooks/.
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