diff --git a/.gitignore b/.gitignore index ced54ef5..071e93da 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ zig-out/ # Node node_modules/ +.next/ # Test outputs tests/visual_output/ diff --git a/web/app/assets/images.d.ts b/web/app/assets/images.d.ts new file mode 100644 index 00000000..9f3bf192 --- /dev/null +++ b/web/app/assets/images.d.ts @@ -0,0 +1,5 @@ +declare module "*.png" { + import type { StaticImageData } from "next/image"; + const content: StaticImageData; + export default content; +} diff --git a/web/app/assets/landing-image.png b/web/app/assets/landing-image.png new file mode 100644 index 00000000..3b03077b Binary files /dev/null and b/web/app/assets/landing-image.png differ diff --git a/web/app/blog/introducing-cmux/page.tsx b/web/app/blog/introducing-cmux/page.tsx index 1632591c..474f1c7f 100644 --- a/web/app/blog/introducing-cmux/page.tsx +++ b/web/app/blog/introducing-cmux/page.tsx @@ -5,6 +5,35 @@ 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.", + keywords: [ + "cmux", + "terminal", + "macOS", + "Ghostty", + "libghostty", + "AI coding agents", + "Claude Code", + "vertical tabs", + "split panes", + "socket API", + ], + openGraph: { + title: "Introducing cmux", + description: + "A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.", + type: "article", + publishedTime: "2026-02-12T00:00:00Z", + url: "https://cmux.dev/blog/introducing-cmux", + }, + twitter: { + card: "summary", + title: "Introducing cmux", + description: + "A native macOS terminal built on Ghostty, designed for running multiple AI coding agents side by side.", + }, + alternates: { + canonical: "https://cmux.dev/blog/introducing-cmux", + }, }; export default function IntroducingCmuxPage() { @@ -20,7 +49,7 @@ export default function IntroducingCmuxPage() {

Introducing cmux

- +

