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
187 changes: 187 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,193 @@
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"contextMenu.muteWorkspaceNotifications": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Mute Workspace Notifications"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペース通知をミュート"
}
}
}
},
"contextMenu.muteWorkspacesNotifications": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Mute Workspaces Notifications"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペース通知をミュート"
}
}
}
},
"contextMenu.unmuteWorkspaceNotifications": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Unmute Workspace Notifications"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペース通知のミュートを解除"
}
}
}
},
"contextMenu.unmuteWorkspacesNotifications": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Unmute Workspaces Notifications"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークスペース通知のミュートを解除"
}
}
}
},
"notificationMute.duration.eightHours": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "8 Hours"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "8時間"
}
}
}
},
"notificationMute.duration.fifteenMinutes": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "15 Minutes"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "15分"
}
}
}
},
"notificationMute.duration.fourHours": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "4 Hours"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "4時間"
}
}
}
},
"notificationMute.duration.oneHour": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "1 Hour"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "1時間"
}
}
}
},
"notificationMute.duration.untilUnmuted": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Until Unmuted"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "解除するまで"
}
}
}
},
"terminalContextMenu.muteTabNotifications": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Mute Tab Notifications"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "タブ通知をミュート"
}
}
}
},
"terminalContextMenu.unmuteTabNotifications": {
Comment on lines +5 to 192

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing translations for all supported locales

All ten new mute/unmute string keys (contextMenu.muteWorkspaceNotifications, contextMenu.muteWorkspacesNotifications, contextMenu.unmuteWorkspaceNotifications, contextMenu.unmuteWorkspacesNotifications, notificationMute.duration.*, terminalContextMenu.*) only carry en and ja entries. Existing catalog strings already include zh-Hans, zh-Hant, ko, de, es, fr, it, da, pl, ru, bs, ar, and potentially others (visible starting at line 241). Users in those locales will see raw English fallback text in the mute menus and submenu items.

Rule Used: Flag production user-facing text that is not fully... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Unmute Tab Notifications"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "タブ通知のミュートを解除"
}
}
}
},
"command.auth.signIn.title": {
"extractionState": "manual",
"localizations": {
Expand Down
106 changes: 16 additions & 90 deletions Sources/CmuxConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,96 +192,6 @@ struct CmuxResolvedWorkspaceGroupConfig: Sendable, Equatable {
let newWorkspacePlacement: WorkspaceGroupNewPlacement?
}

enum CmuxNotificationHooksMode: String, Codable, Sendable, Hashable {
case append
case replace
}

struct CmuxNotificationConfigDefinition: Codable, Sendable, Hashable {
var hooks: [CmuxNotificationHookDefinition]?
var hooksMode: CmuxNotificationHooksMode?

private enum CodingKeys: String, CodingKey {
case hooks
case hooksMode
}
}

struct CmuxNotificationHookDefinition: Codable, Sendable, Hashable {
static let defaultTimeoutSeconds: TimeInterval = 20

var id: String
var command: String
var timeoutSeconds: TimeInterval?
var enabled: Bool

private enum CodingKeys: String, CodingKey {
case id
case command
case timeoutSeconds
case enabled
}

init(
id: String,
command: String,
timeoutSeconds: TimeInterval? = nil,
enabled: Bool = true
) {
self.id = id
self.command = command
self.timeoutSeconds = timeoutSeconds
self.enabled = enabled
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let decodedID = try Self.requiredTrimmedString(forKey: .id, in: container)
let decodedCommand = try Self.requiredTrimmedString(forKey: .command, in: container)
let decodedTimeout = try container.decodeIfPresent(TimeInterval.self, forKey: .timeoutSeconds)
if let decodedTimeout, !decodedTimeout.isFinite || decodedTimeout <= 0 {
throw DecodingError.dataCorruptedError(
forKey: .timeoutSeconds,
in: container,
debugDescription: "timeoutSeconds must be greater than 0"
)
}

id = decodedID
command = decodedCommand
timeoutSeconds = decodedTimeout
enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(command, forKey: .command)
try container.encodeIfPresent(timeoutSeconds, forKey: .timeoutSeconds)
try container.encode(enabled, forKey: .enabled)
}

var resolvedTimeoutSeconds: TimeInterval {
timeoutSeconds ?? Self.defaultTimeoutSeconds
}

private static func requiredTrimmedString(
forKey key: CodingKeys,
in container: KeyedDecodingContainer<CodingKeys>
) throws -> String {
let value = try container.decode(String.self, forKey: key)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "\(key.stringValue) must not be blank"
)
}
return value
}
}

