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

View file

@ -0,0 +1,206 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "EULA — cmux",
description: "End-User License Agreement for cmux",
};
export default function EulaPage() {
return (
<>
<h1>End-User License Agreement</h1>
<p>Last updated: December 2, 2025</p>
<p>
Please read this End-User License Agreement carefully before
downloading or using cmux.
</p>
<h2>Interpretation and Definitions</h2>
<p>For the purposes of this Agreement:</p>
<ul>
<li>
<strong>&ldquo;Agreement&rdquo;</strong> means this End-User License
Agreement that forms the entire agreement between You and the Company
regarding the use of the Application.
</li>
<li>
<strong>&ldquo;Application&rdquo;</strong> means the cmux desktop
application for macOS, a native terminal application built on Ghostty.
</li>
<li>
<strong>&ldquo;Company&rdquo;</strong> (referred to as &ldquo;the
Company&rdquo;, &ldquo;We&rdquo;, &ldquo;Us&rdquo; or
&ldquo;Our&rdquo;) refers to Manaflow.
</li>
<li>
<strong>&ldquo;Content&rdquo;</strong> refers to content such as text,
code, images, or other information that can be created, processed, or
displayed by the Application.
</li>
<li>
<strong>&ldquo;Country&rdquo;</strong> refers to the United States.
</li>
<li>
<strong>&ldquo;Device&rdquo;</strong> means any macOS computer that
can run the Application.
</li>
<li>
<strong>&ldquo;You&rdquo;</strong> means the individual accessing or
using the Application.
</li>
</ul>
<h2>Acknowledgment</h2>
<p>
By downloading or using the Application, You are agreeing to be bound
by the terms of this Agreement. If You do not agree, do not download or
use the Application.
</p>
<p>
The Application is licensed, not sold, to You by the Company for use
strictly in accordance with the terms of this Agreement.
</p>
<h2>License</h2>
<h3>Scope of License</h3>
<p>
The Company grants You a revocable, non-exclusive, non-transferable,
limited license to download, install and use the Application strictly in
accordance with this Agreement, for your personal or internal business
purposes including commercial use in connection with software
development.
</p>
<h3>License Restrictions</h3>
<p>You agree not to, and You will not permit others to:</p>
<ul>
<li>
License, sell, rent, lease, assign, distribute, transmit, host, or
otherwise commercially exploit the Application or make it available to
any third party
</li>
<li>
Remove, alter or obscure any proprietary notice (including copyright
or trademark) of the Company
</li>
<li>
Modify, make derivative works of, disassemble, decrypt, reverse
compile or reverse engineer any part of the Application
</li>
</ul>
<h2>Intellectual Property</h2>
<p>
The Application, including all copyrights, patents, trademarks, trade
secrets and other intellectual property rights, is and shall remain the
sole and exclusive property of the Company.
</p>
<p>
You retain ownership of any code or content you create using the
Application.
</p>
<h2>Modifications and Updates</h2>
<p>
The Company reserves the right to modify, suspend or discontinue the
Application at any time, with or without notice and without liability to
You.
</p>
<p>
The Company may provide updates, patches, bug fixes, and other
modifications. Updates may modify or remove certain features. You agree
that all updates are subject to the terms of this Agreement.
</p>
<h2>Third-Party Services</h2>
<p>
The Application integrates with third-party services including Ghostty
(terminal rendering engine), Sentry (error tracking), and Sparkle
(auto-update framework). You acknowledge that the Company shall not be
responsible for any third-party services, including their accuracy,
completeness, or quality.
</p>
<h2>Term and Termination</h2>
<p>
This Agreement shall remain in effect until terminated by You or the
Company. The Company may terminate this Agreement at any time for any
reason.
</p>
<p>
This Agreement will terminate immediately if you fail to comply with any
provision. You may also terminate by deleting the Application and all
copies from your Device.
</p>
<p>
Upon termination, You shall cease all use of the Application and delete
all copies from your Device.
</p>
<h2>No Warranties</h2>
<p>
The Application is provided &ldquo;AS IS&rdquo; and &ldquo;AS
AVAILABLE&rdquo; without warranty of any kind. The Company expressly
disclaims all warranties, whether express, implied, statutory or
otherwise, including all implied warranties of merchantability, fitness
for a particular purpose, title and non-infringement.
</p>
<p>
Some jurisdictions do not allow the exclusion of certain types of
warranties, so some of the above exclusions may not apply to You.
</p>
<h2>Limitation of Liability</h2>
<p>
The entire liability of the Company under this Agreement shall be
limited to the amount actually paid by You for the Application, or 100
USD if You haven&rsquo;t purchased anything.
</p>
<p>
To the maximum extent permitted by law, in no event shall the Company
be liable for any special, incidental, indirect, or consequential
damages whatsoever.
</p>
<h2>Indemnification</h2>
<p>
You agree to indemnify and hold the Company harmless from any claim or
demand, including reasonable attorneys&rsquo; fees, due to or arising
out of your use of the Application or violation of this Agreement.
</p>
<h2>Severability and Waiver</h2>
<p>
If any provision of this Agreement is held to be unenforceable, it will
be changed and interpreted to accomplish its objectives to the greatest
extent possible, and the remaining provisions will continue in full
force and effect.
</p>
<h2>Governing Law</h2>
<p>
The laws of the United States, excluding conflicts of law rules, shall
govern this Agreement and your use of the Application.
</p>
<h2>Changes to This Agreement</h2>
<p>
The Company reserves the right to modify this Agreement at any time. If
a revision is material, we will provide at least 30 days&rsquo; notice.
By continuing to use the Application after revisions become effective,
You agree to be bound by the revised terms.
</p>
<h2>Contact Us</h2>
<p>If you have any questions about this Agreement:</p>
<ul>
<li>
Email us at{" "}
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>
</li>
</ul>
</>
);
}

View file

@ -0,0 +1,16 @@
import { SiteHeader } from "../components/site-header";
export default function LegalLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen">
<SiteHeader />
<main className="w-full max-w-5xl mx-auto px-6 py-10">
<div className="docs-content text-[15px]">{children}</div>
</main>
</div>
);
}

View file

