Add docs, blog, community pages and polish landing page layout

- Add docs pages (getting-started, changelog, keyboard-shortcuts)
- Add blog, community, and legal pages (privacy, terms, EULA)
- Add site header, footer, download button, and nav components
- Add sitemap and robots.txt generation
- Narrow main page container (max-w-2xl), fix footer positioning
- Switch README feature list to colon style
This commit is contained in:
Lawrence Chen 2026-02-09 23:38:05 -08:00
parent 5febb66873
commit f970cdcf33
37 changed files with 3304 additions and 296 deletions

382
web/app/docs/api/page.tsx Normal file
View file

@ -0,0 +1,382 @@
import type { Metadata } from "next";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";
export const metadata: Metadata = {
title: "API Reference",
description: "CLI and socket API reference for cmux",
};
function Cmd({
name,
desc,
cli,
socket,
}: {
name: string;
desc: string;
cli: string;
socket: string;
}) {
return (
<div className="mb-6">
<h4>{name}</h4>
<p>{desc}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<CodeBlock title="CLI" lang="bash">{cli}</CodeBlock>
<CodeBlock title="Socket" lang="json">{socket}</CodeBlock>
</div>
</div>
);
}
export default function ApiPage() {
return (
<>
<h1>API Reference</h1>
<p>
cmux provides both a CLI tool and a Unix socket for programmatic
control. Every command is available through both interfaces.
</p>
<h2>Socket</h2>
<table>
<thead>
<tr>
<th>Build</th>
<th>Path</th>
</tr>
</thead>
<tbody>
<tr>
<td>Release</td>
<td>
<code>/tmp/cmux.sock</code>
</td>
</tr>
<tr>
<td>Debug</td>
<td>
<code>/tmp/cmux-debug.sock</code>
</td>
</tr>
</tbody>
</table>
<p>
Override with the <code>CMUX_SOCKET_PATH</code> environment variable.
Commands are newline-terminated JSON:
</p>
<CodeBlock lang="json">{`{"command": "command-name", "arg1": "value1"}
// Response:
{"success": true, "data": {...}}`}</CodeBlock>
<h2>Access modes</h2>
<table>
<thead>
<tr>
<th>Mode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>Off</strong>
</td>
<td>Socket disabled</td>
</tr>
<tr>
<td>
<strong>Notifications only</strong>
</td>
<td>Only notification commands allowed</td>
</tr>
<tr>
<td>
<strong>Full control</strong>
</td>
<td>All commands enabled</td>
</tr>
</tbody>
</table>
<Callout type="warn">
On shared machines, use &ldquo;Notifications only&rdquo; mode to prevent
other users from controlling your terminals.
</Callout>
<h2>CLI options</h2>
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>--socket PATH</code>
</td>
<td>Custom socket path</td>
</tr>
<tr>
<td>
<code>--json</code>
</td>
<td>Output in JSON format</td>
</tr>
<tr>
<td>
<code>--workspace ID</code>
</td>
<td>Target a specific workspace</td>
</tr>
<tr>
<td>
<code>--surface ID</code>
</td>
<td>Target a specific surface</td>
</tr>
</tbody>
</table>
<h2>Workspace commands</h2>
<Cmd
name="list-workspaces"
desc="List all open workspaces."
cli={`cmux list-workspaces
cmux list-workspaces --json`}
socket={`{"command": "list-workspaces"}`}
/>
<Cmd
name="new-workspace"
desc="Create a new workspace."
cli={`cmux new-workspace`}
socket={`{"command": "new-workspace"}`}
/>
<Cmd
name="select-workspace"
desc="Switch to a specific workspace."
cli={`cmux select-workspace --workspace <id>`}
socket={`{"command": "select-workspace", "id": "<id>"}`}
/>
<Cmd
name="current-workspace"
desc="Get the currently active workspace."
cli={`cmux current-workspace
cmux current-workspace --json`}
socket={`{"command": "current-workspace"}`}
/>
<Cmd
name="close-workspace"
desc="Close a workspace."
cli={`cmux close-workspace --workspace <id>`}
socket={`{"command": "close-workspace", "id": "<id>"}`}
/>
<h2>Split commands</h2>
<Cmd
name="new-split"
desc="Create a new split pane. Directions: left, right, up, down."
cli={`cmux new-split right
cmux new-split down`}
socket={`{"command": "new-split", "direction": "right"}`}
/>
<Cmd
name="list-surfaces"
desc="List all surfaces in the current workspace."
cli={`cmux list-surfaces
cmux list-surfaces --json`}
socket={`{"command": "list-surfaces"}`}
/>
<Cmd
name="focus-surface"
desc="Focus a specific surface."
cli={`cmux focus-surface --surface <id>`}
socket={`{"command": "focus-surface", "id": "<id>"}`}
/>
<h2>Input commands</h2>
<Cmd
name="send"
desc="Send text input to the focused terminal."
cli={`cmux send "echo hello"
cmux send "ls -la\\n"`}
socket={`{"command": "send", "text": "echo hello\\n"}`}
/>
<Cmd
name="send-key"
desc="Send a key press. Keys: enter, tab, escape, backspace, delete, up, down, left, right."
cli={`cmux send-key enter`}
socket={`{"command": "send-key", "key": "enter"}`}
/>
<Cmd
name="send-surface"
desc="Send text to a specific surface."
cli={`cmux send-surface --surface <id> "command"`}
socket={`{"command": "send-surface", "id": "<id>", "text": "command"}`}
/>
<Cmd
name="send-key-surface"
desc="Send a key press to a specific surface."
cli={`cmux send-key-surface --surface <id> enter`}
socket={`{"command": "send-key-surface", "id": "<id>", "key": "enter"}`}
/>
<h2>Notification commands</h2>
<Cmd
name="notify"
desc="Send a notification."
cli={`cmux notify --title "Title" --body "Body"
cmux notify --title "T" --subtitle "S" --body "B"`}
socket={`{"command": "notify", "title": "Title",
"subtitle": "S", "body": "Body"}`}
/>
<Cmd
name="list-notifications"
desc="List all notifications."
cli={`cmux list-notifications
cmux list-notifications --json`}
socket={`{"command": "list-notifications"}`}
/>
<Cmd
name="clear-notifications"
desc="Clear all notifications."
cli={`cmux clear-notifications`}
socket={`{"command": "clear-notifications"}`}
/>
<h2>Utility commands</h2>
<Cmd
name="ping"
desc="Check if cmux is running and responsive."
cli={`cmux ping`}
socket={`{"command": "ping"}
// Response: {"success": true, "pong": true}`}
/>
<h2>Environment variables</h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>CMUX_SOCKET_PATH</code>
</td>
<td>Override the default socket path</td>
</tr>
<tr>
<td>
<code>CMUX_SOCKET_ENABLE</code>
</td>
<td>
Enable/disable socket (<code>1</code>/<code>0</code>)
</td>
</tr>
<tr>
<td>
<code>CMUX_SOCKET_MODE</code>
</td>
<td>
Override access mode (<code>full</code>,{" "}
<code>notifications</code>, <code>off</code>)
</td>
</tr>
<tr>
<td>
<code>CMUX_WORKSPACE_ID</code>
</td>
<td>Auto-set: current workspace ID</td>
</tr>
<tr>
<td>
<code>CMUX_SURFACE_ID</code>
</td>
<td>Auto-set: current surface ID</td>
</tr>
<tr>
<td>
<code>TERM_PROGRAM</code>
</td>
<td>
Set to <code>ghostty</code>
</td>
</tr>
<tr>
<td>
<code>TERM</code>
</td>
<td>
Set to <code>xterm-ghostty</code>
</td>
</tr>
</tbody>
</table>
<Callout>
Environment variables override app settings. Use the socket check to
distinguish cmux from regular Ghostty.
</Callout>
<h2>Detecting cmux</h2>
<CodeBlock title="bash" lang="bash">{`# Check for the socket
[ -S /tmp/cmux.sock ] && echo "In cmux"
# Check for the CLI
command -v cmux &>/dev/null && echo "cmux available"
# Distinguish from regular Ghostty
[ "$TERM_PROGRAM" = "ghostty" ] && [ -S /tmp/cmux.sock ] && echo "In cmux"`}</CodeBlock>
<h2>Examples</h2>
<h3>Python client</h3>
<CodeBlock title="python" lang="python">{`import socket, json
def send_command(cmd):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect('/tmp/cmux.sock')
sock.send(json.dumps(cmd).encode() + b'\\n')
response = sock.recv(4096).decode()
sock.close()
return json.loads(response)
# List workspaces
print(send_command({"command": "list-workspaces"}))
# Send notification
send_command({
"command": "notify",
"title": "Hello",
"body": "From Python!"
})`}</CodeBlock>
<h3>Shell script</h3>
<CodeBlock title="bash" lang="bash">{`#!/bin/bash
cmux_cmd() {
echo "$1" | nc -U /tmp/cmux.sock
}
cmux_cmd '{"command": "list-workspaces"}'
cmux_cmd '{"command": "notify", "title": "Done", "body": "Task complete"}'`}</CodeBlock>
<h3>Build script with notification</h3>
<CodeBlock title="bash" lang="bash">{`#!/bin/bash
npm run build
if [ $? -eq 0 ]; then
cmux notify --title "✓ Build Success" --body "Ready to deploy"
else
cmux notify --title "✗ Build Failed" --body "Check the logs"
fi`}</CodeBlock>
</>
);
}

View file

@ -0,0 +1,127 @@
import type { Metadata } from "next";
import fs from "fs";
import path from "path";
export const metadata: Metadata = {
title: "Changelog",
description: "Release notes and version history for cmux",
};
interface ChangelogSection {
heading: string;
items: string[];
}
interface ChangelogVersion {
version: string;
date: string;
intro?: string;
sections: ChangelogSection[];
}
function parseChangelog(markdown: string): ChangelogVersion[] {
const versions: ChangelogVersion[] = [];
let current: ChangelogVersion | null = null;
let currentSection: ChangelogSection | null = null;
for (const line of markdown.split("\n")) {
const versionMatch = line.match(/^## \[(.+?)\] - (.+)$/);
if (versionMatch) {
if (current) versions.push(current);
current = {
version: versionMatch[1],
date: versionMatch[2],
sections: [],
};
currentSection = null;
continue;
}
if (!current) continue;
const sectionMatch = line.match(/^### (.+)$/);
if (sectionMatch) {
currentSection = { heading: sectionMatch[1], items: [] };
current.sections.push(currentSection);
continue;
}
const itemMatch = line.match(/^- (.+)$/);
if (itemMatch) {
if (currentSection) {
currentSection.items.push(itemMatch[1]);
} else {
// Items without a ### heading (e.g. 1.0.x initial release)
if (!current.sections.length) {
currentSection = { heading: "", items: [] };
current.sections.push(currentSection);
}
current.sections[current.sections.length - 1].items.push(
itemMatch[1]
);
}
continue;
}
// Non-empty lines that aren't headings or items (intro text)
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) {
current.intro = trimmed;
}
}
if (current) versions.push(current);
return versions;
}
function InlineCode({ text }: { text: string }) {
const parts = text.split(/(`[^`]+`)/g);
return (
<>
{parts.map((part, i) =>
part.startsWith("`") && part.endsWith("`") ? (
<code key={i}>{part.slice(1, -1)}</code>
) : (
<span key={i}>{part}</span>
)
)}
</>
);
}
export default function ChangelogPage() {
const changelogPath = path.join(process.cwd(), "..", "CHANGELOG.md");
const markdown = fs.readFileSync(changelogPath, "utf-8");
const versions = parseChangelog(markdown);
return (
<>
<h1>Changelog</h1>
<p>All notable changes to cmux are documented here.</p>
{versions.map((v) => (
<div key={v.version} className="mb-8">
<h2>
{v.version}{" "}
<span className="text-muted font-normal text-[14px]">
{v.date}
</span>
</h2>
{v.intro && <p>{v.intro}</p>}
{v.sections.map((section, i) => (
<div key={i}>
{section.heading && <h3>{section.heading}</h3>}
<ul>
{section.items.map((item, j) => (
<li key={j}>
<InlineCode text={item} />
</li>
))}
</ul>
</div>
))}
</div>
))}
</>
);
}

View file

@ -0,0 +1,212 @@
import type { Metadata } from "next";
import { CodeBlock } from "../../components/code-block";
export const metadata: Metadata = {
title: "Concepts",
description:
"Understanding cmux's window, workspace, pane, and surface hierarchy",
};
export default function ConceptsPage() {
return (
<>
<h1>Concepts</h1>
<p>
cmux organizes your terminals in a four-level hierarchy. Understanding
these levels helps when using the socket API, CLI, and keyboard
shortcuts.
</p>
<h2>Hierarchy</h2>
<CodeBlock lang="text">{`Window
Workspace (sidebar entry)
Pane (split region)
Surface (tab within pane)
Panel (terminal or browser content)`}</CodeBlock>
<h3>Window</h3>
<p>
A macOS window. Open multiple windows with <code>N</code>. Each
window has its own sidebar with independent workspaces.
</p>
<h3>Workspace</h3>
<p>
A sidebar entry. Each workspace contains one or more split panes.
Workspaces are what you see listed in the left sidebar.
</p>
<p>
In the UI and keyboard shortcuts, workspaces are often called
&ldquo;tabs&rdquo; since they behave like tabs in the sidebar. The
socket API and environment variables use the term
&ldquo;workspace&rdquo;.
</p>
<table>
<thead>
<tr>
<th>Context</th>
<th>Term used</th>
</tr>
</thead>
<tbody>
<tr>
<td>Sidebar UI</td>
<td>Tab</td>
</tr>
<tr>
<td>Keyboard shortcuts</td>
<td>Workspace or tab</td>
</tr>
<tr>
<td>Socket API</td>
<td>
<code>workspace</code>
</td>
</tr>
<tr>
<td>Environment variable</td>
<td>
<code>CMUX_WORKSPACE_ID</code>
</td>
</tr>
</tbody>
</table>
<p>
<strong>Shortcuts:</strong> <code>N</code> (new),{" "}
<code>1</code><code>9</code> (jump), <code>W</code> (close),{" "}
<code>[</code> / <code>]</code> (prev/next)
</p>
<h3>Pane</h3>
<p>
A split region within a workspace. Created by splitting with{" "}
<code>D</code> (right) or <code>D</code> (down). Navigate between
panes with <code></code> + arrow keys.
</p>
<p>Each pane can hold multiple surfaces (tabs within the pane).</p>
<h3>Surface</h3>
<p>
A tab within a pane. Each pane has its own tab bar and can hold multiple
surfaces. Created with <code>T</code>, navigated with{" "}
<code>[</code> / <code>]</code> or <code>1</code>
<code>9</code>.
</p>
<p>
Surfaces are the individual terminal or browser sessions you interact
with. Each surface has its own <code>CMUX_SURFACE_ID</code> environment
variable.
</p>
<h3>Panel</h3>
<p>The content inside a surface. Currently two types:</p>
<ul>
<li>
<strong>Terminal</strong> a Ghostty terminal session
</li>
<li>
<strong>Browser</strong> an embedded web view
</li>
</ul>
<p>
Panel is mostly an internal concept. In the socket API and CLI, you
interact with surfaces rather than panels directly.
</p>
<h2>Visual example</h2>
<CodeBlock variant="ascii">{`┌──────────────────────────────────────────────────────┐
Sidebar Workspace "dev"
> dev Pane 1 Pane 2
server [S1] [S2] [S1]
logs
Terminal Terminal
`}</CodeBlock>
<p>In this example:</p>
<ul>
<li>
The <strong>window</strong> contains a sidebar with three workspaces
(dev, server, logs)
</li>
<li>
<strong>Workspace &ldquo;dev&rdquo;</strong> is selected, showing two{" "}
<strong>panes</strong> side by side
</li>
<li>
<strong>Pane 1</strong> has two <strong>surfaces</strong> ([S1] and
[S2] in the tab bar), with S1 active
</li>
<li>
<strong>Pane 2</strong> has one surface
</li>
<li>
Each surface contains a <strong>panel</strong> (a terminal in this
case)
</li>
</ul>
<h2>Summary</h2>
<table>
<thead>
<tr>
<th>Level</th>
<th>What it is</th>
<th>Created by</th>
<th>Identified by</th>
</tr>
</thead>
<tbody>
<tr>
<td>Window</td>
<td>macOS window</td>
<td>
<code>N</code>
</td>
<td></td>
</tr>
<tr>
<td>Workspace</td>
<td>Sidebar entry</td>
<td>
<code>N</code>
</td>
<td>
<code>CMUX_WORKSPACE_ID</code>
</td>
</tr>
<tr>
<td>Pane</td>
<td>Split region</td>
<td>
<code>D</code> / <code>D</code>
</td>
<td>Pane ID (socket API)</td>
</tr>
<tr>
<td>Surface</td>
<td>Tab within pane</td>
<td>
<code>T</code>
</td>
<td>
<code>CMUX_SURFACE_ID</code>
</td>
</tr>
<tr>
<td>Panel</td>
<td>Terminal or browser</td>
<td>Automatic</td>
<td>Panel ID (internal)</td>
</tr>
</tbody>
</table>
</>
);
}

View file

@ -0,0 +1,127 @@
import type { Metadata } from "next";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";
export const metadata: Metadata = {
title: "Configuration",
description: "Configure cmux appearance and behavior",
};
export default function ConfigurationPage() {
return (
<>
<h1>Configuration</h1>
<p>
cmux reads configuration from Ghostty config files, giving you familiar
options if you&apos;re coming from Ghostty.
</p>
<h2>Config file locations</h2>
<p>cmux looks for configuration in these locations (in order):</p>
<ol>
<li>
<code>~/.config/ghostty/config</code>
</li>
<li>
<code>~/Library/Application Support/com.mitchellh.ghostty/config</code>
</li>
</ol>
<p>Create the config file if it doesn&apos;t exist:</p>
<CodeBlock lang="bash">{`mkdir -p ~/.config/ghostty
touch ~/.config/ghostty/config`}</CodeBlock>
<h2>Appearance</h2>
<h3>Font</h3>
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`font-family = JetBrains Mono
font-size = 14`}</CodeBlock>
<h3>Colors</h3>
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# Theme (or use individual colors below)
theme = Dracula
# Custom colors
background = #1e1e2e
foreground = #cdd6f4
cursor-color = #f5e0dc
cursor-text = #1e1e2e
selection-background = #585b70
selection-foreground = #cdd6f4`}</CodeBlock>
<h3>Split panes</h3>
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# Opacity for unfocused splits (0.0 to 1.0)
unfocused-split-opacity = 0.7
# Fill color for unfocused splits
unfocused-split-fill = #1e1e2e
# Divider color between splits
split-divider-color = #45475a`}</CodeBlock>
<h2>Behavior</h2>
<h3>Scrollback</h3>
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# Number of lines to keep in scrollback buffer
scrollback-limit = 10000`}</CodeBlock>
<h3>Working directory</h3>
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# Default directory for new terminals
working-directory = ~/Projects`}</CodeBlock>
<h2>App settings</h2>
<p>
In-app settings are available via <strong>cmux Settings</strong> (
<code>,</code>):
</p>
<h3>Theme mode</h3>
<ul>
<li>
<strong>System</strong> follow macOS appearance
</li>
<li>
<strong>Light</strong> always light mode
</li>
<li>
<strong>Dark</strong> always dark mode
</li>
</ul>
<h3>Automation mode</h3>
<p>Control socket access level:</p>
<ul>
<li>
<strong>Off</strong> no socket control (most secure)
</li>
<li>
<strong>Notifications only</strong> only allow notification commands
</li>
<li>
<strong>Full control</strong> allow all socket commands
</li>
</ul>
<Callout type="warn">
On shared machines, consider using &ldquo;Notifications only&rdquo; mode
to prevent other processes from controlling your terminals.
</Callout>
<h2>Example config</h2>
<CodeBlock title="~/.config/ghostty/config" lang="ini">{`# Font
font-family = SF Mono
font-size = 13
# Colors
theme = One Dark
# Scrollback
scrollback-limit = 50000
# Splits
unfocused-split-opacity = 0.85
split-divider-color = #3e4451
# Working directory
working-directory = ~/code`}</CodeBlock>
</>
);
}

