diff --git a/.agents/skills/cmux-markdown/SKILL.md b/.agents/skills/cmux-markdown/SKILL.md new file mode 100644 index 000000000..eb6e070c2 --- /dev/null +++ b/.agents/skills/cmux-markdown/SKILL.md @@ -0,0 +1,43 @@ +--- +name: cmux-markdown +description: Open markdown files in a formatted viewer panel with live reload. Use when you need to display plans, documentation, or notes alongside the terminal with rich rendering (headings, code blocks, tables, lists). +--- + +# Markdown Viewer with cmux + +Write a `.md` file, open it in a panel, and the panel re-renders whenever the file changes on disk. Use it for agent plans and task lists alongside the terminal, documentation and changelogs while working, and notes another process updates progressively. + +```bash +cmux markdown open plan.md # split next to the current terminal +cmux markdown open /path/to/PLAN.md +cmux markdown open design.md --workspace workspace:2 # also --surface, --window +``` + +Relative paths resolve against the caller's cwd and `~` expands; the resolved absolute path comes back in the output. + +## Agent usage + +Write the full plan file first, then open it, so the panel never shows a partially written file. After that, overwrite or append freely: each write triggers a re-render, and atomic replacement (editor saves, `sed -i`, VS Code) is handled. + +To instruct coding agents in a project, add to its `AGENTS.md`: + +```markdown +## Plan Display + +When creating a plan or task list, write it to a `.md` file and open it in cmux: + + cmux markdown open plan.md + +The panel renders markdown with rich formatting and auto-updates when the file changes. +``` + +## Rendering + +Headings h1-h6 (dividers on h1/h2), fenced code blocks in monospace, inline code with a highlighted background, tables with alternating row colors, nested ordered and unordered lists, blockquotes with a left border, bold/italic/strikethrough, clickable links, horizontal rules, and inline images. Light and dark mode both supported. + +## Deep-dive references + +| Reference | When to Use | +|-----------|-------------| +| [references/commands.md](references/commands.md) | Full command syntax, options, output shape, panel behavior | +| [references/live-reload.md](references/live-reload.md) | File watching, atomic writes, unavailable-file state, performance | diff --git a/.agents/skills/cmux-markdown/agents/openai.yaml b/.agents/skills/cmux-markdown/agents/openai.yaml new file mode 100644 index 000000000..0ce42fe42 --- /dev/null +++ b/.agents/skills/cmux-markdown/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "cmux Markdown Viewer" + short_description: "Open markdown files in a formatted panel with live reload alongside the terminal." + default_prompt: "Use this skill to display markdown plans, docs, or notes in a cmux panel: write to a .md file, run 'cmux markdown open ', and the panel auto-updates when the file changes." diff --git a/.agents/skills/cmux-markdown/references/commands.md b/.agents/skills/cmux-markdown/references/commands.md new file mode 100644 index 000000000..c738b2718 --- /dev/null +++ b/.agents/skills/cmux-markdown/references/commands.md @@ -0,0 +1,29 @@ +# Command Reference (cmux Markdown) + +```bash +cmux markdown open +cmux markdown # shorthand, "open" is implicit +cmux markdown --help +``` + +| Flag | Description | Default | +|------|-------------|---------| +| `--workspace ` | Target workspace | `$CMUX_WORKSPACE_ID` | +| `--surface ` | Source surface to split from | Focused surface | +| `--window ` | Target window | Current window | + +## Output + +``` +OK surface=surface:8 pane=pane:3 path=/absolute/path/to/file.md +``` + +`--json` returns `window_id`, `workspace_id`, `pane_id`, `surface_id`, and `path`. + +## Panel behavior + +The panel opens as a horizontal split to the right of the source surface. The tab shows the filename and a document icon; the file path appears as a breadcrumb at the top. Content is read-only with text selection enabled. + +Markdown panels are saved and restored across sessions and re-read the file from disk on restore. A panel is not recreated if the file no longer exists at restore time. + +See also [live-reload.md](live-reload.md). diff --git a/.agents/skills/cmux-markdown/references/live-reload.md b/.agents/skills/cmux-markdown/references/live-reload.md new file mode 100644 index 000000000..158a49c8c --- /dev/null +++ b/.agents/skills/cmux-markdown/references/live-reload.md @@ -0,0 +1,19 @@ +# Live Reload Behavior + +The panel watches the file with a kernel-level watcher (`DispatchSource` with `O_EVTONLY`) for write, extend, delete, and rename events, and re-renders on change. + +## Supported write patterns + +Direct writes (`echo >>`), editor saves, atomic replace (write temp then rename), `sed -i`, VS Code/IDE saves, and progressive agent writes all work. Most of these are atomic replace under the hood. + +## Atomic file replacement + +An atomic replace shows up as a delete event followed by a new file at the same path. The panel detects it, re-reads immediately (in case the rename already landed), waits 500 ms and checks again if the file is missing, then reconnects the watcher to the new inode. + +## File unavailable state + +If the file is deleted and does not reappear within the retry window, the panel shows a "file unavailable" state with the original path and stays open until the user closes it. It does not reconnect if the file later reappears; close and reopen the panel. + +## Performance + +Re-reads are dispatched to the main thread and run synchronously, so files over ~100KB can cause brief UI hitches during re-render; split very large documents. The watcher itself runs on a low-priority background queue with negligible CPU impact. diff --git a/.agents/skills/cmux-settings/SKILL.md b/.agents/skills/cmux-settings/SKILL.md new file mode 100644 index 000000000..bd0d59793 --- /dev/null +++ b/.agents/skills/cmux-settings/SKILL.md @@ -0,0 +1,73 @@ +--- +name: cmux-settings +description: "View and edit cmux settings in ~/.config/cmux/cmux.json. Use when the user wants to change cmux preferences (appearance, sidebar, notifications, automation, browser, shortcuts), set a value by JSON path, validate the file, open it in an editor, or look up which keys cmux recognizes. Triggers on '/cmux-settings', 'change cmux setting', 'set in cmux', 'cmux config', 'cmux.json', or 'rebind a cmux shortcut'." +--- + +# cmux-settings + +cmux reads user settings from `~/.config/cmux/cmux.json` (JSONC). A file watcher applies changes on save, no restart. Legacy `~/.config/cmux/settings.json` is read only as a fallback for keys absent from `cmux.json`. + +Schema: `https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json`. The authoritative path list is `Sources/CmuxSettingsJSONPathSupport.swift`; the installed skill carries a generated copy in `references/all-keys.md`. Settings sections are `app`, `terminal`, `notifications`, `sidebar`, `sidebarAppearance`, `workspaceColors`, `automation`, `browser`, `shortcuts`. Non-settings sections (`actions`, `ui`, `commands`, `vault`, `rightSidebar`) share the same file. + +## Helper script + +Use the bundled helper for every read/write. It strips JSONC comments, writes atomically, and validates keys against the schema. + +```bash +skills/cmux-settings/scripts/cmux-settings # from a cmux checkout +~/.codex/skills/cmux-settings/scripts/cmux-settings # installed Codex skill +``` + +The rest of this doc assumes it is on `$PATH` as `cmux-settings`; from a checkout, `export PATH="$PWD/skills/cmux-settings/scripts:$PATH"`. + +| Command | What it does | +|---|---| +| `cmux-settings path` | Print the config path. | +| `cmux-settings dump` | Print the raw file (preserves comments). | +| `cmux-settings dump --no-comments` | Print the parsed JSON. | +| `cmux-settings get ` | Print value at dotted JSON path. | +| `cmux-settings set ` | Set value. `` is parsed as JSON (`true`, `42`, `"text"`, `[…]`, `{…}`); unquoted plain words are stored as strings. | +| `cmux-settings unset ` | Delete key, reverting to the in-app default. | +| `cmux-settings list-supported` | List every settings JSON path the app recognizes. | +| `cmux-settings validate` | Parse the file and flag unknown settings keys. | +| `cmux-settings open` | Open `cmux.json` in `$EDITOR`, VS Code, Cursor, or TextEdit. | + +`--file ` overrides the target file, useful for `--file ~/.config/cmux/settings.json`. + +## Workflow + +1. Look up the key when the user named a setting in plain English: + ```bash + cmux-settings list-supported | rg -i 'sidebar.*terminal|terminal.*sidebar' + ``` +2. Set it. JSON literals must be valid JSON. + ```bash + cmux-settings set sidebarAppearance.matchTerminalBackground true + cmux-settings set app.appearance dark + cmux-settings set shortcuts.bindings.newTab '["ctrl+b","c"]' + cmux-settings set browser.hostsToOpenInEmbeddedBrowser '["localhost","*.internal.example"]' + ``` +3. Read back and `cmux-settings validate`. +4. Tell the user it auto-reloaded, and that `cmux-settings unset ` reverts it. + +## Quick reference + +- Appearance: `app.appearance` (`"system" | "light" | "dark"`), `app.appIcon`, `app.menuBarOnly`, `app.minimalMode`. +- Sidebar tint: `sidebarAppearance.matchTerminalBackground`, `.tintColor`, `.tintOpacity` (0..1). +- Sidebar details: `sidebar.hideAllDetails`, `.showBranchDirectory`, `.showPullRequests`, `.showPorts`, `.showLog`. +- Notifications: `notifications.dockBadge`, `.sound` (enum including `"none"`, `"custom_file"`), `.customSoundFilePath`, `.hooks` (array). +- Browser: `browser.defaultSearchEngine`, `.theme`, `.openTerminalLinksInCmuxBrowser`, `.hostsToOpenInEmbeddedBrowser`. +- Automation: `automation.socketControlMode` (`off | cmuxOnly | automation | password | allowAll`), `.portBase`, `.portRange`. +- Shortcuts: `shortcuts.bindings.` = `"cmd+b"`, `["ctrl+b","c"]`, `null`, or `""` to unbind. Action ids in [references/shortcut-actions.md](references/shortcut-actions.md). + +Full list of settings, defaults, and descriptions: `cmux-settings list-supported` or [references/all-keys.md](references/all-keys.md). + +## Rules + +- Only edit `cmux.json`. Never `settings.json` unless the user explicitly asks; it is legacy and read only when a key is absent from `cmux.json`. +- Never tell the user to restart cmux. The file watcher reloads on save. +- Always `cmux-settings validate` after a bulk edit. Unknown keys mean the user pasted a key the app does not consume. +- Do not blindly overwrite `actions`, `ui`, `commands`, `vault`, or `rightSidebar`; they share the file and hold hand-tuned non-settings config. +- Shortcut action ids must match the schema enum. Look them up before binding. +- Colors are `#RRGGBB`; opacities are `0..1`. +- Translate app-level phrasing ("Settings > Notifications > Dock badge") to the JSON path first; `web/app/[locale]/(landing)/docs/configuration/page.tsx` mirrors the schema 1:1. diff --git a/.agents/skills/cmux-settings/agents/openai.yaml b/.agents/skills/cmux-settings/agents/openai.yaml new file mode 100644 index 000000000..68f98b042 --- /dev/null +++ b/.agents/skills/cmux-settings/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "cmux Settings" + short_description: "Inspect, edit, validate, and reload cmux.json settings." + default_prompt: "Use this skill to change cmux settings safely: map user-facing preferences to cmux.json paths, edit with the bundled helper, validate recognized keys, and rely on cmux's live config reload." diff --git a/.agents/skills/cmux-settings/references/all-keys.md b/.agents/skills/cmux-settings/references/all-keys.md new file mode 100644 index 000000000..fae881c79 --- /dev/null +++ b/.agents/skills/cmux-settings/references/all-keys.md @@ -0,0 +1,140 @@ +# All settings keys + +Auto-generated from `web/data/cmux.schema.json`. For the rendered docs, see `https://cmux.com/docs/configuration`. + +## app + +General app preferences from Settings > App. + +| Key | Type | Default | Description | +|---|---|---|---| +| `app.language` | `"system"` or `"en"` or `"ar"` or `"bs"` or `"zh-Hans"` or `"zh-Hant"` or `"da"` or `"de"` or `"es"` or `"fr"` or `"it"` or `"ja"` or `"ko"` or `"nb"` or `"pl"` or `"pt-BR"` or `"ru"` or `"th"` or `"tr"` | `"system"` | Preferred app language. | +| `app.appearance` | `"system"` or `"light"` or `"dark"` | `"system"` | App appearance mode. | +| `app.appIcon` | `"automatic"` or `"light"` or `"dark"` | `"automatic"` | Dock and app switcher icon style. | +| `app.menuBarOnly` | boolean | `false` | Hide the Dock icon and app switcher entry while keeping cmux available from the menu bar. | +| `app.newWorkspacePlacement` | `"top"` or `"afterCurrent"` or `"end"` | `"afterCurrent"` | Where new workspaces are inserted in the sidebar. | +| `app.workspaceInheritWorkingDirectory` | boolean | `true` | When true, new workspaces inherit the current workspace working directory. When false, new workspaces use Ghostty's working-directory setting instead. | +| `app.minimalMode` | boolean | `false` | Hide the workspace title bar and move controls into the sidebar. | +| `app.keepWorkspaceOpenWhenClosingLastSurface` | boolean | `false` | When true, closing the last surface keeps the workspace open. | +| `app.focusPaneOnFirstClick` | boolean | `true` | When cmux is inactive, the first click can activate and focus the clicked pane. | +| `app.preferredEditor` | string | `""` | Custom editor command used when Cmd-click file previews are disabled or a file is unsupported. Leave empty to use the default. | +| `app.openSupportedFilesInCmux` | boolean | `true` | When enabled, Cmd-clicking readable local files opens supported previews in cmux, including text, code, PDFs, images, audio, video, and Quick Look files. Preview headers include an Open With menu based on the user's default and compatible macOS apps for that file. | +| `app.openMarkdownInCmuxViewer` | boolean | `true` | When enabled, Cmd-clicking .md/.markdown/.mkd/.mdx files opens the rendered cmux markdown viewer panel (with live reload) instead of the generic file preview. | +| `app.reorderOnNotification` | boolean | `true` | Move workspaces with new notifications toward the top. | +| `app.iMessageMode` | boolean | `false` | Move a workspace to the top and show the submitted message when sending an agent prompt. | +| `app.sendAnonymousTelemetry` | boolean | `true` | Allow anonymous telemetry. | +| `app.warnBeforeQuit` | boolean | `true` | Show a confirmation before quitting cmux. | +| `app.warnBeforeClosingTab` | boolean | `true` | Show a confirmation before closing a tab. | +| `app.renameSelectsExistingName` | boolean | `true` | Select the current name when opening rename flows. | +| `app.commandPaletteSearchesAllSurfaces` | boolean | `false` | Search every surface in the command palette switcher instead of only the active workspace. | + +## terminal + +Terminal presentation settings from Settings > Terminal. + +| Key | Type | Default | Description | +|---|---|---|---| +| `terminal.showScrollBar` | boolean | `true` | Show the right-edge terminal scroll bar when scrollback is available. cmux automatically suppresses it for alternate-screen style TUI surfaces. | +| `terminal.autoResumeAgentSessions` | boolean | `true` | Automatically run agent resume commands for restored terminal sessions when cmux reopens after quit. Set false to restore panes while keeping Claude Code, Codex, OpenCode, and other saved agent sessions idle until you resume them manually. | + +## notifications + +Notification behavior from Settings > Notifications. + +| Key | Type | Default | Description | +|---|---|---|---| +| `notifications.dockBadge` | boolean | `true` | Show the unread count in the Dock tile. | +| `notifications.showInMenuBar` | boolean | `true` | Show the menu bar extra. | +| `notifications.unreadPaneRing` | boolean | `true` | Highlight panes with unread notifications. | +| `notifications.paneFlash` | boolean | `true` | Flash the focused pane when requested. | +| `notifications.sound` | `"default"` or `"Basso"` or `"Blow"` or `"Bottle"` or `"Frog"` or `"Funk"` or `"Glass"` or `"Hero"` or `"Morse"` or `"Ping"` or `"Pop"` or `"Purr"` or `"Sosumi"` or `"Submarine"` or `"Tink"` or `"custom_file"` or `"none"` | `"default"` | Notification sound preset. | +| `notifications.customSoundFilePath` | string | `""` | Local path to the custom notification sound file. | +| `notifications.command` | string | `""` | Optional shell command to run alongside notification delivery. | +| `notifications.hooksMode` | `"append"` or `"replace"` | `"append"` | Controls whether project-local notification hooks append to inherited hooks or replace them. | +| `notifications.hooks` | array | `[]` | Composable shell hooks that receive notification policy JSON on stdin and return updated policy JSON on stdout. | + +## sidebar + +Sidebar content and metadata visibility from Settings > Sidebar. + +| Key | Type | Default | Description | +|---|---|---|---| +| `sidebar.hideAllDetails` | boolean | `false` | Hide all per-workspace detail rows. | +| `sidebar.showWorkspaceDescription` | boolean | `true` | Show custom workspace descriptions in the sidebar. | +| `sidebar.branchLayout` | `"vertical"` or `"inline"` | `"vertical"` | Show git branch details stacked vertically or inline. | +| `sidebar.showNotificationMessage` | boolean | `true` | Show the latest notification text in the sidebar. | +| `sidebar.showBranchDirectory` | boolean | `true` | Show the workspace working directory. | +| `sidebar.showPullRequests` | boolean | `true` | Show pull request metadata in the sidebar. | +| `sidebar.makePullRequestsClickable` | boolean | `true` | Allow sidebar pull request metadata to open links when clicked. | +| `sidebar.openPullRequestLinksInCmuxBrowser` | boolean | `true` | Open sidebar pull request links in the embedded cmux browser. | +| `sidebar.openPortLinksInCmuxBrowser` | boolean | `true` | Open sidebar port links in the embedded cmux browser. | +| `sidebar.showSSH` | boolean | `true` | Show SSH connection details. | +| `sidebar.showPorts` | boolean | `true` | Show listening ports. | +| `sidebar.showLog` | boolean | `true` | Show recent log snippets. | +| `sidebar.showProgress` | boolean | `true` | Show progress indicators. | +| `sidebar.showCustomMetadata` | boolean | `true` | Show custom metadata pills. | + +## workspaceColors + +Workspace tab and badge colors from Settings > Workspace Colors. + +| Key | Type | Default | Description | +|---|---|---|---| +| `workspaceColors.indicatorStyle` | `"leftRail"` or `"solidFill"` or `"rail"` or `"border"` or `"wash"` or `"lift"` or `"typography"` or `"washRail"` or `"blueWashColorRail"` | `"leftRail"` | Active workspace indicator style. Legacy aliases are accepted and normalized. | +| `workspaceColors.selectionColor` | colorHexOrNull | `null` | Override the selected workspace background color. | +| `workspaceColors.notificationBadgeColor` | colorHexOrNull | `null` | Override the unread notification badge color. | +| `workspaceColors.colors` | object | `{"Red": "#C0392B", "Crimson": "#922B21", "Orange": "#A04000", "Amber": "#7D6608", "Olive": "#4A5C18", "Green": "#196F3D", "Teal": "#006B6B", "Aqua": "#0E6B8C", "Blue": "#1565C0", "Navy": "#1A5276", "Indigo": "#283593", "Purple": "#6A1B9A", "Magenta": "#AD1457", "Rose": "#880E4F", "Brown": "#7B3F00", "Charcoal": "#3E4B5E"}` | Full named workspace color palette. Include built-in entries you want to keep, remove keys to remove colors, and add more named entries to extend the picker. | +| `workspaceColors.paletteOverrides` | object | `{}` | Legacy workspace color overrides for built-in palette names. Prefer workspaceColors.colors for new configs. | +| `workspaceColors.customColors` | array | `[]` | Legacy list of custom workspace colors. Prefer workspaceColors.colors for new configs. | + +## sidebarAppearance + +Sidebar tint settings from Settings > Sidebar Appearance. + +| Key | Type | Default | Description | +|---|---|---|---| +| `sidebarAppearance.matchTerminalBackground` | boolean | `false` | Use the terminal background instead of the sidebar tint. | +| `sidebarAppearance.tintColor` | colorHex | `"#000000"` | Base sidebar tint color used when light/dark overrides are not set. | +| `sidebarAppearance.lightModeTintColor` | colorHexOrNull | `null` | Sidebar tint override for light appearance. | +| `sidebarAppearance.darkModeTintColor` | colorHexOrNull | `null` | Sidebar tint override for dark appearance. | +| `sidebarAppearance.tintOpacity` | number | `0.03` | Sidebar tint opacity from 0 to 1. Note: this only controls the sidebar tint, not terminal/window transparency. For terminal background transparency or blur, set `background-opacity` and `background-blur` in `~/.config/ghostty/config` and run `cmux reload-config`. | + +## automation + +Socket control and automation settings from Settings > Automation. + +| Key | Type | Default | Description | +|---|---|---|---| +| `automation.socketControlMode` | `"off"` or `"cmuxOnly"` or `"automation"` or `"password"` or `"allowAll"` or `"openAccess"` or `"fullOpenAccess"` or `"notifications"` or `"full"` | `"cmuxOnly"` | Socket control mode. Legacy aliases are accepted and normalized. | +| `automation.socketPassword` | string or null | `""` | Password for password-mode socket access. Use null or an empty string to clear it. | +| `automation.claudeCodeIntegration` | boolean | `true` | Enable cmux integration hooks for Claude Code. | +| `automation.claudeBinaryPath` | string | `""` | Custom path to the claude binary. | +| `automation.cursorIntegration` | boolean | `true` | Enable cmux integration hooks for Cursor. | +| `automation.geminiIntegration` | boolean | `true` | Enable cmux integration hooks for Gemini. | +| `automation.portBase` | integer | `9100` | Starting value for workspace CMUX_PORT assignments. | +| `automation.portRange` | integer | `10` | Number of ports reserved per workspace. | + +## browser + +Embedded browser settings from Settings > Browser. + +| Key | Type | Default | Description | +|---|---|---|---| +| `browser.defaultSearchEngine` | `"google"` or `"duckduckgo"` or `"bing"` or `"kagi"` or `"startpage"` | `"google"` | Default search engine for non-URL queries. | +| `browser.showSearchSuggestions` | boolean | `true` | Show omnibar search suggestions. | +| `browser.theme` | `"system"` or `"light"` or `"dark"` | `"system"` | Embedded browser theme. | +| `browser.openTerminalLinksInCmuxBrowser` | boolean | `true` | Open clicked terminal links in the embedded browser. | +| `browser.interceptTerminalOpenCommandInCmuxBrowser` | boolean | `true` | Intercept terminal open http(s) commands and route them through the embedded browser. | +| `browser.hostsToOpenInEmbeddedBrowser` | array | `[]` | Allowlist of hosts that should stay inside the embedded browser. | +| `browser.urlsToAlwaysOpenExternally` | array | `[]` | Rules that always open matching URLs in the system browser. | +| `browser.insecureHttpHostsAllowedInEmbeddedBrowser` | array | `["localhost", "*.localhost", "127.0.0.1", "::1", "0.0.0.0", "*.localtest.me"]` | HTTP hosts allowed in the embedded browser without a warning prompt. | +| `browser.showImportHintOnBlankTabs` | boolean | `true` | Show the browser import hint on blank tabs. | +| `browser.reactGrabVersion` | string | `"0.1.29"` | Pinned react-grab version for the browser toolbar helper. | + +## shortcuts + +Keyboard shortcut settings from Settings > Keyboard Shortcuts. + +| Key | Type | Default | Description | +|---|---|---|---| +| `shortcuts.bindings` | object | `{}` | Shortcut overrides keyed by cmux action id. Use a string for a single shortcut, an array for a chord, null, an empty string, none, clear, unbound, or disabled to unbind. | diff --git a/.agents/skills/cmux-settings/references/shortcut-actions.md b/.agents/skills/cmux-settings/references/shortcut-actions.md new file mode 100644 index 000000000..c05ee8e2d --- /dev/null +++ b/.agents/skills/cmux-settings/references/shortcut-actions.md @@ -0,0 +1,155 @@ +# Keyboard shortcut action ids + +Auto-generated from `web/data/cmux.schema.json` (`shortcuts.bindings.propertyNames.enum`). + +Values for `shortcuts.bindings.`: + +- A string like `"cmd+b"` for a single shortcut. +- A two-element array like `["ctrl+b","c"]` for a chord. +- `null` or an empty string (`""`, `"none"`, `"clear"`, `"unbound"`, `"disabled"`) to unbind. + +## App + +- `shortcuts.bindings.openSettings` +- `shortcuts.bindings.reloadConfiguration` +- `shortcuts.bindings.showHideAllWindows` +- `shortcuts.bindings.globalSearch` +- `shortcuts.bindings.newWindow` +- `shortcuts.bindings.closeWindow` +- `shortcuts.bindings.toggleFullScreen` +- `shortcuts.bindings.quit` +- `shortcuts.bindings.openFolder` +- `shortcuts.bindings.sendFeedback` + +## Tabs + +- `shortcuts.bindings.newTab` +- `shortcuts.bindings.newBrowserWorkspace` +- `shortcuts.bindings.reopenPreviousSession` +- `shortcuts.bindings.renameTab` +- `shortcuts.bindings.closeTab` +- `shortcuts.bindings.closeOtherTabsInPane` + +## Workspace + +- `shortcuts.bindings.goToWorkspace` +- `shortcuts.bindings.selectWorkspaceByNumber` +- `shortcuts.bindings.renameWorkspace` +- `shortcuts.bindings.editWorkspaceDescription` +- `shortcuts.bindings.closeWorkspace` +- `shortcuts.bindings.newWorkspaceGroup` +- `shortcuts.bindings.groupSelectedWorkspaces` +- `shortcuts.bindings.toggleFocusedWorkspaceGroupCollapsed` +- `shortcuts.bindings.reopenClosedWorkspace` +- `shortcuts.bindings.reopenClosedBrowserPanel` (legacy ID for **Reopen Last Closed**) +- `shortcuts.bindings.moveWorkspaceUp` +- `shortcuts.bindings.moveWorkspaceDown` + +## Panes and surfaces + +- `shortcuts.bindings.nextSurface` +- `shortcuts.bindings.prevSurface` +- `shortcuts.bindings.moveSurfaceLeft` +- `shortcuts.bindings.moveSurfaceRight` +- `shortcuts.bindings.moveSurfaceToPreviousPane` +- `shortcuts.bindings.moveSurfaceToNextPane` +- `shortcuts.bindings.moveSurfaceToPaneLeft` +- `shortcuts.bindings.moveSurfaceToPaneRight` +- `shortcuts.bindings.moveSurfaceToPaneUp` +- `shortcuts.bindings.moveSurfaceToPaneDown` +- `shortcuts.bindings.selectSurfaceByNumber` +- `shortcuts.bindings.newSurface` +- `shortcuts.bindings.toggleTerminalCopyMode` +- `shortcuts.bindings.clearScreenKeepScrollback` +- `shortcuts.bindings.simulatorHome` +- `shortcuts.bindings.simulatorRotateLeft` +- `shortcuts.bindings.simulatorRotateRight` +- `shortcuts.bindings.simulatorToggleAppearance` +- `shortcuts.bindings.simulatorToggleSoftwareKeyboard` +- `shortcuts.bindings.focusLeft` +- `shortcuts.bindings.focusRight` +- `shortcuts.bindings.focusUp` +- `shortcuts.bindings.focusDown` +- `shortcuts.bindings.focusPreviousPane` +- `shortcuts.bindings.focusNextPane` +- `shortcuts.bindings.splitRight` +- `shortcuts.bindings.splitDown` +- `shortcuts.bindings.toggleSplitZoom` +- `shortcuts.bindings.increaseWorkspaceTerminalFontSize` +- `shortcuts.bindings.decreaseWorkspaceTerminalFontSize` +- `shortcuts.bindings.resetWorkspaceTerminalFontSize` +- `shortcuts.bindings.equalizeSplits` + +## Canvas + +- `shortcuts.bindings.toggleCanvasLayout` +- `shortcuts.bindings.canvasRevealFocusedPane` +- `shortcuts.bindings.canvasOverview` +- `shortcuts.bindings.canvasZoomIn` +- `shortcuts.bindings.canvasZoomOut` +- `shortcuts.bindings.canvasZoomReset` +- `shortcuts.bindings.canvasTidy` +- `shortcuts.bindings.canvasAlignLeft` +- `shortcuts.bindings.canvasAlignRight` +- `shortcuts.bindings.canvasAlignTop` +- `shortcuts.bindings.canvasAlignBottom` +- `shortcuts.bindings.canvasEqualizeWidths` +- `shortcuts.bindings.canvasEqualizeHeights` +- `shortcuts.bindings.canvasDistributeHorizontally` +- `shortcuts.bindings.canvasDistributeVertically` + +## Command palette + +- `shortcuts.bindings.commandPalette` +- `shortcuts.bindings.commandPaletteNext` +- `shortcuts.bindings.commandPalettePrevious` + +## Notifications + +- `shortcuts.bindings.showNotifications` +- `shortcuts.bindings.jumpToUnread` +- `shortcuts.bindings.toggleUnread` +- `shortcuts.bindings.markOldestUnreadAndJumpNext` +- `shortcuts.bindings.triggerFlash` + +## Right sidebar + +- `shortcuts.bindings.toggleSidebar` +- `shortcuts.bindings.focusRightSidebar` +- `shortcuts.bindings.switchRightSidebarToFiles` +- `shortcuts.bindings.switchRightSidebarToFind` +- `shortcuts.bindings.switchRightSidebarToSessions` +- `shortcuts.bindings.switchRightSidebarToFeed` +- `shortcuts.bindings.switchRightSidebarToDock` +- `shortcuts.bindings.nextSidebarTab` +- `shortcuts.bindings.prevSidebarTab` + +## Browser + +- `shortcuts.bindings.splitBrowserRight` +- `shortcuts.bindings.splitBrowserDown` +- `shortcuts.bindings.openBrowser` +- `shortcuts.bindings.focusBrowserAddressBar` +- `shortcuts.bindings.browserBack` +- `shortcuts.bindings.browserForward` +- `shortcuts.bindings.browserReload` +- `shortcuts.bindings.browserZoomIn` +- `shortcuts.bindings.browserZoomOut` +- `shortcuts.bindings.browserZoomReset` +- `shortcuts.bindings.toggleBrowserDeveloperTools` +- `shortcuts.bindings.showBrowserJavaScriptConsole` + +## Find + +- `shortcuts.bindings.find` +- `shortcuts.bindings.findInDirectory` +- `shortcuts.bindings.findNext` +- `shortcuts.bindings.findPrevious` +- `shortcuts.bindings.hideFind` +- `shortcuts.bindings.useSelectionForFind` + +## Files and React Grab + +- `shortcuts.bindings.toggleFileExplorer` +- `shortcuts.bindings.saveFilePreview` +- `shortcuts.bindings.toggleReactGrab` diff --git a/.agents/skills/cmux-settings/scripts/cmux-settings b/.agents/skills/cmux-settings/scripts/cmux-settings new file mode 100755 index 000000000..ddff2b9b9 --- /dev/null +++ b/.agents/skills/cmux-settings/scripts/cmux-settings @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 +"""Inspect and edit ~/.config/cmux/cmux.json. + +cmux watches the file and auto-reloads on save, so writes take effect +immediately. The file is JSONC (JSON with // and /* */ comments); this +script writes comment-free formatting (2-space indent, trailing newline) +and only writes when the parsed value actually changes. + +Usage: + cmux-settings path print the config path + cmux-settings dump [--no-comments] print current settings (raw or stripped) + cmux-settings get print value at path (JSON) + cmux-settings set set value (v parsed as JSON, falls back to string) + cmux-settings unset remove value at path + cmux-settings list-supported print every settings path the schema recognizes + cmux-settings validate check JSON parses and all set keys are recognized + cmux-settings open open the file in $EDITOR (or VS Code, then TextEdit) + +Examples: + cmux-settings set app.appearance dark + cmux-settings set notifications.dockBadge false + cmux-settings set shortcuts.bindings.toggleSidebar '"cmd+b"' + cmux-settings set shortcuts.bindings.newTab '["ctrl+b","c"]' + cmux-settings unset app.appearance +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shlex +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Any + +DEFAULT_PATH = Path.home() / ".config" / "cmux" / "cmux.json" +SCHEMA_URL = ( + "https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json" +) + + +def strip_jsonc(text: str) -> str: + """Remove // line comments and /* block comments outside strings.""" + out: list[str] = [] + i, n = 0, len(text) + in_string = False + string_quote = "" + while i < n: + ch = text[i] + if in_string: + out.append(ch) + if ch == "\\" and i + 1 < n: + out.append(text[i + 1]) + i += 2 + continue + if ch == string_quote: + in_string = False + i += 1 + continue + if ch in ('"', "'"): + in_string = True + string_quote = ch + out.append(ch) + i += 1 + continue + if ch == "/" and i + 1 < n and text[i + 1] == "/": + j = text.find("\n", i + 2) + i = n if j == -1 else j + continue + if ch == "/" and i + 1 < n and text[i + 1] == "*": + j = text.find("*/", i + 2) + i = n if j == -1 else j + 2 + continue + out.append(ch) + i += 1 + # Drop trailing commas before ] or } (also legal in JSONC). + return strip_trailing_commas_outside_strings("".join(out)) + + +def strip_trailing_commas_outside_strings(text: str) -> str: + """Remove JSONC trailing commas without touching string contents.""" + out: list[str] = [] + i, n = 0, len(text) + in_string = False + string_quote = "" + while i < n: + ch = text[i] + if in_string: + out.append(ch) + if ch == "\\" and i + 1 < n: + out.append(text[i + 1]) + i += 2 + continue + if ch == string_quote: + in_string = False + i += 1 + continue + if ch in ('"', "'"): + in_string = True + string_quote = ch + out.append(ch) + i += 1 + continue + if ch == ",": + j = i + 1 + while j < n and text[j].isspace(): + j += 1 + if j < n and text[j] in "}]": + i += 1 + continue + out.append(ch) + i += 1 + return "".join(out) + + +def load_settings(path: Path) -> dict[str, Any]: + if not path.exists(): + return {"$schema": SCHEMA_URL, "schemaVersion": 1} + raw = path.read_text() + try: + return json.loads(strip_jsonc(raw)) + except json.JSONDecodeError as e: + raise SystemExit(f"error: {path} is not valid JSONC: {e}") + + +def atomic_write(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + encoded = json.dumps(data, indent=2, ensure_ascii=False) + "\n" + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", dir=path.parent, delete=False, suffix=".tmp" + ) as tmp: + tmp.write(encoded) + tmp_path = Path(tmp.name) + os.replace(tmp_path, path) + + +def split_path(dotted: str) -> list[str]: + if not dotted: + raise SystemExit("error: empty key path") + parts = dotted.split(".") + if any(part == "" for part in parts): + raise SystemExit("error: empty key path segment") + return parts + + +def get_at(data: Any, parts: list[str]) -> Any: + cur = data + for p in parts: + if not isinstance(cur, dict) or p not in cur: + raise SystemExit(f"error: path not present: {'.'.join(parts)}") + cur = cur[p] + return cur + + +def set_at(data: dict[str, Any], parts: list[str], value: Any) -> bool: + cur = data + for p in parts[:-1]: + if p not in cur: + cur[p] = {} + elif not isinstance(cur[p], dict): + raise SystemExit( + f"error: intermediate key '{p}' is not an object " + f"(got {type(cur[p]).__name__}); cannot set nested path" + ) + cur = cur[p] + leaf = parts[-1] + changed = (leaf not in cur) or cur[leaf] != value + cur[leaf] = value + return changed + + +def unset_at(data: dict[str, Any], parts: list[str]) -> bool: + trail: list[tuple[dict[str, Any], str]] = [] + cur: Any = data + for p in parts[:-1]: + if not isinstance(cur, dict) or p not in cur: + return False + trail.append((cur, p)) + cur = cur[p] + if not isinstance(cur, dict) or parts[-1] not in cur: + return False + del cur[parts[-1]] + # Drop now-empty ancestor objects so the file stays tidy. + for parent, key in reversed(trail): + if isinstance(parent[key], dict) and not parent[key]: + del parent[key] + else: + break + return True + + +def parse_value(raw: str) -> Any: + """Try JSON first; if it fails, fall back to a plain string.""" + try: + return json.loads(raw) + except json.JSONDecodeError: + return raw + + +def flatten(prefix: str, value: Any) -> list[str]: + if isinstance(value, dict): + out: list[str] = [] + for k, v in value.items(): + child = f"{prefix}.{k}" if prefix else k + out.extend(flatten(child, v)) + return out + return [prefix] + + +def supported_paths_from_source(source: Path) -> list[str]: + text = source.read_text() + known_sections = ( + "app.", + "terminal.", + "notifications.", + "sidebar.", + "workspaceColors.", + "sidebarAppearance.", + "automation.", + "browser.", + "shortcuts.", + ) + candidates = re.findall(r'"([a-zA-Z]+\.[a-zA-Z0-9_.]+)"', text) + return sorted({p for p in candidates if p.startswith(known_sections)}) + + +def supported_paths_from_reference(skill_root: Path | None) -> list[str]: + if skill_root is None: + return [] + reference = skill_root / "references" / "all-keys.md" + if not reference.exists(): + return [] + text = reference.read_text() + return sorted(set(re.findall(r"^\| `([^`]+)` \|", text, re.MULTILINE))) + + +def find_source_file() -> Path | None: + here = Path(__file__).resolve() + for parent in here.parents: + direct = parent / "Sources" / "CmuxSettingsJSONPathSupport.swift" + if direct.exists(): + return direct + hq_checkout = parent / "repo" / "Sources" / "CmuxSettingsJSONPathSupport.swift" + if hq_checkout.exists(): + return hq_checkout + return None + + +def find_skill_root() -> Path | None: + here = Path(__file__).resolve() + for parent in here.parents: + if parent.name == "cmux-settings" and (parent / "SKILL.md").exists(): + return parent + if (parent / "references" / "all-keys.md").exists(): + return parent + return None + + +def supported_paths() -> list[str]: + source = find_source_file() + if source is not None: + return supported_paths_from_source(source) + return supported_paths_from_reference(find_skill_root()) + + +def cmd_path(args: argparse.Namespace) -> int: + print(args.file) + return 0 + + +def cmd_dump(args: argparse.Namespace) -> int: + path = Path(args.file) + if args.no_comments: + data = load_settings(path) + print(json.dumps(data, indent=2, ensure_ascii=False)) + else: + if path.exists(): + sys.stdout.write(path.read_text()) + else: + print(f"# {path} does not exist", file=sys.stderr) + return 1 + return 0 + + +def cmd_get(args: argparse.Namespace) -> int: + data = load_settings(Path(args.file)) + value = get_at(data, split_path(args.key)) + print(json.dumps(value, indent=2, ensure_ascii=False)) + return 0 + + +def cmd_set(args: argparse.Namespace) -> int: + path = Path(args.file) + data = load_settings(path) + parts = split_path(args.key) + value = parse_value(args.value) + changed = set_at(data, parts, value) + if not changed: + print(f"unchanged: {args.key} = {json.dumps(value)}") + return 0 + atomic_write(path, data) + print(f"set: {args.key} = {json.dumps(value)}") + return 0 + + +def cmd_unset(args: argparse.Namespace) -> int: + path = Path(args.file) + data = load_settings(path) + if not unset_at(data, split_path(args.key)): + print(f"unchanged: {args.key} (not present)") + return 0 + atomic_write(path, data) + print(f"unset: {args.key}") + return 0 + + +def cmd_list_supported(args: argparse.Namespace) -> int: + paths = supported_paths() + if not paths: + print( + "error: could not load the supported-paths list; " + "run from a cmux checkout or reinstall the cmux-settings skill", + file=sys.stderr, + ) + return 1 + for p in paths: + print(p) + return 0 + + +def cmd_validate(args: argparse.Namespace) -> int: + path = Path(args.file) + data = load_settings(path) + supported = set(supported_paths()) + if not supported: + print( + "warn: could not load schema path list; only checked JSON parses", + file=sys.stderr, + ) + print(f"ok: {path} parses") + return 0 + unknown: list[str] = [] + # Keep this in sync with top-level structural keys in web/data/cmux.schema.json. + structural = { + "$schema", + "schemaVersion", + "actions", + "ui", + "commands", + "vault", + "newWorkspaceCommand", + "surfaceTabBarButtons", + "rightSidebar", + } + for top, value in data.items(): + if top in structural: + continue + if not isinstance(value, dict): + unknown.append(top) + continue + for full in flatten(top, value): + if full in supported: + continue + if any(full.startswith(s + ".") for s in supported): + continue + unknown.append(full) + if unknown: + print("unknown settings keys:") + for u in unknown: + print(f" {u}") + return 1 + print(f"ok: {path} parses and all settings keys are recognized") + return 0 + + +def cmd_open(args: argparse.Namespace) -> int: + path = Path(args.file) + path.parent.mkdir(parents=True, exist_ok=True) + if not path.exists(): + atomic_write(path, {"$schema": SCHEMA_URL, "schemaVersion": 1}) + editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") + if editor: + try: + editor_args = shlex.split(editor) + except ValueError as e: + print(f"error: invalid editor command: {e}", file=sys.stderr) + return 1 + if not editor_args: + print("error: editor command is empty", file=sys.stderr) + return 1 + try: + return subprocess.call([*editor_args, str(path)]) + except FileNotFoundError: + print(f"error: editor not found: {editor_args[0]}", file=sys.stderr) + return 1 + for app in (["code", "--wait"], ["cursor", "--wait"], ["open", "-e"], ["xdg-open"]): + if shutil.which(app[0]): + try: + return subprocess.call([*app, str(path)]) + except FileNotFoundError: + continue + print( + "error: no suitable editor found; set $EDITOR or install code, cursor, open, or xdg-open", + file=sys.stderr, + ) + return 1 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + "--file", + default=str(DEFAULT_PATH), + help=f"settings file path (default: {DEFAULT_PATH})", + ) + sub = parser.add_subparsers(dest="cmd", required=True) + sub.add_parser("path").set_defaults(func=cmd_path) + + p_dump = sub.add_parser("dump") + p_dump.add_argument("--no-comments", action="store_true") + p_dump.set_defaults(func=cmd_dump) + + p_get = sub.add_parser("get") + p_get.add_argument("key") + p_get.set_defaults(func=cmd_get) + + p_set = sub.add_parser("set") + p_set.add_argument("key") + p_set.add_argument("value") + p_set.set_defaults(func=cmd_set) + + p_unset = sub.add_parser("unset") + p_unset.add_argument("key") + p_unset.set_defaults(func=cmd_unset) + + sub.add_parser("list-supported").set_defaults(func=cmd_list_supported) + sub.add_parser("validate").set_defaults(func=cmd_validate) + sub.add_parser("open").set_defaults(func=cmd_open) + + args = parser.parse_args() + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.agents/skills/cmux-workspace/SKILL.md b/.agents/skills/cmux-workspace/SKILL.md new file mode 100644 index 000000000..0a7ffb280 --- /dev/null +++ b/.agents/skills/cmux-workspace/SKILL.md @@ -0,0 +1,116 @@ +--- +name: cmux-workspace +description: "Work inside the current cmux workspace and terminal. Use for cmux workspace, current workspace, caller surface, panes, surfaces, socket targeting, and non-interfering cmux automation." +--- + +# cmux Workspace + +Scope work to the cmux workspace that invoked the agent. + +- **Window**: a macOS cmux window. +- **Workspace**: a sidebar entry. The UI calls it a tab; CLI/socket APIs call it a workspace. +- **Pane**: a split region inside a workspace. +- **Surface**: a tab inside a pane, terminal or browser. +- **Panel**: internal content type inside a surface. Prefer CLI surface commands over panel internals. + +## Default rule + +Scope actions to the current caller workspace unless the user explicitly asks for another workspace, another window, or global state. Do not assume the visually focused workspace is the right target: an agent can run in one workspace while the user looks at another. + +```bash +printf 'workspace=%s\nsurface=%s\nsocket=%s\n' \ + "${CMUX_WORKSPACE_ID:-}" "${CMUX_SURFACE_ID:-}" "${CMUX_SOCKET_PATH:-}" +cmux identify --json +``` + +`CMUX_WORKSPACE_ID` is the default workspace anchor and `CMUX_SURFACE_ID` the default caller terminal anchor. If they are missing, fall back to `cmux identify --json` and say explicitly that you are using the currently focused context. + +## Non-disruptive automation + +Treat layout and focus as separate concerns. `select-workspace`, `focus-pane`, `focus-panel`, and focus-changing `tab-action` verbs are user-affecting actions, like clicks. Never call them speculatively, even inside the caller's own workspace, since the user may be looking elsewhere. + +Build layout additively in one shot, using commands that create a pane already populated with the right surface: + +```bash +cmux new-pane --workspace "${CMUX_WORKSPACE_ID}" --type browser --direction right --url "http://127.0.0.1:8765" +cmux new-pane --workspace "${CMUX_WORKSPACE_ID}" --type terminal --direction down +``` + +Avoid create-then-move-then-focus chains. Pass `--focus false` wherever the verb supports it (`move-surface --focus false` preserves the user's attention; more commands may grow the flag, see https://github.com/manaflow-ai/cmux/issues/1418 and https://github.com/manaflow-ai/cmux/issues/2820). If a layout command rejects a valid `surface:` or `pane:` ref, report the bug and stop rather than working around it by focusing. + +## Right-side helper pane + +For auxiliary output (preview apps, TUIs, logs, one-off shells, browser checks), reuse one helper pane to the right of the caller terminal. Inspect first with `cmux identify --json`, `cmux list-panes`, and `cmux list-pane-surfaces`, then: + +- Helper pane exists: add a surface to it. + ```bash + cmux new-surface --workspace "${CMUX_WORKSPACE_ID:-}" --pane pane: --type terminal --focus false + ``` +- No helper pane: create exactly one. + ```bash + cmux new-pane --workspace "${CMUX_WORKSPACE_ID:-}" --type terminal --direction right --focus false + ``` +- Multiple obvious stale helper panes from this same automation, and the user asked to tidy: keep one and clean up duplicates. Never close a pane you cannot confidently identify as stale helper output. + +Send commands to the new or reused surface by explicit surface ref. Repeated "open it" requests create tabs inside the existing right helper pane, not more splits. + +## Caller terminal + +The surface that invoked the agent is the safest anchor for relative operations. + +```bash +cmux send "npm test\n" # focused terminal in caller workspace +cmux send --surface "${CMUX_SURFACE_ID:-}" "git status\n" # exact caller surface +cmux send-key --surface "${CMUX_SURFACE_ID:-}" enter +``` + +Do not send keystrokes, close surfaces, or change focus in another workspace unless the user named that target. + +## Moving surfaces + +```bash +cmux move-surface --surface "${CMUX_SURFACE_ID}" --before surface:3 # also --after, --index +cmux move-surface --surface surface:240 --pane pane:172 --focus false +cmux drag-surface-to-split --surface surface:240 down +``` + +Known papercut: `drag-surface-to-split` routes through V1 and resolves the workspace via UI focus, so it fails with `ERROR: Surface not found` when the caller's workspace is not the visually focused one (https://github.com/manaflow-ai/cmux/issues/1901, related https://github.com/manaflow-ai/cmux/issues/3189). Until that lands, build layout additively. Never call `focus-pane` or `focus-panel` to recover from a failed move; report the failure and stop. + +## Sidebar state + +Attach status, progress, and logs to the current workspace so the sidebar reflects this task. + +```bash +cmux set-status build "running" --workspace "${CMUX_WORKSPACE_ID:-}" --color "#ff9500" +cmux set-progress 0.4 --label "Building" --workspace "${CMUX_WORKSPACE_ID:-}" +cmux log --workspace "${CMUX_WORKSPACE_ID:-}" --level info -- "Started build" +cmux sidebar-state --workspace "${CMUX_WORKSPACE_ID:-}" --json +``` + +## Contributor reloads + +For cmux app/runtime changes in a cmux source checkout, use a tagged reload from the active worktree. It creates an isolated app name, bundle ID, debug socket, and DerivedData path. Never build or launch untagged `cmux DEV`. + +```bash +./scripts/reload.sh --tag +CMUX_SOCKET_PATH=/tmp/cmux-debug-.sock cmux identify --json +``` + +## Socket access + +Use the socket path cmux provided before any default: `SOCK="${CMUX_SOCKET_PATH:-/tmp/cmux.sock}"`. Socket access can be off, restricted to cmux-spawned processes, or open to all local processes. If a command cannot connect, inspect `cmux capabilities --json` and `cmux ping` before changing settings. + +## Rules + +- Work in the caller workspace by default; prefer explicit `--workspace` and `--surface` flags for mutating actions even when env vars are set, so automation is auditable. +- Never call `focus-pane`, `focus-panel`, `select-workspace`, or focus-changing `tab-action` verbs unless the user explicitly asked. +- Pass `--focus false` on `move-surface` and any creation verb that supports it. +- Build layout additively with `new-pane --type ... --url ...`, not create-then-move-then-focus. +- If a CLI command rejects a valid surface or pane ref, report it. Do not work around by focusing. +- Do not close, focus, move, or send input to another workspace unless the user names that target. +- Use short refs in chat and examples; UUIDs only for logs, persistence, or debugging. + +## References + +- [references/commands.md](references/commands.md): full workspace, pane, surface, notification, and utility command list. +- [../cmux-browser/SKILL.md](../cmux-browser/SKILL.md): browser surfaces under the same current-workspace rule. diff --git a/.agents/skills/cmux-workspace/agents/openai.yaml b/.agents/skills/cmux-workspace/agents/openai.yaml new file mode 100644 index 000000000..e71720a9e --- /dev/null +++ b/.agents/skills/cmux-workspace/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "cmux Workspace" + short_description: "Scope cmux automation to the current workspace, caller terminal, panes, and surfaces." + default_prompt: "Use this skill to identify the current cmux caller workspace and surface, then keep workspace, pane, surface, browser, sidebar, and socket actions scoped to that context unless the user explicitly names another target." diff --git a/.agents/skills/cmux-workspace/references/commands.md b/.agents/skills/cmux-workspace/references/commands.md new file mode 100644 index 000000000..6f4852a62 --- /dev/null +++ b/.agents/skills/cmux-workspace/references/commands.md @@ -0,0 +1,104 @@ +# cmux Workspace Command Reference + +Use these commands from a cmux terminal. Most commands infer the caller workspace from `CMUX_WORKSPACE_ID`, but explicit flags are safer for automation. + +## Context + +```bash +cmux identify --json +cmux --json --id-format both identify # stable UUIDs plus human refs, for logs and handoffs +cmux current-workspace --json +cmux capabilities --json +cmux ping +``` + +## Windows and Workspaces + +```bash +cmux list-windows +cmux current-window +cmux new-window +cmux focus-window --window window:2 +cmux close-window --window window:2 + +cmux list-workspaces +cmux list-workspaces --json +cmux new-workspace --name "task" --cwd "$PWD" +cmux new-workspace --command "npm run dev" +cmux new-workspace --layout '{"root":{"type":"terminal"}}' +cmux current-workspace +cmux select-workspace --workspace workspace:2 +cmux rename-workspace --workspace workspace:2 -- "new name" +cmux close-workspace --workspace workspace:2 +cmux reorder-workspace --workspace workspace:4 --before workspace:2 +cmux move-workspace-to-window --workspace workspace:4 --window window:1 +``` + +## Panes and Surfaces + +```bash +cmux list-panes --workspace "$CMUX_WORKSPACE_ID" +cmux list-pane-surfaces --workspace "$CMUX_WORKSPACE_ID" --pane pane:1 +cmux list-panels --workspace "$CMUX_WORKSPACE_ID" +cmux tree --workspace "$CMUX_WORKSPACE_ID" + +cmux new-split right --workspace "$CMUX_WORKSPACE_ID" +cmux new-split down --workspace "$CMUX_WORKSPACE_ID" --surface "$CMUX_SURFACE_ID" +cmux new-pane --workspace "$CMUX_WORKSPACE_ID" --type terminal --direction right +cmux new-pane --workspace "$CMUX_WORKSPACE_ID" --type browser --url http://localhost:3000 +cmux new-surface --workspace "$CMUX_WORKSPACE_ID" --type terminal --pane pane:1 +cmux new-surface --workspace "$CMUX_WORKSPACE_ID" --type browser --pane pane:1 --url http://localhost:3000 + +cmux focus-pane --workspace "$CMUX_WORKSPACE_ID" --pane pane:2 +cmux focus-panel --workspace "$CMUX_WORKSPACE_ID" --panel surface:3 +cmux close-surface --workspace "$CMUX_WORKSPACE_ID" --surface surface:3 +cmux move-surface --surface surface:7 --pane pane:2 --focus true +cmux reorder-surface --surface surface:7 --before surface:3 +cmux move-tab-to-new-workspace --surface surface:7 --title "browser" +``` + +## Input + +```bash +cmux send "echo hello\n" +cmux send-key enter +cmux send --surface "$CMUX_SURFACE_ID" "git status\n" +cmux send-key --surface "$CMUX_SURFACE_ID" enter +cmux read-screen --surface "$CMUX_SURFACE_ID" +``` + +## Sidebar Metadata + +```bash +cmux set-status build "running" --workspace "$CMUX_WORKSPACE_ID" --icon hammer --color "#ff9500" +cmux clear-status build --workspace "$CMUX_WORKSPACE_ID" +cmux list-status --workspace "$CMUX_WORKSPACE_ID" +cmux set-progress 0.5 --workspace "$CMUX_WORKSPACE_ID" --label "Building" +cmux clear-progress --workspace "$CMUX_WORKSPACE_ID" +cmux log --workspace "$CMUX_WORKSPACE_ID" --level info -- "Build started" +cmux list-log --workspace "$CMUX_WORKSPACE_ID" --limit 20 +cmux clear-log --workspace "$CMUX_WORKSPACE_ID" +cmux sidebar-state --workspace "$CMUX_WORKSPACE_ID" --json +``` + +## Notifications and Attention + +```bash +cmux notify --title "Done" --body "Task complete" +cmux list-notifications --json +cmux clear-notifications +cmux trigger-flash --workspace "$CMUX_WORKSPACE_ID" --surface "$CMUX_SURFACE_ID" +cmux surface-health --workspace "$CMUX_WORKSPACE_ID" --json +``` + +## Config and Docs + +```bash +cmux docs api +cmux docs browser +cmux docs settings +cmux settings path +cmux settings cmux-json +cmux settings shortcuts +cmux reload-config +``` diff --git a/.agents/skills/cmux/SKILL.md b/.agents/skills/cmux/SKILL.md new file mode 100644 index 000000000..39858a9a5 --- /dev/null +++ b/.agents/skills/cmux/SKILL.md @@ -0,0 +1,58 @@ +--- +name: cmux +description: End-user control of cmux topology and routing (windows, workspaces, panes/surfaces, focus, moves, reorder, identify, trigger flash). Use when automation needs deterministic placement and navigation in a multi-pane cmux layout. +--- + +# cmux Core Control + +Non-browser cmux topology and routing. + +- **Window**: top-level macOS cmux window. +- **Workspace**: tab-like group within a window. +- **Pane**: split container in a workspace. +- **Surface**: a tab within a pane (terminal or browser panel). + +## Fast start + +```bash +cmux identify --json # current caller context +cmux list-windows / list-workspaces / list-panes +cmux list-pane-surfaces --pane pane:1 +cmux new-workspace +cmux new-split right --panel pane:1 +cmux move-surface --surface surface:7 --pane pane:2 --focus true +cmux split-off --surface surface:7 right +cmux reorder-surface --surface surface:7 --before surface:3 + +# workspace context-menu actions (color, description, rename, pin, ...) +cmux workspace-action --action set-color --color Blue +cmux workspace-action --action set-description --description "Ship checklist" + +# attention cue +cmux trigger-flash --surface surface:7 +``` + +## Handle model + +Output defaults to short refs (`window:N`, `workspace:N`, `pane:N`, `surface:N`). UUIDs are accepted as input; request UUID output only when needed with `--id-format uuids|both`. + +## Settings + +cmux-owned settings live in `~/.config/cmux/cmux.json`. `cmux docs settings` prints the docs URL, schema URL, raw GitHub resources, cmux.json paths, and reload command. `cmux settings`, `cmux settings cmux-json`, and `cmux settings shortcuts` open the UI. + +`cmux reload-config` reloads both `cmux.json` and `~/.config/ghostty/config`, refreshing terminals in place with no app restart. + +Terminal rendering (font, cursor style, theme, scrollback, `background-opacity`, `background-blur`) belongs in Ghostty config, not cmux settings. Everything else (app behavior, sidebar, notifications, browser behavior, automation, workspace colors, cmux-owned shortcuts) is cmux settings. Before editing, copy any existing `cmux.json` to a timestamped `.bak` next to it. Legacy `~/.config/cmux/settings.json` and `~/Library/Application Support/com.cmuxterm.app/settings.json` are read only as fallback for missing keys. + +## Deep-dive references + +| Reference | When to Use | +|-----------|-------------| +| [references/handles-and-identify.md](references/handles-and-identify.md) | Handle syntax, self-identify, caller targeting | +| [references/windows-workspaces.md](references/windows-workspaces.md) | Window/workspace lifecycle, reorder/move, and context-menu actions (color, description, rename) | +| [references/panes-surfaces.md](references/panes-surfaces.md) | Splits, surfaces, move/reorder, focus routing | +| [references/trigger-flash-and-health.md](references/trigger-flash-and-health.md) | Flash cue and surface health checks | +| [../cmux-workspace/SKILL.md](../cmux-workspace/SKILL.md) | Current caller workspace rules and non-disruptive automation | +| [../cmux-settings/SKILL.md](../cmux-settings/SKILL.md) | Safe cmux.json settings edits and validation | +| [../cmux-browser/SKILL.md](../cmux-browser/SKILL.md) | Browser automation on surface-backed webviews | +| [../cmux-markdown/SKILL.md](../cmux-markdown/SKILL.md) | Markdown viewer panel with live file watching | diff --git a/.agents/skills/cmux/agents/openai.yaml b/.agents/skills/cmux/agents/openai.yaml new file mode 100644 index 000000000..8c8f26dfa --- /dev/null +++ b/.agents/skills/cmux/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "cmux Core" + short_description: "Control windows/workspaces/panes/surfaces and routing with cmux CLI." + default_prompt: "Use this skill to inspect and manipulate cmux topology: identify context, target handles, create/focus/move/reorder windows/workspaces/panes/surfaces, and use trigger-flash for visual confirmation." diff --git a/.agents/skills/cmux/references/handles-and-identify.md b/.agents/skills/cmux/references/handles-and-identify.md new file mode 100644 index 000000000..3c70c4c1e --- /dev/null +++ b/.agents/skills/cmux/references/handles-and-identify.md @@ -0,0 +1,12 @@ +# Handles and Identify + +Most v2-backed commands accept a UUID, a short ref (`window:N`, `workspace:N`, `pane:N`, `surface:N`), or an index where legacy index-based commands still allow it. + +```bash +cmux identify --json # focused topology + caller resolution +cmux identify --workspace workspace:2 # route relative actions from a known anchor +cmux identify --workspace workspace:2 --surface surface:8 + +cmux --json --id-format both identify # refs plus UUIDs +cmux --json --id-format uuids identify +``` diff --git a/.agents/skills/cmux/references/panes-surfaces.md b/.agents/skills/cmux/references/panes-surfaces.md new file mode 100644 index 000000000..61abe9807 --- /dev/null +++ b/.agents/skills/cmux/references/panes-surfaces.md @@ -0,0 +1,25 @@ +# Panes and Surfaces + +```bash +# inspect +cmux list-panes +cmux list-pane-surfaces --pane pane:1 + +# create +cmux new-split right --panel pane:1 +cmux new-surface --type terminal --pane pane:1 +cmux new-surface --type browser --pane pane:1 --url https://example.com + +# focus and close +cmux focus-pane --pane pane:2 +cmux focus-panel --panel surface:7 +cmux close-surface --surface surface:7 + +# move and reorder +cmux move-surface --surface surface:7 --pane pane:2 --focus true +cmux move-surface --surface surface:7 --workspace workspace:2 --window window:1 --after surface:4 +cmux split-off --surface surface:7 right +cmux reorder-surface --surface surface:7 --before surface:3 +``` + +Surface identity is stable across move, reorder, and split-off. Layout commands are focus-neutral by default; pass `--focus true` only when the moved or created surface should be selected. diff --git a/.agents/skills/cmux/references/trigger-flash-and-health.md b/.agents/skills/cmux/references/trigger-flash-and-health.md new file mode 100644 index 000000000..f4f55951b --- /dev/null +++ b/.agents/skills/cmux/references/trigger-flash-and-health.md @@ -0,0 +1,15 @@ +# Trigger Flash and Surface Health + +Flash a surface or workspace for visual confirmation in the UI: + +```bash +cmux trigger-flash --surface surface:7 +cmux trigger-flash --workspace workspace:2 +``` + +Detect hidden, detached, or non-windowed surfaces before routing focused input when UI state may be stale: + +```bash +cmux surface-health +cmux surface-health --workspace workspace:2 +``` diff --git a/.agents/skills/cmux/references/windows-workspaces.md b/.agents/skills/cmux/references/windows-workspaces.md new file mode 100644 index 000000000..93224b78b --- /dev/null +++ b/.agents/skills/cmux/references/windows-workspaces.md @@ -0,0 +1,35 @@ +# Windows and Workspaces + +```bash +# inspect +cmux list-windows +cmux current-window +cmux list-workspaces +cmux current-workspace + +# lifecycle +cmux new-window +cmux focus-window --window window:2 +cmux close-window --window window:2 +cmux new-workspace +cmux select-workspace --workspace workspace:4 +cmux close-workspace --workspace workspace:4 + +# reorder and move +cmux reorder-workspace --workspace workspace:4 --before workspace:2 +cmux move-workspace-to-window --workspace workspace:4 --window window:1 +``` + +## Context-Menu Actions + +`cmux workspace-action` runs the workspace right-click actions (color, description, rename, pin, read state, ordering). Defaults to the caller's workspace; override with `--workspace `. + +```bash +cmux workspace-action --action set-color --color Blue # name or #RRGGBB hex +cmux workspace-action --action set-description --description "Ship checklist" +cmux workspace-action --action rename --title "infra" +cmux workspace-action --action pin +cmux workspace-action --action clear-color +``` + +Other actions: `unpin`, `clear-name`, `clear-description`, `mark-read`/`mark-unread`, `move-up`/`move-down`/`move-top`, `close-others`/`close-above`/`close-below`. Named colors: Red, Crimson, Orange, Amber, Olive, Green, Teal, Aqua, Blue, Navy, Indigo, Purple, Magenta, Rose, Brown, Charcoal. `set-color` tints a single workspace (stored as workspace state); the `workspaceColors` block in `cmux.json` defines the shared palette and selection/badge colors, not per-workspace assignments. diff --git a/.claude/skills/cmux b/.claude/skills/cmux new file mode 120000 index 000000000..869e81ce5 --- /dev/null +++ b/.claude/skills/cmux @@ -0,0 +1 @@ +../../.agents/skills/cmux \ No newline at end of file diff --git a/.claude/skills/cmux-markdown b/.claude/skills/cmux-markdown new file mode 120000 index 000000000..1b2a503c6 --- /dev/null +++ b/.claude/skills/cmux-markdown @@ -0,0 +1 @@ +../../.agents/skills/cmux-markdown \ No newline at end of file diff --git a/.claude/skills/cmux-settings b/.claude/skills/cmux-settings new file mode 120000 index 000000000..8fb093712 --- /dev/null +++ b/.claude/skills/cmux-settings @@ -0,0 +1 @@ +../../.agents/skills/cmux-settings \ No newline at end of file diff --git a/.claude/skills/cmux-workspace b/.claude/skills/cmux-workspace new file mode 120000 index 000000000..0700ee1d3 --- /dev/null +++ b/.claude/skills/cmux-workspace @@ -0,0 +1 @@ +../../.agents/skills/cmux-workspace \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json index 49ce00303..ad46fe7b9 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -1,6 +1,30 @@ { "version": 1, "skills": { + "cmux": { + "source": "manaflow-ai/cmux", + "sourceType": "github", + "skillPath": "skills/cmux/SKILL.md", + "computedHash": "75b2d4a46f84a04b23fac4fdcaafeb2b11310d44b9bcc0e9aa44a7ef5413a45e" + }, + "cmux-markdown": { + "source": "manaflow-ai/cmux", + "sourceType": "github", + "skillPath": "skills/cmux-markdown/SKILL.md", + "computedHash": "0870025e9cc20f3a7bde8eeebfafc7a2c5e273168b5252df961acac942c26e28" + }, + "cmux-settings": { + "source": "manaflow-ai/cmux", + "sourceType": "github", + "skillPath": "skills/cmux-settings/SKILL.md", + "computedHash": "26b221f4b15e2ae085e9b4e78f3b83170b1c7cabbf88f86d875f1a64a76be2e3" + }, + "cmux-workspace": { + "source": "manaflow-ai/cmux", + "sourceType": "github", + "skillPath": "skills/cmux-workspace/SKILL.md", + "computedHash": "dd9c0a13ad338511e1e74266146657e32fbeeed29956f1fbe8947cb6494c5296" + }, "instrumentation": { "source": "pydantic/skills", "sourceType": "github",