@ -0,0 +1,162 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Privacy Policy — cmux",
description: "Privacy policy for cmux",
};
export default function PrivacyPolicyPage() {
return (
<>
<h1>Privacy Policy</h1>
<p>Last updated: December 2, 2025</p>
<p>
Manaflow (the &ldquo;Company&rdquo;) is committed to maintaining robust
privacy protections for its users. This Privacy Policy is designed to
help you understand how we collect, use and safeguard the information you
provide to us.
</p>
<p>
For purposes of this policy, &ldquo;Site&rdquo; refers to the
Company&rsquo;s website at{" "}
<a href="https://cmux.dev">cmux.dev</a>.
&ldquo;Application&rdquo; refers to the cmux desktop application for
macOS. &ldquo;Service&rdquo; refers to the Site and Application
collectively. The terms &ldquo;we,&rdquo; &ldquo;us,&rdquo; and
&ldquo;our&rdquo; refer to the Company. &ldquo;You&rdquo; refers to
you, as a user of our Service.
</p>
<p>
By using our Service, you accept this Privacy Policy and our{" "}
<a href="/terms-of-service">Terms of Service</a>, and you consent to
our collection, storage, use and disclosure of your information as
described here.
</p>
<h2>I. Information We Collect</h2>
<p>
We collect &ldquo;Non-Personal Information&rdquo; and &ldquo;Personal
Information.&rdquo; Non-Personal Information includes information that
cannot be used to personally identify you, such as anonymous usage data,
platform types, and crash diagnostics. Personal Information includes
your email address if you choose to contact us.
</p>
<h3>1. Information collected via Technology</h3>
<p>
The Application may collect the following information automatically:
</p>
<ul>
<li>Crash reports and error diagnostics (via Sentry)</li>
<li>Operating system version and application version</li>
<li>Anonymous usage patterns</li>
</ul>
<p>
The Application checks for updates via Sparkle, which may transmit your
operating system version and application version to our update server.
</p>
<h3>2. Information you provide directly</h3>
<p>
If you contact us via email or our contact page, we collect the
information you provide such as your name and email address.
</p>
<h3>3. Children&rsquo;s Privacy</h3>
<p>
The Service is not directed to anyone under the age of 13. We do not
knowingly collect information from anyone under 13. If you believe we
have collected such information, please contact us at{" "}
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
</p>
<h2>II. Third-Party Services</h2>
<p>
The Application integrates with the following third-party services:
</p>
<ul>
<li>
<strong>Sentry</strong> &mdash; error tracking and crash reporting.
May collect error logs, stack traces, device information, and OS
version.
</li>
<li>
<strong>Sparkle</strong> &mdash; auto-update framework. Transmits
application and OS version to check for updates.
</li>
<li>
<strong>Ghostty / libghostty</strong> &mdash; terminal rendering
engine. Runs entirely locally on your device.
</li>
</ul>
<p>
Each of these services has its own privacy policy governing the
collection and use of your data.
</p>
<h2>III. How We Use and Share Information</h2>
<p>
We do not sell, trade, rent or otherwise share your Personal Information
with third parties for marketing purposes. We use crash reports and
diagnostics solely to improve the Application. We may share information
if we have a good-faith belief that disclosure is necessary to meet
legal process or protect against harm.
</p>
<h2>IV. How We Protect Information</h2>
<p>
We implement security measures designed to protect your information from
unauthorized access, including encryption and secure server software.
However, no method of transmission or storage is 100% secure. By using
our Service, you acknowledge and agree to assume these risks.
</p>
<h2>V. Your Rights</h2>
<p>
Depending on your location, you may have rights under applicable data
protection laws (such as GDPR or CCPA), including:
</p>
<ul>
<li>Right to access a copy of data we hold about you</li>
<li>Right to request correction of inaccurate data</li>
<li>Right to request deletion of your data</li>
<li>Right to data portability</li>
<li>Right to restrict or object to processing</li>
</ul>
<p>
To exercise any of these rights, please contact us at{" "}
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
</p>
<h2>VI. Links to Other Websites</h2>
<p>
The Service may provide links to third-party websites. We are not
responsible for the privacy practices of those websites. This Privacy
Policy applies solely to information collected by us.
</p>
<h2>VII. Changes to This Policy</h2>
<p>
We reserve the right to change this policy at any time. Significant
changes will go into effect 30 days following notification. You should
periodically check the Site for updates.
</p>
<h2>VIII. Contact Us</h2>
<p>
If you have any questions regarding this Privacy Policy, please contact
us at{" "}
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
</p>
<h2>IX. Data Retention</h2>
<p>
Crash reports and diagnostics are retained only as long as needed to
diagnose and fix issues. You may request deletion of any data associated
with you by contacting us at{" "}
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
</p>
</>
);
}

View file

@ -0,0 +1,187 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Terms of Service — cmux",
description: "Terms of service for cmux",
};
export default function TermsOfServicePage() {
return (
<>
<h1>Terms of Service</h1>
<p>Last revised on: December 2, 2025</p>
<p>
The website located at{" "}
<a href="https://cmux.dev">cmux.dev</a> (the
&ldquo;Site&rdquo;) and the cmux desktop application (the
&ldquo;Application&rdquo;) are copyrighted works belonging to Manaflow
(&ldquo;Company&rdquo;, &ldquo;us&rdquo;, &ldquo;our&rdquo;, and
&ldquo;we&rdquo;). These Terms of Use (these &ldquo;Terms&rdquo;) set
forth the legally binding terms and conditions that govern your use of
the Site and Application.
</p>
<p>
By accessing or using the Site or Application, you are accepting these
Terms and you represent and warrant that you have the right, authority,
and capacity to enter into these Terms. You may not access or use the
Site or Application if you are not at least 18 years old. If you do not
agree with all of the provisions of these Terms, do not access and/or
use the Site or Application.
</p>
<h2>1. License</h2>
<p>
Subject to these Terms, Company grants you a non-transferable,
non-exclusive, revocable, limited license to use and access the Site and
Application for your personal or internal business purposes, including
commercial use in connection with your software development activities.
</p>
<h3>Restrictions</h3>
<p>The rights granted to you are subject to the following restrictions:</p>
<ul>
<li>
You shall not license, sell, rent, lease, transfer, assign,
distribute, host, or otherwise commercially exploit the Application
</li>
<li>
You shall not modify, make derivative works of, disassemble, reverse
compile or reverse engineer any part of the Application
</li>
<li>
You shall not access the Application in order to build a similar or
competitive product
</li>
</ul>
<h3>Modification</h3>
<p>
Company reserves the right, at any time, to modify, suspend, or
discontinue the Site or Application with or without notice to you.
Company will not be liable to you or any third party for any
modification, suspension, or discontinuation.
</p>
<h3>Ownership</h3>
<p>
You acknowledge that all intellectual property rights, including
copyrights, patents, trademarks, and trade secrets, in the Application
and its content are owned by Company or Company&rsquo;s suppliers.
These Terms do not transfer to you any rights, title or interest in such
intellectual property, except for the limited license above. Company and
its suppliers reserve all rights not granted in these Terms.
</p>
<h3>Feedback</h3>
<p>
If you provide Company with any feedback or suggestions regarding the
Application, you hereby assign to Company all rights in such feedback
and agree that Company shall have the right to use such feedback in any
manner it deems appropriate.
</p>
<h2>2. User Content</h2>
<p>
You retain full ownership of all code, files, and content you create or
process using the Application. The Application runs locally on your
device and your content is not transmitted to our servers during normal
use.
</p>
<h2>3. Indemnification</h2>
<p>
You agree to indemnify and hold Company (and its officers, employees,
and agents) harmless, including costs and attorneys&rsquo; fees, from
any claim or demand made by any third party due to or arising out of (a)
your use of the Application, (b) your violation of these Terms, or (c)
your violation of applicable laws or regulations.
</p>
<h2>4. Third-Party Links</h2>
<p>
The Site may contain links to third-party websites and services. Such
links are not under the control of Company, and Company is not
responsible for them. You use all third-party links at your own risk.
</p>
<h2>5. Disclaimers</h2>
<p>
THE APPLICATION IS PROVIDED ON AN &ldquo;AS-IS&rdquo; AND &ldquo;AS
AVAILABLE&rdquo; BASIS. COMPANY EXPRESSLY DISCLAIMS ANY AND ALL
WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR
STATUTORY, INCLUDING ALL WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT.
</p>
<p>
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO
THE ABOVE EXCLUSION MAY NOT APPLY TO YOU.
</p>
<h2>6. Limitation on Liability</h2>
<p>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL COMPANY BE
LIABLE TO YOU OR ANY THIRD PARTY FOR ANY LOST PROFITS, LOST DATA, OR ANY
INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL OR PUNITIVE
DAMAGES ARISING FROM OR RELATING TO THESE TERMS OR YOUR USE OF THE
APPLICATION.
</p>
<p>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, OUR LIABILITY TO YOU FOR ANY
DAMAGES WILL AT ALL TIMES BE LIMITED TO FIFTY US DOLLARS ($50).
</p>
<h2>7. Term and Termination</h2>
<p>
These Terms will remain in effect while you use the Application. We may
suspend or terminate your rights at any time for any reason at our sole
discretion. Upon termination, you shall cease all use of the Application
and delete all copies from your devices.
</p>
<h2>8. Dispute Resolution</h2>
<p>
You agree that any dispute between you and Company relating to the
Application or these Terms will be resolved by binding arbitration,
rather than in court, except that either party may assert individualized
claims in small claims court or seek equitable relief for intellectual
property misuse. The arbitration will be conducted by JAMS under their
applicable rules.
</p>
<p>
YOU AND COMPANY WAIVE ANY CONSTITUTIONAL AND STATUTORY RIGHTS TO SUE IN
COURT AND HAVE A TRIAL IN FRONT OF A JUDGE OR A JURY.
</p>
<p>
YOU AND COMPANY AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY
ON AN INDIVIDUAL BASIS AND NOT ON A CLASS, REPRESENTATIVE, OR COLLECTIVE
BASIS.
</p>
<p>
You have the right to opt out of this arbitration agreement by sending
written notice to{" "}
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a> within 30
days of first becoming subject to it.
</p>
<h2>9. General</h2>
<p>
These Terms constitute the entire agreement between you and Company
regarding the use of the Application. Our failure to exercise or enforce
any right or provision shall not operate as a waiver. If any provision
is held to be invalid, the remaining provisions will remain in full
force and effect.
</p>
<h2>10. Contact</h2>
<p>
Questions about these Terms should be sent to{" "}
<a href="mailto:founders@manaflow.com">founders@manaflow.com</a>.
</p>
<p>
Copyright &copy; 2025 Manaflow. All rights reserved.
</p>
</>
);
}

