Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_lines<TAB>relative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
34660 CLI/cmux.swift
34499 CLI/cmux.swift
17954 Sources/AppDelegate.swift
16427 Sources/ContentView.swift
14270 Sources/TerminalController.swift
Expand Down
211 changes: 211 additions & 0 deletions CLI/CMUXCLI+CommandSuggestions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import Foundation

extension CMUXCLI {
func unknownCommandError(_ command: String) -> CLIError {
var message = "Unknown command '\(command)'."
if let suggestion = suggestedCommandName(for: command) {
message += " Did you mean '\(suggestion)'?"
}
message += " Run 'cmux --help' for the full command list."
return CLIError(message: message, exitCode: 2)
}

private func suggestedCommandName(for command: String) -> String? {
var bestName: String?
var bestDistance = Int.max

for candidate in Self.topLevelCommandNames where !candidate.hasPrefix("__") {
let distance = editDistance(command, candidate)
guard distance > 0, distance <= 2, distance < candidate.count else { continue }
if distance < bestDistance || (distance == bestDistance && candidate < (bestName ?? candidate)) {
bestName = candidate
bestDistance = distance
}
}

return bestName
}

private func editDistance(_ lhs: String, _ rhs: String) -> Int {
let left = Array(lhs)
let right = Array(rhs)
if left.isEmpty { return right.count }
if right.isEmpty { return left.count }

var previous = Array(0...right.count)
var current = Array(repeating: 0, count: right.count + 1)

for (leftIndex, leftCharacter) in left.enumerated() {
current[0] = leftIndex + 1
for (rightIndex, rightCharacter) in right.enumerated() {
if leftCharacter == rightCharacter {
current[rightIndex + 1] = previous[rightIndex]
} else {
current[rightIndex + 1] = min(min(previous[rightIndex + 1], current[rightIndex]), previous[rightIndex]) + 1
}
}
swap(&previous, &current)
}

return previous[right.count]
}

static let topLevelCommandNames: Set<String> = [
"__codex-teams-watch",
"__tmux-compat",
"agent-hibernation",
"auth",
"bind-key",
"break-pane",
"browser",
"browser-back",
"browser-forward",
"browser-reload",
"browser-status",
"capabilities",
"capture-pane",
"claude-hook",
"claude-teams",
"clear-history",
"clear-log",
"clear-notifications",
"clear-progress",
"clear-status",
"close-surface",
"close-window",
"close-workspace",
"cloud",
"codex",
"codex-hook",
"codex-teams",
"config",
"copy-mode",
"current-window",
"current-workspace",
"debug-terminals",
"detach-tab",
"diff",
"disable-browser",
"dismiss-notification",
"display-message",
"docs",
"drag-surface-to-split",
"enable-browser",
"events",
"feedback",
"feed",
"feed-hook",
"find-window",
"focus-pane",
"focus-panel",
"focus-webview",
"focus-window",
"get-url",
"help",
"hooks",
"identify",
"is-webview-focused",
"join-pane",
"jump-to-unread",
"last-pane",
"last-window",
"list-buffers",
"list-log",
"list-notifications",
"list-pane-surfaces",
"list-panels",
"list-panes",
"list-status",
"list-windows",
"list-workspaces",
"log",
"login",
"logout",
"markdown",
"mark-notification-read",
"memory",
"mobile",
"move-surface",
"move-tab-to-new-workspace",
"move-workspace-to-window",
"navigate",
"new-pane",
"new-split",
"new-surface",
"new-window",
"new-workspace",
"next-window",
"notify",
"omc",
"omo",
"omx",
"open",
"open-browser",
"open-notification",
"paste-buffer",
"ping",
"pipe-pane",
"popup",
"previous-window",
"read-screen",
"refresh-surfaces",
"reload-config",
"remote-daemon-status",
"rename-tab",
"rename-window",
"rename-workspace",
"reorder-surface",
"reorder-workspace",
"reorder-workspaces",
"resize-pane",
"respawn-pane",
"restore-session",
"right-sidebar",
"rpc",
"select-workspace",
"send",
"send-key",
"send-key-panel",
"send-panel",
"set-app-focus",
"set-buffer",
"set-hook",
"set-progress",
"set-status",
"settings",
"setup-hooks",
"shortcuts",
"simulate-app-active",
"sidebar",
"sidebar-state",
"split-off",
"ssh",
"ssh-pty-attach",
"ssh-session-attach",
"ssh-session-cleanup",
"ssh-session-end",
"ssh-session-list",
"ssh-tmux",
"surface",
"surface-health",
"surface-resume",
"swap-pane",
"tab-action",
"themes",
"top",
"tree",
"trigger-flash",
"unbind-key",
"uninstall-hooks",
"version",
"vm",
"vm-pty-attach",
"vm-pty-connect",
"vm-ssh-attach",
"wait-for",
"welcome",
"workspace",
"workspace-action",
"workspace-group",
]
Comment on lines +53 to +210

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated source of truth for the command list.

topLevelCommandNames must be kept manually in sync with the actual command dispatch switch statements in cmux.swift. If a command is added/removed there without updating this list, suggestions silently go stale (missing suggestions for new commands, or suggesting removed ones) — a low-severity but easy-to-miss drift.

Do you want me to add a regression test that enumerates the real dispatch cases (via reflection/parsing or a shared constant) and asserts parity with topLevelCommandNames, so drift is caught in CI?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/CMUXCLI`+CommandSuggestions.swift around lines 53 - 210, The command
suggestion list is duplicated and can drift from the real command dispatch in
cmux.swift. Update CMUXCLI+CommandSuggestions.topLevelCommandNames to be derived
from a single shared source used by the dispatch switch, or add a parity test
that checks the switch cases against topLevelCommandNames so additions/removals
stay in sync. Refer to topLevelCommandNames and the command dispatch in
cmux.swift when making the fix.

}
2 changes: 1 addition & 1 deletion CLI/CMUXCLI+ThemeSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ extension CMUXCLI {

func cmuxThemeOverrideConfigURL(targetBundleIdentifier: String) throws -> URL {
guard let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
throw CLIError(message: "Unable to resolve Application Support directory")
throw CLIError(message: "Failed to locate the Application Support directory")
}
return CmuxGhosttyConfigPathResolver().editableConfigURL(
currentBundleIdentifier: targetBundleIdentifier,
Expand Down
Loading
Loading