struct CmuxResolvedNotificationHook: Sendable, Hashable {
let id: String
let command: String
Expand Down Expand Up @@ -1896,6 +1806,7 @@ final class CmuxConfigStore: ObservableObject {
@Published private(set) var workspaceGroupConfigs: [CmuxResolvedWorkspaceGroupConfig] = []
@Published private(set) var surfaceTabBarButtons: [CmuxSurfaceTabBarButton] = CmuxSurfaceTabBarButton.defaults
@Published private(set) var notificationHooks: [CmuxResolvedNotificationHook] = []
@Published private(set) var notificationMuteMenuOptions: [NotificationMuteMenuOption] = NotificationMuteMenuOption.defaultOptions

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 | 🟠 Major | 🏗️ Heavy lift

Avoid adding new @Published state in cmux-owned production Swift.

This adds new Combine-based state (@Published) to CmuxConfigStore. The repo rules require new state to use modern Observation (@Observable + snapshots) rather than expanding ObservableObject/@Published usage.

As per coding guidelines, “Do not use ObservableObject, @Published, @StateObject, or @EnvironmentObject for new cmux-owned SwiftUI state; use @Observable plus @State or value snapshots instead,” and “Flag new Combine usage … when Observation and async/await are available.”

🤖 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 `@Sources/CmuxConfig.swift` at line 1809, The notificationMuteMenuOptions
property in CmuxConfigStore is using the deprecated `@Published` decorator which
violates the coding guidelines requiring modern Observation framework for new
state. Remove the `@Published` annotation from the notificationMuteMenuOptions
declaration and refactor CmuxConfigStore to use `@Observable` macro instead of
ObservableObject, or alternatively implement this property using value snapshots
and async/await patterns as per the modern state management guidelines specified
in the repo rules.

Source: Coding guidelines

@Published private(set) var configurationIssues: [CmuxConfigIssue] = []
@Published private(set) var configRevision: UInt64 = 0

Expand Down Expand Up @@ -2293,6 +2204,10 @@ final class CmuxConfigStore: ObservableObject {
entry.result.config.map { (path: entry.path, config: $0) }
}
)
let resolvedNotificationMuteMenuOptions = resolveNotificationMuteMenuOptions(
localConfig: localConfig,
globalConfig: globalConfig
)

loadedCommands = commands
loadedActions = resolvedActions
Expand All @@ -2318,6 +2233,7 @@ final class CmuxConfigStore: ObservableObject {
surfaceTabBarWorkspaceCommands = resolvedWorkspaceButtons.workspaceCommands
surfaceTabBarButtons = resolvedWorkspaceButtons.buttons
notificationHooks = resolvedNotificationHooks
notificationMuteMenuOptions = resolvedNotificationMuteMenuOptions
resolvedNewWorkspaceActionCache = resolvedNewWorkspaceAction.action
resolvedNewWorkspaceCommandCache = resolvedNewWorkspaceAction.command
if let issue = resolvedNewWorkspaceAction.issue {
Expand Down Expand Up @@ -2346,6 +2262,16 @@ final class CmuxConfigStore: ObservableObject {
return fallbackLocalPath.map { [$0] } ?? []
}

private func resolveNotificationMuteMenuOptions(
localConfig: CmuxConfigFile?,
globalConfig: CmuxConfigFile?
) -> [NotificationMuteMenuOption] {
NotificationMuteMenuOption.options(
configuredDurations: localConfig?.notifications?.muteDurations ??
globalConfig?.notifications?.muteDurations
)
Comment thread
cursor[bot] marked this conversation as resolved.
}

private func resolveNotificationHooks(
globalConfig: CmuxConfigFile?,
localConfigs: [(path: String, config: CmuxConfigFile)]
Expand Down
Loading
Loading