View file

@ -0,0 +1,70 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Introducing cmux",
description:
"A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.",
};
export default function IntroducingCmuxPage() {
return (
<>
<div className="mb-8">
<Link
href="/blog"
className="text-sm text-muted hover:text-foreground transition-colors"
>
&larr; Back to blog
</Link>
</div>
<h1>Introducing cmux</h1>
<time className="text-sm text-muted">June 1, 2025</time>
<p className="mt-6">
cmux is a native macOS terminal application built on top of Ghostty,
designed from the ground up for developers who run multiple AI coding
agents simultaneously.
</p>
<h2>Why cmux?</h2>
<p>
Modern development workflows often involve running several agents at
once &mdash; Claude Code, Codex, and other tools each in their own
terminal. Keeping track of which ones need attention and switching
between them quickly is the problem cmux solves.
</p>
<h2>Key features</h2>
<ul>
<li>
<strong>Vertical tabs</strong> &mdash; see all your terminals at a
glance in a sidebar
</li>
<li>
<strong>Notification rings</strong> &mdash; tabs flash when an agent
needs your input
</li>
<li>
<strong>Split panes</strong> &mdash; horizontal and vertical splits
within each workspace
</li>
<li>
<strong>Socket API</strong> &mdash; programmatic control for creating
tabs and sending input
</li>
<li>
<strong>GPU-accelerated</strong> &mdash; powered by libghostty for
smooth rendering
</li>
</ul>
<h2>Get started</h2>
<p>
Install cmux via Homebrew or download the DMG from the{" "}
<Link href="/docs/getting-started">getting started guide</Link>.
</p>
</>
);
}

31
web/app/blog/layout.tsx Normal file
View file

@ -0,0 +1,31 @@
import type { Metadata } from "next";
import { SiteHeader } from "../components/site-header";
export const metadata: Metadata = {
title: {
template: "%s — cmux blog",
default: "cmux blog",
},
openGraph: {
siteName: "cmux",
type: "article",
},
alternates: {
canonical: "./",
},
};
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen">
<SiteHeader section="blog" />
<main className="w-full max-w-5xl mx-auto px-6 py-10">
<div className="docs-content text-[15px]">{children}</div>
</main>
</div>
);
}

41
web/app/blog/page.tsx Normal file
View file

@ -0,0 +1,41 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Blog",
description: "News and updates from the cmux team",
};
const posts = [
{
slug: "introducing-cmux",
title: "Introducing cmux",
date: "2025-06-01",
summary:
"A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.",
},
];
export default function BlogPage() {
return (
<>
<h1>Blog</h1>
<div className="space-y-8 mt-6">
{posts.map((post) => (
<article key={post.slug}>
<Link
href={`/blog/${post.slug}`}
className="block group"
>
<h2 className="text-lg font-medium group-hover:underline">
{post.title}
</h2>
<time className="text-sm text-muted">{post.date}</time>
<p className="mt-1 text-muted">{post.summary}</p>
</Link>
</article>
))}
</div>
</>
);
}

119
web/app/community/page.tsx Normal file
View file

@ -0,0 +1,119 @@
import type { Metadata } from "next";
import { SiteHeader } from "../components/site-header";
export const metadata: Metadata = {
title: "Community — cmux",
description: "Join the cmux community on Discord, Twitter, GitHub, and more",
};
function CommunityLink({
href,
icon,
name,
action,
description,
}: {
href: string;
icon: React.ReactNode;
name: string;
action: string;
description: string;
}) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="group flex items-start gap-4 rounded-xl border border-border p-5 hover:bg-code-bg transition-colors"
>
<div className="shrink-0 mt-0.5 text-muted group-hover:text-foreground transition-colors">
{icon}
</div>
<div className="min-w-0">
<div className="font-medium text-[15px]">{name}</div>
<div className="text-sm text-muted mt-0.5">{description}</div>
<div className="text-xs font-medium text-muted mt-2 group-hover:text-foreground transition-colors">
{action} &rarr;
</div>
</div>
</a>
);
}
export default function CommunityPage() {
return (
<div className="min-h-screen">
<SiteHeader section="community" />
<main className="w-full max-w-5xl mx-auto px-6 py-10">
<h1 className="text-2xl font-semibold tracking-tight mb-2">
Community
</h1>
<p className="text-muted text-[15px] mb-8">
Connect with other cmux users and the team behind it.
</p>
<div className="grid gap-4 sm:grid-cols-2">
<CommunityLink
href="https://discord.gg/SDbQmzQhRK"
name="Discord"
action="Join our Discord"
description="Chat with the community, get help, and share feedback"
icon={
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
}
/>
<CommunityLink
href="https://github.com/manaflow-ai/cmux"
name="GitHub"
action="View on GitHub"
description="Star the repo, report issues, and contribute"
icon={
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
}
/>
<CommunityLink
href="https://twitter.com/manaflowai"
name="Twitter"
action="Follow on X"
description="Updates, announcements, and tips"
icon={
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
}
/>
<CommunityLink
href="https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw"
name="YouTube"
action="Subscribe"
description="Demos, tutorials, and walkthroughs"
icon={
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
}
/>
<CommunityLink
href="https://www.linkedin.com/company/manaflow-ai/"
name="LinkedIn"
action="Follow us"
description="Company news and engineering updates"
icon={
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
}
/>
</div>
</main>
</div>
);
}

View file

