Skip to content
Closed
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
82 changes: 72 additions & 10 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,33 @@ struct CMUXCLI {
let payload = try client.sendV2(method: "workspace.rename", params: params)
printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: v2OKSummary(payload, idFormat: idFormat, kinds: ["workspace"]))

case "set-workspace-color":
let (wsArg, rem0) = parseOption(commandArgs, name: "--workspace")
let workspaceArg = wsArg ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let isClear = rem0.contains("--clear")
let rem1 = rem0.filter { $0 != "--clear" }
if let unknownFlag = rem1.first(where: { $0.hasPrefix("--") && $0 != "--" }) {
throw CLIError(message: "set-workspace-color: unknown flag '\(unknownFlag)'")
}
let hasPositionalArgs = !rem1.filter({ !$0.hasPrefix("--") }).isEmpty
if isClear, hasPositionalArgs {
throw CLIError(message: "set-workspace-color: --clear and a color value are mutually exclusive")
}
let wsId = try resolveWorkspaceId(workspaceArg, client: client)
var params: [String: Any] = ["workspace_id": wsId]
Comment on lines +1378 to +1379

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 | 🟠 Major

Scope workspace index resolution to the selected --window.

Line 1375 calls resolveWorkspaceId without any window context. cmux --window <w> set-workspace-color --workspace 1 ... can therefore update the first workspace index 1 returned across all windows, not the workspace inside the selected window.