137
web/app/docs/docs-nav.tsx Normal file
View file

@ -0,0 +1,137 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import { DocsSidebar } from "../components/docs-sidebar";
import { DocsPager } from "../components/docs-pager";
export function DocsNav({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
const sidebarRef = useRef<HTMLElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const close = useCallback(() => {
setOpen(false);
buttonRef.current?.focus();
}, []);
// Close on Escape
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") close();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [open, close]);
// Trap focus inside sidebar when open on mobile
useEffect(() => {
if (!open || !sidebarRef.current) return;
const sidebar = sidebarRef.current;
const focusable = sidebar.querySelectorAll<HTMLElement>(
'a[href], button, [tabindex]:not([tabindex="-1"])'
);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
// Focus first link
first.focus();
const trap = (e: KeyboardEvent) => {
if (e.key !== "Tab") return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
sidebar.addEventListener("keydown", trap);
return () => sidebar.removeEventListener("keydown", trap);
}, [open]);
// Lock body scroll when open on mobile
useEffect(() => {
if (!open) return;
const mq = window.matchMedia("(min-width: 768px)");
if (mq.matches) return; // don't lock on desktop
document.body.style.overflow = "hidden";
return () => { document.body.style.overflow = ""; };
}, [open]);
return (
<div className="max-w-5xl mx-auto flex px-4">
{/* Mobile menu button */}
<button
ref={buttonRef}
onClick={() => setOpen(!open)}
aria-expanded={open}
aria-controls="docs-sidebar"
className="fixed bottom-4 right-4 z-40 md:hidden w-10 h-10 rounded-full bg-foreground text-background flex items-center justify-center shadow-lg"
aria-label={open ? "Close navigation" : "Open navigation"}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
{open ? (
<path d="M18 6L6 18M6 6l12 12" />
) : (
<>
<path d="M3 6h18" />
<path d="M3 12h18" />
<path d="M3 18h18" />
</>
)}
</svg>
</button>
{/* Mobile overlay */}
{open && (
<div
className="fixed inset-0 z-30 bg-black/50 md:hidden"
aria-hidden="true"
onClick={close}
/>
)}
{/* Sidebar */}
<aside
ref={sidebarRef}
id="docs-sidebar"
role="navigation"
aria-label="Documentation"
style={{ height: "calc(100dvh - 3rem)" }}
className={`fixed top-12 left-0 z-40 w-56 bg-background py-4 pr-4 overflow-y-auto transition-transform md:sticky md:top-12 md:shrink-0 md:translate-x-0 ${
open ? "translate-x-0" : "-translate-x-full"
}`}
>
<DocsSidebar onNavigate={close} />
</aside>
{/* Content */}
<main className="flex-1 min-w-0">
<div className="max-w-2xl px-6 pb-10 ml-0" data-dev="docs-content" style={{ paddingTop: 8 }}>
<div className="docs-content text-[15px]">{children}</div>
<DocsPager />
</div>
</main>
</div>
);
}

View file

@ -0,0 +1,77 @@
import type { Metadata } from "next";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";
import { DownloadButton } from "../../components/download-button";
export const metadata: Metadata = {
title: "Getting Started",
description: "Install and set up cmux on macOS",
};
export default function GettingStartedPage() {
return (
<>
<h1>Getting Started</h1>
<p>
cmux is a lightweight, native macOS terminal built on Ghostty for
managing multiple AI coding agents. It features vertical tabs, a
notification panel, and a socket-based control API.
</p>
<h2>Install</h2>
<h3>DMG (recommended)</h3>
<div className="my-4">
<DownloadButton />
</div>
<p>
Open the <code>.dmg</code> and drag cmux to your Applications folder.
cmux auto-updates via Sparkle, so you only need to download once.
</p>
<h3>Homebrew</h3>
<CodeBlock lang="bash">{`brew tap manaflow-ai/cmux
brew install --cask cmux`}</CodeBlock>
<p>To update later:</p>
<CodeBlock lang="bash">{`brew upgrade --cask cmux`}</CodeBlock>
<Callout>
On first launch, macOS may ask you to confirm opening an app from an
identified developer. Click <strong>Open</strong> to proceed.
</Callout>
<h2>Verify installation</h2>
<p>Open cmux and you should see:</p>
<ul>
<li>A terminal window with a vertical tab sidebar on the left</li>
<li>One initial workspace already open</li>
<li>The Ghostty-powered terminal ready for input</li>
</ul>
<h2>CLI setup</h2>
<p>
cmux includes a command-line tool for automation. Inside cmux terminals
it works automatically. To use the CLI from outside cmux, create a
symlink:
</p>
<CodeBlock lang="bash">{`sudo ln -sf "/Applications/cmux.app/Contents/MacOS/cmux" /usr/local/bin/cmux`}</CodeBlock>
<p>Then you can run commands like:</p>
<CodeBlock lang="bash">{`cmux list-workspaces
cmux notify --title "Build Complete" --body "Your build finished"`}</CodeBlock>
<h2>Auto-updates</h2>
<p>
cmux checks for updates automatically via Sparkle. When an update is
available you&apos;ll see an update pill in the titlebar. You can also
check manually via <strong>cmux Check for Updates</strong> in the menu
bar.
</p>
<h2>Requirements</h2>
<ul>
<li>macOS 14.0 or later</li>
<li>Apple Silicon or Intel Mac</li>
</ul>
</>
);
}

View file

@ -0,0 +1,19 @@
import type { Metadata } from "next";
import { KeyboardShortcuts } from "../../keyboard-shortcuts";
export const metadata: Metadata = {
title: "Keyboard Shortcuts",
description: "Complete list of cmux keyboard shortcuts",
};
export default function KeyboardShortcutsPage() {
return (
<>
<h1>Keyboard Shortcuts</h1>
<p>
All keyboard shortcuts available in cmux, grouped by category.
</p>
<KeyboardShortcuts />
</>
);
}

30
web/app/docs/layout.tsx Normal file
View file

@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { DocsNav } from "./docs-nav";
import { SiteHeader } from "../components/site-header";
export const metadata: Metadata = {
title: {
template: "%s — cmux docs",
default: "cmux docs",
},
openGraph: {
siteName: "cmux",
type: "article",
},
alternates: {
canonical: "./",
},
};
export default function DocsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen">
<SiteHeader section="docs" />
<DocsNav>{children}</DocsNav>
</div>
);
}