@ -0,0 +1,20 @@
export function Callout({
type = "info",
children,
}: {
type?: "info" | "warn";
children: React.ReactNode;
}) {
const styles =
type === "warn"
? "border-l-amber-500 bg-amber-500/5"
: "border-l-blue-500 bg-blue-500/5";
return (
<div
className={`${styles} border-l-2 px-4 py-3 mb-4 rounded-r-lg text-[14px] text-muted leading-relaxed`}
>
{children}
</div>
);
}

View file

@ -0,0 +1,59 @@
import { codeToHtml } from "shiki";
export async function CodeBlock({
children,
title,
lang,
variant = "code",
}: {
children: string;
title?: string;
lang?: string;
variant?: "code" | "ascii";
}) {
const lineHeightClass =
variant === "ascii" ? "leading-[1.15]" : "leading-[1.45]";
if (lang && variant !== "ascii") {
const html = await codeToHtml(children, {
lang,
themes: { light: "github-light", dark: "github-dark" },
defaultColor: false,
});
return (
<div className="mb-4">
{title && (
<div className="text-[11px] font-mono text-muted px-4 py-1.5 bg-code-bg border border-border border-b-0 rounded-t-lg">
{title}
</div>
)}
<div
className={`[&_pre]:bg-code-bg [&_pre]:border [&_pre]:border-border [&_pre]:px-4 [&_pre]:py-3 [&_pre]:overflow-x-auto [&_pre]:text-[13px] [&_pre]:${lineHeightClass} [&_pre]:font-mono ${
title
? "[&_pre]:rounded-b-lg [&_pre]:border-t-0"
: "[&_pre]:rounded-lg"
} [&_code]:bg-transparent [&_code]:p-0`}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
}
return (
<div className="mb-4">
{title && (
<div className="text-[11px] font-mono text-muted px-4 py-1.5 bg-code-bg border border-border border-b-0 rounded-t-lg">
{title}
</div>
)}
<pre
className={`bg-code-bg border border-border px-4 py-3 overflow-x-auto text-[13px] ${lineHeightClass} font-mono ${
title ? "rounded-b-lg" : "rounded-lg"
}`}
>
<code>{children}</code>
</pre>
</div>
);
}

View file

@ -0,0 +1,9 @@
export const navItems = [
{ title: "Getting Started", href: "/docs/getting-started" },
{ title: "Concepts", href: "/docs/concepts" },
{ title: "Configuration", href: "/docs/configuration" },
{ title: "Keyboard Shortcuts", href: "/docs/keyboard-shortcuts" },
{ title: "API Reference", href: "/docs/api" },
{ title: "Notifications", href: "/docs/notifications" },
{ title: "Changelog", href: "/docs/changelog" },
];

View file

@ -0,0 +1,41 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { navItems } from "./docs-nav-items";
export function DocsPager() {
const pathname = usePathname();
const index = navItems.findIndex((item) => item.href === pathname);
const prev = index > 0 ? navItems[index - 1] : null;
const next = index < navItems.length - 1 ? navItems[index + 1] : null;
if (!prev && !next) return null;
return (
<nav className="flex items-center justify-between mt-12 pt-6 border-t border-border text-[14px]">
{prev ? (
<Link
href={prev.href}
className="flex items-center gap-1.5 text-muted hover:text-foreground transition-colors"
>
<span aria-hidden>&larr;</span>
{prev.title}
</Link>
) : (
<span />
)}
{next ? (
<Link
href={next.href}
className="flex items-center gap-1.5 text-muted hover:text-foreground transition-colors"
>
{next.title}
<span aria-hidden>&rarr;</span>
</Link>
) : (
<span />
)}
</nav>
);
}

View file

@ -0,0 +1,31 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { navItems } from "./docs-nav-items";
export function DocsSidebar({ onNavigate }: { onNavigate?: () => void }) {
const pathname = usePathname();
return (
<nav className="space-y-0.5">
{navItems.map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
onClick={onNavigate}
className={`block px-3 py-1.5 text-[14px] rounded-md transition-colors ${
active
? "text-foreground font-medium bg-code-bg"
: "text-muted hover:text-foreground"
}`}
>
{item.title}
</Link>
);
})}
</nav>
);
}

View file

@ -0,0 +1,22 @@
export function DownloadButton({ size = "default" }: { size?: "default" | "sm" }) {
const isSmall = size === "sm";
return (
<a
href="https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg"
className={`inline-flex items-center rounded-full font-medium bg-foreground hover:opacity-85 transition-opacity ${
isSmall ? "gap-2 px-4 py-1.5 text-xs" : "gap-2.5 px-5 py-2.5 text-[15px]"
}`}
style={{ color: "var(--background)", textDecoration: "none" }}
>
<svg
width={isSmall ? 12 : 16}
height={isSmall ? 14 : 19}
viewBox="0 0 814 1000"
fill="currentColor"
>
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57.8-155.5-127.4c-58.3-81.6-105.6-208.4-105.6-328.6 0-193 125.6-295.5 249.2-295.5 65.7 0 120.5 43.1 161.7 43.1 39.2 0 100.4-45.8 175.1-45.8 28.3 0 130.3 2.6 197.2 99.2zM554.1 159.4c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.9 32.4-57.2 83.6-57.2 135.4 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 137.6-71.2z" />
</svg>
Download for Mac
</a>
);
}

View file

@ -0,0 +1,56 @@
import Link from "next/link";
import { DownloadButton } from "./download-button";
export function NavLinks() {
return (
<>
<Link
href="/docs/getting-started"
className="hover:text-foreground transition-colors"
>
Docs
</Link>
<Link
href="/blog"
className="hover:text-foreground transition-colors"
>
Blog
</Link>
<Link
href="/docs/changelog"
className="hover:text-foreground transition-colors"
>
Changelog
</Link>
<Link
href="/community"
className="hover:text-foreground transition-colors"
>
Community
</Link>
<Link
href="https://github.com/manaflow-ai/cmux"
className="hover:text-foreground transition-colors"
>
GitHub
</Link>
<DownloadButton size="sm" />
</>
);
}
export function SiteFooter() {
return (
<footer className="py-8 flex justify-center">
<div className="flex items-center gap-4 text-sm text-muted">
<a href="https://github.com/manaflow-ai/cmux" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">GitHub</a>
<a href="https://twitter.com/manaflowai" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">Twitter</a>
<a href="https://discord.gg/SDbQmzQhRK" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">Discord</a>
<Link href="/privacy-policy" className="hover:text-foreground transition-colors">Privacy</Link>
<Link href="/terms-of-service" className="hover:text-foreground transition-colors">Terms</Link>
<Link href="/eula" className="hover:text-foreground transition-colors">EULA</Link>
<a href="mailto:founders@manaflow.com" className="hover:text-foreground transition-colors">Contact</a>
</div>
</footer>
);
}

View file

@ -0,0 +1,39 @@
import Image from "next/image";
import Link from "next/link";
import { NavLinks } from "./nav-links";
export function SiteHeader({ section, hideLogo }: { section?: string; hideLogo?: boolean }) {
return (
<header className="sticky top-0 z-30 w-full bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-5xl mx-auto flex items-center justify-between px-6 h-12">
<div className="flex items-center gap-3">
{!hideLogo && (
<>
<Link href="/" className="flex items-center gap-2.5">
<Image
src="/icon.png"
alt="cmux"
width={24}
height={24}
className="rounded-md"
/>
<span className="text-sm font-semibold tracking-tight">
cmux
</span>
</Link>
{section && (
<>
<span className="text-border text-[13px]">/</span>
<span className="text-[13px] text-muted">{section}</span>
</>
)}
</>
)}
</div>
<nav className="flex items-center gap-4 text-sm text-muted">
<NavLinks />
</nav>
</div>
</header>
);
}

