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:
parent
5febb66873
commit
f970cdcf33
37 changed files with 3304 additions and 296 deletions
382
web/app/docs/api/page.tsx
Normal file
382
web/app/docs/api/page.tsx
Normal 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 “Notifications only” 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
127
web/app/docs/changelog/page.tsx
Normal file
127
web/app/docs/changelog/page.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
212
web/app/docs/concepts/page.tsx
Normal file
212
web/app/docs/concepts/page.tsx
Normal 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
|
||||
“tabs” since they behave like tabs in the sidebar. The
|
||||
socket API and environment variables use the term
|
||||
“workspace”.
|
||||
</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 “dev”</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
127
web/app/docs/configuration/page.tsx
Normal file
127
web/app/docs/configuration/page.tsx
Normal 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'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'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 “Notifications only” 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
137
web/app/docs/docs-nav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
web/app/docs/getting-started/page.tsx
Normal file
77
web/app/docs/getting-started/page.tsx
Normal 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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
web/app/docs/keyboard-shortcuts/page.tsx
Normal file
19
web/app/docs/keyboard-shortcuts/page.tsx
Normal 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
30
web/app/docs/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
202
web/app/docs/notifications/page.tsx
Normal file
202
web/app/docs/notifications/page.tsx
Normal 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
5
web/app/docs/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function DocsPage() {
|
||||
redirect("/docs/getting-started");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue