mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
chore(swift): restore compact conditional bodies (#103832)
* style(swift): restore compact conditional bodies * fix(ios): remove redundant startup timeout await * chore(swift): sync native i18n inventory
This commit is contained in:
committed by
GitHub
parent
906f1b816f
commit
091584b197
+576
-576
File diff suppressed because it is too large
Load Diff
@@ -88,15 +88,9 @@ struct OpenClawLiveActivity: Widget {
|
||||
}
|
||||
|
||||
private func dotColor(state: OpenClawActivityAttributes.ContentState) -> Color {
|
||||
if state.isDisconnected {
|
||||
return OpenClawActivityStyle.danger
|
||||
}
|
||||
if state.isConnecting {
|
||||
return OpenClawActivityStyle.info
|
||||
}
|
||||
if state.isIdle {
|
||||
return OpenClawActivityStyle.ok
|
||||
}
|
||||
if state.isDisconnected { return OpenClawActivityStyle.danger }
|
||||
if state.isConnecting { return OpenClawActivityStyle.info }
|
||||
if state.isIdle { return OpenClawActivityStyle.ok }
|
||||
return OpenClawActivityStyle.warn
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,15 +352,9 @@ final class ShareViewController: UIViewController {
|
||||
let text = self.sanitizeDraftFragment(payload.text)
|
||||
let url = payload.url?.absoluteString.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
|
||||
if let title, !title.isEmpty {
|
||||
lines.append(title)
|
||||
}
|
||||
if let text, !text.isEmpty {
|
||||
lines.append(text)
|
||||
}
|
||||
if !url.isEmpty {
|
||||
lines.append(url)
|
||||
}
|
||||
if let title, !title.isEmpty { lines.append(title) }
|
||||
if let text, !text.isEmpty { lines.append(text) }
|
||||
if !url.isEmpty { lines.append(url) }
|
||||
|
||||
return lines.joined(separator: "\n\n")
|
||||
}
|
||||
@@ -483,9 +477,7 @@ final class ShareViewController: UIViewController {
|
||||
quality -= 0.1
|
||||
}
|
||||
guard let fallback = image.jpegData(compressionQuality: 0.35) else { return nil }
|
||||
if fallback.count <= maxBytes {
|
||||
return fallback
|
||||
}
|
||||
if fallback.count <= maxBytes { return fallback }
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -233,24 +233,12 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
|
||||
unread: Bool? = nil) throws -> String
|
||||
{
|
||||
var params: [String: Any] = ["key": key]
|
||||
if let agentId {
|
||||
params["agentId"] = agentId
|
||||
}
|
||||
if let label {
|
||||
params["label"] = label ?? NSNull()
|
||||
}
|
||||
if let category {
|
||||
params["category"] = category ?? NSNull()
|
||||
}
|
||||
if let pinned {
|
||||
params["pinned"] = pinned
|
||||
}
|
||||
if let archived {
|
||||
params["archived"] = archived
|
||||
}
|
||||
if let unread {
|
||||
params["unread"] = unread
|
||||
}
|
||||
if let agentId { params["agentId"] = agentId }
|
||||
if let label { params["label"] = label ?? NSNull() }
|
||||
if let category { params["category"] = category ?? NSNull() }
|
||||
if let pinned { params["pinned"] = pinned }
|
||||
if let archived { params["archived"] = archived }
|
||||
if let unread { params["unread"] = unread }
|
||||
return try self.encodeJSONObject(params)
|
||||
}
|
||||
|
||||
@@ -806,9 +794,7 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
|
||||
let task = Task {
|
||||
let stream = await self.gateway.subscribeServerEvents()
|
||||
for await evt in stream {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled { return }
|
||||
if let mapped = Self.mapEventFrame(evt) {
|
||||
continuation.yield(mapped)
|
||||
}
|
||||
|
||||
@@ -515,9 +515,7 @@ struct AgentProDreamingDestination: View {
|
||||
}
|
||||
|
||||
private func dreamingPhaseState(_ phase: DreamingPhaseStatusLite) -> String {
|
||||
if phase.enabled == false {
|
||||
return "off"
|
||||
}
|
||||
if phase.enabled == false { return "off" }
|
||||
return phase.managedCronPresent == true ? "scheduled" : "setup"
|
||||
}
|
||||
|
||||
@@ -621,15 +619,9 @@ struct AgentProDreamingDestination: View {
|
||||
let id = markerDay ?? Self.dayID(title)
|
||||
let bodyLines = rawLines.enumerated().compactMap { offset, line -> String? in
|
||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if offset == dateLineIndex {
|
||||
return nil
|
||||
}
|
||||
if trimmed.hasPrefix("<!--") && trimmed.hasSuffix("-->") {
|
||||
return nil
|
||||
}
|
||||
if trimmed == "#" || trimmed == "# Dream Diary" {
|
||||
return nil
|
||||
}
|
||||
if offset == dateLineIndex { return nil }
|
||||
if trimmed.hasPrefix("<!--") && trimmed.hasSuffix("-->") { return nil }
|
||||
if trimmed == "#" || trimmed == "# Dream Diary" { return nil }
|
||||
return line
|
||||
}
|
||||
let body = bodyLines
|
||||
|
||||
@@ -124,9 +124,7 @@ struct AgentProNodesDestination: View {
|
||||
private var sortedPresenceEntries: [PresenceEntry] {
|
||||
(self.overview?.presence ?? [])
|
||||
.sorted { lhs, rhs in
|
||||
if lhs.ts != rhs.ts {
|
||||
return lhs.ts > rhs.ts
|
||||
}
|
||||
if lhs.ts != rhs.ts { return lhs.ts > rhs.ts }
|
||||
return (Self.presenceLabel(lhs) ?? lhs.presenceKey)
|
||||
.localizedCaseInsensitiveCompare(Self.presenceLabel(rhs) ?? rhs.presenceKey) == .orderedAscending
|
||||
}
|
||||
@@ -343,15 +341,9 @@ struct AgentProNodesDestination: View {
|
||||
|
||||
private static func presenceIcon(_ entry: PresenceEntry) -> String {
|
||||
let family = Self.normalized(entry.devicefamily)?.lowercased()
|
||||
if family?.contains("phone") == true {
|
||||
return "iphone"
|
||||
}
|
||||
if family?.contains("tablet") == true || family?.contains("pad") == true {
|
||||
return "ipad"
|
||||
}
|
||||
if family?.contains("desktop") == true || family?.contains("mac") == true {
|
||||
return "desktopcomputer"
|
||||
}
|
||||
if family?.contains("phone") == true { return "iphone" }
|
||||
if family?.contains("tablet") == true || family?.contains("pad") == true { return "ipad" }
|
||||
if family?.contains("desktop") == true || family?.contains("mac") == true { return "desktopcomputer" }
|
||||
return "display"
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,7 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
func agentTint(for agent: AgentSummary, state: AgentRosterState) -> Color {
|
||||
if agent.id == self.activeAgentID {
|
||||
return OpenClawBrand.accent
|
||||
}
|
||||
if agent.id == self.activeAgentID { return OpenClawBrand.accent }
|
||||
return state.color.opacity(0.62)
|
||||
}
|
||||
|
||||
@@ -49,9 +47,7 @@ extension AgentProTab {
|
||||
|
||||
func agentRosterState(for agent: AgentSummary) -> AgentRosterState {
|
||||
guard self.gatewayConnected else { return .ready }
|
||||
if agent.id == self.activeAgentID {
|
||||
return .online
|
||||
}
|
||||
if agent.id == self.activeAgentID { return .online }
|
||||
return .ready
|
||||
}
|
||||
|
||||
@@ -208,17 +204,11 @@ extension AgentProTab {
|
||||
|
||||
static func duration(milliseconds: Int) -> String {
|
||||
let seconds = max(0, milliseconds / 1000)
|
||||
if seconds < 60 {
|
||||
return "\(seconds)s"
|
||||
}
|
||||
if seconds < 60 { return "\(seconds)s" }
|
||||
let minutes = seconds / 60
|
||||
if minutes < 60 {
|
||||
return "\(minutes)m"
|
||||
}
|
||||
if minutes < 60 { return "\(minutes)m" }
|
||||
let hours = minutes / 60
|
||||
if hours < 24 {
|
||||
return "\(hours)h"
|
||||
}
|
||||
if hours < 24 { return "\(hours)h" }
|
||||
return "\(hours / 24)d"
|
||||
}
|
||||
|
||||
|
||||
@@ -491,12 +491,8 @@ extension AgentProTab {
|
||||
|
||||
var sortedAgents: [AgentSummary] {
|
||||
appModel.gatewayAgents.sorted { lhs, rhs in
|
||||
if lhs.id == self.activeAgentID {
|
||||
return true
|
||||
}
|
||||
if rhs.id == self.activeAgentID {
|
||||
return false
|
||||
}
|
||||
if lhs.id == self.activeAgentID { return true }
|
||||
if rhs.id == self.activeAgentID { return false }
|
||||
return self.agentName(for: lhs)
|
||||
.localizedCaseInsensitiveCompare(self.agentName(for: rhs)) == .orderedAscending
|
||||
}
|
||||
@@ -545,28 +541,18 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
var emptyAgentsTitle: String {
|
||||
if !self.gatewayConnected {
|
||||
return "Agents unavailable"
|
||||
}
|
||||
if !agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return "No matches"
|
||||
}
|
||||
if agentRosterFilter != .all {
|
||||
return "No \(agentRosterFilter.title.lowercased()) agents"
|
||||
}
|
||||
if !self.gatewayConnected { return "Agents unavailable" }
|
||||
if !agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return "No matches" }
|
||||
if agentRosterFilter != .all { return "No \(agentRosterFilter.title.lowercased()) agents" }
|
||||
return "No agents reported"
|
||||
}
|
||||
|
||||
var emptyAgentsDetail: String {
|
||||
if !self.gatewayConnected {
|
||||
return "Connect a gateway to load the live agent roster."
|
||||
}
|
||||
if !self.gatewayConnected { return "Connect a gateway to load the live agent roster." }
|
||||
if !agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return "Try another search or clear the agent filters."
|
||||
}
|
||||
if agentRosterFilter != .all {
|
||||
return "Clear the filter to view the full roster."
|
||||
}
|
||||
if agentRosterFilter != .all { return "Clear the filter to view the full roster." }
|
||||
return "The connected gateway did not return an agent list."
|
||||
}
|
||||
|
||||
|
||||
@@ -226,9 +226,7 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
var skillPolicySummary: String {
|
||||
if appModel.isAppleReviewDemoModeEnabled {
|
||||
return "Demo mode keeps live skill changes disabled."
|
||||
}
|
||||
if appModel.isAppleReviewDemoModeEnabled { return "Demo mode keeps live skill changes disabled." }
|
||||
guard gatewayConnected else { return "Connect a gateway to edit skills." }
|
||||
guard let filter = agentSkillFilter else {
|
||||
return "All available skills are allowed for this agent."
|
||||
@@ -282,9 +280,7 @@ extension AgentProTab {
|
||||
func sortSkills(_ lhs: SkillStatusEntryLite, _ rhs: SkillStatusEntryLite) -> Bool {
|
||||
let lhsEnabled = self.isSkillAllowed(lhs)
|
||||
let rhsEnabled = self.isSkillAllowed(rhs)
|
||||
if lhsEnabled != rhsEnabled {
|
||||
return lhsEnabled && !rhsEnabled
|
||||
}
|
||||
if lhsEnabled != rhsEnabled { return lhsEnabled && !rhsEnabled }
|
||||
return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
|
||||
}
|
||||
|
||||
|
||||
@@ -246,11 +246,7 @@ struct CommandSessionActionsModifier: ViewModifier {
|
||||
private var editorBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { self.editor != nil },
|
||||
set: {
|
||||
if !$0 {
|
||||
self.editor = nil
|
||||
}
|
||||
})
|
||||
set: { if !$0 { self.editor = nil } })
|
||||
}
|
||||
|
||||
private var editorTitle: String {
|
||||
|
||||
@@ -626,9 +626,7 @@ struct CommandCenterTab: View {
|
||||
nonisolated static func isRecentChatSession(_ key: String, defaultSessionKey: String) -> Bool {
|
||||
let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return false }
|
||||
if trimmed == defaultSessionKey {
|
||||
return false
|
||||
}
|
||||
if trimmed == defaultSessionKey { return false }
|
||||
let normalized = trimmed.lowercased()
|
||||
let defaultBase = self.sessionBaseKey(defaultSessionKey)
|
||||
if !normalized.contains(":"),
|
||||
@@ -636,9 +634,7 @@ struct CommandCenterTab: View {
|
||||
{
|
||||
return false
|
||||
}
|
||||
if self.isHiddenInternalSession(trimmed) {
|
||||
return false
|
||||
}
|
||||
if self.isHiddenInternalSession(trimmed) { return false }
|
||||
return !self.isAgentDeviceSession(trimmed, defaultSessionKey: defaultSessionKey)
|
||||
}
|
||||
|
||||
@@ -953,21 +949,13 @@ struct CommandSessionsScreen: View {
|
||||
private var groupEditorBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { self.groupEditor != nil },
|
||||
set: {
|
||||
if !$0 {
|
||||
self.groupEditor = nil
|
||||
}
|
||||
})
|
||||
set: { if !$0 { self.groupEditor = nil } })
|
||||
}
|
||||
|
||||
private var groupDeleteBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { self.groupPendingDelete != nil },
|
||||
set: {
|
||||
if !$0 {
|
||||
self.groupPendingDelete = nil
|
||||
}
|
||||
})
|
||||
set: { if !$0 { self.groupPendingDelete = nil } })
|
||||
}
|
||||
|
||||
private func commitGroupEditor() {
|
||||
|
||||
@@ -1223,16 +1223,10 @@ struct IPadSkillProposal: Identifiable {
|
||||
var ageLabel: String {
|
||||
let diff = max(0, Date().timeIntervalSince1970 * 1000 - self.updatedAtMs)
|
||||
let minutes = Int(diff / 60000)
|
||||
if minutes < 1 {
|
||||
return "now"
|
||||
}
|
||||
if minutes < 60 {
|
||||
return "\(minutes)m"
|
||||
}
|
||||
if minutes < 1 { return "now" }
|
||||
if minutes < 60 { return "\(minutes)m" }
|
||||
let hours = minutes / 60
|
||||
if hours < 24 {
|
||||
return "\(hours)h"
|
||||
}
|
||||
if hours < 24 { return "\(hours)h" }
|
||||
return "\(hours / 24)d"
|
||||
}
|
||||
|
||||
|
||||
@@ -705,9 +705,7 @@ struct IPadWorkboardScreen: View {
|
||||
self.errorText = nil
|
||||
return
|
||||
}
|
||||
if self.isLoading {
|
||||
return
|
||||
}
|
||||
if self.isLoading { return }
|
||||
|
||||
self.isLoading = true
|
||||
self.errorText = nil
|
||||
|
||||
@@ -136,9 +136,7 @@ struct SettingsChannelsDestination: View {
|
||||
}
|
||||
|
||||
private var headerValue: String? {
|
||||
if self.isLoading {
|
||||
return "Loading"
|
||||
}
|
||||
if self.isLoading { return "Loading" }
|
||||
guard self.canRead else { return "Offline" }
|
||||
return "\(self.channelEntries.count)"
|
||||
}
|
||||
@@ -155,21 +153,15 @@ struct SettingsChannelsDestination: View {
|
||||
|
||||
private var summaryValue: String {
|
||||
guard self.canRead else { return "offline" }
|
||||
if self.isLoading {
|
||||
return "loading"
|
||||
}
|
||||
if self.errorText != nil {
|
||||
return "error"
|
||||
}
|
||||
if self.isLoading { return "loading" }
|
||||
if self.errorText != nil { return "error" }
|
||||
let configured = self.channelEntries.count(where: { $0.configured })
|
||||
return "\(configured)/\(self.channelEntries.count)"
|
||||
}
|
||||
|
||||
private var summaryColor: Color {
|
||||
guard self.canRead else { return .secondary }
|
||||
if self.errorText != nil {
|
||||
return OpenClawBrand.warn
|
||||
}
|
||||
if self.errorText != nil { return OpenClawBrand.warn }
|
||||
return self.channelEntries.contains(where: { $0.running || $0.connected }) ? OpenClawBrand.ok : OpenClawBrand
|
||||
.accent
|
||||
}
|
||||
@@ -241,9 +233,7 @@ struct SettingsChannelsDestination: View {
|
||||
self.errorText = nil
|
||||
return
|
||||
}
|
||||
if self.isLoading {
|
||||
return
|
||||
}
|
||||
if self.isLoading { return }
|
||||
|
||||
self.isLoading = true
|
||||
self.errorText = nil
|
||||
@@ -328,16 +318,10 @@ struct SettingsChannelsDestination: View {
|
||||
private static func relativeTime(_ milliseconds: Int) -> String {
|
||||
let age = max(0, Int(Date().timeIntervalSince1970 * 1000) - milliseconds)
|
||||
let minutes = age / 60000
|
||||
if minutes < 1 {
|
||||
return "now"
|
||||
}
|
||||
if minutes < 60 {
|
||||
return "\(minutes)m ago"
|
||||
}
|
||||
if minutes < 1 { return "now" }
|
||||
if minutes < 60 { return "\(minutes)m ago" }
|
||||
let hours = minutes / 60
|
||||
if hours < 24 {
|
||||
return "\(hours)h ago"
|
||||
}
|
||||
if hours < 24 { return "\(hours)h ago" }
|
||||
return "\(hours / 24)d ago"
|
||||
}
|
||||
|
||||
@@ -475,28 +459,16 @@ private struct SettingsChannelEntry: Identifiable {
|
||||
let accounts: [SettingsChannelAccount]
|
||||
|
||||
var color: Color {
|
||||
if self.connected || self.running {
|
||||
return OpenClawBrand.ok
|
||||
}
|
||||
if self.lastError != nil {
|
||||
return OpenClawBrand.warn
|
||||
}
|
||||
if self.connected || self.running { return OpenClawBrand.ok }
|
||||
if self.lastError != nil { return OpenClawBrand.warn }
|
||||
return self.configured ? OpenClawBrand.accent : .secondary
|
||||
}
|
||||
|
||||
var statusValue: String {
|
||||
if self.connected {
|
||||
return "connected"
|
||||
}
|
||||
if self.running {
|
||||
return "running"
|
||||
}
|
||||
if self.linked {
|
||||
return "linked"
|
||||
}
|
||||
if self.configured {
|
||||
return "configured"
|
||||
}
|
||||
if self.connected { return "connected" }
|
||||
if self.running { return "running" }
|
||||
if self.linked { return "linked" }
|
||||
if self.configured { return "configured" }
|
||||
return "not set"
|
||||
}
|
||||
|
||||
@@ -557,12 +529,8 @@ private struct SettingsChannelAccount: Identifiable {
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
if self.connected || self.running {
|
||||
return OpenClawBrand.ok
|
||||
}
|
||||
if self.lastError != nil {
|
||||
return OpenClawBrand.warn
|
||||
}
|
||||
if self.connected || self.running { return OpenClawBrand.ok }
|
||||
if self.lastError != nil { return OpenClawBrand.warn }
|
||||
return self.configured ? OpenClawBrand.accent : .secondary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,9 +961,7 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
var manualPortIsValid: Bool {
|
||||
if self.manualGatewayPortText.isEmpty {
|
||||
return true
|
||||
}
|
||||
if self.manualGatewayPortText.isEmpty { return true }
|
||||
return self.manualGatewayPort >= 1 && self.manualGatewayPort <= 65535
|
||||
}
|
||||
|
||||
@@ -980,24 +978,16 @@ extension SettingsProTab {
|
||||
}
|
||||
let trimmedSetup = self.setupStatusText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let gatewayStatus = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let friendly = self.friendlyGatewayMessage(from: gatewayStatus) {
|
||||
return friendly
|
||||
}
|
||||
if let friendly = self.friendlyGatewayMessage(from: trimmedSetup) {
|
||||
return friendly
|
||||
}
|
||||
if let friendly = self.friendlyGatewayMessage(from: gatewayStatus) { return friendly }
|
||||
if let friendly = self.friendlyGatewayMessage(from: trimmedSetup) { return friendly }
|
||||
if self.isTransientSetupStatus(trimmedSetup),
|
||||
!gatewayStatus.isEmpty,
|
||||
gatewayStatus != "Offline"
|
||||
{
|
||||
return gatewayStatus
|
||||
}
|
||||
if !trimmedSetup.isEmpty {
|
||||
return trimmedSetup
|
||||
}
|
||||
if gatewayStatus.isEmpty || gatewayStatus == "Offline" {
|
||||
return nil
|
||||
}
|
||||
if !trimmedSetup.isEmpty { return trimmedSetup }
|
||||
if gatewayStatus.isEmpty || gatewayStatus == "Offline" { return nil }
|
||||
return gatewayStatus
|
||||
}
|
||||
|
||||
@@ -1084,12 +1074,8 @@ extension SettingsProTab {
|
||||
let title = self.appModel.talkMode.gatewayTalkActiveModeTitle.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let subtitle = (self.appModel.talkMode.gatewayTalkActiveModeSubtitle ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if title.isEmpty {
|
||||
return "Not active"
|
||||
}
|
||||
if subtitle.isEmpty {
|
||||
return title
|
||||
}
|
||||
if title.isEmpty { return "Not active" }
|
||||
if subtitle.isEmpty { return title }
|
||||
return "\(title) • \(subtitle)"
|
||||
}
|
||||
|
||||
@@ -1101,12 +1087,8 @@ extension SettingsProTab {
|
||||
|
||||
func gatewayDetailLines(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> [String] {
|
||||
var lines: [String] = []
|
||||
if let lanHost = gateway.lanHost {
|
||||
lines.append("LAN: \(lanHost)")
|
||||
}
|
||||
if let tailnet = gateway.tailnetDns {
|
||||
lines.append("Tailnet: \(tailnet)")
|
||||
}
|
||||
if let lanHost = gateway.lanHost { lines.append("LAN: \(lanHost)") }
|
||||
if let tailnet = gateway.tailnetDns { lines.append("Tailnet: \(tailnet)") }
|
||||
let gw = gateway.gatewayPort.map(String.init)
|
||||
let canvas = gateway.canvasPort.map(String.init)
|
||||
if gw != nil || canvas != nil {
|
||||
@@ -1121,23 +1103,17 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
var gatewayStatusDetail: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "Apple Review demo mode"
|
||||
}
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "Apple Review demo mode" }
|
||||
return self.gatewayConnected ? "Connected" : self.appModel.gatewayDisplayStatusText
|
||||
}
|
||||
|
||||
var gatewayStatusValue: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "demo"
|
||||
}
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "demo" }
|
||||
return self.gatewayConnected ? "online" : "offline"
|
||||
}
|
||||
|
||||
var gatewayStatusColor: Color {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return OpenClawBrand.accent
|
||||
}
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return OpenClawBrand.accent }
|
||||
return self.gatewayConnected ? OpenClawBrand.ok : .secondary
|
||||
}
|
||||
|
||||
@@ -1160,23 +1136,17 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
var gatewayTalkConfigDetail: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "Demo mode only"
|
||||
}
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "Demo mode only" }
|
||||
return self.appModel.talkMode.gatewayTalkTransportLabel
|
||||
}
|
||||
|
||||
var gatewayTalkConfigValue: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "demo"
|
||||
}
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "demo" }
|
||||
return self.appModel.talkMode.gatewayTalkConfigLoaded ? "loaded" : "missing"
|
||||
}
|
||||
|
||||
var gatewayTalkConfigColor: Color {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return .secondary
|
||||
}
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return .secondary }
|
||||
return self.appModel.talkMode.gatewayTalkConfigLoaded ? OpenClawBrand.ok : .secondary
|
||||
}
|
||||
|
||||
@@ -1217,28 +1187,16 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
var voiceDetail: String {
|
||||
if self.talkEnabled, self.voiceWakeEnabled {
|
||||
return "Talk + Wake"
|
||||
}
|
||||
if self.talkEnabled {
|
||||
return "Talk on"
|
||||
}
|
||||
if self.voiceWakeEnabled {
|
||||
return "Wake on"
|
||||
}
|
||||
if self.talkEnabled, self.voiceWakeEnabled { return "Talk + Wake" }
|
||||
if self.talkEnabled { return "Talk on" }
|
||||
if self.voiceWakeEnabled { return "Wake on" }
|
||||
return "Off"
|
||||
}
|
||||
|
||||
var diagnosticsHealthValue: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "demo"
|
||||
}
|
||||
if self.gatewayConnected {
|
||||
return "ready"
|
||||
}
|
||||
if self.gatewayController.gateways.isEmpty {
|
||||
return "check"
|
||||
}
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "demo" }
|
||||
if self.gatewayConnected { return "ready" }
|
||||
if self.gatewayController.gateways.isEmpty { return "check" }
|
||||
return "partial"
|
||||
}
|
||||
|
||||
|
||||
@@ -343,18 +343,10 @@ enum SettingsDiagnostics {
|
||||
notificationsAllowed: Bool) -> [SettingsDiagnosticIssue]
|
||||
{
|
||||
var issues: [SettingsDiagnosticIssue] = []
|
||||
if !gatewayConnected {
|
||||
issues.append(.gatewayOffline)
|
||||
}
|
||||
if discoveredGatewayCount == 0 {
|
||||
issues.append(.discoveryUnavailable)
|
||||
}
|
||||
if gatewayConnected, !talkConfigLoaded {
|
||||
issues.append(.talkConfigMissing)
|
||||
}
|
||||
if !notificationsAllowed {
|
||||
issues.append(.notificationsUnavailable)
|
||||
}
|
||||
if !gatewayConnected { issues.append(.gatewayOffline) }
|
||||
if discoveredGatewayCount == 0 { issues.append(.discoveryUnavailable) }
|
||||
if gatewayConnected, !talkConfigLoaded { issues.append(.talkConfigMissing) }
|
||||
if !notificationsAllowed { issues.append(.notificationsUnavailable) }
|
||||
return issues
|
||||
}
|
||||
|
||||
@@ -387,9 +379,7 @@ extension SettingsProTab {
|
||||
let isLoopback = (flags & IFF_LOOPBACK) != 0
|
||||
guard let addrPtr = ptr.pointee.ifa_addr else { continue }
|
||||
let family = addrPtr.pointee.sa_family
|
||||
if !isUp || isLoopback || family != UInt8(AF_INET) {
|
||||
continue
|
||||
}
|
||||
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
|
||||
var addr = addrPtr.pointee
|
||||
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||
let result = getnameinfo(
|
||||
@@ -403,18 +393,14 @@ extension SettingsProTab {
|
||||
guard result == 0 else { continue }
|
||||
let bytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }
|
||||
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
|
||||
if self.isTailnetIPv4(ip) {
|
||||
return true
|
||||
}
|
||||
if self.isTailnetIPv4(ip) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func isTailnetHostOrIP(_ host: String) -> Bool {
|
||||
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
if trimmed.hasSuffix(".ts.net") || trimmed.hasSuffix(".ts.net.") {
|
||||
return true
|
||||
}
|
||||
if trimmed.hasSuffix(".ts.net") || trimmed.hasSuffix(".ts.net.") { return true }
|
||||
return self.isTailnetIPv4(trimmed)
|
||||
}
|
||||
|
||||
|
||||
@@ -215,36 +215,23 @@ struct TalkProTab: View {
|
||||
|
||||
private var heroSubtitle: String {
|
||||
if self.state
|
||||
.prefersPermissionCopy
|
||||
{
|
||||
return "Gateway approval is required before this phone can capture voice."
|
||||
}
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "Voice is disabled in Apple Review demo mode."
|
||||
}
|
||||
if !self.gatewayConnected {
|
||||
return "Connect to your gateway to start a voice conversation."
|
||||
}
|
||||
.prefersPermissionCopy { return "Gateway approval is required before this phone can capture voice." }
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "Voice is disabled in Apple Review demo mode." }
|
||||
if !self.gatewayConnected { return "Connect to your gateway to start a voice conversation." }
|
||||
if !self.appModel.talkMode.gatewayTalkConfigLoaded {
|
||||
return "Open Voice settings after the gateway loads Talk configuration."
|
||||
}
|
||||
let subtitle = (appModel.talkMode.gatewayTalkVoiceModeSubtitle ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !subtitle.isEmpty {
|
||||
return subtitle
|
||||
}
|
||||
if !subtitle.isEmpty { return subtitle }
|
||||
return "Routes voice to \(self.appModel.chatAgentName)."
|
||||
}
|
||||
|
||||
private var transportText: String {
|
||||
let provider = self.appModel.talkMode.gatewayTalkProviderLabel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let transport = self.appModel.talkMode.gatewayTalkTransportLabel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if provider.isEmpty || provider == "Not loaded" {
|
||||
return transport.isEmpty ? "Not loaded" : transport
|
||||
}
|
||||
if transport.isEmpty || transport == "Not loaded" {
|
||||
return provider
|
||||
}
|
||||
if provider.isEmpty || provider == "Not loaded" { return transport.isEmpty ? "Not loaded" : transport }
|
||||
if transport.isEmpty || transport == "Not loaded" { return provider }
|
||||
return "\(provider) • \(transport)"
|
||||
}
|
||||
|
||||
@@ -252,12 +239,8 @@ struct TalkProTab: View {
|
||||
let title = self.appModel.talkMode.gatewayTalkActiveModeTitle.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let subtitle = (appModel.talkMode.gatewayTalkActiveModeSubtitle ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if title.isEmpty {
|
||||
return "Not active"
|
||||
}
|
||||
if subtitle.isEmpty {
|
||||
return title
|
||||
}
|
||||
if title.isEmpty { return "Not active" }
|
||||
if subtitle.isEmpty { return title }
|
||||
return "\(title) • \(subtitle)"
|
||||
}
|
||||
|
||||
@@ -275,9 +258,7 @@ struct TalkProTab: View {
|
||||
}
|
||||
|
||||
private var speechLocaleText: String {
|
||||
if self.talkSpeechLocale == TalkSpeechLocale.automaticID {
|
||||
return "Automatic"
|
||||
}
|
||||
if self.talkSpeechLocale == TalkSpeechLocale.automaticID { return "Automatic" }
|
||||
return self.talkSpeechLocale
|
||||
}
|
||||
|
||||
@@ -381,12 +362,8 @@ struct TalkProState: Equatable {
|
||||
}
|
||||
|
||||
var title: String {
|
||||
if self.isDemoMode {
|
||||
return "Demo mode only"
|
||||
}
|
||||
if !self.gatewayConnected {
|
||||
return "Gateway offline"
|
||||
}
|
||||
if self.isDemoMode { return "Demo mode only" }
|
||||
if !self.gatewayConnected { return "Gateway offline" }
|
||||
switch self.permissionState {
|
||||
case .missingScope, .requestFailed:
|
||||
return "Gateway permission required"
|
||||
@@ -401,54 +378,32 @@ struct TalkProState: Equatable {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if !self.isConfigLoaded {
|
||||
return "Voice config unavailable"
|
||||
}
|
||||
if self.isSpeaking {
|
||||
return "Speaking"
|
||||
}
|
||||
if self.isListening {
|
||||
return "Listening"
|
||||
}
|
||||
if self.normalizedStatus.contains("connecting") {
|
||||
return "Connecting"
|
||||
}
|
||||
if self.normalizedStatus.contains("thinking") {
|
||||
return "Asking OpenClaw"
|
||||
}
|
||||
if self.isEnabled {
|
||||
return "Ready to talk"
|
||||
}
|
||||
if !self.isConfigLoaded { return "Voice config unavailable" }
|
||||
if self.isSpeaking { return "Speaking" }
|
||||
if self.isListening { return "Listening" }
|
||||
if self.normalizedStatus.contains("connecting") { return "Connecting" }
|
||||
if self.normalizedStatus.contains("thinking") { return "Asking OpenClaw" }
|
||||
if self.isEnabled { return "Ready to talk" }
|
||||
return "Talk is off"
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
if self.isDemoMode {
|
||||
return .secondary
|
||||
}
|
||||
if !self.gatewayConnected {
|
||||
return .secondary
|
||||
}
|
||||
if self.isDemoMode { return .secondary }
|
||||
if !self.gatewayConnected { return .secondary }
|
||||
switch self.permissionState {
|
||||
case .requestFailed, .loadFailed:
|
||||
return OpenClawBrand.danger
|
||||
case .missingScope, .requestingUpgrade, .upgradeRequested, .apiKeyMissing:
|
||||
return OpenClawBrand.warn
|
||||
default:
|
||||
if !self.isConfigLoaded {
|
||||
return OpenClawBrand.warn
|
||||
}
|
||||
if !self.isConfigLoaded { return OpenClawBrand.warn }
|
||||
return self.isEnabled ? OpenClawBrand.ok : OpenClawBrand.accentHot
|
||||
}
|
||||
}
|
||||
|
||||
var primaryAction: TalkProPrimaryAction {
|
||||
if self.isDemoMode {
|
||||
return .waiting
|
||||
}
|
||||
if !self.gatewayConnected {
|
||||
return .openSettings
|
||||
}
|
||||
if self.isDemoMode { return .waiting }
|
||||
if !self.gatewayConnected { return .openSettings }
|
||||
switch self.permissionState {
|
||||
case .missingScope, .requestFailed:
|
||||
return .enablePermission
|
||||
@@ -491,12 +446,8 @@ struct TalkProState: Equatable {
|
||||
}
|
||||
|
||||
func waveformPhase(micLevel: Double, playbackLevel: Double?) -> TalkWaveformPhase {
|
||||
if self.isDemoMode {
|
||||
return .idle
|
||||
}
|
||||
if !self.gatewayConnected {
|
||||
return .idle
|
||||
}
|
||||
if self.isDemoMode { return .idle }
|
||||
if !self.gatewayConnected { return .idle }
|
||||
switch self.permissionState {
|
||||
case .requestingUpgrade, .upgradeRequested:
|
||||
return .thinking
|
||||
@@ -505,15 +456,9 @@ struct TalkProState: Equatable {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if !self.isConfigLoaded {
|
||||
return .idle
|
||||
}
|
||||
if self.isSpeaking {
|
||||
return .speaking(level: playbackLevel)
|
||||
}
|
||||
if self.isListening {
|
||||
return .listening(level: micLevel, speechActive: self.isUserSpeechDetected)
|
||||
}
|
||||
if !self.isConfigLoaded { return .idle }
|
||||
if self.isSpeaking { return .speaking(level: playbackLevel) }
|
||||
if self.isListening { return .listening(level: micLevel, speechActive: self.isUserSpeechDetected) }
|
||||
if self.normalizedStatus.contains("connecting") || self.normalizedStatus.contains("thinking") {
|
||||
return .thinking
|
||||
}
|
||||
|
||||
@@ -34,18 +34,10 @@ final class NetworkStatusService: @unchecked Sendable {
|
||||
}
|
||||
|
||||
var interfaces: [OpenClawNetworkInterfaceType] = []
|
||||
if path.usesInterfaceType(.wifi) {
|
||||
interfaces.append(.wifi)
|
||||
}
|
||||
if path.usesInterfaceType(.cellular) {
|
||||
interfaces.append(.cellular)
|
||||
}
|
||||
if path.usesInterfaceType(.wiredEthernet) {
|
||||
interfaces.append(.wired)
|
||||
}
|
||||
if interfaces.isEmpty {
|
||||
interfaces.append(.other)
|
||||
}
|
||||
if path.usesInterfaceType(.wifi) { interfaces.append(.wifi) }
|
||||
if path.usesInterfaceType(.cellular) { interfaces.append(.cellular) }
|
||||
if path.usesInterfaceType(.wiredEthernet) { interfaces.append(.wired) }
|
||||
if interfaces.isEmpty { interfaces.append(.other) }
|
||||
|
||||
return OpenClawNetworkStatusPayload(
|
||||
status: status,
|
||||
@@ -70,9 +62,7 @@ private final class NetworkStatusState: @unchecked Sendable {
|
||||
func markCompleted() -> Bool {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
if self.completed {
|
||||
return false
|
||||
}
|
||||
if self.completed { return false }
|
||||
self.completed = true
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ struct GatewayConnectConfig {
|
||||
/// If the caller doesn't provide a stableID, fall back to URL identity.
|
||||
var effectiveStableID: String {
|
||||
let trimmed = self.stableID.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
return self.url.absoluteString
|
||||
}
|
||||
if trimmed.isEmpty { return self.url.absoluteString }
|
||||
return trimmed
|
||||
}
|
||||
|
||||
|
||||
@@ -117,20 +117,14 @@ extension GatewayConnectionController {
|
||||
UserDefaults.standard.object(forKey: "camera.enabled") == nil
|
||||
? true
|
||||
: UserDefaults.standard.bool(forKey: "camera.enabled")
|
||||
if cameraEnabled {
|
||||
caps.append(OpenClawCapability.camera.rawValue)
|
||||
}
|
||||
if cameraEnabled { caps.append(OpenClawCapability.camera.rawValue) }
|
||||
|
||||
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
|
||||
if voiceWakeEnabled {
|
||||
caps.append(OpenClawCapability.voiceWake.rawValue)
|
||||
}
|
||||
if voiceWakeEnabled { caps.append(OpenClawCapability.voiceWake.rawValue) }
|
||||
|
||||
let locationModeRaw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off"
|
||||
let locationMode = OpenClawLocationMode(rawValue: locationModeRaw) ?? .off
|
||||
if locationMode != .off {
|
||||
caps.append(OpenClawCapability.location.rawValue)
|
||||
}
|
||||
if locationMode != .off { caps.append(OpenClawCapability.location.rawValue) }
|
||||
|
||||
caps.append(OpenClawCapability.device.rawValue)
|
||||
caps.append(OpenClawCapability.talk.rawValue)
|
||||
|
||||
@@ -1028,9 +1028,7 @@ extension GatewayConnectionController {
|
||||
.max { lhs, rhs in
|
||||
let lhsConnected = lhs.lastConnectedAtMs ?? Int.min
|
||||
let rhsConnected = rhs.lastConnectedAtMs ?? Int.min
|
||||
if lhsConnected != rhsConnected {
|
||||
return lhsConnected < rhsConnected
|
||||
}
|
||||
if lhsConnected != rhsConnected { return lhsConnected < rhsConnected }
|
||||
return lhs.stableID > rhs.stableID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@ enum GatewayConnectionIssue: Equatable {
|
||||
}
|
||||
|
||||
var needsPairing: Bool {
|
||||
if case .pairingRequired = self {
|
||||
return true
|
||||
}
|
||||
if case .pairingRequired = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,7 @@ final class GatewayDiscoveryModel {
|
||||
}
|
||||
|
||||
func start() {
|
||||
if !self.browsers.isEmpty {
|
||||
return
|
||||
}
|
||||
if !self.browsers.isEmpty { return }
|
||||
self.appendDebugLog("start()")
|
||||
|
||||
for domain in OpenClawBonjour.gatewayServiceDomains {
|
||||
|
||||
@@ -44,9 +44,7 @@ final class GatewayHealthMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
if Task.isCancelled {
|
||||
break
|
||||
}
|
||||
if Task.isCancelled { break }
|
||||
let interval = max(0.0, config.intervalSeconds)
|
||||
let nanos = UInt64(interval * 1_000_000_000)
|
||||
if nanos > 0 {
|
||||
|
||||
@@ -464,9 +464,7 @@ enum GatewaySettingsStore {
|
||||
service: self.talkService,
|
||||
account: account)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if value?.isEmpty == false {
|
||||
return value
|
||||
}
|
||||
if value?.isEmpty == false { return value }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -586,9 +584,7 @@ enum GatewaySettingsStore {
|
||||
.compactMap(self.normalizedGatewayRegistryEntry)
|
||||
.filter { seen.insert($0.stableID).inserted }
|
||||
.sorted { lhs, rhs in
|
||||
if lhs.name != rhs.name {
|
||||
return lhs.name < rhs.name
|
||||
}
|
||||
if lhs.name != rhs.name { return lhs.name < rhs.name }
|
||||
return lhs.stableID < rhs.stableID
|
||||
}
|
||||
let activeStableID = registry.activeStableID.flatMap { activeID in
|
||||
@@ -715,9 +711,7 @@ enum GatewaySettingsStore {
|
||||
let key = self.clientIdOverrideDefaultsPrefix + trimmedID
|
||||
let value = UserDefaults.standard.string(forKey: key)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if value?.isEmpty == false {
|
||||
return value
|
||||
}
|
||||
if value?.isEmpty == false { return value }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -739,9 +733,7 @@ enum GatewaySettingsStore {
|
||||
let key = self.selectedAgentDefaultsPrefix + trimmedID
|
||||
let value = UserDefaults.standard.string(forKey: key)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if value?.isEmpty == false {
|
||||
return value
|
||||
}
|
||||
if value?.isEmpty == false { return value }
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@ enum KeychainStore {
|
||||
]
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
if status == errSecItemNotFound {
|
||||
return true
|
||||
}
|
||||
if status == errSecItemNotFound { return true }
|
||||
guard status == errSecSuccess else { return false }
|
||||
let matches = item as? [[String: Any]] ?? []
|
||||
let accounts = matches
|
||||
|
||||
@@ -15,9 +15,7 @@ enum TCPProbe {
|
||||
let finished = OSAllocatedUnfairLock(initialState: false)
|
||||
let finish: @Sendable (Bool) -> Void = { ok in
|
||||
let shouldResume = finished.withLock { flag -> Bool in
|
||||
if flag {
|
||||
return false
|
||||
}
|
||||
if flag { return false }
|
||||
flag = true
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -44,9 +44,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate, ConcurrentLoca
|
||||
let updated = await self.requestAuthorization(requiresDeterminedStatus: true) {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
if mode != .always {
|
||||
return updated
|
||||
}
|
||||
if mode != .always { return updated }
|
||||
}
|
||||
|
||||
if mode == .always {
|
||||
|
||||
@@ -67,9 +67,7 @@ extension NodeAppModel {
|
||||
_ risk: OpenClawWatchRisk?,
|
||||
priority: OpenClawNotificationPriority?) -> OpenClawWatchRisk?
|
||||
{
|
||||
if let risk {
|
||||
return risk
|
||||
}
|
||||
if let risk { return risk }
|
||||
switch priority {
|
||||
case .passive:
|
||||
return .low
|
||||
@@ -86,9 +84,7 @@ extension NodeAppModel {
|
||||
_ priority: OpenClawNotificationPriority?,
|
||||
risk: OpenClawWatchRisk?) -> OpenClawNotificationPriority?
|
||||
{
|
||||
if let priority {
|
||||
return priority
|
||||
}
|
||||
if let priority { return priority }
|
||||
switch risk {
|
||||
case .low:
|
||||
return .passive
|
||||
|
||||
@@ -237,11 +237,7 @@ final class NodeAppModel {
|
||||
// value equality alone cannot tell the UI to re-surface or shake the toast.
|
||||
private(set) var gatewayProblemReportCount = 0
|
||||
private(set) var lastGatewayProblem: GatewayConnectionProblem? {
|
||||
didSet {
|
||||
if self.lastGatewayProblem != nil {
|
||||
self.gatewayProblemReportCount &+= 1
|
||||
}
|
||||
}
|
||||
didSet { if self.lastGatewayProblem != nil { self.gatewayProblemReportCount &+= 1 } }
|
||||
}
|
||||
|
||||
private var operatorGatewayProblem: GatewayConnectionProblem?
|
||||
@@ -366,12 +362,8 @@ final class NodeAppModel {
|
||||
}
|
||||
|
||||
var localChatFixture: LocalChatFixture? {
|
||||
if self.isScreenshotFixtureModeEnabled {
|
||||
return .appScreenshots
|
||||
}
|
||||
if self.isAppleReviewDemoModeEnabled {
|
||||
return .appleReviewDemo
|
||||
}
|
||||
if self.isScreenshotFixtureModeEnabled { return .appScreenshots }
|
||||
if self.isAppleReviewDemoModeEnabled { return .appleReviewDemo }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -384,12 +376,8 @@ final class NodeAppModel {
|
||||
}
|
||||
|
||||
var chatTransportModeID: String {
|
||||
if self.isScreenshotFixtureModeEnabled {
|
||||
return "screenshots"
|
||||
}
|
||||
if self.isAppleReviewDemoModeEnabled {
|
||||
return "apple-review-demo"
|
||||
}
|
||||
if self.isScreenshotFixtureModeEnabled { return "screenshots" }
|
||||
if self.isAppleReviewDemoModeEnabled { return "apple-review-demo" }
|
||||
return self.isOperatorGatewayConnected ? "operator" : "offline"
|
||||
}
|
||||
|
||||
@@ -707,9 +695,7 @@ final class NodeAppModel {
|
||||
private func handleCanvasA2UIAction(body: [String: Any]) async {
|
||||
let userActionAny = body["userAction"] ?? body
|
||||
let userAction: [String: Any] = {
|
||||
if let dict = userActionAny as? [String: Any] {
|
||||
return dict
|
||||
}
|
||||
if let dict = userActionAny as? [String: Any] { return dict }
|
||||
if let dict = userActionAny as? [AnyHashable: Any] {
|
||||
return dict.reduce(into: [String: Any]()) { acc, pair in
|
||||
guard let key = pair.key as? String else { return }
|
||||
@@ -1297,9 +1283,7 @@ final class NodeAppModel {
|
||||
guard let operatorRoute = await self.operatorGateway.currentRoute(), shouldContinue() else { return }
|
||||
let stream = await self.operatorGateway.subscribeServerEvents(bufferingNewest: 200)
|
||||
for await evt in stream {
|
||||
if Task.isCancelled || !shouldContinue() {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled || !shouldContinue() { return }
|
||||
guard evt.payload != nil else { continue }
|
||||
await self.handleOperatorGatewayServerEvent(
|
||||
evt,
|
||||
@@ -1386,9 +1370,7 @@ final class NodeAppModel {
|
||||
self.gatewayHealthMonitor.start(
|
||||
check: { [weak self] in
|
||||
guard let self else { return false }
|
||||
if await MainActor.run(body: { self.isGatewayHealthMonitorDisabled() }) {
|
||||
return true
|
||||
}
|
||||
if await MainActor.run(body: { self.isGatewayHealthMonitorDisabled() }) { return true }
|
||||
do {
|
||||
let data = try await self.operatorGateway.request(
|
||||
method: "health",
|
||||
@@ -1599,9 +1581,7 @@ final class NodeAppModel {
|
||||
let params = try? Self.decodeParams(OpenClawCanvasSnapshotParams.self, from: req.paramsJSON)
|
||||
let format = params?.format ?? .jpeg
|
||||
let maxWidth: CGFloat? = {
|
||||
if let raw = params?.maxWidth, raw > 0 {
|
||||
return CGFloat(raw)
|
||||
}
|
||||
if let raw = params?.maxWidth, raw > 0 { return CGFloat(raw) }
|
||||
// Keep default snapshots comfortably below the gateway client's maxPayload.
|
||||
// For full-res, clients should explicitly request a larger maxWidth.
|
||||
return switch format {
|
||||
@@ -2640,9 +2620,7 @@ extension NodeAppModel {
|
||||
|
||||
private func isCameraEnabled() -> Bool {
|
||||
// Default-on: if the key doesn't exist yet, treat it as enabled.
|
||||
if UserDefaults.standard.object(forKey: "camera.enabled") == nil {
|
||||
return true
|
||||
}
|
||||
if UserDefaults.standard.object(forKey: "camera.enabled") == nil { return true }
|
||||
return UserDefaults.standard.bool(forKey: "camera.enabled")
|
||||
}
|
||||
|
||||
@@ -2702,9 +2680,7 @@ extension NodeAppModel {
|
||||
let base = SessionKey.normalizeMainKey(self.mainSessionBaseKey)
|
||||
let agentId = (selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let defaultId = (gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if agentId.isEmpty || (!defaultId.isEmpty && agentId == defaultId) {
|
||||
return base
|
||||
}
|
||||
if agentId.isEmpty || (!defaultId.isEmpty && agentId == defaultId) { return base }
|
||||
return SessionKey.makeAgentSessionKey(agentId: agentId, baseKey: base)
|
||||
}
|
||||
|
||||
@@ -2807,9 +2783,7 @@ extension NodeAppModel {
|
||||
return sessionAgentId.lowercased()
|
||||
}
|
||||
let selected = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !selected.isEmpty {
|
||||
return selected.lowercased()
|
||||
}
|
||||
if !selected.isEmpty { return selected.lowercased() }
|
||||
let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return defaultId.isEmpty ? nil : defaultId.lowercased()
|
||||
}
|
||||
@@ -2845,9 +2819,7 @@ extension NodeAppModel {
|
||||
|
||||
private func agentDisplayName(for agentId: String, fallback: String) -> String {
|
||||
let resolvedId = agentId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if resolvedId.isEmpty {
|
||||
return fallback
|
||||
}
|
||||
if resolvedId.isEmpty { return fallback }
|
||||
if let match = gatewayAgents.first(where: { $0.id == resolvedId }) {
|
||||
let name = (match.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return name.isEmpty ? match.id : name
|
||||
@@ -6432,17 +6404,13 @@ extension NodeAppModel {
|
||||
let kind = payload["kind"] as? String
|
||||
{
|
||||
let trimmed = kind.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
return trimmed
|
||||
}
|
||||
if !trimmed.isEmpty { return trimmed }
|
||||
}
|
||||
if let payload = userInfo["openclaw"] as? [AnyHashable: Any],
|
||||
let kind = payload["kind"] as? String
|
||||
{
|
||||
let trimmed = kind.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
return trimmed
|
||||
}
|
||||
if !trimmed.isEmpty { return trimmed }
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
@@ -7434,9 +7402,7 @@ extension NodeAppModel {
|
||||
let trimmed = (key ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
let current = self.mainSessionBaseKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed == current {
|
||||
return
|
||||
}
|
||||
if trimmed == current { return }
|
||||
self.mainSessionBaseKey = trimmed
|
||||
self.synchronizeTalkSessionKey()
|
||||
}
|
||||
|
||||
@@ -30,14 +30,10 @@ enum OnboardingStateStore {
|
||||
hasSavedGatewayConnection: Bool? = nil)
|
||||
-> Bool
|
||||
{
|
||||
if defaults.bool(forKey: self.completedDefaultsKey) {
|
||||
return false
|
||||
}
|
||||
if defaults.bool(forKey: self.completedDefaultsKey) { return false }
|
||||
let hasSavedGatewayConnection =
|
||||
hasSavedGatewayConnection ?? (GatewaySettingsStore.activeGatewayEntry() != nil)
|
||||
if hasSavedGatewayConnection {
|
||||
return false
|
||||
}
|
||||
if hasSavedGatewayConnection { return false }
|
||||
return appModel.gatewayServerName == nil
|
||||
}
|
||||
|
||||
|
||||
@@ -192,9 +192,7 @@ struct OnboardingWizardView: View {
|
||||
.alert("QR Scanner Unavailable", isPresented: Binding(
|
||||
get: { self.scannerError != nil },
|
||||
set: {
|
||||
if !$0 {
|
||||
self.scannerError = nil
|
||||
}
|
||||
if !$0 { self.scannerError = nil }
|
||||
})) {
|
||||
Button(role: .cancel) {} label: {
|
||||
Text("OK")
|
||||
@@ -1395,29 +1393,17 @@ extension OnboardingWizardView {
|
||||
|
||||
switch mode {
|
||||
case .homeNetwork:
|
||||
if hostIsDefaultLike {
|
||||
self.manualHost = "openclaw.local"
|
||||
}
|
||||
if hostIsDefaultLike { self.manualHost = "openclaw.local" }
|
||||
self.manualTLS = true
|
||||
if self.manualPort <= 0 || self.manualPort > 65535 {
|
||||
self.manualPort = 18789
|
||||
}
|
||||
if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 }
|
||||
case .remoteDomain:
|
||||
if host == "openclaw.local" || host == "localhost" {
|
||||
self.manualHost = ""
|
||||
}
|
||||
if host == "openclaw.local" || host == "localhost" { self.manualHost = "" }
|
||||
self.manualTLS = true
|
||||
if self.manualPort <= 0 || self.manualPort > 65535 {
|
||||
self.manualPort = 18789
|
||||
}
|
||||
if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 }
|
||||
case .developerLocal:
|
||||
if hostIsDefaultLike {
|
||||
self.manualHost = "localhost"
|
||||
}
|
||||
if hostIsDefaultLike { self.manualHost = "localhost" }
|
||||
self.manualTLS = false
|
||||
if self.manualPort <= 0 || self.manualPort > 65535 {
|
||||
self.manualPort = 18789
|
||||
}
|
||||
if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,7 @@ struct GatewayPendingTargetSuppression {
|
||||
|
||||
mutating func take(ifOwnedBy owner: Owner? = nil) -> GatewayConnectionController.AutoConnectSuppressionLease? {
|
||||
guard let value = self.value else { return nil }
|
||||
if let owner, value.owner != owner {
|
||||
return nil
|
||||
}
|
||||
if let owner, value.owner != owner { return nil }
|
||||
self.value = nil
|
||||
return value.lease
|
||||
}
|
||||
|
||||
@@ -96,9 +96,7 @@ enum PushRelayRegistrationStore {
|
||||
service: self.service,
|
||||
account: self.scopedAccount(self.appAttestKeyIDAccount, scope: scope))?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if value?.isEmpty == false {
|
||||
return value
|
||||
}
|
||||
if value?.isEmpty == false { return value }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,9 +120,7 @@ enum PushRelayRegistrationStore {
|
||||
service: self.service,
|
||||
account: self.scopedAccount(self.appAttestedKeyIDAccount, scope: scope))?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if value?.isEmpty == false {
|
||||
return value
|
||||
}
|
||||
if value?.isEmpty == false { return value }
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1290,17 +1290,11 @@ extension RootTabs {
|
||||
}
|
||||
|
||||
private func hasExistingGatewayConfig() -> Bool {
|
||||
if self.appModel.activeGatewayConnectConfig != nil {
|
||||
return true
|
||||
}
|
||||
if GatewaySettingsStore.activeGatewayEntry() != nil {
|
||||
return true
|
||||
}
|
||||
if self.appModel.activeGatewayConnectConfig != nil { return true }
|
||||
if GatewaySettingsStore.activeGatewayEntry() != nil { return true }
|
||||
|
||||
let preferredStableID = self.preferredGatewayStableID.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !preferredStableID.isEmpty {
|
||||
return true
|
||||
}
|
||||
if !preferredStableID.isEmpty { return true }
|
||||
|
||||
let manualHost = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return self.manualGatewayEnabled && !manualHost.isEmpty
|
||||
|
||||
@@ -163,9 +163,7 @@ final class ScreenController {
|
||||
})()
|
||||
""")
|
||||
let trimmed = res.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
if trimmed == "true" || trimmed == "1" {
|
||||
return true
|
||||
}
|
||||
if trimmed == "true" || trimmed == "1" { return true }
|
||||
} catch {
|
||||
// ignore; page likely still loading
|
||||
}
|
||||
@@ -285,9 +283,7 @@ final class ScreenController {
|
||||
}
|
||||
|
||||
nonisolated static func parseA2UIActionBody(_ body: Any) -> [String: Any]? {
|
||||
if let dict = body as? [String: Any] {
|
||||
return dict.isEmpty ? nil : dict
|
||||
}
|
||||
if let dict = body as? [String: Any] { return dict.isEmpty ? nil : dict }
|
||||
if let str = body as? String,
|
||||
let data = str.data(using: .utf8),
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
@@ -317,12 +313,8 @@ final class ScreenController {
|
||||
|
||||
extension Double {
|
||||
fileprivate func clamped(to range: ClosedRange<Double>) -> Double {
|
||||
if self < range.lowerBound {
|
||||
return range.lowerBound
|
||||
}
|
||||
if self > range.upperBound {
|
||||
return range.upperBound
|
||||
}
|
||||
if self < range.lowerBound { return range.lowerBound }
|
||||
if self > range.upperBound { return range.upperBound }
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ enum SessionKey {
|
||||
static func makeAgentSessionKey(agentId: String, baseKey: String) -> String {
|
||||
let trimmedAgent = agentId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let trimmedBase = baseKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmedAgent.isEmpty {
|
||||
return trimmedBase.isEmpty ? "main" : trimmedBase
|
||||
}
|
||||
if trimmedAgent.isEmpty { return trimmedBase.isEmpty ? "main" : trimmedBase }
|
||||
let normalizedBase = trimmedBase.isEmpty ? "main" : trimmedBase
|
||||
return "agent:\(trimmedAgent):\(normalizedBase)"
|
||||
}
|
||||
|
||||
@@ -22,12 +22,8 @@ enum GatewayStatusBuilder {
|
||||
lastGatewayProblem: GatewayConnectionProblem?,
|
||||
gatewayStatusText: String) -> GatewayDisplayState
|
||||
{
|
||||
if gatewayServerName != nil {
|
||||
return .connected
|
||||
}
|
||||
if let lastGatewayProblem, lastGatewayProblem.pauseReconnect {
|
||||
return .error
|
||||
}
|
||||
if gatewayServerName != nil { return .connected }
|
||||
if let lastGatewayProblem, lastGatewayProblem.pauseReconnect { return .error }
|
||||
|
||||
let text = gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if text.localizedCaseInsensitiveContains("connecting") ||
|
||||
|
||||
@@ -391,9 +391,7 @@ final class RealtimeTalkRelaySession {
|
||||
self.eventTask?.cancel()
|
||||
self.eventTask = Task { [weak self] in
|
||||
for await event in stream {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled { return }
|
||||
await self?.handleGatewayEvent(event, lifecycleGeneration: lifecycleGeneration)
|
||||
}
|
||||
}
|
||||
@@ -482,15 +480,9 @@ final class RealtimeTalkRelaySession {
|
||||
timeoutSeconds: Int,
|
||||
lifecycleGeneration: UInt64) async -> StartupWaitResult
|
||||
{
|
||||
if self.isClosed {
|
||||
return .cancelled
|
||||
}
|
||||
if self.hasReceivedReady {
|
||||
return .ready
|
||||
}
|
||||
if let startupIssue {
|
||||
return .failed(startupIssue)
|
||||
}
|
||||
if self.isClosed { return .cancelled }
|
||||
if self.hasReceivedReady { return .ready }
|
||||
if let startupIssue { return .failed(startupIssue) }
|
||||
return await withCheckedContinuation { continuation in
|
||||
if self.isClosed {
|
||||
continuation.resume(returning: .cancelled)
|
||||
@@ -499,7 +491,7 @@ final class RealtimeTalkRelaySession {
|
||||
self.startupWaiter = continuation
|
||||
Task { [weak self] in
|
||||
try? await Task.sleep(nanoseconds: UInt64(max(0, timeoutSeconds)) * 1_000_000_000)
|
||||
await self?.timeoutStartupWaiterIfNeeded(lifecycleGeneration: lifecycleGeneration)
|
||||
self?.timeoutStartupWaiterIfNeeded(lifecycleGeneration: lifecycleGeneration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ struct TalkRuntimeIssue: Equatable {
|
||||
}
|
||||
|
||||
var displayMessage: String {
|
||||
if !self.message.isEmpty {
|
||||
return self.message
|
||||
}
|
||||
if !self.message.isEmpty { return self.message }
|
||||
return "Realtime voice did not start."
|
||||
}
|
||||
|
||||
@@ -66,35 +64,19 @@ struct TalkRuntimeIssue: Equatable {
|
||||
"code: \(code.rawValue)",
|
||||
"message: \(self.displayMessage)",
|
||||
]
|
||||
if let provider, !provider.isEmpty {
|
||||
lines.append("provider: \(provider)")
|
||||
}
|
||||
if let model, !model.isEmpty {
|
||||
lines.append("model: \(model)")
|
||||
}
|
||||
if let transport, !transport.isEmpty {
|
||||
lines.append("transport: \(transport)")
|
||||
}
|
||||
if let phase, !phase.isEmpty {
|
||||
lines.append("phase: \(phase)")
|
||||
}
|
||||
if let provider, !provider.isEmpty { lines.append("provider: \(provider)") }
|
||||
if let model, !model.isEmpty { lines.append("model: \(model)") }
|
||||
if let transport, !transport.isEmpty { lines.append("transport: \(transport)") }
|
||||
if let phase, !phase.isEmpty { lines.append("phase: \(phase)") }
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
var diagnosticSummary: String {
|
||||
var parts = [displayMessage]
|
||||
if let provider, !provider.isEmpty {
|
||||
parts.append("provider: \(provider)")
|
||||
}
|
||||
if let model, !model.isEmpty {
|
||||
parts.append("model: \(model)")
|
||||
}
|
||||
if let transport, !transport.isEmpty {
|
||||
parts.append("transport: \(transport)")
|
||||
}
|
||||
if let phase, !phase.isEmpty {
|
||||
parts.append("phase: \(phase)")
|
||||
}
|
||||
if let provider, !provider.isEmpty { parts.append("provider: \(provider)") }
|
||||
if let model, !model.isEmpty { parts.append("model: \(model)") }
|
||||
if let transport, !transport.isEmpty { parts.append("transport: \(transport)") }
|
||||
if let phase, !phase.isEmpty { parts.append("phase: \(phase)") }
|
||||
return parts.joined(separator: " • ")
|
||||
}
|
||||
|
||||
@@ -466,9 +448,7 @@ enum TalkModeGatewayConfigParser {
|
||||
guard let config else { return nil }
|
||||
for key in keys {
|
||||
let value = config[key]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if value?.isEmpty == false {
|
||||
return value
|
||||
}
|
||||
if value?.isEmpty == false { return value }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -476,9 +476,7 @@ final class TalkModeManager: NSObject {
|
||||
func updateMainSessionKey(_ sessionKey: String?) -> Bool {
|
||||
let trimmed = (sessionKey ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return false }
|
||||
if trimmed == self.mainSessionKey {
|
||||
return false
|
||||
}
|
||||
if trimmed == self.mainSessionKey { return false }
|
||||
let shouldRestartTalk = self.isEnabled &&
|
||||
(self.hasRealtimeOwnerOrStart || self.hasContinuousTalkOwner)
|
||||
if let captureId = self.activePTTCaptureId {
|
||||
@@ -837,9 +835,7 @@ final class TalkModeManager: NSObject {
|
||||
func resumeAfterBackground(wasKeptActive: Bool = false) {
|
||||
self.foregroundPushToTalkAllowed = true
|
||||
self.foregroundAudioCaptureAllowed = true
|
||||
if wasKeptActive, self.hasContinuousTalkOwner {
|
||||
return
|
||||
}
|
||||
if wasKeptActive, self.hasContinuousTalkOwner { return }
|
||||
guard self.isEnabled else { return }
|
||||
Task { @MainActor [weak self] in
|
||||
await self?.start()
|
||||
@@ -1486,9 +1482,7 @@ final class TalkModeManager: NSObject {
|
||||
private func restartRecognitionAfterError(expectedGeneration: UInt64) async {
|
||||
guard self.canRestartNativeRecognition(expectedGeneration: expectedGeneration) else { return }
|
||||
// Avoid thrashing the audio engine if it’s already running.
|
||||
if self.recognitionTask != nil, self.audioEngine.isRunning {
|
||||
return
|
||||
}
|
||||
if self.recognitionTask != nil, self.audioEngine.isRunning { return }
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
guard self.canRestartNativeRecognition(expectedGeneration: expectedGeneration) else { return }
|
||||
do {
|
||||
@@ -1625,9 +1619,7 @@ final class TalkModeManager: NSObject {
|
||||
guard !transcript.isEmpty else { return }
|
||||
let lastActivity = [lastHeard, lastAudioActivity].compactMap(\.self).max()
|
||||
guard let lastActivity else { return }
|
||||
if Date().timeIntervalSince(lastActivity) < self.silenceWindow {
|
||||
return
|
||||
}
|
||||
if Date().timeIntervalSince(lastActivity) < self.silenceWindow { return }
|
||||
await self.processTranscript(transcript, restartAfter: true)
|
||||
return
|
||||
}
|
||||
@@ -1641,9 +1633,7 @@ final class TalkModeManager: NSObject {
|
||||
guard !transcript.isEmpty else { return }
|
||||
let lastActivity = [lastHeard, lastAudioActivity].compactMap(\.self).max()
|
||||
guard let lastActivity else { return }
|
||||
if Date().timeIntervalSince(lastActivity) < self.silenceWindow {
|
||||
return
|
||||
}
|
||||
if Date().timeIntervalSince(lastActivity) < self.silenceWindow { return }
|
||||
if let pttCaptureId {
|
||||
_ = self.endPushToTalk(captureId: pttCaptureId)
|
||||
} else {
|
||||
@@ -2416,9 +2406,7 @@ final class TalkModeManager: NSObject {
|
||||
guard let content = msg["content"] as? [[String: Any]] else { continue }
|
||||
let text = content.compactMap { $0["text"] as? String }.joined(separator: "\n")
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
return trimmed
|
||||
}
|
||||
if !trimmed.isEmpty { return trimmed }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -3463,9 +3451,7 @@ extension TalkModeManager {
|
||||
let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
let normalized = trimmed.lowercased()
|
||||
if let mapped = voiceAliases[normalized] {
|
||||
return mapped
|
||||
}
|
||||
if let mapped = voiceAliases[normalized] { return mapped }
|
||||
if self.voiceAliases.values.contains(where: { $0.caseInsensitiveCompare(trimmed) == .orderedSame }) {
|
||||
return trimmed
|
||||
}
|
||||
@@ -3484,14 +3470,10 @@ extension TalkModeManager {
|
||||
if Self.isLikelyVoiceId(trimmed) {
|
||||
return trimmed
|
||||
}
|
||||
if let resolved = resolveVoiceAlias(trimmed) {
|
||||
return resolved
|
||||
}
|
||||
if let resolved = resolveVoiceAlias(trimmed) { return resolved }
|
||||
self.logger.warning("unknown voice alias \(trimmed, privacy: .public)")
|
||||
}
|
||||
if let fallbackVoiceId {
|
||||
return fallbackVoiceId
|
||||
}
|
||||
if let fallbackVoiceId { return fallbackVoiceId }
|
||||
|
||||
do {
|
||||
let voices = try await ElevenLabsTTSClient(apiKey: apiKey).listVoices()
|
||||
@@ -3527,9 +3509,7 @@ extension TalkModeManager {
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
guard trimmed != Self.redactedConfigSentinel else { return nil }
|
||||
// Config values may be env placeholders (for example `${ELEVENLABS_API_KEY}`).
|
||||
if trimmed.hasPrefix("${"), trimmed.hasSuffix("}") {
|
||||
return nil
|
||||
}
|
||||
if trimmed.hasPrefix("${"), trimmed.hasSuffix("}") { return nil }
|
||||
return trimmed
|
||||
}
|
||||
|
||||
@@ -4128,13 +4108,9 @@ private final class AudioTapDiagnostics: @unchecked Sendable {
|
||||
let resolvedRms = Float(TalkAudioLevel.rms(buffer: buffer))
|
||||
self.lock.lock()
|
||||
self.lastRms = resolvedRms
|
||||
if resolvedRms > self.maxRmsWindow {
|
||||
self.maxRmsWindow = resolvedRms
|
||||
}
|
||||
if resolvedRms > self.maxRmsWindow { self.maxRmsWindow = resolvedRms }
|
||||
let maxRms = self.maxRmsWindow
|
||||
if shouldLog {
|
||||
self.maxRmsWindow = 0
|
||||
}
|
||||
if shouldLog { self.maxRmsWindow = 0 }
|
||||
self.lock.unlock()
|
||||
|
||||
if shouldEmitLevel, let onLevel {
|
||||
|
||||
@@ -168,9 +168,7 @@ struct TalkPermissionPromptView: View {
|
||||
private func pollUntilReady() async {
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: 3_000_000_000)
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled { return }
|
||||
await self.appModel.pollTalkPermissionUpgrade()
|
||||
if !self.appModel.talkMode.gatewayTalkPermissionState.requiresTalkPermissionAction {
|
||||
return
|
||||
|
||||
@@ -199,12 +199,8 @@ final class TalkRealtimeWebRTCSession: NSObject {
|
||||
else { continue }
|
||||
// Per the WebRTC stats spec audioLevel is linear 0...1:
|
||||
// media-source is the local mic, inbound-rtp the remote voice.
|
||||
if stat.type == "media-source" {
|
||||
input = level.doubleValue
|
||||
}
|
||||
if stat.type == "inbound-rtp" {
|
||||
output = level.doubleValue
|
||||
}
|
||||
if stat.type == "media-source" { input = level.doubleValue }
|
||||
if stat.type == "inbound-rtp" { output = level.doubleValue }
|
||||
}
|
||||
continuation.resume(returning: (input, output))
|
||||
}
|
||||
@@ -514,17 +510,13 @@ final class TalkRealtimeWebRTCSession: NSObject {
|
||||
stream: stream,
|
||||
since: historySince,
|
||||
timeoutSeconds: Self.toolResultTimeoutSeconds)
|
||||
if Task.isCancelled || self.stopped {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled || self.stopped { return }
|
||||
self.trace("tool call chat result ready callId=\(callId) runId=\(runId) chars=\(result.count)")
|
||||
self.submitToolResult(callId: callId, result: ["result": result])
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
if Task.isCancelled || self.stopped {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled || self.stopped { return }
|
||||
Self.logger.error("realtime tool call failed: \(error.localizedDescription, privacy: .public)")
|
||||
self.trace("tool call failed callId=\(callId) error=\(error.localizedDescription)")
|
||||
if let runId = activeToolRunIds[callId] {
|
||||
@@ -567,9 +559,7 @@ final class TalkRealtimeWebRTCSession: NSObject {
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
if Task.isCancelled || self.stopped {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled || self.stopped { return }
|
||||
Self.logger.error("realtime control tool failed: \(error.localizedDescription, privacy: .public)")
|
||||
self.trace("control tool failed callId=\(callId) error=\(error.localizedDescription)")
|
||||
self.submitToolResult(callId: callId, result: [
|
||||
@@ -703,9 +693,7 @@ final class TalkRealtimeWebRTCSession: NSObject {
|
||||
private nonisolated static func matchesSessionKey(_ incoming: String, _ current: String) -> Bool {
|
||||
let incoming = incoming.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
let current = current.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
if incoming == current {
|
||||
return true
|
||||
}
|
||||
if incoming == current { return true }
|
||||
return (incoming == "agent:main:main" && current == "main") ||
|
||||
(incoming == "main" && current == "agent:main:main")
|
||||
}
|
||||
|
||||
@@ -255,12 +255,8 @@ final class VoiceWakeManager: NSObject {
|
||||
|
||||
func start() async {
|
||||
guard self.isEnabled else { return }
|
||||
if self.isListening {
|
||||
return
|
||||
}
|
||||
if self.isStarting {
|
||||
return
|
||||
}
|
||||
if self.isListening { return }
|
||||
if self.isStarting { return }
|
||||
|
||||
self.isStarting = true
|
||||
defer { self.isStarting = false }
|
||||
@@ -377,9 +373,7 @@ final class VoiceWakeManager: NSObject {
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: 40_000_000)
|
||||
let drained = queue.drain()
|
||||
if drained.isEmpty {
|
||||
continue
|
||||
}
|
||||
if drained.isEmpty { continue }
|
||||
for buf in drained {
|
||||
request.append(buf)
|
||||
}
|
||||
@@ -453,9 +447,7 @@ final class VoiceWakeManager: NSObject {
|
||||
guard let transcript else { return }
|
||||
guard let cmd = self.extractCommand(from: transcript, segments: segments) else { return }
|
||||
|
||||
if cmd == self.lastDispatched {
|
||||
return
|
||||
}
|
||||
if cmd == self.lastDispatched { return }
|
||||
self.lastDispatched = cmd
|
||||
self.lastTriggeredCommand = cmd
|
||||
self.statusText = "Triggered"
|
||||
|
||||
@@ -152,9 +152,7 @@ struct OpenClawWatchApp: App {
|
||||
self.execApprovalRefreshTask = Task { @MainActor in
|
||||
self.inboxStore.beginExecApprovalReviewLoading()
|
||||
for attempt in 0..<5 {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled { return }
|
||||
await receiver.requestExecApprovalSnapshot()
|
||||
if !self.inboxStore.execApprovals.isEmpty
|
||||
|| self.inboxStore.hasCompletedExecApprovalSnapshotRefresh
|
||||
|
||||
@@ -398,9 +398,7 @@ private struct WatchControlSurfaceView: View {
|
||||
}
|
||||
|
||||
private var primaryLabel: String {
|
||||
if self.store.activeExecApproval != nil {
|
||||
return "Next up"
|
||||
}
|
||||
if self.store.activeExecApproval != nil { return "Next up" }
|
||||
return self.store.appSnapshot?.gatewayConnected == true ? "Running" : "Pairing"
|
||||
}
|
||||
|
||||
@@ -505,12 +503,8 @@ private struct WatchControlSurfaceView: View {
|
||||
return greetingTextOverride
|
||||
}
|
||||
let hour = Calendar.current.component(.hour, from: Date())
|
||||
if hour < 12 {
|
||||
return "Good morning"
|
||||
}
|
||||
if hour < 18 {
|
||||
return "Good afternoon"
|
||||
}
|
||||
if hour < 12 { return "Good morning" }
|
||||
if hour < 18 { return "Good afternoon" }
|
||||
return "Good evening"
|
||||
}
|
||||
|
||||
|
||||
@@ -7,23 +7,11 @@ func age(from date: Date, now: Date = .init()) -> String {
|
||||
let hours = minutes / 60
|
||||
let days = hours / 24
|
||||
|
||||
if seconds < 60 {
|
||||
return "just now"
|
||||
}
|
||||
if minutes == 1 {
|
||||
return "1 minute ago"
|
||||
}
|
||||
if minutes < 60 {
|
||||
return "\(minutes)m ago"
|
||||
}
|
||||
if hours == 1 {
|
||||
return "1 hour ago"
|
||||
}
|
||||
if hours < 24 {
|
||||
return "\(hours)h ago"
|
||||
}
|
||||
if days == 1 {
|
||||
return "yesterday"
|
||||
}
|
||||
if seconds < 60 { return "just now" }
|
||||
if minutes == 1 { return "1 minute ago" }
|
||||
if minutes < 60 { return "\(minutes)m ago" }
|
||||
if hours == 1 { return "1 hour ago" }
|
||||
if hours < 24 { return "\(hours)h ago" }
|
||||
if days == 1 { return "yesterday" }
|
||||
return "\(days)d ago"
|
||||
}
|
||||
|
||||
@@ -30,9 +30,7 @@ enum AgentWorkspace {
|
||||
static func displayPath(for url: URL) -> String {
|
||||
let home = FileManager().homeDirectoryForCurrentUser.path
|
||||
let path = url.path
|
||||
if path == home {
|
||||
return "~"
|
||||
}
|
||||
if path == home { return "~" }
|
||||
if path.hasPrefix(home + "/") {
|
||||
return "~/" + String(path.dropFirst(home.count + 1))
|
||||
}
|
||||
@@ -41,9 +39,7 @@ enum AgentWorkspace {
|
||||
|
||||
static func resolveWorkspaceURL(from userInput: String?) -> URL {
|
||||
let trimmed = userInput?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if trimmed.isEmpty {
|
||||
return OpenClawConfigFile.defaultWorkspaceURL()
|
||||
}
|
||||
if trimmed.isEmpty { return OpenClawConfigFile.defaultWorkspaceURL() }
|
||||
let expanded = (trimmed as NSString).expandingTildeInPath
|
||||
return URL(fileURLWithPath: expanded, isDirectory: true)
|
||||
}
|
||||
@@ -80,9 +76,7 @@ enum AgentWorkspace {
|
||||
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
||||
return .safe
|
||||
}
|
||||
if !isDir.boolValue {
|
||||
return .blocked("Workspace path points to a file.")
|
||||
}
|
||||
if !isDir.boolValue { return .blocked("Workspace path points to a file.") }
|
||||
let agentsURL = self.agentsURL(workspaceURL: workspaceURL)
|
||||
if fm.fileExists(atPath: agentsURL.path) {
|
||||
return .safe
|
||||
|
||||
@@ -15,9 +15,7 @@ enum CLIInstaller {
|
||||
case incompatible(location: String, found: String, required: String)
|
||||
|
||||
var isReady: Bool {
|
||||
if case .ready = self {
|
||||
return true
|
||||
}
|
||||
if case .ready = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +44,7 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
}
|
||||
|
||||
let body: [String: Any] = {
|
||||
if let dict = message.body as? [String: Any] {
|
||||
return dict
|
||||
}
|
||||
if let dict = message.body as? [String: Any] { return dict }
|
||||
if let dict = message.body as? [AnyHashable: Any] {
|
||||
return dict.reduce(into: [String: Any]()) { acc, pair in
|
||||
guard let key = pair.key as? String else { return }
|
||||
@@ -59,9 +57,7 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
|
||||
let userActionAny = body["userAction"] ?? body
|
||||
let userAction: [String: Any] = {
|
||||
if let dict = userActionAny as? [String: Any] {
|
||||
return dict
|
||||
}
|
||||
if let dict = userActionAny as? [String: Any] { return dict }
|
||||
if let dict = userActionAny as? [AnyHashable: Any] {
|
||||
return dict.reduce(into: [String: Any]()) { acc, pair in
|
||||
guard let key = pair.key as? String else { return }
|
||||
@@ -137,24 +133,12 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
|
||||
private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
|
||||
let (a, b, _, _) = ip
|
||||
if a == 10 {
|
||||
return true
|
||||
}
|
||||
if a == 172, (16...31).contains(Int(b)) {
|
||||
return true
|
||||
}
|
||||
if a == 192, b == 168 {
|
||||
return true
|
||||
}
|
||||
if a == 127 {
|
||||
return true
|
||||
}
|
||||
if a == 169, b == 254 {
|
||||
return true
|
||||
}
|
||||
if a == 100, (64...127).contains(Int(b)) {
|
||||
return true
|
||||
}
|
||||
if a == 10 { return true }
|
||||
if a == 172, (16...31).contains(Int(b)) { return true }
|
||||
if a == 192, b == 168 { return true }
|
||||
if a == 127 { return true }
|
||||
if a == 169, b == 254 { return true }
|
||||
if a == 100, (64...127).contains(Int(b)) { return true }
|
||||
return false
|
||||
}
|
||||
// Formatting helpers live in OpenClawKit (`OpenClawCanvasA2UIAction`).
|
||||
|
||||
@@ -206,15 +206,9 @@ final class HoverChromeContainerView: NSView {
|
||||
// When the chrome is hidden, do not intercept any mouse events (let the WKWebView receive them).
|
||||
guard self.alphaValue > 0.02 else { return nil }
|
||||
|
||||
if self.closeButton.frame.contains(point) {
|
||||
return self.closeButton
|
||||
}
|
||||
if self.dragHandle.frame.contains(point) {
|
||||
return self.dragHandle
|
||||
}
|
||||
if self.resizeHandle.frame.contains(point) {
|
||||
return self.resizeHandle
|
||||
}
|
||||
if self.closeButton.frame.contains(point) { return self.closeButton }
|
||||
if self.dragHandle.frame.contains(point) { return self.dragHandle }
|
||||
if self.resizeHandle.frame.contains(point) { return self.resizeHandle }
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -255,9 +255,7 @@ final class CanvasManager {
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
|
||||
if let url = URL(string: trimmed), let scheme = url.scheme?.lowercased() {
|
||||
if scheme == "https" || scheme == "http" || scheme == "file" {
|
||||
return url
|
||||
}
|
||||
if scheme == "https" || scheme == "http" || scheme == "file" { return url }
|
||||
}
|
||||
|
||||
// Convenience: existing absolute *file* paths resolve as local files.
|
||||
@@ -304,18 +302,14 @@ final class CanvasManager {
|
||||
let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first
|
||||
.map(String.init) ?? trimmed
|
||||
var path = withoutQuery
|
||||
if path.hasPrefix("/") {
|
||||
path.removeFirst()
|
||||
}
|
||||
if path.hasPrefix("/") { path.removeFirst() }
|
||||
path = path.removingPercentEncoding ?? path
|
||||
|
||||
// Root special-case: built-in scaffold page when no index exists.
|
||||
if path.isEmpty {
|
||||
let a = sessionDir.appendingPathComponent("index.html", isDirectory: false)
|
||||
let b = sessionDir.appendingPathComponent("index.htm", isDirectory: false)
|
||||
if fm.fileExists(atPath: a.path) || fm.fileExists(atPath: b.path) {
|
||||
return .ok
|
||||
}
|
||||
if fm.fileExists(atPath: a.path) || fm.fileExists(atPath: b.path) { return .ok }
|
||||
return .welcome
|
||||
}
|
||||
|
||||
@@ -343,9 +337,7 @@ final class CanvasManager {
|
||||
private static func indexExists(in dir: URL) -> Bool {
|
||||
let fm = FileManager()
|
||||
let a = dir.appendingPathComponent("index.html", isDirectory: false)
|
||||
if fm.fileExists(atPath: a.path) {
|
||||
return true
|
||||
}
|
||||
if fm.fileExists(atPath: a.path) { return true }
|
||||
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
|
||||
return fm.fileExists(atPath: b.path)
|
||||
}
|
||||
|
||||
@@ -61,12 +61,8 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
|
||||
// Path mapping: request path maps directly into the session dir.
|
||||
var path = url.path
|
||||
if let qIdx = path.firstIndex(of: "?") {
|
||||
path = String(path[..<qIdx])
|
||||
}
|
||||
if path.hasPrefix("/") {
|
||||
path.removeFirst()
|
||||
}
|
||||
if let qIdx = path.firstIndex(of: "?") { path = String(path[..<qIdx]) }
|
||||
if path.hasPrefix("/") { path.removeFirst() }
|
||||
path = path.removingPercentEncoding ?? path
|
||||
|
||||
// Special-case: welcome page when root index is missing.
|
||||
@@ -117,9 +113,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
var isDir: ObjCBool = false
|
||||
if fm.fileExists(atPath: candidate.path, isDirectory: &isDir) {
|
||||
if isDir.boolValue {
|
||||
if let idx = self.resolveIndex(in: candidate) {
|
||||
return idx
|
||||
}
|
||||
if let idx = self.resolveIndex(in: candidate) { return idx }
|
||||
return nil
|
||||
}
|
||||
return candidate
|
||||
@@ -130,9 +124,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
if !requestPath.isEmpty, !requestPath.hasSuffix("/") {
|
||||
candidate = sessionRoot.appendingPathComponent(requestPath, isDirectory: true)
|
||||
if fm.fileExists(atPath: candidate.path, isDirectory: &isDir), isDir.boolValue {
|
||||
if let idx = self.resolveIndex(in: candidate) {
|
||||
return idx
|
||||
}
|
||||
if let idx = self.resolveIndex(in: candidate) { return idx }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,13 +140,9 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
private func resolveIndex(in dir: URL) -> URL? {
|
||||
let fm = FileManager()
|
||||
let a = dir.appendingPathComponent("index.html", isDirectory: false)
|
||||
if fm.fileExists(atPath: a.path) {
|
||||
return a
|
||||
}
|
||||
if fm.fileExists(atPath: a.path) { return a }
|
||||
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
|
||||
if fm.fileExists(atPath: b.path) {
|
||||
return b
|
||||
}
|
||||
if fm.fileExists(atPath: b.path) { return b }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -229,9 +217,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
private func loadBundledResourceData(relativePath: String) -> Data? {
|
||||
let trimmed = relativePath.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
if trimmed.contains("..") || trimmed.contains("\\") {
|
||||
return nil
|
||||
}
|
||||
if trimmed.contains("..") || trimmed.contains("\\") { return nil }
|
||||
|
||||
let parts = trimmed.split(separator: "/")
|
||||
guard let filename = parts.last else { return nil }
|
||||
@@ -251,9 +237,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
}
|
||||
|
||||
private func textEncodingName(forMimeType mimeType: String) -> String? {
|
||||
if mimeType.hasPrefix("text/") {
|
||||
return "utf-8"
|
||||
}
|
||||
if mimeType.hasPrefix("text/") { return "utf-8" }
|
||||
switch mimeType {
|
||||
case "application/javascript", "application/json", "image/svg+xml":
|
||||
return "utf-8"
|
||||
|
||||
@@ -25,9 +25,7 @@ enum CanvasPresentation {
|
||||
case panel(anchorProvider: () -> NSRect?)
|
||||
|
||||
var isPanel: Bool {
|
||||
if case .panel = self {
|
||||
return true
|
||||
}
|
||||
if case .panel = self { return true }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ extension CanvasWindowController {
|
||||
|
||||
static func sanitizeSessionKey(_ key: String) -> String {
|
||||
let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
return "main"
|
||||
}
|
||||
if trimmed.isEmpty { return "main" }
|
||||
let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+")
|
||||
let scalars = trimmed.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" }
|
||||
return String(scalars)
|
||||
@@ -32,9 +30,7 @@ extension CanvasWindowController {
|
||||
let key = self.storedFrameDefaultsKey(sessionKey: sessionKey)
|
||||
guard let arr = UserDefaults.standard.array(forKey: key) as? [Double], arr.count == 4 else { return nil }
|
||||
let rect = NSRect(x: arr[0], y: arr[1], width: arr[2], height: arr[3])
|
||||
if rect.width < CanvasLayout.minPanelSize.width || rect.height < CanvasLayout.minPanelSize.height {
|
||||
return nil
|
||||
}
|
||||
if rect.width < CanvasLayout.minPanelSize.width || rect.height < CanvasLayout.minPanelSize.height { return nil }
|
||||
return rect
|
||||
}
|
||||
|
||||
|
||||
@@ -34,24 +34,12 @@ extension CanvasWindowController {
|
||||
|
||||
static func _testIsLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
|
||||
let (a, b, _, _) = ip
|
||||
if a == 10 {
|
||||
return true
|
||||
}
|
||||
if a == 172, (16...31).contains(Int(b)) {
|
||||
return true
|
||||
}
|
||||
if a == 192, b == 168 {
|
||||
return true
|
||||
}
|
||||
if a == 127 {
|
||||
return true
|
||||
}
|
||||
if a == 169, b == 254 {
|
||||
return true
|
||||
}
|
||||
if a == 100, (64...127).contains(Int(b)) {
|
||||
return true
|
||||
}
|
||||
if a == 10 { return true }
|
||||
if a == 172, (16...31).contains(Int(b)) { return true }
|
||||
if a == 192, b == 168 { return true }
|
||||
if a == 127 { return true }
|
||||
if a == 169, b == 254 { return true }
|
||||
if a == 100, (64...127).contains(Int(b)) { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -79,18 +79,10 @@ extension CanvasWindowController {
|
||||
// - If agent provides width/height, override size.
|
||||
// - If agent provides only size, keep the remembered origin.
|
||||
if let placement = self.preferredPlacement {
|
||||
if let x = placement.x {
|
||||
frame.origin.x = x
|
||||
}
|
||||
if let y = placement.y {
|
||||
frame.origin.y = y
|
||||
}
|
||||
if let w = placement.width {
|
||||
frame.size.width = max(CanvasLayout.minPanelSize.width, CGFloat(w))
|
||||
}
|
||||
if let h = placement.height {
|
||||
frame.size.height = max(CanvasLayout.minPanelSize.height, CGFloat(h))
|
||||
}
|
||||
if let x = placement.x { frame.origin.x = x }
|
||||
if let y = placement.y { frame.origin.y = y }
|
||||
if let w = placement.width { frame.size.width = max(CanvasLayout.minPanelSize.width, CGFloat(w)) }
|
||||
if let h = placement.height { frame.size.height = max(CanvasLayout.minPanelSize.height, CGFloat(h)) }
|
||||
}
|
||||
|
||||
self.setPanelFrame(frame, on: targetScreen)
|
||||
@@ -136,9 +128,7 @@ extension CanvasWindowController {
|
||||
}
|
||||
|
||||
static func constrainFrame(_ frame: NSRect, toVisibleFrame bounds: NSRect) -> NSRect {
|
||||
if bounds == .zero {
|
||||
return frame
|
||||
}
|
||||
if bounds == .zero { return frame }
|
||||
|
||||
var next = frame
|
||||
next.size.width = min(max(CanvasLayout.minPanelSize.width, next.size.width), bounds.width)
|
||||
|
||||
@@ -323,12 +323,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, WK
|
||||
func shouldAutoNavigateToA2UI(lastAutoTarget: String?, candidateTarget: String) -> Bool {
|
||||
let current = (self.currentTarget ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let candidate = candidateTarget.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if current.isEmpty || current == "/" {
|
||||
return true
|
||||
}
|
||||
if !candidate.isEmpty, current == candidate {
|
||||
return false
|
||||
}
|
||||
if current.isEmpty || current == "/" { return true }
|
||||
if !candidate.isEmpty, current == candidate { return false }
|
||||
if let lastAuto = lastAutoTarget?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!lastAuto.isEmpty,
|
||||
current == lastAuto
|
||||
|
||||
@@ -121,9 +121,7 @@ struct ConfigSchemaForm: View {
|
||||
}
|
||||
return AnyView(
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if let label {
|
||||
Text(label).font(.callout.weight(.semibold))
|
||||
}
|
||||
if let label { Text(label).font(.callout.weight(.semibold)) }
|
||||
Text("Unsupported field type.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -154,9 +152,7 @@ struct ConfigSchemaForm: View {
|
||||
let sortedKeys = properties.keys.sorted { lhs, rhs in
|
||||
let orderA = hintForPath(path + [.key(lhs)], hints: store.configUiHints)?.order ?? 0
|
||||
let orderB = hintForPath(path + [.key(rhs)], hints: store.configUiHints)?.order ?? 0
|
||||
if orderA != orderB {
|
||||
return orderA < orderB
|
||||
}
|
||||
if orderA != orderB { return orderA < orderB }
|
||||
return lhs < rhs
|
||||
}
|
||||
|
||||
@@ -210,9 +206,7 @@ struct ConfigSchemaForm: View {
|
||||
value: Any?) -> Bool
|
||||
{
|
||||
guard schema.allowsAdditionalProperties else { return false }
|
||||
if self.mode != .channelQuick {
|
||||
return true
|
||||
}
|
||||
if self.mode != .channelQuick { return true }
|
||||
guard let dict = value as? [String: Any] else { return false }
|
||||
let reserved = Set(schema.properties.keys)
|
||||
return dict.keys.contains { !reserved.contains($0) }
|
||||
@@ -341,9 +335,7 @@ struct ConfigSchemaForm: View {
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if let label {
|
||||
Text(label).font(.callout.weight(.semibold))
|
||||
}
|
||||
if let label { Text(label).font(.callout.weight(.semibold)) }
|
||||
if let help {
|
||||
Text(help)
|
||||
.font(.caption)
|
||||
@@ -379,9 +371,7 @@ struct ConfigSchemaForm: View {
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if let label {
|
||||
Text(label).font(.callout.weight(.semibold))
|
||||
}
|
||||
if let label { Text(label).font(.callout.weight(.semibold)) }
|
||||
if let help {
|
||||
Text(help)
|
||||
.font(.caption)
|
||||
@@ -420,9 +410,7 @@ struct ConfigSchemaForm: View {
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if let label {
|
||||
Text(label).font(.callout.weight(.semibold))
|
||||
}
|
||||
if let label { Text(label).font(.callout.weight(.semibold)) }
|
||||
if let help {
|
||||
Text(help)
|
||||
.font(.caption)
|
||||
@@ -450,9 +438,7 @@ struct ConfigSchemaForm: View {
|
||||
let items = value as? [Any] ?? []
|
||||
let itemSchema = schema.items
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if let label {
|
||||
Text(label).font(.callout.weight(.semibold))
|
||||
}
|
||||
if let label { Text(label).font(.callout.weight(.semibold)) }
|
||||
if let help {
|
||||
Text(help)
|
||||
.font(.caption)
|
||||
@@ -544,9 +530,7 @@ struct ConfigSchemaForm: View {
|
||||
private func stringBinding(_ path: ConfigPath, defaultValue: String?) -> Binding<String> {
|
||||
Binding(
|
||||
get: {
|
||||
if let value = store.configValue(at: path) as? String {
|
||||
return value
|
||||
}
|
||||
if let value = store.configValue(at: path) as? String { return value }
|
||||
return defaultValue ?? ""
|
||||
},
|
||||
set: { newValue in
|
||||
@@ -558,9 +542,7 @@ struct ConfigSchemaForm: View {
|
||||
private func boolBinding(_ path: ConfigPath, defaultValue: Bool?) -> Binding<Bool> {
|
||||
Binding(
|
||||
get: {
|
||||
if let value = store.configValue(at: path) as? Bool {
|
||||
return value
|
||||
}
|
||||
if let value = store.configValue(at: path) as? Bool { return value }
|
||||
return defaultValue ?? false
|
||||
},
|
||||
set: { newValue in
|
||||
@@ -575,9 +557,7 @@ struct ConfigSchemaForm: View {
|
||||
{
|
||||
Binding(
|
||||
get: {
|
||||
if let value = store.configValue(at: path) {
|
||||
return String(describing: value)
|
||||
}
|
||||
if let value = store.configValue(at: path) { return String(describing: value) }
|
||||
guard let defaultValue else { return "" }
|
||||
return isInteger ? String(Int(defaultValue)) : String(defaultValue)
|
||||
},
|
||||
|
||||
@@ -10,28 +10,16 @@ extension ChannelsSettings {
|
||||
}
|
||||
|
||||
private func configuredChannelTint(configured: Bool, running: Bool, hasError: Bool, probeOk: Bool?) -> Color {
|
||||
if !configured {
|
||||
return .secondary
|
||||
}
|
||||
if hasError {
|
||||
return .orange
|
||||
}
|
||||
if probeOk == false {
|
||||
return .orange
|
||||
}
|
||||
if running {
|
||||
return .green
|
||||
}
|
||||
if !configured { return .secondary }
|
||||
if hasError { return .orange }
|
||||
if probeOk == false { return .orange }
|
||||
if running { return .green }
|
||||
return .orange
|
||||
}
|
||||
|
||||
private func configuredChannelSummary(configured: Bool, running: Bool) -> String {
|
||||
if !configured {
|
||||
return "Not configured"
|
||||
}
|
||||
if running {
|
||||
return "Running"
|
||||
}
|
||||
if !configured { return "Not configured" }
|
||||
if running { return "Running" }
|
||||
return "Configured"
|
||||
}
|
||||
|
||||
@@ -108,21 +96,11 @@ extension ChannelsSettings {
|
||||
var whatsAppTint: Color {
|
||||
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||
else { return .secondary }
|
||||
if !status.configured {
|
||||
return .secondary
|
||||
}
|
||||
if !status.linked {
|
||||
return .red
|
||||
}
|
||||
if status.lastError != nil {
|
||||
return .orange
|
||||
}
|
||||
if status.connected {
|
||||
return .green
|
||||
}
|
||||
if status.running {
|
||||
return .orange
|
||||
}
|
||||
if !status.configured { return .secondary }
|
||||
if !status.linked { return .red }
|
||||
if status.lastError != nil { return .orange }
|
||||
if status.connected { return .green }
|
||||
if status.running { return .orange }
|
||||
return .orange
|
||||
}
|
||||
|
||||
@@ -179,15 +157,9 @@ extension ChannelsSettings {
|
||||
var whatsAppSummary: String {
|
||||
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||
else { return "Checking…" }
|
||||
if !status.linked {
|
||||
return "Not linked"
|
||||
}
|
||||
if status.connected {
|
||||
return "Connected"
|
||||
}
|
||||
if status.running {
|
||||
return "Running"
|
||||
}
|
||||
if !status.linked { return "Not linked" }
|
||||
if status.connected { return "Connected" }
|
||||
if status.running { return "Running" }
|
||||
return "Linked"
|
||||
}
|
||||
|
||||
@@ -372,9 +344,7 @@ extension ChannelsSettings {
|
||||
return channels.sorted { lhs, rhs in
|
||||
let lhsEnabled = self.channelEnabled(lhs)
|
||||
let rhsEnabled = self.channelEnabled(rhs)
|
||||
if lhsEnabled != rhsEnabled {
|
||||
return lhsEnabled && !rhsEnabled
|
||||
}
|
||||
if lhsEnabled != rhsEnabled { return lhsEnabled && !rhsEnabled }
|
||||
return lhs.sortOrder < rhs.sortOrder
|
||||
}
|
||||
}
|
||||
@@ -435,12 +405,8 @@ extension ChannelsSettings {
|
||||
case "imessage":
|
||||
return self.imessageTint
|
||||
default:
|
||||
if self.channelHasError(channel) {
|
||||
return .orange
|
||||
}
|
||||
if self.channelEnabled(channel) {
|
||||
return .green
|
||||
}
|
||||
if self.channelHasError(channel) { return .orange }
|
||||
if self.channelEnabled(channel) { return .green }
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
@@ -460,12 +426,8 @@ extension ChannelsSettings {
|
||||
case "imessage":
|
||||
return self.imessageSummary
|
||||
default:
|
||||
if self.channelHasError(channel) {
|
||||
return "Error"
|
||||
}
|
||||
if self.channelEnabled(channel) {
|
||||
return "Active"
|
||||
}
|
||||
if self.channelHasError(channel) { return "Error" }
|
||||
if self.channelEnabled(channel) { return "Active" }
|
||||
return "Not configured"
|
||||
}
|
||||
}
|
||||
@@ -570,9 +532,7 @@ extension ChannelsSettings {
|
||||
|
||||
private func resolveChannelTitle(_ id: String) -> String {
|
||||
let label = self.store.resolveChannelLabel(id)
|
||||
if label != id {
|
||||
return label
|
||||
}
|
||||
if label != id { return label }
|
||||
return id.prefix(1).uppercased() + id.dropFirst()
|
||||
}
|
||||
|
||||
|
||||
@@ -99,9 +99,7 @@ extension CoalescingFSEventsWatcher {
|
||||
guard self.shouldNotify(numEvents, eventPaths) else { return }
|
||||
|
||||
// Coalesce rapid changes (common during builds/atomic saves).
|
||||
if self.pending {
|
||||
return
|
||||
}
|
||||
if self.pending { return }
|
||||
self.pending = true
|
||||
self.queue.asyncAfter(deadline: .now() + self.coalesceDelay) { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -6,17 +6,11 @@ enum CommandResolver {
|
||||
|
||||
static func gatewayEntrypoint(in root: URL) -> String? {
|
||||
let distEntry = root.appendingPathComponent("dist/index.js").path
|
||||
if FileManager().isReadableFile(atPath: distEntry) {
|
||||
return distEntry
|
||||
}
|
||||
if FileManager().isReadableFile(atPath: distEntry) { return distEntry }
|
||||
let openclawEntry = root.appendingPathComponent("openclaw.mjs").path
|
||||
if FileManager().isReadableFile(atPath: openclawEntry) {
|
||||
return openclawEntry
|
||||
}
|
||||
if FileManager().isReadableFile(atPath: openclawEntry) { return openclawEntry }
|
||||
let binEntry = root.appendingPathComponent("bin/openclaw.js").path
|
||||
if FileManager().isReadableFile(atPath: binEntry) {
|
||||
return binEntry
|
||||
}
|
||||
if FileManager().isReadableFile(atPath: binEntry) { return binEntry }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -207,9 +201,7 @@ enum CommandResolver {
|
||||
for i in 0..<maxCount {
|
||||
let ai = i < va.count ? va[i] : 0
|
||||
let bi = i < vb.count ? vb[i] : 0
|
||||
if ai != bi {
|
||||
return ai > bi
|
||||
}
|
||||
if ai != bi { return ai > bi }
|
||||
}
|
||||
// If identical numerically, keep stable ordering.
|
||||
return a > b
|
||||
@@ -263,12 +255,8 @@ enum CommandResolver {
|
||||
}
|
||||
|
||||
static func hasAnyOpenClawInvoker(searchPaths: [String]? = nil) -> Bool {
|
||||
if self.openclawExecutable(searchPaths: searchPaths) != nil {
|
||||
return true
|
||||
}
|
||||
if self.findExecutable(named: "pnpm", searchPaths: searchPaths) != nil {
|
||||
return true
|
||||
}
|
||||
if self.openclawExecutable(searchPaths: searchPaths) != nil { return true }
|
||||
if self.findExecutable(named: "pnpm", searchPaths: searchPaths) != nil { return true }
|
||||
if self.findExecutable(named: "node", searchPaths: searchPaths) != nil,
|
||||
self.nodeCliPath() != nil
|
||||
{
|
||||
@@ -599,9 +587,7 @@ enum CommandResolver {
|
||||
}
|
||||
|
||||
private static func shellQuote(_ text: String) -> String {
|
||||
if text.isEmpty {
|
||||
return "''"
|
||||
}
|
||||
if text.isEmpty { return "''" }
|
||||
let escaped = text.replacingOccurrences(of: "'", with: "'\\''")
|
||||
return "'\(escaped)'"
|
||||
}
|
||||
@@ -625,12 +611,8 @@ enum CommandResolver {
|
||||
}
|
||||
|
||||
private static func isValidSSHComponent(_ value: String, allowLeadingDash: Bool = false) -> Bool {
|
||||
if value.isEmpty {
|
||||
return false
|
||||
}
|
||||
if !allowLeadingDash, value.hasPrefix("-") {
|
||||
return false
|
||||
}
|
||||
if value.isEmpty { return false }
|
||||
if !allowLeadingDash, value.hasPrefix("-") { return false }
|
||||
let invalid = CharacterSet.whitespacesAndNewlines.union(.controlCharacters)
|
||||
return value.rangeOfCharacter(from: invalid) == nil
|
||||
}
|
||||
|
||||
@@ -22,15 +22,9 @@ final class ConfigFileWatcher: @unchecked Sendable, SimpleFileWatcherOwner {
|
||||
guard let eventPaths else { return true }
|
||||
let paths = unsafeBitCast(eventPaths, to: NSArray.self)
|
||||
for case let path as String in paths {
|
||||
if path == targetPath {
|
||||
return true
|
||||
}
|
||||
if path.hasSuffix("/\(targetName)") {
|
||||
return true
|
||||
}
|
||||
if path == watchedDirPath {
|
||||
return true
|
||||
}
|
||||
if path == targetPath { return true }
|
||||
if path.hasSuffix("/\(targetName)") { return true }
|
||||
if path == watchedDirPath { return true }
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
@@ -64,20 +64,14 @@ struct ConfigSchemaNode {
|
||||
}
|
||||
|
||||
var typeList: [String] {
|
||||
if let type = self.raw["type"] as? String {
|
||||
return [type]
|
||||
}
|
||||
if let types = self.raw["type"] as? [String] {
|
||||
return types
|
||||
}
|
||||
if let type = self.raw["type"] as? String { return [type] }
|
||||
if let types = self.raw["type"] as? [String] { return types }
|
||||
return []
|
||||
}
|
||||
|
||||
var schemaType: String? {
|
||||
let filtered = self.typeList.filter { $0 != "null" }
|
||||
if let first = filtered.first {
|
||||
return first
|
||||
}
|
||||
if let first = filtered.first { return first }
|
||||
return self.typeList.first
|
||||
}
|
||||
|
||||
@@ -102,12 +96,8 @@ struct ConfigSchemaNode {
|
||||
}
|
||||
|
||||
var literalValue: Any? {
|
||||
if let constValue {
|
||||
return constValue
|
||||
}
|
||||
if let enumValues, enumValues.count == 1 {
|
||||
return enumValues[0]
|
||||
}
|
||||
if let constValue { return constValue }
|
||||
if let enumValues, enumValues.count == 1 { return enumValues[0] }
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -129,16 +119,12 @@ struct ConfigSchemaNode {
|
||||
}
|
||||
|
||||
var allowsAdditionalProperties: Bool {
|
||||
if let allow = self.raw["additionalProperties"] as? Bool {
|
||||
return allow
|
||||
}
|
||||
if let allow = self.raw["additionalProperties"] as? Bool { return allow }
|
||||
return self.additionalProperties != nil
|
||||
}
|
||||
|
||||
var defaultValue: Any {
|
||||
if let value = self.raw["default"] {
|
||||
return value
|
||||
}
|
||||
if let value = self.raw["default"] { return value }
|
||||
switch self.schemaType {
|
||||
case "object":
|
||||
return [String: Any]()
|
||||
@@ -194,9 +180,7 @@ func decodeUiHints(_ raw: [String: Any]) -> [String: ConfigUiHint] {
|
||||
|
||||
func hintForPath(_ path: ConfigPath, hints: [String: ConfigUiHint]) -> ConfigUiHint? {
|
||||
let key = pathKey(path)
|
||||
if let direct = hints[key] {
|
||||
return direct
|
||||
}
|
||||
if let direct = hints[key] { return direct }
|
||||
let segments = key.split(separator: ".").map(String.init)
|
||||
for (hintKey, hint) in hints {
|
||||
guard hintKey.contains("*") else { continue }
|
||||
@@ -210,9 +194,7 @@ func hintForPath(_ path: ConfigPath, hints: [String: ConfigUiHint]) -> ConfigUiH
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return hint
|
||||
}
|
||||
if match { return hint }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -419,9 +419,7 @@ extension ConfigSettings {
|
||||
private func sortLookupChildren(_ lhs: ConfigSchemaLookupChild, _ rhs: ConfigSchemaLookupChild) -> Bool {
|
||||
let orderA = lhs.hint?.order ?? 0
|
||||
let orderB = rhs.hint?.order ?? 0
|
||||
if orderA != orderB {
|
||||
return orderA < orderB
|
||||
}
|
||||
if orderA != orderB { return orderA < orderB }
|
||||
return lhs.key < rhs.key
|
||||
}
|
||||
|
||||
@@ -444,9 +442,7 @@ extension ConfigSettings {
|
||||
}
|
||||
|
||||
private static func shouldRenderFormEditor(for schema: ConfigSchemaNode) -> Bool {
|
||||
if schema.schemaType == "array" {
|
||||
return true
|
||||
}
|
||||
if schema.schemaType == "array" { return true }
|
||||
return schema.additionalProperties != nil
|
||||
}
|
||||
|
||||
|
||||
@@ -49,9 +49,7 @@ struct ContextMenuCardView: View {
|
||||
|
||||
private var subtitle: String {
|
||||
let count = self.rows.count
|
||||
if count == 1 {
|
||||
return "1 session · 24h"
|
||||
}
|
||||
if count == 1 { return "1 session · 24h" }
|
||||
return "\(count) sessions · 24h"
|
||||
}
|
||||
|
||||
|
||||
@@ -9,25 +9,19 @@ struct ContextUsageBar: View {
|
||||
private static let okGreen: NSColor = .init(name: nil) { appearance in
|
||||
let base = NSColor.systemGreen
|
||||
let match = appearance.bestMatch(from: [.aqua, .darkAqua])
|
||||
if match == .darkAqua {
|
||||
return base
|
||||
}
|
||||
if match == .darkAqua { return base }
|
||||
return base.blended(withFraction: 0.24, of: .black) ?? base
|
||||
}
|
||||
|
||||
private static let trackFill: NSColor = .init(name: nil) { appearance in
|
||||
let match = appearance.bestMatch(from: [.aqua, .darkAqua])
|
||||
if match == .darkAqua {
|
||||
return NSColor.white.withAlphaComponent(0.14)
|
||||
}
|
||||
if match == .darkAqua { return NSColor.white.withAlphaComponent(0.14) }
|
||||
return NSColor.black.withAlphaComponent(0.12)
|
||||
}
|
||||
|
||||
private static let trackStroke: NSColor = .init(name: nil) { appearance in
|
||||
let match = appearance.bestMatch(from: [.aqua, .darkAqua])
|
||||
if match == .darkAqua {
|
||||
return NSColor.white.withAlphaComponent(0.22)
|
||||
}
|
||||
if match == .darkAqua { return NSColor.white.withAlphaComponent(0.22) }
|
||||
return NSColor.black.withAlphaComponent(0.2)
|
||||
}
|
||||
|
||||
@@ -43,15 +37,9 @@ struct ContextUsageBar: View {
|
||||
|
||||
private var tint: Color {
|
||||
guard let pct = self.percentUsed else { return .secondary }
|
||||
if pct >= 95 {
|
||||
return Color(nsColor: .systemRed)
|
||||
}
|
||||
if pct >= 80 {
|
||||
return Color(nsColor: .systemOrange)
|
||||
}
|
||||
if pct >= 60 {
|
||||
return Color(nsColor: .systemYellow)
|
||||
}
|
||||
if pct >= 95 { return Color(nsColor: .systemRed) }
|
||||
if pct >= 80 { return Color(nsColor: .systemOrange) }
|
||||
if pct >= 60 { return Color(nsColor: .systemYellow) }
|
||||
return Color(nsColor: Self.okGreen)
|
||||
}
|
||||
|
||||
@@ -74,9 +62,7 @@ struct ContextUsageBar: View {
|
||||
}
|
||||
|
||||
private var accessibilityValue: String {
|
||||
if self.contextTokens <= 0 {
|
||||
return "Unknown context window"
|
||||
}
|
||||
if self.contextTokens <= 0 { return "Unknown context window" }
|
||||
let pct = Int(round(self.clampedFractionUsed * 100))
|
||||
return "\(pct) percent used"
|
||||
}
|
||||
|
||||
@@ -343,9 +343,7 @@ final class ControlChannel {
|
||||
|
||||
let detail = nsError.localizedDescription.isEmpty ? "unknown gateway error" : nsError.localizedDescription
|
||||
let trimmed = detail.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.lowercased().hasPrefix("gateway error:") {
|
||||
return trimmed
|
||||
}
|
||||
if trimmed.lowercased().hasPrefix("gateway error:") { return trimmed }
|
||||
return "Gateway error: \(trimmed)"
|
||||
}
|
||||
|
||||
@@ -367,9 +365,7 @@ final class ControlChannel {
|
||||
|
||||
private func scheduleRecovery(reason: String) {
|
||||
let now = Date()
|
||||
if let last = self.lastRecoveryAt, now.timeIntervalSince(last) < 10 {
|
||||
return
|
||||
}
|
||||
if let last = self.lastRecoveryAt, now.timeIntervalSince(last) < 10 { return }
|
||||
guard self.recoveryTask == nil else { return }
|
||||
self.lastRecoveryAt = now
|
||||
|
||||
|
||||
@@ -99,9 +99,7 @@ extension CritterStatusLabel {
|
||||
deadlines: [Date]) -> TimeInterval
|
||||
{
|
||||
// Working motion needs a steady cadence; idle motion only wakes for its next visible event.
|
||||
if isWorking {
|
||||
return 0.35
|
||||
}
|
||||
if isWorking { return 0.35 }
|
||||
guard let nextDeadline = deadlines.min() else { return 1 }
|
||||
return max(0.05, nextDeadline.timeIntervalSince(now))
|
||||
}
|
||||
@@ -248,9 +246,7 @@ extension CritterStatusLabel {
|
||||
}
|
||||
|
||||
private var gatewayNeedsAttention: Bool {
|
||||
if self.isSleeping {
|
||||
return false
|
||||
}
|
||||
if self.isSleeping { return false }
|
||||
switch self.gatewayStatus {
|
||||
case .failed, .stopped:
|
||||
return !self.isPaused
|
||||
|
||||
@@ -110,9 +110,7 @@ extension CronJobEditor {
|
||||
"payload": payload,
|
||||
]
|
||||
self.applyDeleteAfterRun(to: &root)
|
||||
if !description.isEmpty {
|
||||
root["description"] = description
|
||||
}
|
||||
if !description.isEmpty { root["description"] = description }
|
||||
if !agentId.isEmpty {
|
||||
root["agentId"] = agentId
|
||||
} else if self.job?.agentId != nil {
|
||||
@@ -133,9 +131,7 @@ extension CronJobEditor {
|
||||
let trimmed = self.channel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
delivery["channel"] = trimmed.isEmpty ? "last" : trimmed
|
||||
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !to.isEmpty {
|
||||
delivery["to"] = to
|
||||
}
|
||||
if !to.isEmpty { delivery["to"] = to }
|
||||
if self.bestEffortDeliver {
|
||||
delivery["bestEffort"] = true
|
||||
} else if self.job?.delivery?.bestEffort == true {
|
||||
@@ -189,9 +185,7 @@ extension CronJobEditor {
|
||||
}
|
||||
|
||||
func buildSelectedPayload() throws -> [String: Any] {
|
||||
if self.isIsolatedLikeSessionTarget {
|
||||
return self.buildAgentTurnPayload()
|
||||
}
|
||||
if self.isIsolatedLikeSessionTarget { return self.buildAgentTurnPayload() }
|
||||
switch self.payloadKind {
|
||||
case .systemEvent:
|
||||
let text = self.trimmed(self.systemEventText)
|
||||
@@ -257,20 +251,14 @@ extension CronJobEditor {
|
||||
let msg = self.agentMessage.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var payload: [String: Any] = ["kind": "agentTurn", "message": msg]
|
||||
let thinking = self.thinking.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !thinking.isEmpty {
|
||||
payload["thinking"] = thinking
|
||||
}
|
||||
if let n = Int(self.timeoutSeconds), n > 0 {
|
||||
payload["timeoutSeconds"] = n
|
||||
}
|
||||
if !thinking.isEmpty { payload["thinking"] = thinking }
|
||||
if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n }
|
||||
return payload
|
||||
}
|
||||
|
||||
static func parseDurationMs(_ input: String) -> Int? {
|
||||
let raw = input.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if raw.isEmpty {
|
||||
return nil
|
||||
}
|
||||
if raw.isEmpty { return nil }
|
||||
|
||||
let rx = try? NSRegularExpression(pattern: "^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$", options: [.caseInsensitive])
|
||||
guard let match = rx?.firstMatch(in: raw, range: NSRange(location: 0, length: raw.utf16.count)) else {
|
||||
@@ -282,9 +270,7 @@ extension CronJobEditor {
|
||||
return String(raw[r])
|
||||
}
|
||||
let n = Double(group(1)) ?? 0
|
||||
if !n.isFinite || n <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !n.isFinite || n <= 0 { return nil }
|
||||
let unit = group(2).lowercased()
|
||||
let factor: Double = switch unit {
|
||||
case "ms": 1
|
||||
|
||||
@@ -85,9 +85,7 @@ struct CronJobEditor: View {
|
||||
}
|
||||
|
||||
func channelLabel(for id: String) -> String {
|
||||
if id == "last" {
|
||||
return "last"
|
||||
}
|
||||
if id == "last" { return "last" }
|
||||
return self.channelsStore.resolveChannelLabel(id)
|
||||
}
|
||||
|
||||
|
||||
@@ -139,12 +139,8 @@ enum CronSchedule: Codable, Equatable {
|
||||
|
||||
static func parseAtDate(_ value: String) -> Date? {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
return nil
|
||||
}
|
||||
if let date = makeIsoFormatter(withFractional: true).date(from: trimmed) {
|
||||
return date
|
||||
}
|
||||
if trimmed.isEmpty { return nil }
|
||||
if let date = makeIsoFormatter(withFractional: true).date(from: trimmed) { return date }
|
||||
return self.makeIsoFormatter(withFractional: false).date(from: trimmed)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,10 @@ extension CronSettings {
|
||||
case let .every(everyMs, _):
|
||||
return "every \(self.formatDuration(ms: everyMs))"
|
||||
case let .cron(expr, tz):
|
||||
if let tz, !tz.isEmpty {
|
||||
return "cron \(expr) (\(tz))"
|
||||
}
|
||||
if let tz, !tz.isEmpty { return "cron \(expr) (\(tz))" }
|
||||
return "cron \(expr)"
|
||||
case let .onExit(command, cwd):
|
||||
if let cwd, !cwd.isEmpty {
|
||||
return "on exit: \(command) (cwd: \(cwd))"
|
||||
}
|
||||
if let cwd, !cwd.isEmpty { return "on exit: \(command) (cwd: \(cwd))" }
|
||||
return "on exit: \(command)"
|
||||
}
|
||||
}
|
||||
@@ -43,20 +39,12 @@ extension CronSettings {
|
||||
|
||||
func nextRunLabel(_ date: Date, now: Date = .init()) -> String {
|
||||
let delta = date.timeIntervalSince(now)
|
||||
if delta <= 0 {
|
||||
return "due"
|
||||
}
|
||||
if delta < 60 {
|
||||
return "in <1m"
|
||||
}
|
||||
if delta <= 0 { return "due" }
|
||||
if delta < 60 { return "in <1m" }
|
||||
let minutes = Int(round(delta / 60))
|
||||
if minutes < 60 {
|
||||
return "in \(minutes)m"
|
||||
}
|
||||
if minutes < 60 { return "in \(minutes)m" }
|
||||
let hours = Int(round(Double(minutes) / 60))
|
||||
if hours < 48 {
|
||||
return "in \(hours)h"
|
||||
}
|
||||
if hours < 48 { return "in \(hours)h" }
|
||||
let days = Int(round(Double(hours) / 24))
|
||||
return "in \(days)d"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ extension CronSettings {
|
||||
.alert("Delete cron job?", isPresented: Binding(
|
||||
get: { self.confirmDelete != nil },
|
||||
set: {
|
||||
if !$0 {
|
||||
self.confirmDelete = nil
|
||||
}
|
||||
if !$0 { self.confirmDelete = nil }
|
||||
})) {
|
||||
Button("Cancel", role: .cancel) { self.confirmDelete = nil }
|
||||
Button("Delete", role: .destructive) {
|
||||
|
||||
@@ -222,12 +222,8 @@ extension CronSettings {
|
||||
.font(.callout)
|
||||
.textSelection(.enabled)
|
||||
HStack(spacing: 8) {
|
||||
if let thinking, !thinking.isEmpty {
|
||||
StatusPill(text: "think \(thinking)", tint: .secondary)
|
||||
}
|
||||
if let timeoutSeconds {
|
||||
StatusPill(text: "\(timeoutSeconds)s", tint: .secondary)
|
||||
}
|
||||
if let thinking, !thinking.isEmpty { StatusPill(text: "think \(thinking)", tint: .secondary) }
|
||||
if let timeoutSeconds { StatusPill(text: "\(timeoutSeconds)s", tint: .secondary) }
|
||||
if job.supportsAnnounceDelivery {
|
||||
let delivery = job.delivery
|
||||
if let delivery {
|
||||
@@ -236,9 +232,7 @@ extension CronSettings {
|
||||
if let channel = delivery.channel, !channel.isEmpty {
|
||||
StatusPill(text: channel, tint: .secondary)
|
||||
}
|
||||
if let to = delivery.to, !to.isEmpty {
|
||||
StatusPill(text: to, tint: .secondary)
|
||||
}
|
||||
if let to = delivery.to, !to.isEmpty { StatusPill(text: to, tint: .secondary) }
|
||||
} else {
|
||||
StatusPill(text: "no delivery", tint: .secondary)
|
||||
}
|
||||
|
||||
@@ -236,13 +236,9 @@ enum DebugActions {
|
||||
|
||||
static func killProcess(_ pid: Int) async -> Result<Void, DebugActionError> {
|
||||
let primary = await ShellExecutor.run(command: ["kill", "-TERM", "\(pid)"], cwd: nil, env: nil, timeout: 2)
|
||||
if primary.ok {
|
||||
return .success(())
|
||||
}
|
||||
if primary.ok { return .success(()) }
|
||||
let force = await ShellExecutor.run(command: ["kill", "-KILL", "\(pid)"], cwd: nil, env: nil, timeout: 2)
|
||||
if force.ok {
|
||||
return .success(())
|
||||
}
|
||||
if force.ok { return .success(()) }
|
||||
let detail = force.message ?? primary.message ?? "kill failed"
|
||||
return .failure(.message(detail))
|
||||
}
|
||||
|
||||
@@ -318,9 +318,7 @@ struct DebugSettings: View {
|
||||
HStack(spacing: 8) {
|
||||
Text("Port diagnostics")
|
||||
.font(.caption.weight(.semibold))
|
||||
if self.portCheckInFlight {
|
||||
ProgressView().controlSize(.small)
|
||||
}
|
||||
if self.portCheckInFlight { ProgressView().controlSize(.small) }
|
||||
Spacer()
|
||||
Button("Check gateway ports") {
|
||||
Task { await self.runPortCheck() }
|
||||
|
||||
@@ -29,9 +29,7 @@ enum DeviceModelCatalog {
|
||||
""
|
||||
}
|
||||
|
||||
if title.isEmpty {
|
||||
return nil
|
||||
}
|
||||
if title.isEmpty { return nil }
|
||||
return DevicePresentation(title: title, symbol: symbol)
|
||||
}
|
||||
|
||||
@@ -52,54 +50,26 @@ enum DeviceModelCatalog {
|
||||
guard !modelIdentifier.isEmpty else { return nil }
|
||||
|
||||
let lower = modelIdentifier.lowercased()
|
||||
if lower.hasPrefix("ipad") {
|
||||
return "ipad"
|
||||
}
|
||||
if lower.hasPrefix("iphone") {
|
||||
return "iphone"
|
||||
}
|
||||
if lower.hasPrefix("ipod") {
|
||||
return "iphone"
|
||||
}
|
||||
if lower.hasPrefix("watch") {
|
||||
return "applewatch"
|
||||
}
|
||||
if lower.hasPrefix("appletv") {
|
||||
return "appletv"
|
||||
}
|
||||
if lower.hasPrefix("audio") || lower.hasPrefix("homepod") {
|
||||
return "speaker"
|
||||
}
|
||||
if lower.hasPrefix("ipad") { return "ipad" }
|
||||
if lower.hasPrefix("iphone") { return "iphone" }
|
||||
if lower.hasPrefix("ipod") { return "iphone" }
|
||||
if lower.hasPrefix("watch") { return "applewatch" }
|
||||
if lower.hasPrefix("appletv") { return "appletv" }
|
||||
if lower.hasPrefix("audio") || lower.hasPrefix("homepod") { return "speaker" }
|
||||
|
||||
if lower.hasPrefix("macbook") || lower.hasPrefix("macbookpro") || lower.hasPrefix("macbookair") {
|
||||
return "laptopcomputer"
|
||||
}
|
||||
if lower.hasPrefix("macstudio") {
|
||||
return "macstudio"
|
||||
}
|
||||
if lower.hasPrefix("macmini") {
|
||||
return "macmini"
|
||||
}
|
||||
if lower.hasPrefix("imac") || lower.hasPrefix("macpro") {
|
||||
return "desktopcomputer"
|
||||
}
|
||||
if lower.hasPrefix("macstudio") { return "macstudio" }
|
||||
if lower.hasPrefix("macmini") { return "macmini" }
|
||||
if lower.hasPrefix("imac") || lower.hasPrefix("macpro") { return "desktopcomputer" }
|
||||
|
||||
if lower.hasPrefix("mac"), let friendlyNameLower = friendlyName?.lowercased() {
|
||||
if friendlyNameLower.contains("macbook") {
|
||||
return "laptopcomputer"
|
||||
}
|
||||
if friendlyNameLower.contains("imac") {
|
||||
return "desktopcomputer"
|
||||
}
|
||||
if friendlyNameLower.contains("mac mini") {
|
||||
return "macmini"
|
||||
}
|
||||
if friendlyNameLower.contains("mac studio") {
|
||||
return "macstudio"
|
||||
}
|
||||
if friendlyNameLower.contains("mac pro") {
|
||||
return "desktopcomputer"
|
||||
}
|
||||
if friendlyNameLower.contains("macbook") { return "laptopcomputer" }
|
||||
if friendlyNameLower.contains("imac") { return "desktopcomputer" }
|
||||
if friendlyNameLower.contains("mac mini") { return "macmini" }
|
||||
if friendlyNameLower.contains("mac studio") { return "macstudio" }
|
||||
if friendlyNameLower.contains("mac pro") { return "desktopcomputer" }
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -107,9 +77,7 @@ enum DeviceModelCatalog {
|
||||
|
||||
private static func fallbackSymbol(for familyRaw: String, modelIdentifier: String) -> String? {
|
||||
let family = familyRaw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if family.isEmpty {
|
||||
return nil
|
||||
}
|
||||
if family.isEmpty { return nil }
|
||||
switch family.lowercased() {
|
||||
case "ipad":
|
||||
return "ipad"
|
||||
|
||||
@@ -96,9 +96,7 @@ actor DiagnosticsFileLog {
|
||||
let size = attrs[.size] as? NSNumber
|
||||
else { return }
|
||||
|
||||
if size.int64Value < self.maxBytes {
|
||||
return
|
||||
}
|
||||
if size.int64Value < self.maxBytes { return }
|
||||
|
||||
let fm = FileManager()
|
||||
|
||||
|
||||
@@ -2,21 +2,13 @@ import Foundation
|
||||
|
||||
enum DurationFormattingSupport {
|
||||
static func conciseDuration(ms: Int) -> String {
|
||||
if ms < 1000 {
|
||||
return "\(ms)ms"
|
||||
}
|
||||
if ms < 1000 { return "\(ms)ms" }
|
||||
let s = Double(ms) / 1000.0
|
||||
if s < 60 {
|
||||
return "\(Int(round(s)))s"
|
||||
}
|
||||
if s < 60 { return "\(Int(round(s)))s" }
|
||||
let m = s / 60.0
|
||||
if m < 60 {
|
||||
return "\(Int(round(m)))m"
|
||||
}
|
||||
if m < 60 { return "\(Int(round(m)))m" }
|
||||
let h = m / 60.0
|
||||
if h < 48 {
|
||||
return "\(Int(round(h)))h"
|
||||
}
|
||||
if h < 48 { return "\(Int(round(h)))h" }
|
||||
let d = h / 24.0
|
||||
return "\(Int(round(d)))d"
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ enum ExecAllowlistMatcher {
|
||||
case let .valid(pattern):
|
||||
if ExecApprovalHelpers.patternHasPathSelector(pattern) {
|
||||
let target = resolvedPath ?? rawExecutable
|
||||
if self.matches(pattern: pattern, target: target) {
|
||||
return entry
|
||||
}
|
||||
if self.matches(pattern: pattern, target: target) { return entry }
|
||||
} else if pattern != "*",
|
||||
!ExecApprovalHelpers.patternHasPathSelector(rawExecutable),
|
||||
self.matchesExecutableBasename(pattern: pattern, resolution: resolution)
|
||||
|
||||
@@ -301,9 +301,7 @@ enum ExecApprovalsStore {
|
||||
|
||||
private static func isLegacyDefaultSocketPath(_ raw: String, legacyFileURL: URL) -> Bool {
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
return true
|
||||
}
|
||||
if trimmed.isEmpty { return true }
|
||||
let expanded = self.expandPath(trimmed)
|
||||
let legacySocket = legacyFileURL.deletingLastPathComponent()
|
||||
.appendingPathComponent("exec-approvals.sock", isDirectory: false)
|
||||
@@ -323,9 +321,7 @@ enum ExecApprovalsStore {
|
||||
}
|
||||
}
|
||||
let parent = cursor.deletingLastPathComponent()
|
||||
if parent.path == cursor.path {
|
||||
return false
|
||||
}
|
||||
if parent.path == cursor.path { return false }
|
||||
cursor = parent
|
||||
}
|
||||
}
|
||||
@@ -349,9 +345,7 @@ enum ExecApprovalsStore {
|
||||
}
|
||||
var closed = false
|
||||
defer {
|
||||
if !closed {
|
||||
close(fd)
|
||||
}
|
||||
if !closed { close(fd) }
|
||||
}
|
||||
do {
|
||||
try data.withUnsafeBytes { rawBuffer in
|
||||
@@ -411,9 +405,7 @@ enum ExecApprovalsStore {
|
||||
let rawSocketPath = file.socket?.path?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if self.isLegacyDefaultSocketPath(rawSocketPath, legacyFileURL: legacyURL) {
|
||||
if file.socket == nil {
|
||||
file.socket = ExecApprovalsSocketConfig(path: nil, token: nil)
|
||||
}
|
||||
if file.socket == nil { file.socket = ExecApprovalsSocketConfig(path: nil, token: nil) }
|
||||
file.socket?.path = self.socketPath()
|
||||
}
|
||||
let encoder = JSONEncoder()
|
||||
@@ -423,13 +415,9 @@ enum ExecApprovalsStore {
|
||||
try FileManager().createDirectory(
|
||||
at: targetURL.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
if FileManager().fileExists(atPath: targetURL.path) {
|
||||
return .notNeeded
|
||||
}
|
||||
if FileManager().fileExists(atPath: targetURL.path) { return .notNeeded }
|
||||
let created = try self.writeMigratedFileExclusively(migrated, to: targetURL)
|
||||
if !created {
|
||||
return .notNeeded
|
||||
}
|
||||
if !created { return .notNeeded }
|
||||
try? FileManager().setAttributes(
|
||||
[.posixPermissions: 0o600],
|
||||
ofItemAtPath: targetURL.path)
|
||||
@@ -598,9 +586,7 @@ enum ExecApprovalsStore {
|
||||
let loadedHash = self.hashFile(loaded)
|
||||
|
||||
var file = self.normalizeIncoming(loaded)
|
||||
if file.socket == nil {
|
||||
file.socket = ExecApprovalsSocketConfig(path: nil, token: nil)
|
||||
}
|
||||
if file.socket == nil { file.socket = ExecApprovalsSocketConfig(path: nil, token: nil) }
|
||||
let path = file.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if path.isEmpty {
|
||||
file.socket?.path = self.socketPath()
|
||||
@@ -609,9 +595,7 @@ enum ExecApprovalsStore {
|
||||
if token.isEmpty {
|
||||
file.socket?.token = self.generateToken()
|
||||
}
|
||||
if file.agents == nil {
|
||||
file.agents = [:]
|
||||
}
|
||||
if file.agents == nil { file.agents = [:] }
|
||||
if !existed || loadedHash != self.hashFile(file) {
|
||||
self.saveFile(file)
|
||||
}
|
||||
@@ -715,9 +699,7 @@ enum ExecApprovalsStore {
|
||||
var agents = file.agents ?? [:]
|
||||
var entry = agents[key] ?? ExecApprovalsAgent()
|
||||
var allowlist = entry.allowlist ?? []
|
||||
if allowlist.contains(where: { $0.pattern == normalizedPattern }) {
|
||||
return
|
||||
}
|
||||
if allowlist.contains(where: { $0.pattern == normalizedPattern }) { return }
|
||||
allowlist.append(ExecAllowlistEntry(
|
||||
pattern: normalizedPattern,
|
||||
lastUsedAt: Date().timeIntervalSince1970 * 1000))
|
||||
@@ -1020,12 +1002,8 @@ enum ExecApprovalHelpers {
|
||||
allowlistMatch: ExecAllowlistEntry?,
|
||||
skillAllow: Bool) -> Bool
|
||||
{
|
||||
if ask == .always {
|
||||
return true
|
||||
}
|
||||
if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow {
|
||||
return true
|
||||
}
|
||||
if ask == .always { return true }
|
||||
if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1053,9 +1031,7 @@ struct ExecEventPayload: Codable {
|
||||
static func truncateOutput(_ raw: String, maxChars: Int = 20000) -> String? {
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
if trimmed.count <= maxChars {
|
||||
return trimmed
|
||||
}
|
||||
if trimmed.count <= maxChars { return trimmed }
|
||||
let suffix = trimmed.suffix(maxChars)
|
||||
return "... (truncated) \(suffix)"
|
||||
}
|
||||
|
||||
@@ -31,9 +31,7 @@ final class ExecApprovalsGatewayPrompter {
|
||||
private func run() async {
|
||||
let stream = await GatewayConnection.shared.subscribe(bufferingNewest: 200)
|
||||
for await push in stream {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled { return }
|
||||
await self.handle(push: push)
|
||||
}
|
||||
}
|
||||
@@ -199,9 +197,7 @@ final class ExecApprovalsGatewayPrompter {
|
||||
private static func lastInputSeconds() -> Int? {
|
||||
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
|
||||
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
|
||||
if seconds.isNaN || seconds.isInfinite || seconds < 0 {
|
||||
return nil
|
||||
}
|
||||
if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil }
|
||||
return Int(seconds.rounded())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,13 +169,9 @@ private func readLineFromHandle(_ handle: FileHandle, maxBytes: Int) throws -> S
|
||||
var buffer = Data()
|
||||
while buffer.count < maxBytes {
|
||||
let chunk = try handle.read(upToCount: 4096) ?? Data()
|
||||
if chunk.isEmpty {
|
||||
break
|
||||
}
|
||||
if chunk.isEmpty { break }
|
||||
buffer.append(chunk)
|
||||
if buffer.contains(0x0A) {
|
||||
break
|
||||
}
|
||||
if buffer.contains(0x0A) { break }
|
||||
}
|
||||
guard let newlineIndex = buffer.firstIndex(of: 0x0A) else {
|
||||
guard !buffer.isEmpty else { return nil }
|
||||
@@ -619,9 +615,7 @@ private enum ExecHostExecutor {
|
||||
guard needsScreenRecording == true else { return nil }
|
||||
let authorized = await PermissionManager
|
||||
.status([.screenRecording])[.screenRecording] ?? false
|
||||
if authorized {
|
||||
return nil
|
||||
}
|
||||
if authorized { return nil }
|
||||
return self.errorResponse(
|
||||
code: "UNAVAILABLE",
|
||||
message: "PERMISSION_MISSING: screenRecording",
|
||||
@@ -734,15 +728,9 @@ enum ExecApprovalsSocketPathGuard {
|
||||
}
|
||||
|
||||
let fileType = status.st_mode & mode_t(S_IFMT)
|
||||
if fileType == mode_t(S_IFDIR) {
|
||||
return .directory
|
||||
}
|
||||
if fileType == mode_t(S_IFSOCK) {
|
||||
return .socket
|
||||
}
|
||||
if fileType == mode_t(S_IFLNK) {
|
||||
return .symlink
|
||||
}
|
||||
if fileType == mode_t(S_IFDIR) { return .directory }
|
||||
if fileType == mode_t(S_IFSOCK) { return .socket }
|
||||
if fileType == mode_t(S_IFLNK) { return .symlink }
|
||||
return .other
|
||||
}
|
||||
|
||||
@@ -856,9 +844,7 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
if client < 0 {
|
||||
if errno == EINTR {
|
||||
continue
|
||||
}
|
||||
if errno == EINTR { continue }
|
||||
break
|
||||
}
|
||||
Task.detached { [weak self] in
|
||||
|
||||
@@ -332,9 +332,7 @@ struct ExecCommandResolution {
|
||||
current.append(ch)
|
||||
}
|
||||
|
||||
if escaped {
|
||||
current.append("\\")
|
||||
}
|
||||
if escaped { current.append("\\") }
|
||||
appendCurrent()
|
||||
return tokens
|
||||
}
|
||||
@@ -443,9 +441,7 @@ struct ExecCommandResolution {
|
||||
idx += 1
|
||||
}
|
||||
|
||||
if escaped || inSingle || inDouble {
|
||||
return nil
|
||||
}
|
||||
if escaped || inSingle || inDouble { return nil }
|
||||
guard appendCurrent() else { return nil }
|
||||
return segments
|
||||
}
|
||||
@@ -519,9 +515,7 @@ enum ExecCommandFormatter {
|
||||
let trimmed = arg.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return "\"\"" }
|
||||
let needsQuotes = trimmed.contains { $0.isWhitespace || $0 == "\"" }
|
||||
if !needsQuotes {
|
||||
return trimmed
|
||||
}
|
||||
if !needsQuotes { return trimmed }
|
||||
let escaped = trimmed.replacingOccurrences(of: "\"", with: "\\\"")
|
||||
return "\"\(escaped)\""
|
||||
}.joined(separator: " ")
|
||||
@@ -529,9 +523,7 @@ enum ExecCommandFormatter {
|
||||
|
||||
static func displayString(for argv: [String], rawCommand: String?) -> String {
|
||||
let trimmed = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !trimmed.isEmpty {
|
||||
return trimmed
|
||||
}
|
||||
if !trimmed.isEmpty { return trimmed }
|
||||
return self.displayString(for: argv)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,9 +165,7 @@ enum ExecInlineCommandParser {
|
||||
idx += 1
|
||||
continue
|
||||
}
|
||||
if token == "--" {
|
||||
break
|
||||
}
|
||||
if token == "--" { break }
|
||||
let comparableToken = allowCombinedC ? token : token.lowercased()
|
||||
if flags.contains(comparableToken) {
|
||||
return Match(tokenIndex: idx, inlineCommand: nil)
|
||||
|
||||
@@ -190,12 +190,8 @@ enum ExecShellWrapperParser {
|
||||
private static func extractPowerShellInlineCommand(_ command: [String]) -> String? {
|
||||
for idx in 1..<command.count {
|
||||
let token = command[idx].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
if token.isEmpty {
|
||||
continue
|
||||
}
|
||||
if token == "--" {
|
||||
break
|
||||
}
|
||||
if token.isEmpty { continue }
|
||||
if token == "--" { break }
|
||||
if self.powershellInlineFlags.contains(token) {
|
||||
return ExecInlineCommandParser.extractInlineCommand(
|
||||
command,
|
||||
|
||||
@@ -492,16 +492,12 @@ actor GatewayConnection {
|
||||
}
|
||||
if let deviceToken = lastSnapshot?.auth["deviceToken"]?.value as? String {
|
||||
let trimmed = deviceToken.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
return trimmed
|
||||
}
|
||||
if !trimmed.isEmpty { return trimmed }
|
||||
}
|
||||
let identity = DeviceIdentityStore.loadOrCreate()
|
||||
if let entry = DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator") {
|
||||
let trimmed = entry.token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
return trimmed
|
||||
}
|
||||
if !trimmed.isEmpty { return trimmed }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -880,9 +876,7 @@ extension GatewayConnection {
|
||||
|
||||
func healthSnapshot(timeoutMs: Double? = nil) async throws -> HealthSnapshot {
|
||||
let data = try await requestRaw(method: .health, timeoutMs: timeoutMs)
|
||||
if let snap = decodeHealthSnapshot(from: data) {
|
||||
return snap
|
||||
}
|
||||
if let snap = decodeHealthSnapshot(from: data) { return snap }
|
||||
throw GatewayDecodingError(method: Method.health.rawValue, message: "failed to decode health snapshot")
|
||||
}
|
||||
|
||||
@@ -925,15 +919,9 @@ extension GatewayConnection {
|
||||
var params: [String: AnyCodable] = [
|
||||
"skillKey": AnyCodable(skillKey),
|
||||
]
|
||||
if let enabled {
|
||||
params["enabled"] = AnyCodable(enabled)
|
||||
}
|
||||
if let apiKey {
|
||||
params["apiKey"] = AnyCodable(apiKey)
|
||||
}
|
||||
if let env, !env.isEmpty {
|
||||
params["env"] = AnyCodable(env)
|
||||
}
|
||||
if let enabled { params["enabled"] = AnyCodable(enabled) }
|
||||
if let apiKey { params["apiKey"] = AnyCodable(apiKey) }
|
||||
if let env, !env.isEmpty { params["env"] = AnyCodable(env) }
|
||||
return try await self.requestDecoded(method: .skillsUpdate, params: params)
|
||||
}
|
||||
|
||||
@@ -952,12 +940,8 @@ extension GatewayConnection {
|
||||
return OpenClawSessionsPreviewPayload(ts: 0, previews: [])
|
||||
}
|
||||
var params: [String: AnyCodable] = ["keys": AnyCodable(resolvedKeys)]
|
||||
if let limit {
|
||||
params["limit"] = AnyCodable(limit)
|
||||
}
|
||||
if let maxChars {
|
||||
params["maxChars"] = AnyCodable(maxChars)
|
||||
}
|
||||
if let limit { params["limit"] = AnyCodable(limit) }
|
||||
if let maxChars { params["maxChars"] = AnyCodable(maxChars) }
|
||||
let timeout = timeoutMs.map { Double($0) }
|
||||
return try await self.requestDecoded(
|
||||
method: .sessionsPreview,
|
||||
@@ -980,12 +964,8 @@ extension GatewayConnection {
|
||||
if let agentID = agentID?.trimmingCharacters(in: .whitespacesAndNewlines), !agentID.isEmpty {
|
||||
params["agentId"] = AnyCodable(agentID)
|
||||
}
|
||||
if let limit {
|
||||
params["limit"] = AnyCodable(limit)
|
||||
}
|
||||
if let maxChars {
|
||||
params["maxChars"] = AnyCodable(maxChars)
|
||||
}
|
||||
if let limit { params["limit"] = AnyCodable(limit) }
|
||||
if let maxChars { params["maxChars"] = AnyCodable(maxChars) }
|
||||
let timeout = timeoutMs.map { Double($0) }
|
||||
if let route {
|
||||
let data = try await request(
|
||||
@@ -1070,9 +1050,7 @@ extension GatewayConnection {
|
||||
|
||||
func talkMode(enabled: Bool, phase: String? = nil) async {
|
||||
var params: [String: AnyCodable] = ["enabled": AnyCodable(enabled)]
|
||||
if let phase {
|
||||
params["phase"] = AnyCodable(phase)
|
||||
}
|
||||
if let phase { params["phase"] = AnyCodable(phase) }
|
||||
try? await self.requestVoid(method: .talkMode, params: params)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,9 +57,7 @@ final class GatewayConnectivityCoordinator {
|
||||
|
||||
private static func hostLabel(for url: URL) -> String {
|
||||
let host = url.host ?? url.absoluteString
|
||||
if let port = url.port {
|
||||
return "\(host):\(port)"
|
||||
}
|
||||
if let port = url.port { return "\(host):\(port)" }
|
||||
return host
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,8 @@ struct Semver: Comparable, CustomStringConvertible {
|
||||
}
|
||||
|
||||
static func < (lhs: Semver, rhs: Semver) -> Bool {
|
||||
if lhs.major != rhs.major {
|
||||
return lhs.major < rhs.major
|
||||
}
|
||||
if lhs.minor != rhs.minor {
|
||||
return lhs.minor < rhs.minor
|
||||
}
|
||||
if lhs.major != rhs.major { return lhs.major < rhs.major }
|
||||
if lhs.minor != rhs.minor { return lhs.minor < rhs.minor }
|
||||
return lhs.patch < rhs.patch
|
||||
}
|
||||
|
||||
@@ -80,9 +76,7 @@ enum GatewayEnvironment {
|
||||
static func gatewayPort() -> Int {
|
||||
if let raw = ProcessInfo.processInfo.environment["OPENCLAW_GATEWAY_PORT"] {
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let parsed = Int(trimmed), parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
if let parsed = Int(trimmed), parsed > 0 { return parsed }
|
||||
}
|
||||
if let configPort = OpenClawConfigFile.gatewayPort(), configPort > 0 {
|
||||
return configPort
|
||||
|
||||
@@ -24,9 +24,7 @@ enum GatewayLaunchAgentManager {
|
||||
}
|
||||
|
||||
static func isLaunchAgentWriteDisabled() -> Bool {
|
||||
if FileManager().fileExists(atPath: self.disableLaunchAgentMarkerURL.path) {
|
||||
return true
|
||||
}
|
||||
if FileManager().fileExists(atPath: self.disableLaunchAgentMarkerURL.path) { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -155,9 +153,7 @@ extension GatewayLaunchAgentManager {
|
||||
quiet: Bool = false) async -> String?
|
||||
{
|
||||
let result = await self.runDaemonCommandResult(args, timeout: timeout, quiet: quiet)
|
||||
if result.success {
|
||||
return nil
|
||||
}
|
||||
if result.success { return nil }
|
||||
return result.message ?? "Gateway daemon command failed"
|
||||
}
|
||||
|
||||
@@ -206,9 +202,7 @@ extension GatewayLaunchAgentManager {
|
||||
}
|
||||
|
||||
private static func withJsonFlag(_ args: [String]) -> [String] {
|
||||
if args.contains("--json") {
|
||||
return args
|
||||
}
|
||||
if args.contains("--json") { return args }
|
||||
return args + ["--json"]
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ final class GatewayProcessManager {
|
||||
case .stopped: return "Stopped"
|
||||
case .starting: return "Starting…"
|
||||
case let .running(details):
|
||||
if let details, !details.isEmpty {
|
||||
return "Running (\(details))"
|
||||
}
|
||||
if let details, !details.isEmpty { return "Running (\(details))" }
|
||||
return "Running"
|
||||
case let .attachedExisting(details):
|
||||
if let details, !details.isEmpty {
|
||||
@@ -151,9 +149,7 @@ final class GatewayProcessManager {
|
||||
func refreshEnvironmentStatus(force: Bool = false) {
|
||||
let now = Date()
|
||||
if !force {
|
||||
if self.environmentRefreshTask != nil {
|
||||
return
|
||||
}
|
||||
if self.environmentRefreshTask != nil { return }
|
||||
if let last = self.lastEnvironmentRefresh,
|
||||
now.timeIntervalSince(last) < self.environmentRefreshMinInterval
|
||||
{
|
||||
@@ -297,9 +293,7 @@ final class GatewayProcessManager {
|
||||
return true
|
||||
}
|
||||
let ns = error as NSError
|
||||
if ns.domain == "Gateway", ns.code == 1008 {
|
||||
return true
|
||||
}
|
||||
if ns.domain == "Gateway", ns.code == 1008 { return true }
|
||||
let lower = ns.localizedDescription.lowercased()
|
||||
return lower.contains("unauthorized") || lower.contains("auth")
|
||||
}
|
||||
@@ -342,9 +336,7 @@ final class GatewayProcessManager {
|
||||
// Best-effort: wait for the gateway to accept connections.
|
||||
let deadline = Date().addingTimeInterval(6)
|
||||
while Date() < deadline {
|
||||
if !self.desiredActive {
|
||||
return
|
||||
}
|
||||
if !self.desiredActive { return }
|
||||
do {
|
||||
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
|
||||
let instance = await PortGuardian.shared.describe(port: port)
|
||||
@@ -392,9 +384,7 @@ final class GatewayProcessManager {
|
||||
func waitForGatewayReady(timeout: TimeInterval = 6) async -> Bool {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
if !self.desiredActive {
|
||||
return false
|
||||
}
|
||||
if !self.desiredActive { return false }
|
||||
do {
|
||||
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
|
||||
self.clearLastFailure()
|
||||
@@ -426,9 +416,7 @@ final class GatewayProcessManager {
|
||||
guard FileManager().fileExists(atPath: path) else { return "" }
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return "" }
|
||||
let text = String(data: data, encoding: .utf8) ?? ""
|
||||
if text.count <= limit {
|
||||
return text
|
||||
}
|
||||
if text.count <= limit { return text }
|
||||
return String(text.suffix(limit))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ enum GatewayPushSubscription {
|
||||
}
|
||||
|
||||
for await push in stream {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
if Task.isCancelled { return }
|
||||
await MainActor.run {
|
||||
onPush(push)
|
||||
}
|
||||
|
||||
@@ -248,9 +248,7 @@ enum GatewayRemoteConfig {
|
||||
}
|
||||
|
||||
static func defaultPort(for url: URL) -> Int? {
|
||||
if let port = url.port {
|
||||
return port
|
||||
}
|
||||
if let port = url.port { return port }
|
||||
let scheme = url.scheme?.lowercased() ?? ""
|
||||
switch scheme {
|
||||
case "wss":
|
||||
|
||||
@@ -129,9 +129,7 @@ final class HealthStore {
|
||||
}
|
||||
} else {
|
||||
self.lastError = "health output not JSON"
|
||||
if onDemand {
|
||||
self.snapshot = nil
|
||||
}
|
||||
if onDemand { self.snapshot = nil }
|
||||
if previousError != self.lastError {
|
||||
Self.logger.warning("health refresh failed: output not JSON")
|
||||
}
|
||||
@@ -139,9 +137,7 @@ final class HealthStore {
|
||||
} catch {
|
||||
let desc = error.localizedDescription
|
||||
self.lastError = desc
|
||||
if onDemand {
|
||||
self.snapshot = nil
|
||||
}
|
||||
if onDemand { self.snapshot = nil }
|
||||
if previousError != desc {
|
||||
Self.logger.error("health refresh failed \(desc, privacy: .public)")
|
||||
}
|
||||
@@ -157,16 +153,12 @@ final class HealthStore {
|
||||
private static func describeProbeFailure(_ probe: HealthSnapshot.ChannelSummary.Probe) -> String {
|
||||
let elapsed = probe.elapsedMs.map { "\(Int($0))ms" }
|
||||
if let error = probe.error, error.lowercased().contains("timeout") || probe.status == nil {
|
||||
if let elapsed {
|
||||
return "Health check timed out (\(elapsed))"
|
||||
}
|
||||
if let elapsed { return "Health check timed out (\(elapsed))" }
|
||||
return "Health check timed out"
|
||||
}
|
||||
let code = probe.status.map { "status \($0)" } ?? "status unknown"
|
||||
let reason = probe.error?.isEmpty == false ? probe.error! : "health probe failed"
|
||||
if let elapsed {
|
||||
return "\(reason) (\(code), \(elapsed))"
|
||||
}
|
||||
if let elapsed { return "\(reason) (\(code), \(elapsed))" }
|
||||
return "\(reason) (\(code))"
|
||||
}
|
||||
|
||||
@@ -193,9 +185,7 @@ final class HealthStore {
|
||||
{
|
||||
let order = snap.channelOrder ?? Array(snap.channels.keys)
|
||||
for channelId in order {
|
||||
if channelId == id {
|
||||
continue
|
||||
}
|
||||
if channelId == id { continue }
|
||||
guard let summary = snap.channels[channelId] else { continue }
|
||||
if Self.isChannelHealthy(summary) {
|
||||
return (id: channelId, summary: summary)
|
||||
@@ -223,12 +213,8 @@ final class HealthStore {
|
||||
}
|
||||
|
||||
var summaryLine: String {
|
||||
if self.isRefreshing {
|
||||
return "Health check running…"
|
||||
}
|
||||
if let error = self.lastError {
|
||||
return "Health check failed: \(error)"
|
||||
}
|
||||
if self.isRefreshing { return "Health check running…" }
|
||||
if let error = self.lastError { return "Health check failed: \(error)" }
|
||||
guard let snap = self.snapshot else { return "Health check pending" }
|
||||
guard let link = self.resolveLinkChannel(snap) else { return "Health check pending" }
|
||||
if link.summary.linked != true {
|
||||
@@ -291,16 +277,10 @@ final class HealthStore {
|
||||
|
||||
func msToAge(_ ms: Double) -> String {
|
||||
let minutes = Int(round(ms / 60000))
|
||||
if minutes < 1 {
|
||||
return "just now"
|
||||
}
|
||||
if minutes < 60 {
|
||||
return "\(minutes)m"
|
||||
}
|
||||
if minutes < 1 { return "just now" }
|
||||
if minutes < 60 { return "\(minutes)m" }
|
||||
let hours = Int(round(Double(minutes) / 60))
|
||||
if hours < 48 {
|
||||
return "\(hours)h"
|
||||
}
|
||||
if hours < 48 { return "\(hours)h" }
|
||||
let days = Int(round(Double(hours) / 24))
|
||||
return "\(days)d"
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ final class HeartbeatStore {
|
||||
|
||||
@MainActor
|
||||
deinit {
|
||||
if let observer {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
if let observer { NotificationCenter.default.removeObserver(observer) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,23 +37,17 @@ enum HostEnvSanitizer {
|
||||
]
|
||||
|
||||
private static func isBlocked(_ upperKey: String) -> Bool {
|
||||
if self.blockedKeys.contains(upperKey) {
|
||||
return true
|
||||
}
|
||||
if self.blockedKeys.contains(upperKey) { return true }
|
||||
return self.blockedPrefixes.contains(where: { upperKey.hasPrefix($0) })
|
||||
}
|
||||
|
||||
private static func isBlockedInherited(_ upperKey: String) -> Bool {
|
||||
if self.blockedInheritedKeys.contains(upperKey) {
|
||||
return true
|
||||
}
|
||||
if self.blockedInheritedKeys.contains(upperKey) { return true }
|
||||
return self.blockedInheritedPrefixes.contains(where: { upperKey.hasPrefix($0) })
|
||||
}
|
||||
|
||||
private static func isBlockedOverride(_ upperKey: String) -> Bool {
|
||||
if self.blockedOverrideKeys.contains(upperKey) {
|
||||
return true
|
||||
}
|
||||
if self.blockedOverrideKeys.contains(upperKey) { return true }
|
||||
if upperKey.range(
|
||||
of: self.cargoTargetExecutableOverridePattern,
|
||||
options: .regularExpression) != nil
|
||||
@@ -117,9 +111,7 @@ enum HostEnvSanitizer {
|
||||
|
||||
private static func sanitizeInheritedGitAllowProtocolValue(_ value: String) -> String {
|
||||
let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if normalized.isEmpty {
|
||||
return ""
|
||||
}
|
||||
if normalized.isEmpty { return "" }
|
||||
let safeProtocols = normalized
|
||||
.split(separator: ":", omittingEmptySubsequences: false)
|
||||
.filter { self.gitDefaultAlwaysAllowedProtocols.contains(String($0)) }
|
||||
@@ -180,9 +172,7 @@ enum HostEnvSanitizer {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if self.isBlockedInherited(upper) {
|
||||
continue
|
||||
}
|
||||
if self.isBlockedInherited(upper) { continue }
|
||||
merged[key] = value
|
||||
}
|
||||
|
||||
@@ -196,15 +186,9 @@ enum HostEnvSanitizer {
|
||||
let upper = key.uppercased()
|
||||
// PATH is part of the security boundary (command resolution + safe-bin checks). Never
|
||||
// allow request-scoped PATH overrides from agents/gateways.
|
||||
if upper == "PATH" {
|
||||
continue
|
||||
}
|
||||
if self.isBlockedOverride(upper) {
|
||||
continue
|
||||
}
|
||||
if self.isBlocked(upper) {
|
||||
continue
|
||||
}
|
||||
if upper == "PATH" { continue }
|
||||
if self.isBlockedOverride(upper) { continue }
|
||||
if self.isBlocked(upper) { continue }
|
||||
merged[key] = value
|
||||
}
|
||||
return merged
|
||||
|
||||
@@ -113,9 +113,7 @@ final class HoverHUDController {
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
await MainActor.run {
|
||||
guard let self else { return }
|
||||
if self.model.hoveringStatusItem || self.model.hoveringPanel {
|
||||
return
|
||||
}
|
||||
if self.model.hoveringStatusItem || self.model.hoveringPanel { return }
|
||||
self.dismiss(reason: "hoverExit")
|
||||
}
|
||||
}
|
||||
@@ -141,9 +139,7 @@ final class HoverHUDController {
|
||||
}
|
||||
|
||||
private func ensureWindow() {
|
||||
if self.window != nil {
|
||||
return
|
||||
}
|
||||
if self.window != nil { return }
|
||||
let panel = OverlayPanelFactory.makePanel(
|
||||
contentRect: NSRect(x: 0, y: 0, width: self.width, height: self.height),
|
||||
level: .statusBar,
|
||||
@@ -180,9 +176,7 @@ final class HoverHUDController {
|
||||
}
|
||||
|
||||
private func installDismissMonitor() {
|
||||
if ProcessInfo.processInfo.isRunningTests {
|
||||
return
|
||||
}
|
||||
if ProcessInfo.processInfo.isRunningTests { return }
|
||||
guard self.dismissMonitor == nil, let window else { return }
|
||||
self.dismissMonitor = NSEvent.addGlobalMonitorForEvents(matching: [
|
||||
.leftMouseDown,
|
||||
@@ -207,19 +201,13 @@ private struct HoverHUDView: View {
|
||||
private let activityStore = WorkActivityStore.shared
|
||||
|
||||
private var statusTitle: String {
|
||||
if self.activityStore.iconState.isWorking {
|
||||
return "Working"
|
||||
}
|
||||
if self.activityStore.iconState.isWorking { return "Working" }
|
||||
return "Idle"
|
||||
}
|
||||
|
||||
private var detail: String {
|
||||
if let current = self.activityStore.current?.label, !current.isEmpty {
|
||||
return current
|
||||
}
|
||||
if let last = self.activityStore.lastToolLabel, !last.isEmpty {
|
||||
return last
|
||||
}
|
||||
if let current = self.activityStore.current?.label, !current.isEmpty { return current }
|
||||
if let last = self.activityStore.lastToolLabel, !last.isEmpty { return last }
|
||||
return "No recent activity"
|
||||
}
|
||||
|
||||
|
||||
@@ -84,9 +84,7 @@ struct InstancesSettings: View {
|
||||
HStack(spacing: 8) {
|
||||
Text(inst.host ?? "unknown host").font(.subheadline.bold())
|
||||
self.presenceIndicator(inst)
|
||||
if let ip = inst.ip {
|
||||
Text("(") + Text(ip).monospaced() + Text(")")
|
||||
}
|
||||
if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") }
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
@@ -111,9 +109,7 @@ struct InstancesSettings: View {
|
||||
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
|
||||
}
|
||||
|
||||
if let mode = inst.mode {
|
||||
self.label(icon: "network", text: mode)
|
||||
}
|
||||
if let mode = inst.mode { self.label(icon: "network", text: mode) }
|
||||
}
|
||||
.layoutPriority(1)
|
||||
|
||||
@@ -179,12 +175,8 @@ struct InstancesSettings: View {
|
||||
private func presenceStatus(for inst: InstanceInfo) -> (label: String, color: Color) {
|
||||
let nowMs = Date().timeIntervalSince1970 * 1000
|
||||
let ageSeconds = max(0, Int((nowMs - inst.ts) / 1000))
|
||||
if ageSeconds <= 120 {
|
||||
return ("Active", .green)
|
||||
}
|
||||
if ageSeconds <= 300 {
|
||||
return ("Idle", .yellow)
|
||||
}
|
||||
if ageSeconds <= 120 { return ("Active", .green) }
|
||||
if ageSeconds <= 300 { return ("Idle", .yellow) }
|
||||
return ("Stale", .gray)
|
||||
}
|
||||
|
||||
@@ -239,19 +231,13 @@ struct InstancesSettings: View {
|
||||
}
|
||||
|
||||
private func shouldShowUpdateRow(_ inst: InstanceInfo) -> Bool {
|
||||
if inst.lastInputSeconds != nil {
|
||||
return true
|
||||
}
|
||||
if self.updateSummaryText(inst, isGateway: false) != nil {
|
||||
return true
|
||||
}
|
||||
if inst.lastInputSeconds != nil { return true }
|
||||
if self.updateSummaryText(inst, isGateway: false) != nil { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
private func safeSystemSymbol(_ preferred: String, fallback: String) -> String {
|
||||
if self.isSystemSymbolAvailable(preferred) {
|
||||
return preferred
|
||||
}
|
||||
if self.isSystemSymbolAvailable(preferred) { return preferred }
|
||||
return fallback
|
||||
}
|
||||
|
||||
|
||||
@@ -97,9 +97,7 @@ final class InstancesStore {
|
||||
}
|
||||
|
||||
func refresh() async {
|
||||
if self.isLoading {
|
||||
return
|
||||
}
|
||||
if self.isLoading { return }
|
||||
self.statusMessage = nil
|
||||
self.isLoading = true
|
||||
defer { self.isLoading = false }
|
||||
@@ -171,9 +169,7 @@ final class InstancesStore {
|
||||
|
||||
private func snippet(_ data: Data?, limit: Int = 256) -> String {
|
||||
guard let data else { return "<none>" }
|
||||
if data.isEmpty {
|
||||
return "<empty>"
|
||||
}
|
||||
if data.isEmpty { return "<empty>" }
|
||||
let prefix = data.prefix(limit)
|
||||
if let asString = String(data: prefix, encoding: .utf8) {
|
||||
return asString.replacingOccurrences(of: "\n", with: " ")
|
||||
@@ -186,14 +182,10 @@ final class InstancesStore {
|
||||
let data = try await ControlChannel.shared.health(timeout: 8)
|
||||
guard let snap = decodeHealthSnapshot(from: data) else { return }
|
||||
let linkId = snap.channelOrder?.first(where: {
|
||||
if let summary = snap.channels[$0] {
|
||||
return summary.linked != nil
|
||||
}
|
||||
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||
return false
|
||||
}) ?? snap.channels.keys.first(where: {
|
||||
if let summary = snap.channels[$0] {
|
||||
return summary.linked != nil
|
||||
}
|
||||
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||
return false
|
||||
})
|
||||
let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false
|
||||
@@ -271,19 +263,13 @@ final class InstancesStore {
|
||||
for inst in instances {
|
||||
guard let reason = inst.reason?.trimmingCharacters(in: .whitespacesAndNewlines) else { continue }
|
||||
guard reason == "node-connected" else { continue }
|
||||
if let mode = inst.mode?.lowercased(), mode == "local" {
|
||||
continue
|
||||
}
|
||||
if let mode = inst.mode?.lowercased(), mode == "local" { continue }
|
||||
|
||||
let previous = self.lastPresenceById[inst.id]
|
||||
if previous?.reason == "node-connected", previous?.ts == inst.ts {
|
||||
continue
|
||||
}
|
||||
if previous?.reason == "node-connected", previous?.ts == inst.ts { continue }
|
||||
|
||||
let lastNotified = self.lastLoginNotifiedAtMs[inst.id] ?? 0
|
||||
if inst.ts <= lastNotified {
|
||||
continue
|
||||
}
|
||||
if inst.ts <= lastNotified { continue }
|
||||
self.lastLoginNotifiedAtMs[inst.id] = inst.ts
|
||||
|
||||
let name = inst.host?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user