View file

@ -0,0 +1,221 @@
"use client";
import { useState, useEffect, useRef, useCallback, useSyncExternalStore } from "react";
type DevValues = {
headerTx: number;
cursorTop: number;
cursorBlink: boolean;
subtitleLh: number;
downloadAbove: number;
downloadBelow: number;
featuresLh: number;
featuresMb: number;
docsPt: number;
};
const defaults: DevValues = {
headerTx: -4,
cursorTop: 2.5,
cursorBlink: true,
subtitleLh: 1.5,
downloadAbove: 21,
downloadBelow: 33,
featuresLh: 1.275,
featuresMb: 23,
docsPt: 8,
};
// Tiny external store (avoids setState-during-render)
let snapshot = { ...defaults };
const listeners = new Set<() => void>();
function getSnapshot() { return snapshot; }
function getServerSnapshot() { return defaults; }
function setStore(patch: Partial<DevValues>) {
snapshot = { ...snapshot, ...patch };
listeners.forEach((l) => l());
}
function subscribe(cb: () => void) {
listeners.add(cb);
return () => { listeners.delete(cb); };
}
export function useDevValues() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
function el(name: string) {
return document.querySelector(`[data-dev="${name}"]`) as HTMLElement | null;
}
function applyToDOM(v: DevValues) {
const header = el("header");
if (header) header.style.transform = `translateX(${v.headerTx}px)`;
const subtitle = el("subtitle");
if (subtitle) subtitle.style.lineHeight = `${v.subtitleLh}`;
const download = el("download");
if (download) {
download.style.marginTop = `${v.downloadAbove}px`;
download.style.marginBottom = `${v.downloadBelow}px`;
}
const featuresUl = el("features-ul");
if (featuresUl) featuresUl.style.lineHeight = `${v.featuresLh}`;
const featuresSpacer = el("features-spacer");
if (featuresSpacer) featuresSpacer.style.height = `${v.featuresMb}px`;
const docsContent = el("docs-content");
if (docsContent) docsContent.style.paddingTop = `${v.docsPt}px`;
}
export function DevPanel() {
const [visible, setVisible] = useState(false);
const [pos, setPos] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
const [copied, setCopied] = useState(false);
const vals = useDevValues();
const dragOffset = useRef({ x: 0, y: 0 });
useEffect(() => {
setPos({ x: window.innerWidth - 340, y: window.innerHeight - 320 });
}, []);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === ".") {
e.preventDefault();
setVisible((v) => !v);
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
const update = useCallback((patch: Partial<DevValues>) => {
setStore(patch);
applyToDOM({ ...snapshot, ...patch });
}, []);
const onPointerDown = useCallback((e: React.PointerEvent) => {
if ((e.target as HTMLElement).closest("input, button, label")) return;
setDragging(true);
dragOffset.current = { x: e.clientX - pos.x, y: e.clientY - pos.y };
(e.target as HTMLElement).setPointerCapture(e.pointerId);
}, [pos]);
const onPointerMove = useCallback((e: React.PointerEvent) => {
if (!dragging) return;
setPos({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y });
}, [dragging]);
const onPointerUp = useCallback(() => setDragging(false), []);
if (process.env.NODE_ENV !== "development" || !visible) return null;
return (
<div
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
style={{ left: pos.x, top: pos.y, cursor: dragging ? "grabbing" : "grab" }}
className="fixed z-[9999] bg-[#222] text-white text-xs rounded-xl p-4 space-y-3 font-mono shadow-lg select-none"
>
<div className="flex items-center justify-between gap-4">
<span className="text-white/50">Dev Controls</span>
<span className="text-white/20">.</span>
</div>
<Section label="Header">
<Row label="tx" value={vals.headerTx} onChange={(v) => update({ headerTx: v })} min={-50} max={50} step={1} unit="px" />
</Section>
<Section label="Cursor">
<div className="flex items-center gap-3">
<Row label="top" value={vals.cursorTop} onChange={(v) => update({ cursorTop: v })} min={-5} max={5} step={0.5} unit="px" />
<label className="flex items-center gap-2 text-white/70 cursor-pointer">
<input type="checkbox" checked={vals.cursorBlink} onChange={(e) => update({ cursorBlink: e.target.checked })} />
blink
</label>
</div>
</Section>
<Section label="Subtitle">
<Row label="line-h" value={vals.subtitleLh} onChange={(v) => update({ subtitleLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} />
</Section>
<Section label="Download buttons">
<Row label="above" value={vals.downloadAbove} onChange={(v) => update({ downloadAbove: v })} />
<Row label="below" value={vals.downloadBelow} onChange={(v) => update({ downloadBelow: v })} />
</Section>
<Section label="Features">
<Row label="line-h" value={vals.featuresLh} onChange={(v) => update({ featuresLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} />
<Row label="mb" value={vals.featuresMb} onChange={(v) => update({ featuresMb: v })} />
</Section>
<Section label="Docs">
<Row label="pt" value={vals.docsPt} onChange={(v) => update({ docsPt: v })} />
</Section>
<button
onClick={() => {
const text = [
`header-tx: ${vals.headerTx}px`,
`cursor-top: ${vals.cursorTop}px`,
`cursor-blink: ${vals.cursorBlink}`,
`subtitle-lh: ${vals.subtitleLh}`,
`download-above: ${vals.downloadAbove}px`,
`download-below: ${vals.downloadBelow}px`,
`features-lh: ${vals.featuresLh}`,
`features-mb: ${vals.featuresMb}px`,
`docs-pt: ${vals.docsPt}px`,
].join(", ");
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
className="w-full py-1.5 rounded-lg bg-white/10 hover:bg-white/20 text-white/70 cursor-pointer transition-colors"
>
{copied ? "Copied!" : "Copy values"}
</button>
</div>
);
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="space-y-2">
<div className="text-white/40 text-[10px] uppercase tracking-wider">{label}</div>
{children}
</div>
);
}
function Row({ label, value, onChange, min = 0, max = 128, step = 1, unit = "px", w = 10 }: {
label: string; value: number; onChange: (v: number) => void;
min?: number; max?: number; step?: number; unit?: string; w?: number;
}) {
return (
<div className="flex items-center gap-2">
<span className="w-12 text-white/70">{label}</span>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
className="w-28 accent-blue-500 cursor-pointer"
/>
<span className="text-right tabular-nums" style={{ width: `${w * 4}px` }}>
{Number.isInteger(step) ? value : value.toFixed(step < 0.1 ? 2 : 1)}{unit}
</span>
</div>
);
}

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");
}

View file

@ -72,3 +72,143 @@ body {
.animate-blink {
animation: blink 1s step-end infinite;
}
/* Docs prose styles */
.docs-content h1 {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.025em;
margin-bottom: 0.75rem;
}
.docs-content h2 {
font-size: 1.25rem;
font-weight: 600;
margin-top: 2.5rem;
margin-bottom: 0.75rem;
letter-spacing: -0.01em;
}
.docs-content h3 {
font-size: 1rem;
font-weight: 600;
margin-top: 1.75rem;
margin-bottom: 0.5rem;
}
.docs-content h4 {
font-size: 0.9375rem;
font-weight: 600;
margin-top: 1.25rem;
margin-bottom: 0.375rem;
font-family: var(--font-geist-mono);
}
.docs-content > p {
line-height: 1.7;
margin-bottom: 1rem;
color: var(--muted);
}
.docs-content ul,
.docs-content ol {
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.docs-content ul {
list-style: disc;
}
.docs-content ol {
list-style: decimal;
}
.docs-content li {
line-height: 1.7;
margin-bottom: 0.25rem;
color: var(--muted);
}
.docs-content code {
font-family: var(--font-geist-mono);
font-size: 0.8125em;
background: var(--code-bg);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
}
.docs-content kbd {
font-family: var(--font-geist-mono);
font-size: 0.75em;
line-height: 1;
white-space: nowrap;
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 0.3125rem;
padding: 0.2rem 0.375rem;
min-width: 1.4em;
text-align: center;
display: inline-block;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08);
}
.dark .docs-content kbd {
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
}
.docs-content pre code {
background: none;
padding: 0;
font-size: 1em;
}
/* Shiki dual theme */
.shiki,
.shiki span {
color: var(--shiki-light) !important;
background-color: transparent !important;
}
.dark .shiki,
.dark .shiki span {
color: var(--shiki-dark) !important;
}
.docs-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
font-size: 0.875rem;
}
.docs-content th {
text-align: left;
font-weight: 600;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border);
}
.docs-content td {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border);
color: var(--muted);
}
.docs-content strong {
font-weight: 600;
color: var(--foreground);
}
.docs-content a:not([class]) {
color: var(--foreground);
text-decoration: underline;
text-decoration-skip-ink: none;
text-underline-offset: 3px;
text-decoration-thickness: 1px;
text-decoration-color: var(--border);
}
.docs-content a:not([class]):hover {
text-decoration-color: var(--foreground);
}

View file

@ -1,198 +1,350 @@
export function KeyboardShortcuts() {
"use client";
import { useMemo, useState } from "react";
type Shortcut = {
id: string;
combos: string[][];
description: string;
note?: string;
};
type ShortcutCategory = {
id: string;
title: string;
blurb?: string;
shortcuts: Shortcut[];
};
const CATEGORIES: ShortcutCategory[] = [
{
id: "workspaces",
title: "Workspaces",
blurb: "Workspaces live in the sidebar. Each workspace has its own set of panes and surfaces.",
shortcuts: [
{ id: "ws-new", combos: [["⌘", "N"]], description: "New workspace" },
{
id: "ws-jump-1-8",
combos: [["⌘", "18"]],
description: "Jump to workspace 18",
},
{
id: "ws-jump-last",
combos: [["⌘", "9"]],
description: "Jump to last workspace",
},
{
id: "ws-close",
combos: [["⌘", "⇧", "W"]],
description: "Close workspace",
},
],
},
{
id: "surfaces",
title: "Surfaces",
blurb: "Surfaces are tabs inside a pane.",
shortcuts: [
{ id: "sf-new", combos: [["⌘", "T"]], description: "New surface" },
{
id: "sf-prev-1",
combos: [["⌘", "⇧", "["]],
description: "Previous surface",
},
{
id: "sf-prev-2",
combos: [["⌃", "⇧", "Tab"]],
description: "Previous surface",
},
{
id: "sf-jump-1-8",
combos: [["⌃", "18"]],
description: "Jump to surface 18",
},
{
id: "sf-jump-last",
combos: [["⌃", "9"]],
description: "Jump to last surface",
},
{ id: "sf-close", combos: [["⌘", "W"]], description: "Close surface" },
],
},
{
id: "split-panes",
title: "Split Panes",
shortcuts: [
{ id: "sp-right", combos: [["⌘", "D"]], description: "Split right" },
{ id: "sp-down", combos: [["⌘", "⇧", "D"]], description: "Split down" },
{
id: "sp-focus",
combos: [["⌥", "⌘", "←/→/↑/↓"]],
description: "Focus pane directionally",
},
],
},
{
id: "browser",
title: "Browser",
shortcuts: [
{
id: "br-open",
combos: [["⌘", "⇧", "B"]],
description: "Open browser in split",
},
{ id: "br-addr", combos: [["⌘", "L"]], description: "Focus address bar" },
{ id: "br-forward", combos: [["⌘", "]"]], description: "Forward" },
{ id: "br-reload", combos: [["⌘", "R"]], description: "Reload page" },
{
id: "br-devtools",
combos: [["⌥", "⌘", "I"]],
description: "Open Developer Tools",
},
],
},
{
id: "notifications",
title: "Notifications",
shortcuts: [
{
id: "nt-panel",
combos: [["⌘", "⇧", "I"]],
description: "Show notifications panel",
},
{
id: "nt-latest",
combos: [["⌘", "⇧", "U"]],
description: "Jump to latest unread",
},
{
id: "nt-flash",
combos: [["⌘", "⇧", "L"]],
description: "Trigger flash",
},
],
},
{
id: "find",
title: "Find",
shortcuts: [
{ id: "fd-find", combos: [["⌘", "F"]], description: "Find" },
{
id: "fd-next-prev",
combos: [
["⌘", "G"],
["⌘", "⇧", "G"],
],
description: "Find next / previous",
},
{
id: "fd-hide",
combos: [["⌘", "⇧", "F"]],
description: "Hide find bar",
},
{
id: "fd-selection",
combos: [["⌘", "E"]],
description: "Use selection for find",
},
],
},
{
id: "terminal",
title: "Terminal",
shortcuts: [
{
id: "tm-clear",
combos: [["⌘", "K"]],
description: "Clear scrollback",
},
{
id: "tm-copy",
combos: [["⌘", "C"]],
description: "Copy (with selection)",
},
{ id: "tm-paste", combos: [["⌘", "V"]], description: "Paste" },
{
id: "tm-font",
combos: [
["⌘", "+"],
["⌘", "-"],
],
description: "Increase / decrease font size",
},
{ id: "tm-reset", combos: [["⌘", "0"]], description: "Reset font size" },
],
},
{
id: "window",
title: "Window",
shortcuts: [
{ id: "wn-new", combos: [["⌘", "⇧", "N"]], description: "New window" },
{ id: "wn-settings", combos: [["⌘", ","]], description: "Settings" },
{
id: "wn-reload",
combos: [["⌘", "⇧", "R"]],
description: "Reload configuration",
},
{ id: "wn-quit", combos: [["⌘", "Q"]], description: "Quit" },
],
},
];
function normalize(s: string) {
return s.toLowerCase().replace(/\s+/g, " ").trim();
}
function comboToText(combo: string[]) {
return combo.join(" ");
}
function shortcutSearchText(category: ShortcutCategory, s: Shortcut) {
const combos = s.combos.map(comboToText).join(" ");
return normalize(`${category.title} ${combos} ${s.description} ${s.note ?? ""}`);
}
function KeyCombo({ combo }: { combo: string[] }) {
return (
<section className="mb-12">
<h2 className="text-xs font-medium text-muted tracking-tight mb-6">
Keyboard Shortcuts
</h2>
<div className="space-y-8 text-[15px]">
{/* Workspaces */}
<div>
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Workspaces</h3>
<ul className="space-y-3">
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> N</span>
<span className="text-muted">New workspace</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> 1 8</span>
<span className="text-muted">Jump to workspace 18</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> 9</span>
<span className="text-muted">Jump to last workspace</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> W</span>
<span className="text-muted">Close workspace</span>
</li>
</ul>
</div>
{/* Surfaces */}
<div>
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Surfaces</h3>
<ul className="space-y-3">
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> T</span>
<span className="text-muted">New surface</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> [</span>
<span className="text-muted">Previous surface</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> Tab</span>
<span className="text-muted">Previous surface</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> 1 8</span>
<span className="text-muted">Jump to surface 18</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> 9</span>
<span className="text-muted">Jump to last surface</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> W</span>
<span className="text-muted">Close surface</span>
</li>
</ul>
</div>
{/* Split Panes */}
<div>
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Split Panes</h3>
<ul className="space-y-3">
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> D</span>
<span className="text-muted">Split right</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> D</span>
<span className="text-muted">Split down</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> </span>
<span className="text-muted">Focus pane directionally</span>
</li>
</ul>
</div>
{/* Browser */}
<div>
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Browser</h3>
<ul className="space-y-3">
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> B</span>
<span className="text-muted">Open browser in split</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> L</span>
<span className="text-muted">Focus address bar</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> ]</span>
<span className="text-muted">Forward</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> R</span>
<span className="text-muted">Reload page</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> I</span>
<span className="text-muted">Open Developer Tools</span>
</li>
</ul>
</div>
{/* Notifications */}
<div>
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Notifications</h3>
<ul className="space-y-3">
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> I</span>
<span className="text-muted">Show notifications panel</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> U</span>
<span className="text-muted">Jump to latest unread</span>
</li>
</ul>
</div>
{/* Find */}
<div>
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Find</h3>
<ul className="space-y-3">
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> F</span>
<span className="text-muted">Find</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> G &nbsp;/&nbsp; G</span>
<span className="text-muted">Find next / previous</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> F</span>
<span className="text-muted">Hide find bar</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> E</span>
<span className="text-muted">Use selection for find</span>
</li>
</ul>
</div>
{/* Terminal */}
<div>
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Terminal</h3>
<ul className="space-y-3">
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> K</span>
<span className="text-muted">Clear scrollback</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> C</span>
<span className="text-muted">Copy (with selection)</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> V</span>
<span className="text-muted">Paste</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> + &nbsp;/&nbsp; -</span>
<span className="text-muted">Increase / decrease font size</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> 0</span>
<span className="text-muted">Reset font size</span>
</li>
</ul>
</div>
{/* Window */}
<div>
<h3 className="text-[11px] uppercase tracking-widest text-muted/60 mb-3">Window</h3>
<ul className="space-y-3">
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> N</span>
<span className="text-muted">New window</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> ,</span>
<span className="text-muted">Settings</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> R</span>
<span className="text-muted">Reload configuration</span>
</li>
<li className="flex items-baseline justify-between">
<span className="font-mono text-[13px]"> Q</span>
<span className="text-muted">Quit</span>
</li>
</ul>
</div>
</div>
</section>
<span className="inline-flex items-center">
{combo.map((k, idx) => (
<span key={`${k}-${idx}`} className="inline-flex items-center">
<kbd>{k}</kbd>
{idx < combo.length - 1 && (
<span className="text-muted/30 text-[10px] mx-[3px] select-none font-mono">
+
</span>
)}
</span>
))}
</span>
);
}
function ShortcutRow({ shortcut }: { shortcut: Shortcut }) {
return (
<div className="flex items-center justify-between gap-4 py-[11px] px-4 hover:bg-foreground/[0.025] transition-colors">
<div className="min-w-0">
<span className="text-[14px] text-foreground/90">
{shortcut.description}
</span>
{shortcut.note && (
<span className="text-[12px] text-muted/50 ml-2">
{shortcut.note}
</span>
)}
</div>
<div className="flex items-center gap-3 shrink-0">
{shortcut.combos.map((combo, idx) => (
<span
key={`${shortcut.id}-combo-${idx}`}
className="inline-flex items-center"
>
{idx > 0 && (
<span className="text-muted/30 text-[11px] select-none mr-3 font-mono">
/
</span>
)}
<KeyCombo combo={combo} />
</span>
))}
</div>
</div>
);
}
export function KeyboardShortcuts() {
const [query, setQuery] = useState("");
const filtered = useMemo(() => {
const q = normalize(query);
if (!q) return CATEGORIES;
return CATEGORIES.map((cat) => ({
...cat,
shortcuts: cat.shortcuts.filter((s) =>
shortcutSearchText(cat, s).includes(q),
),
})).filter((cat) => cat.shortcuts.length > 0);
}, [query]);
return (
<div className="mt-2 mb-12">
{/* Search */}
<div className="relative mb-8">
<div className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-muted/40">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.3-4.3" />
</svg>
</div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search shortcuts..."
className="w-full pl-9 pr-3 py-1.5 rounded-lg border border-border bg-transparent text-[13px] placeholder:text-muted/40 focus:outline-none focus:border-foreground/20 transition-colors"
aria-label="Search keyboard shortcuts"
/>
</div>
{/* Category jump links */}
{!query && (
<nav className="flex flex-wrap items-center gap-y-2 mb-10">
{CATEGORIES.map((cat, idx) => (
<span key={cat.id} className="inline-flex items-center">
<a
href={`#${cat.id}`}
className="text-[13px] text-muted hover:text-foreground transition-colors"
>
{cat.title}
</a>
{idx < CATEGORIES.length - 1 && (
<span className="text-border mx-2.5 text-[10px] select-none">
·
</span>
)}
</span>
))}
</nav>
)}
{/* Content */}
{filtered.length === 0 ? (
<div className="py-16 text-center">
<p className="text-[14px] text-muted/70">No shortcuts found</p>
<p className="text-[13px] text-muted/40 mt-1.5">
Try a different search term
</p>
</div>
) : (
<div className="space-y-10">
{filtered.map((cat) => (
<section key={cat.id} id={cat.id} className="scroll-mt-20">
<div className="mb-3">
<div className="text-[13px] font-medium text-muted/60">
{cat.title}
</div>
{cat.blurb && (
<p className="text-[13px] text-muted/50 mt-1">{cat.blurb}</p>
)}
</div>
<div className="rounded-xl border border-border overflow-hidden">
<div className="divide-y divide-border/60">
{cat.shortcuts.map((s) => (
<ShortcutRow key={s.id} shortcut={s} />
))}
</div>
</div>
</section>
))}
</div>
)}
</div>
);
}

View file

@ -1,6 +1,9 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Providers } from "./providers";
import { ThemeToggle } from "./theme";
import { DevPanel } from "./components/spacing-control";
import { SiteFooter } from "./components/nav-links";
import "./globals.css";
const geistSans = Geist({
@ -53,7 +56,14 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
>
<Providers>{children}</Providers>
<Providers>
<div className="fixed top-2 right-4 z-50">
<ThemeToggle />
</div>
{children}
<SiteFooter />
<DevPanel />
</Providers>
</body>
</html>
);

View file

@ -1,19 +1,17 @@
import Image from "next/image";
import Balancer from "react-wrap-balancer";
import { TypingTagline } from "./typing";
import { ThemeToggle } from "./theme";
import { DownloadButton } from "./components/download-button";
import { SiteHeader } from "./components/site-header";
export default function Home() {
return (
<div className="flex min-h-screen justify-center px-6 py-20 sm:py-32">
{/* Theme toggle */}
<div className="fixed top-5 right-5">
<ThemeToggle />
</div>
<div className="min-h-screen">
<SiteHeader hideLogo />
<main className="w-full max-w-xl">
<main className="w-full max-w-2xl mx-auto px-6 py-16 sm:py-24">
{/* Header */}
<div className="flex items-center gap-4 mb-10">
<div className="flex items-center gap-4 mb-10" data-dev="header">
<Image
src="/icon.png"
alt="cmux icon"
@ -27,25 +25,19 @@ export default function Home() {
{/* Tagline */}
<p className="text-lg leading-relaxed mb-3 text-foreground">
A terminal built for <TypingTagline />
The terminal built for <TypingTagline />
</p>
<p className="text-base leading-relaxed text-muted mb-12">
Native macOS app built on Ghostty. Vertical tabs, notification rings
when agents need attention, split panes, and a socket API for
automation.
<p className="text-base text-muted" data-dev="subtitle" style={{ lineHeight: 1.5 }}>
<Balancer>
Native macOS app built on Ghostty. Vertical tabs, notification rings
when agents need attention, split panes, and a socket API for
automation.
</Balancer>
</p>
{/* Download */}
<div className="mb-12 flex items-center gap-3">
<a
href="https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg"
className="inline-flex items-center gap-2.5 rounded-full px-5 py-2.5 text-[15px] font-medium bg-foreground text-background hover:opacity-85 transition-opacity"
>
<svg width="16" height="19" viewBox="0 0 814 1000" fill="currentColor">
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57.8-155.5-127.4c-58.3-81.6-105.6-208.4-105.6-328.6 0-193 125.6-295.5 249.2-295.5 65.7 0 120.5 43.1 161.7 43.1 39.2 0 100.4-45.8 175.1-45.8 28.3 0 130.3 2.6 197.2 99.2zM554.1 159.4c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.9 32.4-57.2 83.6-57.2 135.4 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 137.6-71.2z" />
</svg>
Download for Mac
</a>
<div className="flex items-center gap-3" data-dev="download" style={{ marginTop: 21, marginBottom: 33 }}>
<DownloadButton />
<a
href="https://github.com/manaflow-ai/cmux"
target="_blank"
@ -60,11 +52,11 @@ export default function Home() {
</div>
{/* Features */}
<section className="mb-12">
<section data-dev="features">
<h2 className="text-xs font-medium text-muted tracking-tight mb-3">
Features
</h2>
<ul className="space-y-3 text-[15px] leading-relaxed">
<ul className="space-y-3 text-[15px]" data-dev="features-ul" style={{ lineHeight: 1.275 }}>
<li className="flex gap-3">
<span className="text-muted shrink-0">-</span>
<span>
@ -119,25 +111,21 @@ export default function Home() {
</span>
</span>
</li>
<li className="flex gap-3">
<span className="text-muted shrink-0">-</span>
<span>
<strong className="font-medium">Keyboard shortcuts</strong>
<span className="text-muted">
: <a href="/docs/keyboard-shortcuts" className="underline underline-offset-2 decoration-border hover:decoration-foreground transition-colors">extensive shortcuts</a> for workspaces, splits, browser, and more
</span>
</span>
</li>
</ul>
<div data-dev="features-spacer" style={{ height: 23 }} />
</section>
{/* Footer */}
<footer className="flex items-center gap-4 text-sm text-muted pt-4 border-t border-border">
<a
href="https://github.com/manaflow-ai/cmux"
className="hover:text-foreground transition-colors"
>
GitHub
</a>
<a
href="https://cmux.term.sh"
className="hover:text-foreground transition-colors"
>
Docs
</a>
</footer>
</main>
</div>
);
}

8
web/app/robots.ts Normal file
View file

@ -0,0 +1,8 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/" },
sitemap: "https://cmux.dev/sitemap.xml",
};
}

16
web/app/sitemap.ts Normal file
View file

@ -0,0 +1,16 @@
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const base = "https://cmux.dev";
return [
{ url: base, lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
{ url: `${base}/docs/getting-started`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.9 },
{ url: `${base}/docs/concepts`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: `${base}/docs/configuration`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: `${base}/docs/keyboard-shortcuts`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 },
{ url: `${base}/docs/api`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: `${base}/docs/notifications`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: `${base}/docs/changelog`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.5 },
];
}

View file

@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useDevValues } from "./components/spacing-control";
const phrases = [
"coding agents",
@ -15,21 +16,7 @@ export function TypingTagline() {
const [phraseIndex, setPhraseIndex] = useState(0);
const [charIndex, setCharIndex] = useState(0);
const [deleting, setDeleting] = useState(false);
const [showControls, setShowControls] = useState(false);
const [topOffset, setTopOffset] = useState(0);
const [blink, setBlink] = useState(true);
useEffect(() => {
if (process.env.NODE_ENV !== "development") return;
const handler = (e: KeyboardEvent) => {
if (e.key === "." && e.metaKey) {
e.preventDefault();
setShowControls((s) => !s);
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
const dev = useDevValues();
useEffect(() => {
const phrase = phrases[phraseIndex];
@ -57,49 +44,14 @@ export function TypingTagline() {
const phrase = phrases[phraseIndex];
const displayed = phrase.slice(0, charIndex);
const tailwindClass =
topOffset > 0
? `-top-[${topOffset}px]`
: topOffset < 0
? `top-[${Math.abs(topOffset)}px]`
: "";
return (
<span>
{displayed}
<span
className={`inline-block w-[2px] h-[1.1em] bg-foreground/70 ml-[1px] ${blink ? "animate-blink" : ""}`}
style={{ position: "relative", top: "2.5px" }}
onDoubleClick={() => setShowControls((s) => !s)}
className={`inline-block w-[2px] h-[1.1em] bg-foreground/70 ml-[1px] ${dev.cursorBlink ? "animate-blink" : ""}`}
style={{ position: "relative", top: `${dev.cursorTop}px` }}
/>
{showControls && (
<span className="fixed bottom-5 right-5 z-50 flex w-[420px] items-center gap-3 rounded-xl bg-[#222] px-4 py-3 font-mono text-xs text-white shadow-lg">
<label className="flex items-center gap-2">
top:
<input
type="range"
min={-5}
max={5}
step={0.5}
value={topOffset}
onChange={(e) => setTopOffset(parseFloat(e.target.value))}
className="w-24"
/>
<span className="w-12">{topOffset}px</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={blink}
onChange={(e) => setBlink(e.target.checked)}
/>
blink
</label>
<code className="select-all cursor-pointer rounded bg-[#333] px-2 py-0.5">
{tailwindClass || "0px"}
</code>
</span>
)}
</span>
);
}

View file

@ -9,6 +9,8 @@
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-wrap-balancer": "^1.1.1",
"shiki": "^3.22.0",
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -185,6 +187,20 @@
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="],
"@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="],
"@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="],
"@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
@ -221,16 +237,22 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
"@types/react": ["@types/react@19.2.13", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.55.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw=="],
@ -251,6 +273,8 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
"@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="],
@ -347,14 +371,22 @@
"caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
@ -379,8 +411,12 @@
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@ -509,10 +545,16 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@ -641,8 +683,20 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
@ -679,6 +733,10 @@
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
@ -707,6 +765,8 @@
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
@ -717,8 +777,16 @@
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-wrap-balancer": ["react-wrap-balancer@1.1.1", "", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18" } }, "sha512-AB+l7FPRWl6uZ28VcJ8skkwLn2+UC62bjiw8tQUrZPlEWDVnR9MG0lghyn7EyxuJSsFEpht4G+yh2WikEqQ/5Q=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
@ -753,6 +821,8 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
@ -763,6 +833,8 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
@ -779,6 +851,8 @@
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
@ -797,6 +871,8 @@
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
@ -821,12 +897,26 @@
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@ -847,6 +937,8 @@
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],

View file

@ -12,7 +12,9 @@
"next": "16.1.6",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"react-wrap-balancer": "^1.1.1",
"shiki": "^3.22.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",