Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
697f4ba
Add `cmux omo` command for OpenCode + oh-my-openagent integration
lawrencecchen Mar 25, 2026
60aca2a
Auto-install oh-my-opencode plugin when running cmux omo
lawrencecchen Mar 25, 2026
1b43d33
Use shadow config dir to avoid modifying user's opencode setup
lawrencecchen Mar 25, 2026
763d7b1
Add Agent Integrations docs section with Claude Code Teams and oh-my-…
lawrencecchen Mar 25, 2026
1025196
Remove uppercase from sidebar section headers
lawrencecchen Mar 25, 2026
ca5497e
Add more spacing above and below sidebar section headers
lawrencecchen Mar 25, 2026
b2a0bc7
Enable tmux mode in oh-my-opencode config, improve docs
lawrencecchen Mar 25, 2026
6b42f9f
Add terminal-notifier shim to route oh-my-openagent notifications to …
lawrencecchen Mar 25, 2026
45a0c37
Add pane geometry to tmux-compat for oh-my-openagent grid planning
lawrencecchen Mar 25, 2026
29135b4
Add socket tests for tmux-compat pane geometry
lawrencecchen Mar 25, 2026
44da195
Handle tmux -V in shim script directly (no socket needed)
lawrencecchen Mar 25, 2026
7d3b53f
Lower default tmux pane min widths for cmux omo
lawrencecchen Mar 25, 2026
2aa7f14
Merge origin/main into issue-2085-cmux-omo
lawrencecchen Mar 26, 2026
4622d66
Merge remote-tracking branch 'origin/main' into issue-2085-cmux-omo
lawrencecchen Mar 26, 2026
1804a31
Resolve merge conflicts with main (main-vertical layout, focus param)
lawrencecchen Mar 26, 2026
e5799e5
Implement select-layout equalize and resize-pane absolute width
lawrencecchen Mar 26, 2026
91deb44
Fix equalize to use proportional divider positions
lawrencecchen Mar 26, 2026
31c3c4d
Fix select-layout main-vertical to only equalize vertical splits
lawrencecchen Mar 26, 2026
714fe00
Re-equalize agent column after kill-pane
lawrencecchen Mar 26, 2026
5487d5e
Address PR review comments
lawrencecchen Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 336 additions & 0 deletions CLI/cmux.swift

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@
}
}
},
"cli.omo.usage": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Usage: cmux omo [opencode-args...]\n\nLaunch OpenCode with oh-my-openagent in a cmux-aware environment.\n\noh-my-openagent orchestrates multiple AI models as specialized agents in\nparallel. This command sets up a tmux shim so agent panes become native\ncmux splits with sidebar metadata and notifications.\n\nThis command:\n - sets a tmux-like environment so oh-my-openagent uses cmux splits\n - prepends a private tmux shim to PATH\n - forwards all remaining arguments to opencode\n\nThe tmux shim translates tmux window/pane commands into cmux workspace\nand split operations in the current cmux session.\n\nExamples:\n cmux omo\n cmux omo --continue\n cmux omo --model claude-sonnet-4-6"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "使い方: cmux omo [opencode-args...]\n\ncmux 対応の環境で OpenCode と oh-my-openagent を起動します。\n\noh-my-openagent は複数の AI モデルを専門エージェントとして並列に\nオーケストレーションします。このコマンドは tmux shim を設定し、\nエージェントのペインをネイティブの cmux split に変換します。\n\nこのコマンドは次を行います:\n - oh-my-openagent が cmux の split を使うよう tmux 風の環境を設定\n - 専用の tmux shim を PATH の先頭に追加\n - 残りの引数をそのまま opencode に渡す\n\ntmux shim は、tmux の window/pane コマンドを、現在の cmux セッション内の\nworkspace と split 操作に変換します。\n\n例:\n cmux omo\n cmux omo --continue\n cmux omo --model claude-sonnet-4-6"
}
}
}
},
"applescript.error.disabled": {
"extractionState": "manual",
"localizations": {
Expand Down
38 changes: 29 additions & 9 deletions web/app/[locale]/components/docs-nav-items.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
export const navItems = [
{ titleKey: "gettingStarted" as const, href: "/docs/getting-started" },
{ titleKey: "concepts" as const, href: "/docs/concepts" },
{ titleKey: "configuration" as const, href: "/docs/configuration" },
{ titleKey: "keyboardShortcuts" as const, href: "/docs/keyboard-shortcuts" },
{ titleKey: "apiReference" as const, href: "/docs/api" },
{ titleKey: "browserAutomation" as const, href: "/docs/browser-automation" },
{ titleKey: "notifications" as const, href: "/docs/notifications" },
{ titleKey: "changelog" as const, href: "/docs/changelog" },
export type NavLink = { titleKey: string; href: string };
export type NavSection = { sectionKey: string; children: NavLink[] };
export type NavEntry = NavLink | NavSection;

export function isSection(entry: NavEntry): entry is NavSection {
return "sectionKey" in entry;
}

/** Flatten sections into an ordered list of links (for pager prev/next). */
export function flatNavItems(entries: NavEntry[]): NavLink[] {
return entries.flatMap((e) => (isSection(e) ? e.children : [e]));
}

export const navItems: NavEntry[] = [
{ titleKey: "gettingStarted", href: "/docs/getting-started" },
{ titleKey: "concepts", href: "/docs/concepts" },
{ titleKey: "configuration", href: "/docs/configuration" },
{ titleKey: "keyboardShortcuts", href: "/docs/keyboard-shortcuts" },
{ titleKey: "apiReference", href: "/docs/api" },
{ titleKey: "browserAutomation", href: "/docs/browser-automation" },
{ titleKey: "notifications", href: "/docs/notifications" },
{
sectionKey: "agentIntegrations",
children: [
{ titleKey: "claudeCodeTeams", href: "/docs/agent-integrations/claude-code-teams" },
{ titleKey: "ohMyOpenCode", href: "/docs/agent-integrations/oh-my-opencode" },
],
},
{ titleKey: "changelog", href: "/docs/changelog" },
];
9 changes: 5 additions & 4 deletions web/app/[locale]/components/docs-pager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import { useTranslations } from "next-intl";
import { Link, usePathname } from "../../../i18n/navigation";
import { navItems } from "./docs-nav-items";
import { navItems, flatNavItems } from "./docs-nav-items";

export function DocsPager() {
const pathname = usePathname();
const t = useTranslations("docs.navItems");
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;
const flat = flatNavItems(navItems);
const index = flat.findIndex((item) => item.href === pathname);
const prev = index > 0 ? flat[index - 1] : null;
const next = index < flat.length - 1 ? flat[index + 1] : null;
Comment on lines +11 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard the -1 index case before computing neighbors.

Line 11 can return -1; then Line 13 incorrectly sets next to the first doc item. Add an early return when the path is not in the flattened nav.

Suggested fix
   const flat = flatNavItems(navItems);
   const index = flat.findIndex((item) => item.href === pathname);
+  if (index === -1) return null;
   const prev = index > 0 ? flat[index - 1] : null;
   const next = index < flat.length - 1 ? flat[index + 1] : null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const index = flat.findIndex((item) => item.href === pathname);
const prev = index > 0 ? flat[index - 1] : null;
const next = index < flat.length - 1 ? flat[index + 1] : null;
const flat = flatNavItems(navItems);
const index = flat.findIndex((item) => item.href === pathname);
if (index === -1) return null;
const prev = index > 0 ? flat[index - 1] : null;
const next = index < flat.length - 1 ? flat[index + 1] : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/components/docs-pager.tsx around lines 11 - 13, The code
computes index = flat.findIndex(...) which can be -1; update the logic in the
docs-pager component to guard that case before computing prev/next: after
computing index, if index === -1 return early (or set both prev and next to
null) so you don't treat flat[-1] as a valid neighbor; adjust the component's
return path accordingly to avoid rendering incorrect links when pathname is not
found.


if (!prev && !next) return null;

Expand Down
74 changes: 59 additions & 15 deletions web/app/[locale]/components/docs-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,73 @@

import { useTranslations } from "next-intl";
import { Link, usePathname } from "../../../i18n/navigation";
import { navItems } from "./docs-nav-items";
import { navItems, isSection, type NavLink } from "./docs-nav-items";

function SidebarLink({
item,
pathname,
onNavigate,
indent,
t,
}: {
item: NavLink;
pathname: string;
onNavigate?: () => void;
indent?: boolean;
t: (key: string) => string;
}) {
const active = pathname === item.href;
return (
<Link
href={item.href}
onClick={onNavigate}
className={`block py-1.5 text-[14px] rounded-md transition-colors ${
indent ? "px-5" : "px-3"
} ${
active
? "text-foreground font-medium bg-code-bg"
: "text-muted hover:text-foreground"
}`}
>
{t(item.titleKey)}
</Link>
);
}

export function DocsSidebar({ onNavigate }: { onNavigate?: () => void }) {
const pathname = usePathname();
const t = useTranslations("docs.navItems");

return (
<nav className="space-y-0.5">
{navItems.map((item) => {
const active = pathname === item.href;
{navItems.map((entry) => {
if (isSection(entry)) {
return (
<div key={entry.sectionKey} className="pt-5 pb-2 first:pt-0">
<div className="px-3 pb-1 text-[12px] font-medium text-muted tracking-wider">
{t(entry.sectionKey)}
</div>
{entry.children.map((child) => (
<SidebarLink
key={child.href}
item={child}
pathname={pathname}
onNavigate={onNavigate}
indent
t={t}
/>
))}
</div>
);
}
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"
}`}
>
{t(item.titleKey)}
</Link>
<SidebarLink
key={entry.href}
item={entry}
pathname={pathname}
onNavigate={onNavigate}
t={t}
/>
);
})}
</nav>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { CodeBlock } from "../../../components/code-block";
import { Callout } from "../../../components/callout";
import { Link } from "../../../../../i18n/navigation";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "docs.claudeCodeTeams" });
return {
title: t("metaTitle"),
description: t("metaDescription"),
};
}

export default function ClaudeCodeTeamsPage() {
const t = useTranslations("docs.claudeCodeTeams");

return (
<>
<h1>{t("title")}</h1>

<Callout type="warn">
{t.rich("nightlyWarning", {
nightly: (chunks) => <Link href="/nightly" className="underline">{chunks}</Link>,
})}
</Callout>

<p>{t("intro")}</p>

<h2>{t("usage")}</h2>
<CodeBlock lang="bash">{`cmux claude-teams
cmux claude-teams --continue
cmux claude-teams --model sonnet`}</CodeBlock>
<p>{t("usageDesc")}</p>

<h2>{t("howItWorks")}</h2>
<p>{t("howItWorksDesc")}</p>
<ul>
<li>{t("shimStep1")}</li>
<li>{t("shimStep2")}</li>
<li>{t("shimStep3")}</li>
<li>{t("shimStep4")}</li>
</ul>

<h2>{t("envVars")}</h2>
<table>
<thead>
<tr>
<th>{t("envVarName")}</th>
<th>{t("envVarPurpose")}</th>
</tr>
</thead>
<tbody>
<tr><td><code>TMUX</code></td><td>{t("envTmux")}</td></tr>
<tr><td><code>TMUX_PANE</code></td><td>{t("envTmuxPane")}</td></tr>
<tr><td><code>CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS</code></td><td>{t("envTeams")}</td></tr>
<tr><td><code>CMUX_SOCKET_PATH</code></td><td>{t("envSocket")}</td></tr>
</tbody>
</table>

<h2>{t("directories")}</h2>
<table>
<thead>
<tr>
<th>{t("dirPath")}</th>
<th>{t("dirPurpose")}</th>
</tr>
</thead>
<tbody>
<tr><td><code>~/.cmuxterm/claude-teams-bin/</code></td><td>{t("dirShim")}</td></tr>
<tr><td><code>~/.cmuxterm/tmux-compat-store.json</code></td><td>{t("dirStore")}</td></tr>
</tbody>
</table>

<h2>{t("tmuxCommands")}</h2>
<p>{t("tmuxCommandsDesc")}</p>
<ul>
<li><code>new-session</code>, <code>new-window</code> &rarr; {t("mapWorkspace")}</li>
<li><code>split-window</code> &rarr; {t("mapSplit")}</li>
<li><code>send-keys</code> &rarr; {t("mapSendText")}</li>
<li><code>capture-pane</code> &rarr; {t("mapReadText")}</li>
<li><code>select-pane</code>, <code>select-window</code> &rarr; {t("mapFocus")}</li>
<li><code>kill-pane</code>, <code>kill-window</code> &rarr; {t("mapClose")}</li>
<li><code>list-panes</code>, <code>list-windows</code> &rarr; {t("mapList")}</li>
</ul>
</>
);
}
108 changes: 108 additions & 0 deletions web/app/[locale]/docs/agent-integrations/oh-my-opencode/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { CodeBlock } from "../../../components/code-block";
import { Callout } from "../../../components/callout";
import { Link } from "../../../../../i18n/navigation";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "docs.ohMyOpenCode" });
return {
title: t("metaTitle"),
description: t("metaDescription"),
};
}

export default function OhMyOpenCodePage() {
const t = useTranslations("docs.ohMyOpenCode");

return (
<>
<h1>{t("title")}</h1>

<Callout type="warn">
{t.rich("nightlyWarning", {
nightly: (chunks) => <Link href="/nightly" className="underline">{chunks}</Link>,
})}
</Callout>

<p>{t("intro")}</p>

<h2>{t("usage")}</h2>
<CodeBlock lang="bash">{`cmux omo
cmux omo --continue
cmux omo --model claude-sonnet-4-6`}</CodeBlock>
<p>{t("usageDesc")}</p>

<h2>{t("whatYouGet")}</h2>
<p>{t("whatYouGetDesc")}</p>
<ul>
<li>{t("whatYouGet1")}</li>
<li>{t("whatYouGet2")}</li>
<li>{t("whatYouGet3")}</li>
<li>{t("whatYouGet4")}</li>
<li>{t("whatYouGet5")}</li>
</ul>

<h2>{t("firstRun")}</h2>
<p>{t("firstRunDesc")}</p>
<ol>
<li>{t("firstRunStep1")}</li>
<li>{t("firstRunStep2")}</li>
<li>{t("firstRunStep3")}</li>
<li>{t("firstRunStep4")}</li>
</ol>
<p>{t("firstRunSafe")}</p>

<h2>{t("howItWorks")}</h2>
<p>{t("howItWorksDesc")}</p>
<ul>
<li>{t("shimStep1")}</li>
<li>{t("shimStep2")}</li>
<li>{t("shimStep3")}</li>
<li>{t("shimStep4")}</li>
<li>{t("shimStep5")}</li>
</ul>

<h2>{t("directories")}</h2>
<table>
<thead>
<tr>
<th>{t("dirPath")}</th>
<th>{t("dirPurpose")}</th>
</tr>
</thead>
<tbody>
<tr><td><code>~/.cmuxterm/omo-bin/</code></td><td>{t("dirShim")}</td></tr>
<tr><td><code>~/.cmuxterm/omo-config/</code></td><td>{t("dirShadow")}</td></tr>
<tr><td><code>~/.cmuxterm/tmux-compat-store.json</code></td><td>{t("dirStore")}</td></tr>
</tbody>
</table>

<h2>{t("shadowConfig")}</h2>
<p>{t("shadowConfigDesc")}</p>
<ul>
<li>{t("shadowStep1")}</li>
<li>{t("shadowStep2")}</li>
<li>{t("shadowStep3")}</li>
<li>{t("shadowStep4")}</li>
</ul>

<h2>{t("envVars")}</h2>
<table>
<thead>
<tr>
<th>{t("envVarName")}</th>
<th>{t("envVarPurpose")}</th>
</tr>
</thead>
<tbody>
<tr><td><code>TMUX</code></td><td>{t("envTmux")}</td></tr>
<tr><td><code>TMUX_PANE</code></td><td>{t("envTmuxPane")}</td></tr>
<tr><td><code>OPENCODE_CONFIG_DIR</code></td><td>{t("envConfigDir")}</td></tr>
<tr><td><code>CMUX_SOCKET_PATH</code></td><td>{t("envSocket")}</td></tr>
</tbody>
</table>
</>
);
}
3 changes: 3 additions & 0 deletions web/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@
"apiReference": "مرجع الواجهة البرمجية",
"browserAutomation": "أتمتة المتصفح",
"notifications": "الإشعارات",
"agentIntegrations": "Agent Integrations",
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: agentIntegrations is left untranslated in the Arabic locale, causing mixed-language navigation text.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/messages/ar.json, line 554:

<comment>`agentIntegrations` is left untranslated in the Arabic locale, causing mixed-language navigation text.</comment>

<file context>
@@ -551,6 +551,9 @@
       "apiReference": "مرجع الواجهة البرمجية",
       "browserAutomation": "أتمتة المتصفح",
       "notifications": "الإشعارات",
+      "agentIntegrations": "Agent Integrations",
+      "claudeCodeTeams": "Claude Code Teams",
+      "ohMyOpenCode": "oh-my-opencode",
</file context>
Suggested change
"agentIntegrations": "Agent Integrations",
"agentIntegrations": "تكاملات الوكلاء",
Fix with Cubic

"claudeCodeTeams": "Claude Code Teams",
"ohMyOpenCode": "oh-my-opencode",
"changelog": "سجل التغييرات"
}
},
Expand Down
3 changes: 3 additions & 0 deletions web/messages/bs.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@
"apiReference": "API Referenca",
"browserAutomation": "Automatizacija preglednika",
"notifications": "Notifikacije",
"agentIntegrations": "Agent Integrations",
"claudeCodeTeams": "Claude Code Teams",
"ohMyOpenCode": "oh-my-opencode",
"changelog": "Zapisnik promjena"
}
},
Expand Down
Loading