cmux is a native macOS terminal application built on top of Ghostty, @@ -31,7 +60,7 @@ export default function IntroducingCmuxPage() {

Why cmux?

Modern development workflows often involve running several agents at - once — Claude Code, Codex, and other tools each in their own + once. 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.

@@ -39,23 +68,23 @@ export default function IntroducingCmuxPage() {

Key features

diff --git a/web/app/blog/page.tsx b/web/app/blog/page.tsx index d9ad9c4a..d7707772 100644 --- a/web/app/blog/page.tsx +++ b/web/app/blog/page.tsx @@ -7,6 +7,13 @@ export const metadata: Metadata = { }; const posts = [ + { + slug: "show-hn-launch", + title: "Launching cmux on Show HN", + date: "2026-02-21", + summary: + "cmux hit the front page, went viral in Japan, and shipped 18 releases in 48 hours.", + }, { slug: "introducing-cmux", title: "Introducing cmux", diff --git a/web/app/blog/show-hn-launch/page.tsx b/web/app/blog/show-hn-launch/page.tsx new file mode 100644 index 00000000..0800297b --- /dev/null +++ b/web/app/blog/show-hn-launch/page.tsx @@ -0,0 +1,212 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { Tweet } from "react-tweet"; +import { DownloadButton } from "../../components/download-button"; +import { GitHubButton } from "../../components/github-button"; +import starHistory from "./star-history.png"; + +export const metadata: Metadata = { + title: "Launching cmux on Show HN", + description: + "cmux launched on Hacker News, hit #2, went viral in Japan, and people started building extensions on the CLI. Here's what happened.", + keywords: [ + "cmux", + "Show HN", + "Hacker News", + "terminal", + "macOS", + "Ghostty", + "libghostty", + "AI coding agents", + "Claude Code", + "Codex", + "launch", + "vertical tabs", + "notification rings", + ], + openGraph: { + title: "Launching cmux on Show HN", + description: + "cmux launched on Hacker News, hit #2, went viral in Japan, and people started building extensions on the CLI.", + type: "article", + publishedTime: "2026-02-21T00:00:00Z", + url: "https://cmux.dev/blog/show-hn-launch", + }, + twitter: { + card: "summary", + title: "Launching cmux on Show HN", + description: + "cmux launched on Hacker News, hit #2, went viral in Japan, and people started building extensions on the CLI.", + }, + alternates: { + canonical: "https://cmux.dev/blog/show-hn-launch", + }, +}; + +export default function ShowHNLaunchPage() { + return ( + <> +
+ + ← Back to blog + +
+ +

Launching cmux on Show HN

+ + +

+ We posted cmux on{" "} + Show HN{" "} + on Feb 19: +

+ +
+

+ I run a lot of Claude Code and Codex sessions in parallel. I was using + Ghostty with a bunch of split panes, and relying on native macOS + notifications to know when an agent needed me. But Claude Code's + notification body is always just "Claude is waiting for your + input" with no context, and with enough tabs open, I couldn't + even read the titles anymore. +

+

+ I tried a few coding orchestrators but most of them were Electron/Tauri + apps and the performance bugged me. I also just prefer the terminal + since GUI orchestrators lock you into their workflow. So I built cmux as + a native macOS app in Swift/AppKit. It uses libghostty for terminal + rendering and reads your existing Ghostty config for themes, fonts, + colors, and more. +

+

+ The main additions are the sidebar and notification system. The sidebar + has vertical tabs that show git branch, working directory, listening + ports, and the latest notification text for each workspace. The + notification system picks up terminal sequences (OSC 9/99/777) and has a + CLI (cmux notify) you can wire into agent hooks for Claude Code, + OpenCode, etc. When an agent is waiting, its pane gets a blue ring and + the tab lights up in the sidebar, so I can tell which one needs me + across splits and tabs. Cmd+Shift+U jumps to the most recent unread. +

+

+ The in-app browser has a scriptable API. Agents can snapshot the + accessibility tree, get element refs, click, fill forms, evaluate JS, + and read console logs. You can split a browser pane next to your + terminal and have Claude Code interact with your dev server directly. +

+

+ Everything is scriptable through the CLI and socket API: create + workspaces/tabs, split panes, send keystrokes, open URLs in the browser. +

+
+ +

+ At peak it hit #2 on Hacker News. Mitchell Hashimoto shared it: +

+ + + +

+ My favorite comment from the{" "} + HN thread: +

+ +
+

+ Hey, this looks seriously awesome. Love the ideas here, specifically: + the programmability (I haven't tried it yet, but had been + considering learning tmux partly for this), layered UI, browser w/ + api. Looking forward to giving this a spin. Also want to add that I + really appreciate Mitchell Hashimoto creating libghostty; it feels + like an exciting time to be a terminal user. +

+

Some feedback (since you were asking for it elsewhere in the thread!):

+ +

+ —{" "} + + johnthedebs + +

+
+ +

+ Surprisingly, cmux went semi-viral in Japan! +

+ + + +

+ Translation: "This looks good. A Ghostty-based terminal app + designed so you don't get lost running multiple CLIs like Claude + Code in parallel. The waiting-for-input panel gets a blue frame, and + it has its own notification system." +

+ +

+ Another exciting thing was seeing people build on top of the cmux + CLI. sasha built a pi-cmux extension that shows model info, token + usage, and agent state in the sidebar: +

+ + + +

+ Everything in cmux is scriptable through the CLI: creating workspaces, + sending keystrokes, controlling the browser, reading notifications. + Part of the cmux philosophy is being programmable and composable, so + people can customize the way they work with coding agents. The + state of the art for coding agents is changing fast, and you don't + want to be locked into an inflexible GUI orchestrator that can't + keep up. +

+ +

+ If you're running multiple coding agents,{" "} + give cmux a try. +

+ +
+ cmux GitHub star history showing growth from near 0 to 900+ stars after the Show HN launch +
+ +
+ + +
+ + ); +} diff --git a/web/app/blog/show-hn-launch/star-history.png b/web/app/blog/show-hn-launch/star-history.png new file mode 100644 index 00000000..4b7cb5fa Binary files /dev/null and b/web/app/blog/show-hn-launch/star-history.png differ diff --git a/web/app/components/nav-links.tsx b/web/app/components/nav-links.tsx index bcf4c9b3..1a533f0b 100644 --- a/web/app/components/nav-links.tsx +++ b/web/app/components/nav-links.tsx @@ -43,26 +43,3 @@ export function NavLinks() { ); } -export function SiteFooter() { - return ( - - ); -} diff --git a/web/app/components/site-footer.tsx b/web/app/components/site-footer.tsx new file mode 100644 index 00000000..af247c76 --- /dev/null +++ b/web/app/components/site-footer.tsx @@ -0,0 +1,85 @@ +import Link from "next/link"; + +const columns = [ + { + heading: "Product", + links: [ + { label: "Blog", href: "/blog" }, + { label: "Community", href: "/community" }, + ], + }, + { + heading: "Resources", + links: [ + { label: "Docs", href: "/docs/getting-started" }, + { label: "Changelog", href: "/docs/changelog" }, + ], + }, + { + heading: "Legal", + links: [ + { label: "Privacy", href: "/privacy-policy" }, + { label: "Terms", href: "/terms-of-service" }, + { label: "EULA", href: "/eula" }, + ], + }, + { + heading: "Social", + links: [ + { label: "GitHub", href: "https://github.com/manaflow-ai/cmux" }, + { label: "X / Twitter", href: "https://twitter.com/manaflowai" }, + { label: "Discord", href: "https://discord.gg/xsgFEVrWCZ" }, + { label: "Contact", href: "mailto:founders@manaflow.com" }, + ], + }, +]; + +function isExternal(href: string) { + return href.startsWith("http") || href.startsWith("mailto:"); +} + +export function SiteFooter() { + const year = new Date().getFullYear(); + + return ( +
+
+
+ {columns.map((col) => ( +
+

+ {col.heading} +

+
    + {col.links.map((link) => ( +
  • + {isExternal(link.href) ? ( + + {link.label} + + ) : ( + + {link.label} + + )} +
  • + ))} +
+
+ ))} +
+

+ © {year} Manaflow +

+
+
+ ); +} diff --git a/web/app/components/spacing-control.tsx b/web/app/components/spacing-control.tsx index 0a525617..c995715b 100644 --- a/web/app/components/spacing-control.tsx +++ b/web/app/components/spacing-control.tsx @@ -10,7 +10,10 @@ type DevValues = { downloadAbove: number; downloadBelow: number; featuresLh: number; - featuresMb: number; + featuresPt: number; + featuresPb: number; + communityGap: number; + faqPt: number; docsPt: number; }; @@ -20,9 +23,12 @@ const defaults: DevValues = { cursorBlink: true, subtitleLh: 1.5, downloadAbove: 21, - downloadBelow: 33, + downloadBelow: 16, featuresLh: 1.275, - featuresMb: 23, + featuresPt: 12, + featuresPb: 15, + communityGap: 6, + faqPt: 0, docsPt: 8, }; @@ -67,8 +73,21 @@ function applyToDOM(v: DevValues) { 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 features = el("features"); + if (features) { + features.style.paddingTop = `${v.featuresPt}px`; + features.style.paddingBottom = `${v.featuresPb}px`; + } + + const communityUl = el("community-ul"); + if (communityUl) { + communityUl.style.display = "flex"; + communityUl.style.flexDirection = "column"; + communityUl.style.gap = `${v.communityGap}px`; + } + + const faqTopSpacer = el("faq-top-spacer"); + if (faqTopSpacer) faqTopSpacer.style.height = `${v.faqPt}px`; const docsContent = el("docs-content"); if (docsContent) docsContent.style.paddingTop = `${v.docsPt}px`; @@ -156,7 +175,16 @@ export function DevPanel() {
update({ featuresLh: v })} min={1} max={2.5} step={0.025} unit="" w={16} /> - update({ featuresMb: v })} /> + update({ featuresPt: v })} /> + update({ featuresPb: v })} /> +
+ +
+ update({ communityGap: v })} /> +
+ +
+ update({ faqPt: v })} />
@@ -173,7 +201,10 @@ export function DevPanel() { `download-above: ${vals.downloadAbove}px`, `download-below: ${vals.downloadBelow}px`, `features-lh: ${vals.featuresLh}`, - `features-mb: ${vals.featuresMb}px`, + `features-pt: ${vals.featuresPt}px`, + `features-pb: ${vals.featuresPb}px`, + `community-gap: ${vals.communityGap}px`, + `faq-pt: ${vals.faqPt}px`, `docs-pt: ${vals.docsPt}px`, ].join(", "); navigator.clipboard.writeText(text); diff --git a/web/app/layout.tsx b/web/app/layout.tsx index a67526c3..9d184d01 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -3,7 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import { Providers } from "./providers"; import { DevPanel } from "./components/spacing-control"; -import { SiteFooter } from "./components/nav-links"; +import { SiteFooter } from "./components/site-footer"; import "./globals.css"; const geistSans = Geist({ @@ -76,6 +76,10 @@ export default function RootLayout({ +