🧭 Suggested direction
-            let wsId = try resolveWorkspaceId(workspaceArg, client: client)
+            let windowHandle = try normalizeWindowHandle(windowId, client: client)
+            let wsId = try resolveWorkspaceId(workspaceArg, windowHandle: windowHandle, client: client)
private func resolveWorkspaceId(_ raw: String?, windowHandle: String?, client: SocketClient) throws -> String {
    // For numeric handles, call workspace.list with ["window_id": windowHandle]
    // when a window override is present.
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 1375 - 1376, The workspace-ID resolution needs
to be scoped to the selected window: change calls to resolveWorkspaceId (here in
set-workspace-color) to pass the window handle (e.g., windowArg/windowHandle)
and update resolveWorkspaceId to accept windowHandle: when a numeric workspace
identifier is given and a window override is present, call the backend
workspace.list with parameters including ["window_id": windowHandle] so the
lookup only searches workspaces in that window; ensure the returned wsId is
derived from that filtered list and keep the existing params dictionary (var
params) intact for later use.

if isClear {
params["color"] = NSNull()
} else {
let colorArgs = rem1.dropFirst(rem1.first == "--" ? 1 : 0)
let color = colorArgs.joined(separator: " ").trimmingCharacters(in: .whitespacesAndNewlines)
guard !color.isEmpty else {
throw CLIError(message: "set-workspace-color requires a color hex (e.g. \"#C0392B\") or --clear")
}
params["color"] = color
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let payload = try client.sendV2(method: "workspace.set_color", params: params)
printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: v2OKSummary(payload, idFormat: idFormat, kinds: ["workspace"]))

case "current-workspace":
let response = try sendV1Command("current_workspace", client: client)
if jsonOutput {
Expand Down Expand Up @@ -1479,17 +1506,33 @@ struct CMUXCLI {
let title = optionValue(commandArgs, name: "--title") ?? "Notification"
let subtitle = optionValue(commandArgs, name: "--subtitle") ?? ""
let body = optionValue(commandArgs, name: "--body") ?? ""
let color = optionValue(commandArgs, name: "--color")
if commandArgs.contains("--color"), color == nil || (color?.hasPrefix("-") ?? false) {
throw CLIError(message: "notify: --color requires a hex value (e.g. \"#C0392B\")")
}

let notifyWsFlag = optionValue(commandArgs, name: "--workspace")
let workspaceArg = notifyWsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil)
let surfaceArg = optionValue(commandArgs, name: "--surface") ?? (notifyWsFlag == nil && windowId == nil ? ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"] : nil)

let targetWorkspace = try resolveWorkspaceId(workspaceArg, client: client)
let targetSurface = try resolveSurfaceId(surfaceArg, workspaceId: targetWorkspace, client: client)
let wsId = try normalizeWorkspaceHandle(workspaceArg, client: client)
let sfId = try normalizeSurfaceHandle(surfaceArg, client: client, workspaceHandle: wsId)

let payload = "\(title)|\(subtitle)|\(body)"
let response = try sendV1Command("notify_target \(targetWorkspace) \(targetSurface) \(payload)", client: client)
print(response)
var params: [String: Any] = ["title": title, "subtitle": subtitle, "body": body]
if let wsId { params["workspace_id"] = wsId }
if let sfId { params["surface_id"] = sfId }
if let color { params["color"] = color }
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let method: String
if wsId != nil, sfId != nil {
method = "notification.create_for_target"
} else if sfId != nil {
method = "notification.create_for_surface"
} else {
method = "notification.create"
}
let payload = try client.sendV2(method: method, params: params)
printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: v2OKSummary(payload, idFormat: idFormat))

case "list-notifications":
let response = try sendV1Command("list_notifications", client: client)
Expand Down Expand Up @@ -4531,6 +4574,21 @@ struct CMUXCLI {
cmux rename-workspace "backend logs"
cmux rename-window --workspace workspace:2 "agent run"
"""
case "set-workspace-color":
return """
Usage: cmux set-workspace-color [--workspace <id|ref|index>] (--clear | [--] <#hex>)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

Set or clear the custom color for a workspace tab.

Flags:
--workspace <id|ref|index> Workspace to color (default: current/$CMUX_WORKSPACE_ID)
--clear Remove the custom color

Example:
cmux set-workspace-color "#C0392B"
cmux set-workspace-color --workspace workspace:2 "#1565C0"
cmux set-workspace-color --clear
"""
case "current-workspace":
return """
Usage: cmux current-workspace
Expand Down Expand Up @@ -4799,18 +4857,21 @@ struct CMUXCLI {
return """
Usage: cmux notify [flags]

Send a notification to a workspace/surface.
Send a notification to a workspace/surface. If the workspace has a custom
color and no explicit --color is given, the notification inherits the
workspace color automatically.

Flags:
--title <text> Notification title (default: "Notification")
--subtitle <text> Notification subtitle
--body <text> Notification body
--workspace <id|ref> Target workspace (default: $CMUX_WORKSPACE_ID)
--surface <id|ref> Target surface (default: $CMUX_SURFACE_ID)
--color <#hex> Notification accent color (default: workspace color or blue)
--workspace <id|ref|index> Target workspace (default: $CMUX_WORKSPACE_ID)
--surface <id|ref|index> Target surface (default: $CMUX_SURFACE_ID)

Example:
cmux notify --title "Build done" --body "All tests passed"
cmux notify --title "Error" --subtitle "test.swift" --body "Line 42: syntax error"
cmux notify --title "Error" --body "Failed" --color "#C0392B"
"""
case "list-notifications":
return """
Expand Down Expand Up @@ -6950,13 +7011,14 @@ struct CMUXCLI {
select-workspace --workspace <id|ref>
rename-workspace [--workspace <id|ref>] <title>
rename-window [--workspace <id|ref>] <title>
set-workspace-color [--workspace <id|ref|index>] (--clear | <#hex>)
current-workspace
read-screen [--workspace <id|ref>] [--surface <id|ref>] [--scrollback] [--lines <n>]
send [--workspace <id|ref>] [--surface <id|ref>] <text>
send-key [--workspace <id|ref>] [--surface <id|ref>] <key>
send-panel --panel <id|ref> [--workspace <id|ref>] <text>
send-key-panel --panel <id|ref> [--workspace <id|ref>] <key>
notify --title <text> [--subtitle <text>] [--body <text>] [--workspace <id|ref>] [--surface <id|ref>]
notify --title <text> [--subtitle <text>] [--body <text>] [--color <#hex>] [--workspace <id|ref|index>] [--surface <id|ref|index>]
list-notifications
clear-notifications
claude-hook <session-start|stop|notification> [--workspace <id|ref>] [--surface <id|ref>]
Expand Down
8 changes: 6 additions & 2 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1463,12 +1463,14 @@ class GhosttyApp {
let command = actionTitle.isEmpty ? tabTitle : actionTitle
let body = actionBody
let surfaceId = tabManager.focusedSurfaceId(for: tabId)
let wsColor = tabManager.tabs.first(where: { $0.id == tabId })?.customColor
TerminalNotificationStore.shared.addNotification(
tabId: tabId,
surfaceId: surfaceId,
title: command,
subtitle: "",
body: body
body: body,
color: wsColor
)
return true
}
Expand Down Expand Up @@ -1722,12 +1724,14 @@ class GhosttyApp {
let tabTitle = AppDelegate.shared?.tabManager?.titleForTab(tabId) ?? "Terminal"
let command = actionTitle.isEmpty ? tabTitle : actionTitle
let body = actionBody
let wsColor = AppDelegate.shared?.tabManager?.tabs.first(where: { $0.id == tabId })?.customColor
TerminalNotificationStore.shared.addNotification(
tabId: tabId,
surfaceId: surfaceId,
title: command,
subtitle: "",
body: body
body: body,
color: wsColor
)
}
return true
Expand Down
28 changes: 24 additions & 4 deletions Sources/NotificationsPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,22 +172,31 @@ private struct ShortcutAnnotation: View {
}

private struct NotificationRow: View {
@Environment(\.colorScheme) private var colorScheme
let notification: TerminalNotification
let tabTitle: String?
let onOpen: () -> Void
let onClear: () -> Void
let focusedNotificationId: FocusState<UUID?>.Binding

private var dotColor: Color {
if let hex = notification.color,
let custom = WorkspaceTabColorSettings.displayColor(hex: hex, colorScheme: colorScheme) {
return custom
}
return cmuxAccentColor()
}

var body: some View {
HStack(alignment: .top, spacing: 12) {
Button(action: onOpen) {
HStack(alignment: .top, spacing: 12) {
Circle()
.fill(notification.isRead ? Color.clear : cmuxAccentColor())
.fill(notification.isRead ? Color.clear : dotColor)
.frame(width: 8, height: 8)
.overlay(
Circle()
.stroke(cmuxAccentColor().opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
.stroke(dotColor.opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
)
.padding(.top, 6)

Expand Down Expand Up @@ -236,8 +245,19 @@ private struct NotificationRow: View {
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color(nsColor: .controlBackgroundColor))
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(Color(nsColor: .controlBackgroundColor))
if notification.color != nil {
HStack(spacing: 0) {
Rectangle()
.fill(dotColor)
.frame(width: 3)
Spacer()
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 10))
)
}
}
Expand Down
Loading