View file

@ -0,0 +1,202 @@
import type { Metadata } from "next";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";
export const metadata: Metadata = {
title: "Notifications",
description: "Desktop notifications in cmux for AI agents and scripts",
};
export default function NotificationsPage() {
return (
<>
<h1>Notifications</h1>
<p>
cmux supports desktop notifications, allowing AI agents and scripts to
alert you when they need attention.
</p>
<h2>Lifecycle</h2>
<ol>
<li>
<strong>Received</strong> notification appears in panel, desktop
alert fires (if not suppressed)
</li>
<li>
<strong>Unread</strong> badge shown on workspace tab
</li>
<li>
<strong>Read</strong> cleared when you view that workspace
</li>
<li>
<strong>Cleared</strong> removed from panel
</li>
</ol>
<h3>Suppression</h3>
<p>Desktop alerts are suppressed when:</p>
<ul>
<li>The cmux window is focused</li>
<li>The specific workspace sending the notification is active</li>
<li>The notification panel is open</li>
</ul>
<h3>Notification panel</h3>
<p>
Press <code>I</code> to open the notification panel. Click a
notification to jump to that workspace. Press <code>U</code> to jump
directly to the workspace with the most recent unread notification.
</p>
<h2>Sending notifications</h2>
<h3>CLI</h3>
<CodeBlock lang="bash">{`cmux notify --title "Task Complete" --body "Your build finished"
cmux notify --title "Claude Code" --subtitle "Waiting" --body "Agent needs input"`}</CodeBlock>
<h3>OSC 777 (simple)</h3>
<p>
The RXVT protocol uses a fixed format with title and body:
</p>
<CodeBlock lang="bash">{`printf '\\e]777;notify;My Title;Message body here\\a'`}</CodeBlock>
<CodeBlock title="Shell function" lang="bash">{`notify_osc777() {
local title="$1"
local body="$2"
printf '\\e]777;notify;%s;%s\\a' "$title" "$body"
}
notify_osc777 "Build Complete" "All tests passed"`}</CodeBlock>
<h3>OSC 99 (rich)</h3>
<p>
The Kitty protocol supports subtitles and notification IDs:
</p>
<CodeBlock lang="bash">{`# Format: ESC ] 99 ; <params> ; <payload> ESC \\
# Simple notification
printf '\\e]99;i=1;e=1;d=0:Hello World\\e\\\\'
# With title, subtitle, and body
printf '\\e]99;i=1;e=1;d=0;p=title:Build Complete\\e\\\\'
printf '\\e]99;i=1;e=1;d=0;p=subtitle:Project X\\e\\\\'
printf '\\e]99;i=1;e=1;d=1;p=body:All tests passed\\e\\\\'`}</CodeBlock>
<table>
<thead>
<tr>
<th>Feature</th>
<th>OSC 99</th>
<th>OSC 777</th>
</tr>
</thead>
<tbody>
<tr>
<td>Title + body</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Subtitle</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Notification ID</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Complexity</td>
<td>Higher</td>
<td>Lower</td>
</tr>
</tbody>
</table>
<Callout>
Use OSC 777 for simple notifications. Use OSC 99 when you need subtitles
or notification IDs. Use the CLI (<code>cmux notify</code>) for the
easiest integration.
</Callout>
<h2>Claude Code hooks</h2>
<p>
cmux integrates with{" "}
<a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a>{" "}
via hooks to notify you when tasks complete.
</p>
<h3>1. Create the hook script</h3>
<CodeBlock title="~/.claude/hooks/cmux-notify.sh" lang="bash">{`#!/bin/bash
# Skip if not in cmux
[ -S /tmp/cmux.sock ] || exit 0
EVENT=$(cat)
EVENT_TYPE=$(echo "$EVENT" | jq -r '.event // "unknown"')
TOOL=$(echo "$EVENT" | jq -r '.tool_name // ""')
case "$EVENT_TYPE" in
"Stop")
cmux notify --title "Claude Code" --body "Session complete"
;;
"PostToolUse")
[ "$TOOL" = "Task" ] && cmux notify --title "Claude Code" --body "Agent finished"
;;
esac`}</CodeBlock>
<CodeBlock lang="bash">{`chmod +x ~/.claude/hooks/cmux-notify.sh`}</CodeBlock>
<h3>2. Configure Claude Code</h3>
<CodeBlock title="~/.claude/settings.json" lang="json">{`{
"hooks": {
"Stop": ["~/.claude/hooks/cmux-notify.sh"],
"PostToolUse": [
{
"matcher": "Task",
"hooks": ["~/.claude/hooks/cmux-notify.sh"]
}
]
}
}`}</CodeBlock>
<p>Restart Claude Code to apply the hooks.</p>
<h2>Integration examples</h2>
<h3>Notify after long command</h3>
<CodeBlock title="~/.zshrc" lang="bash">{`# Add to your shell config
notify-after() {
"$@"
local exit_code=$?
if [ $exit_code -eq 0 ]; then
cmux notify --title "✓ Command Complete" --body "$1"
else
cmux notify --title "✗ Command Failed" --body "$1 (exit $exit_code)"
fi
return $exit_code
}
# Usage: notify-after npm run build`}</CodeBlock>
<h3>Python</h3>
<CodeBlock title="python" lang="python">{`import sys
def notify(title: str, body: str):
"""Send OSC 777 notification."""
sys.stdout.write(f'\\x1b]777;notify;{title};{body}\\x07')
sys.stdout.flush()
notify("Script Complete", "Processing finished")`}</CodeBlock>
<h3>Node.js</h3>
<CodeBlock title="node" lang="javascript">{`function notify(title, body) {
process.stdout.write(\`\\x1b]777;notify;\${title};\${body}\\x07\`);
}
notify('Build Done', 'webpack finished');`}</CodeBlock>
<h3>tmux passthrough</h3>
<p>If using tmux inside cmux, enable passthrough:</p>
<CodeBlock title=".tmux.conf" lang="bash">{`set -g allow-passthrough on`}</CodeBlock>
<CodeBlock lang="bash">{`printf '\\ePtmux;\\e\\e]777;notify;Title;Body\\a\\e\\\\'`}</CodeBlock>
</>
);
}

5
web/app/docs/page.tsx Normal file
View file

@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function DocsPage() {
redirect("/docs/getting-started");
}