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
68 changes: 62 additions & 6 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19506,6 +19506,8 @@ struct CMUXCLI {
launchCommand: launchCommand,
isRestorable: false,
agentLifecycle: shouldPromoteActiveSession ? .running : .unknown,
runtimeStatus: shouldPromoteActiveSession ? .running : nil,
updateRuntimeStatus: shouldPromoteActiveSession,
markActive: shouldPromoteActiveSession,
turnId: parsedInput.turnId
)
Expand Down Expand Up @@ -19553,7 +19555,7 @@ struct CMUXCLI {
client: client,
workspaceId: workspaceId,
surfaceId: surfaceId,
value: "Running",
value: String(localized: "agent.generic.status.running", defaultValue: "Running"),
icon: "bolt.fill",
color: "#4C8DFF",
pid: claudePid
Expand Down Expand Up @@ -19618,6 +19620,10 @@ struct CMUXCLI {
agentLifecycle: .idle,
lastSubtitle: completion?.subtitle,
lastBody: completion?.body,
lastNotificationStatus: .idle,
updateLastNotificationStatus: true,
runtimeStatus: .idle,
updateRuntimeStatus: true,
markActive: true,
allowsNewSessionReplacement: true
)
Expand All @@ -19644,7 +19650,7 @@ struct CMUXCLI {
client: client,
workspaceId: workspaceId,
surfaceId: surfaceId,
value: "Idle",
value: String(localized: "agent.generic.notification.status.idle", defaultValue: "Idle"),
icon: "pause.circle.fill",
color: "#8E8E93"
)
Expand Down Expand Up @@ -19718,6 +19724,8 @@ struct CMUXCLI {
transcriptPath: parsedInput.transcriptPath,
isRestorable: true,
agentLifecycle: .running,
runtimeStatus: .running,
updateRuntimeStatus: true,
markActive: true,
turnId: parsedInput.turnId
)
Expand All @@ -19744,7 +19752,7 @@ struct CMUXCLI {
client: client,
workspaceId: workspaceId,
surfaceId: surfaceId,
value: "Running",
value: String(localized: "agent.generic.status.running", defaultValue: "Running"),
icon: "bolt.fill",
color: "#4C8DFF"
)
Expand Down Expand Up @@ -19786,6 +19794,14 @@ struct CMUXCLI {
summary.body.contains("needs your attention") || summary.body.contains("needs your input") {
summary = (subtitle: mappedSession.lastSubtitle ?? summary.subtitle, body: savedBody)
}
if shouldSuppressClaudeIdlePromptNotification(
parsedInput: parsedInput,
mappedSession: mappedSession
) {
telemetry.breadcrumb("claude-hook.notification.idle-prompt-suppressed")
print("{}")
return
}

let surfaceId = try resolvePreferredSurfaceIdForClaudeHook(
preferred: mappedSession?.surfaceId,
Expand All @@ -19809,7 +19825,11 @@ struct CMUXCLI {
transcriptPath: parsedInput.transcriptPath,
agentLifecycle: .needsInput,
lastSubtitle: summary.subtitle,
lastBody: summary.body
lastBody: summary.body,
lastNotificationStatus: .needsInput,
updateLastNotificationStatus: true,
runtimeStatus: .needsInput,
updateRuntimeStatus: true
)
}

Expand All @@ -19824,7 +19844,10 @@ struct CMUXCLI {
client: client,
workspaceId: workspaceId,
surfaceId: surfaceId,
value: "Needs input",
value: String.localizedStringWithFormat(
String(localized: "agent.generic.notification.status.needsInput", defaultValue: "%@ needs input"),
title
),
icon: "bell.fill",
color: "#4C8DFF"
)
Expand Down Expand Up @@ -20001,7 +20024,7 @@ struct CMUXCLI {
let toolStatus = describeToolUse(parsedInput.object) {
statusValue = toolStatus
} else {
statusValue = "Running"
statusValue = String(localized: "agent.generic.status.running", defaultValue: "Running")
}
try setClaudeStatus(
client: client,
Expand Down Expand Up @@ -20202,6 +20225,39 @@ struct CMUXCLI {
return source.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "clear"
}

private func shouldSuppressClaudeIdlePromptNotification(
parsedInput: ClaudeHookParsedInput,
mappedSession: ClaudeHookSessionRecord?
) -> Bool {
guard isIdlePromptNotification(parsedInput) else {
return false
}
return mappedSession?.runtimeStatus == .idle
}

private func isIdlePromptNotification(_ parsedInput: ClaudeHookParsedInput) -> Bool {
let signalParts = notificationSignalParts(in: parsedInput.object)
+ notificationSignalParts(in: parsedInput.rawObject)
return signalParts.contains(where: notificationSignalIsIdlePrompt)
}

private func notificationSignalParts(in object: [String: Any]?) -> [String] {
guard let object else { return [] }
let nested = (object["notification"] as? [String: Any]) ?? (object["data"] as? [String: Any]) ?? [:]
return [
firstString(in: object, keys: ["notification_type", "notificationType", "matcher", "reason", "type", "kind"]),
firstString(in: nested, keys: ["notification_type", "notificationType", "matcher", "reason", "type", "kind"]),
].compactMap { $0 }
}

private func notificationSignalIsIdlePrompt(_ signal: String) -> Bool {
let lower = signal.lowercased()
let tokens = notificationCueTokens(lower)
let compact = lower.filter { $0.isLetter || $0.isNumber }
return (tokens.count == 2 && tokens[0] == "idle" && tokens[1] == "prompt")
|| compact == "idleprompt"
}

private func socketPanelOption(_ surfaceId: String?) -> String {
guard let surfaceId = surfaceId?.trimmingCharacters(in: .whitespacesAndNewlines),
!surfaceId.isEmpty,
Expand Down
Loading
Loading