From 84bfaad6e6bbae812244cd0d9dab2d19c840a238 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 13 Jan 2026 08:11:59 +0000 Subject: [PATCH] fix: finish channels rename sweep --- .../Clawdbot/Bridge/BridgeServer.swift | 6 +- ...ConnectionsSettings+ProviderSections.swift | 6 +- .../ConnectionsSettings+ProviderState.swift | 124 +++++++++--------- .../Clawdbot/ConnectionsSettings+View.swift | 54 ++++---- .../Clawdbot/ConnectionsSettings.swift | 4 +- .../Clawdbot/ConnectionsStore+Lifecycle.swift | 20 +-- .../Sources/Clawdbot/ConnectionsStore.swift | 20 +-- .../Clawdbot/CronJobEditor+Helpers.swift | 6 +- .../Clawdbot/CronJobEditor+Testing.swift | 2 +- .../Sources/Clawdbot/CronJobEditor.swift | 24 ++-- apps/macos/Sources/Clawdbot/CronModels.swift | 67 +++++----- apps/macos/Sources/Clawdbot/DeepLinks.swift | 6 +- .../Sources/Clawdbot/GatewayConnection.swift | 16 +-- .../Clawdbot/GatewayProcessManager.swift | 16 +-- .../Sources/Clawdbot/GeneralSettings.swift | 14 +- apps/macos/Sources/Clawdbot/HealthStore.swift | 54 ++++---- .../Sources/Clawdbot/InstancesStore.swift | 14 +- .../Clawdbot/OnboardingView+Pages.swift | 2 +- .../Sources/Clawdbot/VoiceWakeForwarder.swift | 6 +- .../ConnectionsSettingsSmokeTests.swift | 92 ++++++------- .../ClawdbotIPCTests/HealthDecodeTests.swift | 12 +- .../HealthStoreStateTests.swift | 10 +- docs/concepts/agent-loop.md | 2 +- docs/concepts/model-providers.md | 2 +- docs/platforms/mac/bundled-gateway.md | 2 +- scripts/e2e/gateway-network-docker.sh | 18 +-- src/auto-reply/reply/queue.ts | 50 +++---- src/cli/gateway.sigterm.test.ts | 2 +- ...board-non-interactive.gateway-auth.test.ts | 30 ++--- ...ard-non-interactive.lan-auto-token.test.ts | 30 ++--- .../onboard-non-interactive.remote.test.ts | 52 ++++---- src/daemon/service-audit.ts | 2 +- src/gateway/gateway-cli-backend.live.test.ts | 28 ++-- .../gateway-models.profiles.live.test.ts | 12 +- .../gateway.tool-calling.mock-openai.test.ts | 50 +++---- src/gateway/gateway.wizard.e2e.test.ts | 30 ++--- src/gateway/server.reload.test.ts | 12 +- src/gateway/test-helpers.ts | 2 +- src/logging.ts | 6 +- src/wizard/onboarding.ts | 4 +- test/gateway.multi.e2e.test.ts | 2 +- ui/src/ui/app-render.ts | 24 ++-- ui/src/ui/app.ts | 34 ++--- ui/src/ui/controllers/connections.ts | 40 +++--- ui/src/ui/controllers/cron.ts | 4 +- ui/src/ui/navigation.ts | 2 +- ui/src/ui/types.ts | 14 +- ui/src/ui/ui-types.ts | 2 +- ui/src/ui/views/connections.ts | 84 ++++++------ ui/src/ui/views/cron.ts | 28 ++-- ui/src/ui/views/overview.ts | 8 +- vitest.config.ts | 6 +- 52 files changed, 579 insertions(+), 578 deletions(-) diff --git a/apps/macos/Sources/Clawdbot/Bridge/BridgeServer.swift b/apps/macos/Sources/Clawdbot/Bridge/BridgeServer.swift index 4b71e6dec..f45dcae20 100644 --- a/apps/macos/Sources/Clawdbot/Bridge/BridgeServer.swift +++ b/apps/macos/Sources/Clawdbot/Bridge/BridgeServer.swift @@ -187,7 +187,7 @@ actor BridgeServer { thinking: "low", deliver: false, to: nil, - provider: .last)) + channel: .last)) case "agent.request": guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { @@ -205,7 +205,7 @@ actor BridgeServer { ?? "node-\(nodeId)" let thinking = link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty let to = link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let provider = GatewayAgentProvider(raw: link.channel) + let channel = GatewayAgentChannel(raw: link.channel) _ = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation( message: message, @@ -213,7 +213,7 @@ actor BridgeServer { thinking: thinking, deliver: link.deliver, to: to, - provider: provider)) + channel: channel)) default: break diff --git a/apps/macos/Sources/Clawdbot/ConnectionsSettings+ProviderSections.swift b/apps/macos/Sources/Clawdbot/ConnectionsSettings+ProviderSections.swift index 611f21cc9..8ad413b0a 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionsSettings+ProviderSections.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionsSettings+ProviderSections.swift @@ -11,9 +11,9 @@ extension ConnectionsSettings { } @ViewBuilder - func providerHeaderActions(_ provider: ConnectionProvider) -> some View { + func channelHeaderActions(_ channel: ConnectionChannel) -> some View { HStack(spacing: 8) { - if provider == .whatsapp { + if channel == .whatsapp { Button("Logout") { Task { await self.store.logoutWhatsApp() } } @@ -21,7 +21,7 @@ extension ConnectionsSettings { .disabled(self.store.whatsappBusy) } - if provider == .telegram { + if channel == .telegram { Button("Logout") { Task { await self.store.logoutTelegram() } } diff --git a/apps/macos/Sources/Clawdbot/ConnectionsSettings+ProviderState.swift b/apps/macos/Sources/Clawdbot/ConnectionsSettings+ProviderState.swift index ee7439c63..58d602223 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionsSettings+ProviderState.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionsSettings+ProviderState.swift @@ -1,15 +1,15 @@ import SwiftUI extension ConnectionsSettings { - private func providerStatus( + private func channelStatus( _ id: String, as type: T.Type) -> T? { - self.store.snapshot?.decodeProvider(id, as: type) + self.store.snapshot?.decodeChannel(id, as: type) } var whatsAppTint: Color { - guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self) + guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self) else { return .secondary } if !status.configured { return .secondary } if !status.linked { return .red } @@ -20,7 +20,7 @@ extension ConnectionsSettings { } var telegramTint: Color { - guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self) + guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self) else { return .secondary } if !status.configured { return .secondary } if status.lastError != nil { return .orange } @@ -30,7 +30,7 @@ extension ConnectionsSettings { } var discordTint: Color { - guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self) + guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self) else { return .secondary } if !status.configured { return .secondary } if status.lastError != nil { return .orange } @@ -40,7 +40,7 @@ extension ConnectionsSettings { } var signalTint: Color { - guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self) + guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self) else { return .secondary } if !status.configured { return .secondary } if status.lastError != nil { return .orange } @@ -50,7 +50,7 @@ extension ConnectionsSettings { } var imessageTint: Color { - guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self) + guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self) else { return .secondary } if !status.configured { return .secondary } if status.lastError != nil { return .orange } @@ -60,7 +60,7 @@ extension ConnectionsSettings { } var whatsAppSummary: String { - guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self) + guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self) else { return "Checking…" } if !status.linked { return "Not linked" } if status.connected { return "Connected" } @@ -69,7 +69,7 @@ extension ConnectionsSettings { } var telegramSummary: String { - guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self) + guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self) else { return "Checking…" } if !status.configured { return "Not configured" } if status.running { return "Running" } @@ -77,7 +77,7 @@ extension ConnectionsSettings { } var discordSummary: String { - guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self) + guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self) else { return "Checking…" } if !status.configured { return "Not configured" } if status.running { return "Running" } @@ -85,7 +85,7 @@ extension ConnectionsSettings { } var signalSummary: String { - guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self) + guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self) else { return "Checking…" } if !status.configured { return "Not configured" } if status.running { return "Running" } @@ -93,7 +93,7 @@ extension ConnectionsSettings { } var imessageSummary: String { - guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self) + guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self) else { return "Checking…" } if !status.configured { return "Not configured" } if status.running { return "Running" } @@ -101,7 +101,7 @@ extension ConnectionsSettings { } var whatsAppDetails: String? { - guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self) + guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self) else { return nil } var lines: [String] = [] if let e164 = status.`self`?.e164 ?? status.`self`?.jid { @@ -132,7 +132,7 @@ extension ConnectionsSettings { } var telegramDetails: String? { - guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self) + guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self) else { return nil } var lines: [String] = [] if let source = status.tokenSource { @@ -164,7 +164,7 @@ extension ConnectionsSettings { } var discordDetails: String? { - guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self) + guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self) else { return nil } var lines: [String] = [] if let source = status.tokenSource { @@ -193,7 +193,7 @@ extension ConnectionsSettings { } var signalDetails: String? { - guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self) + guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self) else { return nil } var lines: [String] = [] lines.append("Base URL: \(status.baseUrl)") @@ -220,7 +220,7 @@ extension ConnectionsSettings { } var imessageDetails: String? { - guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self) + guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self) else { return nil } var lines: [String] = [] if let cliPath = status.cliPath, !cliPath.isEmpty { @@ -243,68 +243,68 @@ extension ConnectionsSettings { } var isTelegramTokenLocked: Bool { - self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)?.tokenSource == "env" + self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)?.tokenSource == "env" } var isDiscordTokenLocked: Bool { - self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)?.tokenSource == "env" + self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)?.tokenSource == "env" } - var orderedProviders: [ConnectionProvider] { - ConnectionProvider.allCases.sorted { lhs, rhs in - let lhsEnabled = self.providerEnabled(lhs) - let rhsEnabled = self.providerEnabled(rhs) + var orderedChannels: [ConnectionChannel] { + ConnectionChannel.allCases.sorted { lhs, rhs in + let lhsEnabled = self.channelEnabled(lhs) + let rhsEnabled = self.channelEnabled(rhs) if lhsEnabled != rhsEnabled { return lhsEnabled && !rhsEnabled } return lhs.sortOrder < rhs.sortOrder } } - var enabledProviders: [ConnectionProvider] { - self.orderedProviders.filter { self.providerEnabled($0) } + var enabledChannels: [ConnectionChannel] { + self.orderedChannels.filter { self.channelEnabled($0) } } - var availableProviders: [ConnectionProvider] { - self.orderedProviders.filter { !self.providerEnabled($0) } + var availableChannels: [ConnectionChannel] { + self.orderedChannels.filter { !self.channelEnabled($0) } } func ensureSelection() { - guard let selected = self.selectedProvider else { - self.selectedProvider = self.orderedProviders.first + guard let selected = self.selectedChannel else { + self.selectedChannel = self.orderedChannels.first return } - if !self.orderedProviders.contains(selected) { - self.selectedProvider = self.orderedProviders.first + if !self.orderedChannels.contains(selected) { + self.selectedChannel = self.orderedChannels.first } } - func providerEnabled(_ provider: ConnectionProvider) -> Bool { - switch provider { + func channelEnabled(_ channel: ConnectionChannel) -> Bool { + switch channel { case .whatsapp: - guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self) + guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self) else { return false } return status.configured || status.linked || status.running case .telegram: - guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self) + guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self) else { return false } return status.configured || status.running case .discord: - guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self) + guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self) else { return false } return status.configured || status.running case .signal: - guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self) + guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self) else { return false } return status.configured || status.running case .imessage: - guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self) + guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self) else { return false } return status.configured || status.running } } @ViewBuilder - func providerSection(_ provider: ConnectionProvider) -> some View { - switch provider { + func channelSection(_ channel: ConnectionChannel) -> some View { + switch channel { case .whatsapp: self.whatsAppSection case .telegram: @@ -318,8 +318,8 @@ extension ConnectionsSettings { } } - func providerTint(_ provider: ConnectionProvider) -> Color { - switch provider { + func channelTint(_ channel: ConnectionChannel) -> Color { + switch channel { case .whatsapp: self.whatsAppTint case .telegram: @@ -333,8 +333,8 @@ extension ConnectionsSettings { } } - func providerSummary(_ provider: ConnectionProvider) -> String { - switch provider { + func channelSummary(_ channel: ConnectionChannel) -> String { + switch channel { case .whatsapp: self.whatsAppSummary case .telegram: @@ -348,8 +348,8 @@ extension ConnectionsSettings { } } - func providerDetails(_ provider: ConnectionProvider) -> String? { - switch provider { + func channelDetails(_ channel: ConnectionChannel) -> String? { + switch channel { case .whatsapp: self.whatsAppDetails case .telegram: @@ -363,55 +363,55 @@ extension ConnectionsSettings { } } - func providerLastCheckText(_ provider: ConnectionProvider) -> String { - guard let date = self.providerLastCheck(provider) else { return "never" } + func channelLastCheckText(_ channel: ConnectionChannel) -> String { + guard let date = self.channelLastCheck(channel) else { return "never" } return relativeAge(from: date) } - func providerLastCheck(_ provider: ConnectionProvider) -> Date? { - switch provider { + func channelLastCheck(_ channel: ConnectionChannel) -> Date? { + switch channel { case .whatsapp: - guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self) + guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self) else { return nil } return self.date(fromMs: status.lastEventAt ?? status.lastMessageAt ?? status.lastConnectedAt) case .telegram: return self - .date(fromMs: self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)? + .date(fromMs: self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)? .lastProbeAt) case .discord: return self - .date(fromMs: self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)? + .date(fromMs: self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)? .lastProbeAt) case .signal: return self - .date(fromMs: self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)?.lastProbeAt) + .date(fromMs: self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)?.lastProbeAt) case .imessage: return self - .date(fromMs: self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)? + .date(fromMs: self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)? .lastProbeAt) } } - func providerHasError(_ provider: ConnectionProvider) -> Bool { - switch provider { + func channelHasError(_ channel: ConnectionChannel) -> Bool { + switch channel { case .whatsapp: - guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self) + guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self) else { return false } return status.lastError?.isEmpty == false || status.lastDisconnect?.loggedOut == true case .telegram: - guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self) + guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self) else { return false } return status.lastError?.isEmpty == false || status.probe?.ok == false case .discord: - guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self) + guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self) else { return false } return status.lastError?.isEmpty == false || status.probe?.ok == false case .signal: - guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self) + guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self) else { return false } return status.lastError?.isEmpty == false || status.probe?.ok == false case .imessage: - guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self) + guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self) else { return false } return status.lastError?.isEmpty == false || status.probe?.ok == false } diff --git a/apps/macos/Sources/Clawdbot/ConnectionsSettings+View.swift b/apps/macos/Sources/Clawdbot/ConnectionsSettings+View.swift index 0527394fb..817c719eb 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionsSettings+View.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionsSettings+View.swift @@ -11,7 +11,7 @@ extension ConnectionsSettings { self.store.start() self.ensureSelection() } - .onChange(of: self.orderedProviders) { _, _ in + .onChange(of: self.orderedChannels) { _, _ in self.ensureSelection() } .onDisappear { self.store.stop() } @@ -20,17 +20,17 @@ extension ConnectionsSettings { private var sidebar: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 8) { - if !self.enabledProviders.isEmpty { + if !self.enabledChannels.isEmpty { self.sidebarSectionHeader("Configured") - ForEach(self.enabledProviders) { provider in - self.sidebarRow(provider) + ForEach(self.enabledChannels) { channel in + self.sidebarRow(channel) } } - if !self.availableProviders.isEmpty { + if !self.availableChannels.isEmpty { self.sidebarSectionHeader("Available") - ForEach(self.availableProviders) { provider in - self.sidebarRow(provider) + ForEach(self.availableChannels) { channel in + self.sidebarRow(channel) } } } @@ -46,8 +46,8 @@ extension ConnectionsSettings { private var detail: some View { Group { - if let provider = self.selectedProvider { - self.providerDetail(provider) + if let channel = self.selectedChannel { + self.channelDetail(channel) } else { self.emptyDetail } @@ -59,7 +59,7 @@ extension ConnectionsSettings { VStack(alignment: .leading, spacing: 8) { Text("Connections") .font(.title3.weight(.semibold)) - Text("Select a provider to view status and settings.") + Text("Select a channel to view status and settings.") .font(.callout) .foregroundStyle(.secondary) } @@ -67,12 +67,12 @@ extension ConnectionsSettings { .padding(.vertical, 18) } - private func providerDetail(_ provider: ConnectionProvider) -> some View { + private func channelDetail(_ channel: ConnectionChannel) -> some View { ScrollView(.vertical) { VStack(alignment: .leading, spacing: 16) { - self.detailHeader(for: provider) + self.detailHeader(for: channel) Divider() - self.providerSection(provider) + self.channelSection(channel) Spacer(minLength: 0) } .frame(maxWidth: .infinity, alignment: .leading) @@ -81,18 +81,18 @@ extension ConnectionsSettings { } } - private func sidebarRow(_ provider: ConnectionProvider) -> some View { - let isSelected = self.selectedProvider == provider + private func sidebarRow(_ channel: ConnectionChannel) -> some View { + let isSelected = self.selectedChannel == channel return Button { - self.selectedProvider = provider + self.selectedChannel = channel } label: { HStack(spacing: 8) { Circle() - .fill(self.providerTint(provider)) + .fill(self.channelTint(channel)) .frame(width: 8, height: 8) VStack(alignment: .leading, spacing: 2) { - Text(provider.title) - Text(self.providerSummary(provider)) + Text(channel.title) + Text(self.channelSummary(channel)) .font(.caption) .foregroundStyle(.secondary) } @@ -119,23 +119,23 @@ extension ConnectionsSettings { .padding(.top, 2) } - private func detailHeader(for provider: ConnectionProvider) -> some View { + private func detailHeader(for channel: ConnectionChannel) -> some View { VStack(alignment: .leading, spacing: 8) { HStack(alignment: .firstTextBaseline, spacing: 10) { - Label(provider.detailTitle, systemImage: provider.systemImage) + Label(channel.detailTitle, systemImage: channel.systemImage) .font(.title3.weight(.semibold)) self.statusBadge( - self.providerSummary(provider), - color: self.providerTint(provider)) + self.channelSummary(channel), + color: self.channelTint(channel)) Spacer() - self.providerHeaderActions(provider) + self.channelHeaderActions(channel) } HStack(spacing: 10) { - Text("Last check \(self.providerLastCheckText(provider))") + Text("Last check \(self.channelLastCheckText(channel))") .font(.caption) .foregroundStyle(.secondary) - if self.providerHasError(provider) { + if self.channelHasError(channel) { Text("Error") .font(.caption2.weight(.semibold)) .padding(.horizontal, 6) @@ -146,7 +146,7 @@ extension ConnectionsSettings { } } - if let details = self.providerDetails(provider) { + if let details = self.channelDetails(channel) { Text(details) .font(.caption) .foregroundStyle(.secondary) diff --git a/apps/macos/Sources/Clawdbot/ConnectionsSettings.swift b/apps/macos/Sources/Clawdbot/ConnectionsSettings.swift index 91dea51be..0976f7397 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionsSettings.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionsSettings.swift @@ -2,7 +2,7 @@ import AppKit import SwiftUI struct ConnectionsSettings: View { - enum ConnectionProvider: String, CaseIterable, Identifiable, Hashable { + enum ConnectionChannel: String, CaseIterable, Identifiable, Hashable { case whatsapp case telegram case discord @@ -53,7 +53,7 @@ struct ConnectionsSettings: View { } @Bindable var store: ConnectionsStore - @State var selectedProvider: ConnectionProvider? + @State var selectedChannel: ConnectionChannel? @State var showTelegramToken = false @State var showDiscordToken = false diff --git a/apps/macos/Sources/Clawdbot/ConnectionsStore+Lifecycle.swift b/apps/macos/Sources/Clawdbot/ConnectionsStore+Lifecycle.swift index 58d372c50..f24cc3dfb 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionsStore+Lifecycle.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionsStore+Lifecycle.swift @@ -31,8 +31,8 @@ extension ConnectionsStore { "probe": AnyCodable(probe), "timeoutMs": AnyCodable(8000), ] - let snap: ProvidersStatusSnapshot = try await GatewayConnection.shared.requestDecoded( - method: .providersStatus, + let snap: ChannelsStatusSnapshot = try await GatewayConnection.shared.requestDecoded( + method: .channelsStatus, params: params, timeoutMs: 12000) self.snapshot = snap @@ -101,10 +101,10 @@ extension ConnectionsStore { defer { self.whatsappBusy = false } do { let params: [String: AnyCodable] = [ - "provider": AnyCodable("whatsapp"), + "channel": AnyCodable("whatsapp"), ] - let result: ProviderLogoutResult = try await GatewayConnection.shared.requestDecoded( - method: .providersLogout, + let result: ChannelLogoutResult = try await GatewayConnection.shared.requestDecoded( + method: .channelsLogout, params: params, timeoutMs: 15000) self.whatsappLoginMessage = result.cleared @@ -123,10 +123,10 @@ extension ConnectionsStore { defer { self.telegramBusy = false } do { let params: [String: AnyCodable] = [ - "provider": AnyCodable("telegram"), + "channel": AnyCodable("telegram"), ] - let result: ProviderLogoutResult = try await GatewayConnection.shared.requestDecoded( - method: .providersLogout, + let result: ChannelLogoutResult = try await GatewayConnection.shared.requestDecoded( + method: .channelsLogout, params: params, timeoutMs: 15000) if result.envToken == true { @@ -154,8 +154,8 @@ private struct WhatsAppLoginWaitResult: Codable { let message: String } -private struct ProviderLogoutResult: Codable { - let provider: String? +private struct ChannelLogoutResult: Codable { + let channel: String? let accountId: String? let cleared: Bool let envToken: Bool? diff --git a/apps/macos/Sources/Clawdbot/ConnectionsStore.swift b/apps/macos/Sources/Clawdbot/ConnectionsStore.swift index 1302f1532..d8daaec1c 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionsStore.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionsStore.swift @@ -2,7 +2,7 @@ import ClawdbotProtocol import Foundation import Observation -struct ProvidersStatusSnapshot: Codable { +struct ChannelsStatusSnapshot: Codable { struct WhatsAppSelf: Codable { let e164: String? let jid: String? @@ -121,7 +121,7 @@ struct ProvidersStatusSnapshot: Codable { let lastProbeAt: Double? } - struct ProviderAccountSnapshot: Codable { + struct ChannelAccountSnapshot: Codable { let accountId: String let name: String? let enabled: Bool? @@ -154,14 +154,14 @@ struct ProvidersStatusSnapshot: Codable { } let ts: Double - let providerOrder: [String] - let providerLabels: [String: String] - let providers: [String: AnyCodable] - let providerAccounts: [String: [ProviderAccountSnapshot]] - let providerDefaultAccountId: [String: String] + let channelOrder: [String] + let channelLabels: [String: String] + let channels: [String: AnyCodable] + let channelAccounts: [String: [ChannelAccountSnapshot]] + let channelDefaultAccountId: [String: String] - func decodeProvider(_ id: String, as type: T.Type) -> T? { - guard let value = self.providers[id] else { return nil } + func decodeChannel(_ id: String, as type: T.Type) -> T? { + guard let value = self.channels[id] else { return nil } do { let data = try JSONEncoder().encode(value) return try JSONDecoder().decode(type, from: data) @@ -230,7 +230,7 @@ struct DiscordGuildForm: Identifiable { final class ConnectionsStore { static let shared = ConnectionsStore() - var snapshot: ProvidersStatusSnapshot? + var snapshot: ChannelsStatusSnapshot? var lastError: String? var lastSuccess: Date? var isRefreshing = false diff --git a/apps/macos/Sources/Clawdbot/CronJobEditor+Helpers.swift b/apps/macos/Sources/Clawdbot/CronJobEditor+Helpers.swift index 966b7ae0e..5d8acf6a3 100644 --- a/apps/macos/Sources/Clawdbot/CronJobEditor+Helpers.swift +++ b/apps/macos/Sources/Clawdbot/CronJobEditor+Helpers.swift @@ -36,13 +36,13 @@ extension CronJobEditor { case let .systemEvent(text): self.payloadKind = .systemEvent self.systemEventText = text - case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver): + case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver): self.payloadKind = .agentTurn self.agentMessage = message self.thinking = thinking ?? "" self.timeoutSeconds = timeoutSeconds.map(String.init) ?? "" self.deliver = deliver ?? false - self.provider = GatewayAgentProvider(raw: provider) + self.channel = GatewayAgentChannel(raw: channel) self.to = to ?? "" self.bestEffortDeliver = bestEffortDeliver ?? false } @@ -204,7 +204,7 @@ extension CronJobEditor { if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n } payload["deliver"] = self.deliver if self.deliver { - payload["provider"] = self.provider.rawValue + payload["channel"] = self.channel.rawValue let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines) if !to.isEmpty { payload["to"] = to } payload["bestEffortDeliver"] = self.bestEffortDeliver diff --git a/apps/macos/Sources/Clawdbot/CronJobEditor+Testing.swift b/apps/macos/Sources/Clawdbot/CronJobEditor+Testing.swift index 12f8d4903..603180b45 100644 --- a/apps/macos/Sources/Clawdbot/CronJobEditor+Testing.swift +++ b/apps/macos/Sources/Clawdbot/CronJobEditor+Testing.swift @@ -14,7 +14,7 @@ extension CronJobEditor { self.payloadKind = .agentTurn self.agentMessage = "Run diagnostic" self.deliver = true - self.provider = .last + self.channel = .last self.to = "+15551230000" self.thinking = "low" self.timeoutSeconds = "90" diff --git a/apps/macos/Sources/Clawdbot/CronJobEditor.swift b/apps/macos/Sources/Clawdbot/CronJobEditor.swift index 7201b738e..05659d179 100644 --- a/apps/macos/Sources/Clawdbot/CronJobEditor.swift +++ b/apps/macos/Sources/Clawdbot/CronJobEditor.swift @@ -18,7 +18,7 @@ struct CronJobEditor: View { static let scheduleKindNote = "“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression." static let isolatedPayloadNote = - "Isolated jobs always run an agent turn. The result can be delivered to a provider, " + "Isolated jobs always run an agent turn. The result can be delivered to a channel, " + "and a short summary is posted back to your main chat." static let mainPayloadNote = "System events are injected into the current main session. Agent turns require an isolated session target." @@ -45,7 +45,7 @@ struct CronJobEditor: View { @State var systemEventText: String = "" @State var agentMessage: String = "" @State var deliver: Bool = false - @State var provider: GatewayAgentProvider = .last + @State var channel: GatewayAgentChannel = .last @State var to: String = "" @State var thinking: String = "" @State var timeoutSeconds: String = "" @@ -323,7 +323,7 @@ struct CronJobEditor: View { } GridRow { self.gridLabel("Deliver") - Toggle("Deliver result to a provider", isOn: self.$deliver) + Toggle("Deliver result to a channel", isOn: self.$deliver) .toggleStyle(.switch) } } @@ -331,15 +331,15 @@ struct CronJobEditor: View { if self.deliver { Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) { GridRow { - self.gridLabel("Provider") - Picker("", selection: self.$provider) { - Text("last").tag(GatewayAgentProvider.last) - Text("whatsapp").tag(GatewayAgentProvider.whatsapp) - Text("telegram").tag(GatewayAgentProvider.telegram) - Text("discord").tag(GatewayAgentProvider.discord) - Text("slack").tag(GatewayAgentProvider.slack) - Text("signal").tag(GatewayAgentProvider.signal) - Text("imessage").tag(GatewayAgentProvider.imessage) + self.gridLabel("Channel") + Picker("", selection: self.$channel) { + Text("last").tag(GatewayAgentChannel.last) + Text("whatsapp").tag(GatewayAgentChannel.whatsapp) + Text("telegram").tag(GatewayAgentChannel.telegram) + Text("discord").tag(GatewayAgentChannel.discord) + Text("slack").tag(GatewayAgentChannel.slack) + Text("signal").tag(GatewayAgentChannel.signal) + Text("imessage").tag(GatewayAgentChannel.imessage) } .labelsHidden() .pickerStyle(.segmented) diff --git a/apps/macos/Sources/Clawdbot/CronModels.swift b/apps/macos/Sources/Clawdbot/CronModels.swift index 437cc99fe..1b40310d8 100644 --- a/apps/macos/Sources/Clawdbot/CronModels.swift +++ b/apps/macos/Sources/Clawdbot/CronModels.swift @@ -67,20 +67,20 @@ enum CronSchedule: Codable, Equatable { } } -enum CronPayload: Codable, Equatable { - case systemEvent(text: String) - case agentTurn( - message: String, - thinking: String?, - timeoutSeconds: Int?, - deliver: Bool?, - provider: String?, - to: String?, - bestEffortDeliver: Bool?) + enum CronPayload: Codable, Equatable { + case systemEvent(text: String) + case agentTurn( + message: String, + thinking: String?, + timeoutSeconds: Int?, + deliver: Bool?, + channel: String?, + to: String?, + bestEffortDeliver: Bool?) - enum CodingKeys: String, CodingKey { - case kind, text, message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver - } + enum CodingKeys: String, CodingKey { + case kind, text, message, thinking, timeoutSeconds, deliver, channel, provider, to, bestEffortDeliver + } var kind: String { switch self { @@ -95,15 +95,16 @@ enum CronPayload: Codable, Equatable { switch kind { case "systemEvent": self = try .systemEvent(text: container.decode(String.self, forKey: .text)) - case "agentTurn": - self = try .agentTurn( - message: container.decode(String.self, forKey: .message), - thinking: container.decodeIfPresent(String.self, forKey: .thinking), - timeoutSeconds: container.decodeIfPresent(Int.self, forKey: .timeoutSeconds), - deliver: container.decodeIfPresent(Bool.self, forKey: .deliver), - provider: container.decodeIfPresent(String.self, forKey: .provider), - to: container.decodeIfPresent(String.self, forKey: .to), - bestEffortDeliver: container.decodeIfPresent(Bool.self, forKey: .bestEffortDeliver)) + case "agentTurn": + self = try .agentTurn( + message: container.decode(String.self, forKey: .message), + thinking: container.decodeIfPresent(String.self, forKey: .thinking), + timeoutSeconds: container.decodeIfPresent(Int.self, forKey: .timeoutSeconds), + deliver: container.decodeIfPresent(Bool.self, forKey: .deliver), + channel: container.decodeIfPresent(String.self, forKey: .channel) + ?? container.decodeIfPresent(String.self, forKey: .provider), + to: container.decodeIfPresent(String.self, forKey: .to), + bestEffortDeliver: container.decodeIfPresent(Bool.self, forKey: .bestEffortDeliver)) default: throw DecodingError.dataCorruptedError( forKey: .kind, @@ -118,17 +119,17 @@ enum CronPayload: Codable, Equatable { switch self { case let .systemEvent(text): try container.encode(text, forKey: .text) - case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver): - try container.encode(message, forKey: .message) - try container.encodeIfPresent(thinking, forKey: .thinking) - try container.encodeIfPresent(timeoutSeconds, forKey: .timeoutSeconds) - try container.encodeIfPresent(deliver, forKey: .deliver) - try container.encodeIfPresent(provider, forKey: .provider) - try container.encodeIfPresent(to, forKey: .to) - try container.encodeIfPresent(bestEffortDeliver, forKey: .bestEffortDeliver) - } - } -} + case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver): + try container.encode(message, forKey: .message) + try container.encodeIfPresent(thinking, forKey: .thinking) + try container.encodeIfPresent(timeoutSeconds, forKey: .timeoutSeconds) + try container.encodeIfPresent(deliver, forKey: .deliver) + try container.encodeIfPresent(channel, forKey: .channel) + try container.encodeIfPresent(to, forKey: .to) + try container.encodeIfPresent(bestEffortDeliver, forKey: .bestEffortDeliver) + } + } + } struct CronIsolation: Codable, Equatable { var postToMainPrefix: String? diff --git a/apps/macos/Sources/Clawdbot/DeepLinks.swift b/apps/macos/Sources/Clawdbot/DeepLinks.swift index b1960b239..0ffa87908 100644 --- a/apps/macos/Sources/Clawdbot/DeepLinks.swift +++ b/apps/macos/Sources/Clawdbot/DeepLinks.swift @@ -59,7 +59,7 @@ final class DeepLinkHandler { } do { - let provider = GatewayAgentProvider(raw: link.channel) + let channel = GatewayAgentChannel(raw: link.channel) let explicitSessionKey = link.sessionKey? .trimmingCharacters(in: .whitespacesAndNewlines) .nonEmpty @@ -72,9 +72,9 @@ final class DeepLinkHandler { message: messagePreview, sessionKey: resolvedSessionKey, thinking: link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty, - deliver: provider.shouldDeliver(link.deliver), + deliver: channel.shouldDeliver(link.deliver), to: link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty, - provider: provider, + channel: channel, timeoutSeconds: link.timeoutSeconds, idempotencyKey: UUID().uuidString) diff --git a/apps/macos/Sources/Clawdbot/GatewayConnection.swift b/apps/macos/Sources/Clawdbot/GatewayConnection.swift index 00699f651..29ee39fed 100644 --- a/apps/macos/Sources/Clawdbot/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdbot/GatewayConnection.swift @@ -5,7 +5,7 @@ import OSLog private let gatewayConnectionLogger = Logger(subsystem: "com.clawdbot", category: "gateway.connection") -enum GatewayAgentProvider: String, Codable, CaseIterable, Sendable { +enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable { case last case whatsapp case telegram @@ -18,7 +18,7 @@ enum GatewayAgentProvider: String, Codable, CaseIterable, Sendable { init(raw: String?) { let normalized = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - self = GatewayAgentProvider(rawValue: normalized) ?? .last + self = GatewayAgentChannel(rawValue: normalized) ?? .last } var isDeliverable: Bool { self != .webchat } @@ -32,7 +32,7 @@ struct GatewayAgentInvocation: Sendable { var thinking: String? var deliver: Bool = false var to: String? - var provider: GatewayAgentProvider = .last + var channel: GatewayAgentChannel = .last var timeoutSeconds: Int? var idempotencyKey: String = UUID().uuidString } @@ -52,7 +52,7 @@ actor GatewayConnection { case setHeartbeats = "set-heartbeats" case systemEvent = "system-event" case health - case providersStatus = "providers.status" + case channelsStatus = "channels.status" case configGet = "config.get" case configSet = "config.set" case wizardStart = "wizard.start" @@ -62,7 +62,7 @@ actor GatewayConnection { case talkMode = "talk.mode" case webLoginStart = "web.login.start" case webLoginWait = "web.login.wait" - case providersLogout = "providers.logout" + case channelsLogout = "channels.logout" case modelsList = "models.list" case chatHistory = "chat.history" case chatSend = "chat.send" @@ -368,7 +368,7 @@ extension GatewayConnection { "thinking": AnyCodable(invocation.thinking ?? "default"), "deliver": AnyCodable(invocation.deliver), "to": AnyCodable(invocation.to ?? ""), - "provider": AnyCodable(invocation.provider.rawValue), + "channel": AnyCodable(invocation.channel.rawValue), "idempotencyKey": AnyCodable(invocation.idempotencyKey), ] if let timeout = invocation.timeoutSeconds { @@ -389,7 +389,7 @@ extension GatewayConnection { sessionKey: String, deliver: Bool, to: String?, - provider: GatewayAgentProvider = .last, + channel: GatewayAgentChannel = .last, timeoutSeconds: Int? = nil, idempotencyKey: String = UUID().uuidString) async -> (ok: Bool, error: String?) { @@ -399,7 +399,7 @@ extension GatewayConnection { thinking: thinking, deliver: deliver, to: to, - provider: provider, + channel: channel, timeoutSeconds: timeoutSeconds, idempotencyKey: idempotencyKey)) } diff --git a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift index 8b712b9cd..9dc71d690 100644 --- a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift @@ -211,19 +211,19 @@ final class GatewayProcessManager { private func describe(details instance: String?, port: Int, snap: HealthSnapshot?) -> String { let instanceText = instance ?? "pid unknown" if let snap { - let linkId = snap.providerOrder?.first(where: { - if let summary = snap.providers[$0] { return summary.linked != nil } + let linkId = snap.channelOrder?.first(where: { + if let summary = snap.channels[$0] { return summary.linked != nil } return false - }) ?? snap.providers.keys.first(where: { - if let summary = snap.providers[$0] { return summary.linked != nil } + }) ?? snap.channels.keys.first(where: { + if let summary = snap.channels[$0] { return summary.linked != nil } return false }) - let linked = linkId.flatMap { snap.providers[$0]?.linked } ?? false - let authAge = linkId.flatMap { snap.providers[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age" + let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false + let authAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age" let label = - linkId.flatMap { snap.providerLabels?[$0] } ?? + linkId.flatMap { snap.channelLabels?[$0] } ?? linkId?.capitalized ?? - "provider" + "channel" let linkText = linked ? "linked" : "not linked" return "port \(port), \(label) \(linkText), auth \(authAge), \(instanceText)" } diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index 0faf179f3..37bb8d76b 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -496,18 +496,18 @@ struct GeneralSettings: View { } if let snap = snapshot { - let linkId = snap.providerOrder?.first(where: { - if let summary = snap.providers[$0] { return summary.linked != nil } + let linkId = snap.channelOrder?.first(where: { + if let summary = snap.channels[$0] { return summary.linked != nil } return false - }) ?? snap.providers.keys.first(where: { - if let summary = snap.providers[$0] { return summary.linked != nil } + }) ?? snap.channels.keys.first(where: { + if let summary = snap.channels[$0] { return summary.linked != nil } return false }) let linkLabel = - linkId.flatMap { snap.providerLabels?[$0] } ?? + linkId.flatMap { snap.channelLabels?[$0] } ?? linkId?.capitalized ?? - "Link provider" - let linkAge = linkId.flatMap { snap.providers[$0]?.authAgeMs } + "Link channel" + let linkAge = linkId.flatMap { snap.channels[$0]?.authAgeMs } Text("\(linkLabel) auth age: \(healthAgeString(linkAge))") .font(.caption) .foregroundStyle(.secondary) diff --git a/apps/macos/Sources/Clawdbot/HealthStore.swift b/apps/macos/Sources/Clawdbot/HealthStore.swift index ef5d01e82..668056e00 100644 --- a/apps/macos/Sources/Clawdbot/HealthStore.swift +++ b/apps/macos/Sources/Clawdbot/HealthStore.swift @@ -4,7 +4,7 @@ import Observation import SwiftUI struct HealthSnapshot: Codable, Sendable { - struct ProviderSummary: Codable, Sendable { + struct ChannelSummary: Codable, Sendable { struct Probe: Codable, Sendable { struct Bot: Codable, Sendable { let username: String? @@ -44,9 +44,9 @@ struct HealthSnapshot: Codable, Sendable { let ok: Bool? let ts: Double let durationMs: Double - let providers: [String: ProviderSummary] - let providerOrder: [String]? - let providerLabels: [String: String]? + let channels: [String: ChannelSummary] + let channelOrder: [String]? + let channelLabels: [String: String]? let heartbeatSeconds: Int? let sessions: Sessions } @@ -144,13 +144,13 @@ final class HealthStore { } } - private static func isProviderHealthy(_ summary: HealthSnapshot.ProviderSummary) -> Bool { + private static func isChannelHealthy(_ summary: HealthSnapshot.ChannelSummary) -> Bool { guard summary.configured == true else { return false } // If probe is missing, treat it as "configured but unknown health" (not a hard fail). return summary.probe?.ok ?? true } - private static func describeProbeFailure(_ probe: HealthSnapshot.ProviderSummary.Probe) -> String { + 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))" } @@ -162,28 +162,28 @@ final class HealthStore { return "\(reason) (\(code))" } - private func resolveLinkProvider( - _ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ProviderSummary)? + private func resolveLinkChannel( + _ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ChannelSummary)? { - let order = snap.providerOrder ?? Array(snap.providers.keys) + let order = snap.channelOrder ?? Array(snap.channels.keys) for id in order { - if let summary = snap.providers[id], summary.linked != nil { + if let summary = snap.channels[id], summary.linked != nil { return (id: id, summary: summary) } } return nil } - private func resolveFallbackProvider( + private func resolveFallbackChannel( _ snap: HealthSnapshot, - excluding id: String?) -> (id: String, summary: HealthSnapshot.ProviderSummary)? + excluding id: String?) -> (id: String, summary: HealthSnapshot.ChannelSummary)? { - let order = snap.providerOrder ?? Array(snap.providers.keys) - for providerId in order { - if providerId == id { continue } - guard let summary = snap.providers[providerId] else { continue } - if Self.isProviderHealthy(summary) { - return (id: providerId, summary: summary) + let order = snap.channelOrder ?? Array(snap.channels.keys) + for channelId in order { + if channelId == id { continue } + guard let summary = snap.channels[channelId] else { continue } + if Self.isChannelHealthy(summary) { + return (id: channelId, summary: summary) } } return nil @@ -194,13 +194,13 @@ final class HealthStore { return .degraded(error) } guard let snap = self.snapshot else { return .unknown } - guard let link = self.resolveLinkProvider(snap) else { return .unknown } + guard let link = self.resolveLinkChannel(snap) else { return .unknown } if link.summary.linked != true { - // Linking is optional if any other provider is healthy; don't paint the whole app red. - let fallback = self.resolveFallbackProvider(snap, excluding: link.id) + // Linking is optional if any other channel is healthy; don't paint the whole app red. + let fallback = self.resolveFallbackChannel(snap, excluding: link.id) return fallback != nil ? .degraded("Not linked") : .linkingNeeded } - // A provider can be "linked" but still unhealthy (failed probe / cannot connect). + // A channel can be "linked" but still unhealthy (failed probe / cannot connect). if let probe = link.summary.probe, probe.ok == false { return .degraded(Self.describeProbeFailure(probe)) } @@ -211,10 +211,10 @@ final class HealthStore { 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.resolveLinkProvider(snap) else { return "Health check pending" } + guard let link = self.resolveLinkChannel(snap) else { return "Health check pending" } if link.summary.linked != true { - if let fallback = self.resolveFallbackProvider(snap, excluding: link.id) { - let fallbackLabel = snap.providerLabels?[fallback.id] ?? fallback.id.capitalized + if let fallback = self.resolveFallbackChannel(snap, excluding: link.id) { + let fallbackLabel = snap.channelLabels?[fallback.id] ?? fallback.id.capitalized let fallbackState = (fallback.summary.probe?.ok ?? true) ? "ok" : "degraded" return "\(fallbackLabel) \(fallbackState) · Not linked — run clawdbot login" } @@ -247,10 +247,10 @@ final class HealthStore { } func describeFailure(from snap: HealthSnapshot, fallback: String?) -> String { - if let link = self.resolveLinkProvider(snap), link.summary.linked != true { + if let link = self.resolveLinkChannel(snap), link.summary.linked != true { return "Not linked — run clawdbot login" } - if let link = self.resolveLinkProvider(snap), let probe = link.summary.probe, probe.ok == false { + if let link = self.resolveLinkChannel(snap), let probe = link.summary.probe, probe.ok == false { return Self.describeProbeFailure(probe) } if let fallback, !fallback.isEmpty { diff --git a/apps/macos/Sources/Clawdbot/InstancesStore.swift b/apps/macos/Sources/Clawdbot/InstancesStore.swift index d2a0e6ce0..196737f07 100644 --- a/apps/macos/Sources/Clawdbot/InstancesStore.swift +++ b/apps/macos/Sources/Clawdbot/InstancesStore.swift @@ -242,18 +242,18 @@ final class InstancesStore { do { let data = try await ControlChannel.shared.health(timeout: 8) guard let snap = decodeHealthSnapshot(from: data) else { return } - let linkId = snap.providerOrder?.first(where: { - if let summary = snap.providers[$0] { return summary.linked != nil } + let linkId = snap.channelOrder?.first(where: { + if let summary = snap.channels[$0] { return summary.linked != nil } return false - }) ?? snap.providers.keys.first(where: { - if let summary = snap.providers[$0] { return summary.linked != nil } + }) ?? snap.channels.keys.first(where: { + if let summary = snap.channels[$0] { return summary.linked != nil } return false }) - let linked = linkId.flatMap { snap.providers[$0]?.linked } ?? false + let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false let linkLabel = - linkId.flatMap { snap.providerLabels?[$0] } ?? + linkId.flatMap { snap.channelLabels?[$0] } ?? linkId?.capitalized ?? - "provider" + "channel" let entry = InstanceInfo( id: "health-\(snap.ts)", host: "gateway (health)", diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift index 6d5d8b01a..4ad376db7 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift @@ -694,7 +694,7 @@ extension OnboardingView { systemImage: "bubble.left.and.bubble.right") self.featureActionRow( title: "Connect WhatsApp or Telegram", - subtitle: "Open Settings → Connections to link providers and monitor status.", + subtitle: "Open Settings → Connections to link channels and monitor status.", systemImage: "link") { self.openSettings(tab: .connections) diff --git a/apps/macos/Sources/Clawdbot/VoiceWakeForwarder.swift b/apps/macos/Sources/Clawdbot/VoiceWakeForwarder.swift index a30d389f3..3fd9f827b 100644 --- a/apps/macos/Sources/Clawdbot/VoiceWakeForwarder.swift +++ b/apps/macos/Sources/Clawdbot/VoiceWakeForwarder.swift @@ -37,7 +37,7 @@ enum VoiceWakeForwarder { var thinking: String = "low" var deliver: Bool = true var to: String? - var provider: GatewayAgentProvider = .last + var channel: GatewayAgentChannel = .last } @discardableResult @@ -46,14 +46,14 @@ enum VoiceWakeForwarder { options: ForwardOptions = ForwardOptions()) async -> Result { let payload = Self.prefixedTranscript(transcript) - let deliver = options.provider.shouldDeliver(options.deliver) + let deliver = options.channel.shouldDeliver(options.deliver) let result = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation( message: payload, sessionKey: options.sessionKey, thinking: options.thinking, deliver: deliver, to: options.to, - provider: options.provider)) + channel: options.channel)) if result.ok { self.logger.info("voice wake forward ok") diff --git a/apps/macos/Tests/ClawdbotIPCTests/ConnectionsSettingsSmokeTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ConnectionsSettingsSmokeTests.swift index aea7e8ddb..6e451d13c 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/ConnectionsSettingsSmokeTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/ConnectionsSettingsSmokeTests.swift @@ -4,22 +4,22 @@ import Testing @Suite(.serialized) @MainActor -struct ConnectionsSettingsSmokeTests { - @Test func connectionsSettingsBuildsBodyWithSnapshot() { - let store = ConnectionsStore(isPreview: true) - store.snapshot = ProvidersStatusSnapshot( - ts: 1_700_000_000_000, - providerOrder: ["whatsapp", "telegram", "signal", "imessage"], - providerLabels: [ - "whatsapp": "WhatsApp", - "telegram": "Telegram", - "signal": "Signal", - "imessage": "iMessage", - ], - providers: [ - "whatsapp": AnyCodable([ - "configured": true, - "linked": true, + struct ConnectionsSettingsSmokeTests { + @Test func connectionsSettingsBuildsBodyWithSnapshot() { + let store = ConnectionsStore(isPreview: true) + store.snapshot = ChannelsStatusSnapshot( + ts: 1_700_000_000_000, + channelOrder: ["whatsapp", "telegram", "signal", "imessage"], + channelLabels: [ + "whatsapp": "WhatsApp", + "telegram": "Telegram", + "signal": "Signal", + "imessage": "iMessage", + ], + channels: [ + "whatsapp": AnyCodable([ + "configured": true, + "linked": true, "authAgeMs": 86_400_000, "self": ["e164": "+15551234567"], "running": true, @@ -70,13 +70,13 @@ struct ConnectionsSettingsSmokeTests { "lastError": "not configured", "probe": ["ok": false, "error": "imsg not found (imsg)"], "lastProbeAt": 1_700_000_050_000, - ]), - ], - providerAccounts: [:], - providerDefaultAccountId: [ - "whatsapp": "default", - "telegram": "default", - "signal": "default", + ]), + ], + channelAccounts: [:], + channelDefaultAccountId: [ + "whatsapp": "default", + "telegram": "default", + "signal": "default", "imessage": "default", ]) @@ -93,23 +93,23 @@ struct ConnectionsSettingsSmokeTests { let view = ConnectionsSettings(store: store) _ = view.body - } + } - @Test func connectionsSettingsBuildsBodyWithoutSnapshot() { - let store = ConnectionsStore(isPreview: true) - store.snapshot = ProvidersStatusSnapshot( - ts: 1_700_000_000_000, - providerOrder: ["whatsapp", "telegram", "signal", "imessage"], - providerLabels: [ - "whatsapp": "WhatsApp", - "telegram": "Telegram", - "signal": "Signal", - "imessage": "iMessage", - ], - providers: [ - "whatsapp": AnyCodable([ - "configured": false, - "linked": false, + @Test func connectionsSettingsBuildsBodyWithoutSnapshot() { + let store = ConnectionsStore(isPreview: true) + store.snapshot = ChannelsStatusSnapshot( + ts: 1_700_000_000_000, + channelOrder: ["whatsapp", "telegram", "signal", "imessage"], + channelLabels: [ + "whatsapp": "WhatsApp", + "telegram": "Telegram", + "signal": "Signal", + "imessage": "iMessage", + ], + channels: [ + "whatsapp": AnyCodable([ + "configured": false, + "linked": false, "running": false, "connected": false, "reconnectAttempts": 0, @@ -146,13 +146,13 @@ struct ConnectionsSettingsSmokeTests { "cliPath": "imsg", "probe": ["ok": false, "error": "imsg not found (imsg)"], "lastProbeAt": 1_700_000_200_000, - ]), - ], - providerAccounts: [:], - providerDefaultAccountId: [ - "whatsapp": "default", - "telegram": "default", - "signal": "default", + ]), + ], + channelAccounts: [:], + channelDefaultAccountId: [ + "whatsapp": "default", + "telegram": "default", + "signal": "default", "imessage": "default", ]) diff --git a/apps/macos/Tests/ClawdbotIPCTests/HealthDecodeTests.swift b/apps/macos/Tests/ClawdbotIPCTests/HealthDecodeTests.swift index 0796a05d8..2d7672563 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/HealthDecodeTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/HealthDecodeTests.swift @@ -2,17 +2,17 @@ import Foundation import Testing @testable import Clawdbot -@Suite struct HealthDecodeTests { - private let sampleJSON: String = // minimal but complete payload - """ - {"ts":1733622000,"durationMs":420,"providers":{"whatsapp":{"linked":true,"authAgeMs":120000},"telegram":{"configured":true,"probe":{"ok":true,"elapsedMs":800}}},"providerOrder":["whatsapp","telegram"],"heartbeatSeconds":60,"sessions":{"path":"/tmp/sessions.json","count":1,"recent":[{"key":"abc","updatedAt":1733621900,"age":120000}]}} + @Suite struct HealthDecodeTests { + private let sampleJSON: String = // minimal but complete payload + """ + {"ts":1733622000,"durationMs":420,"channels":{"whatsapp":{"linked":true,"authAgeMs":120000},"telegram":{"configured":true,"probe":{"ok":true,"elapsedMs":800}}},"channelOrder":["whatsapp","telegram"],"heartbeatSeconds":60,"sessions":{"path":"/tmp/sessions.json","count":1,"recent":[{"key":"abc","updatedAt":1733621900,"age":120000}]}} """ @Test func decodesCleanJSON() async throws { let data = Data(sampleJSON.utf8) let snap = decodeHealthSnapshot(from: data) - #expect(snap?.providers["whatsapp"]?.linked == true) + #expect(snap?.channels["whatsapp"]?.linked == true) #expect(snap?.sessions.count == 1) } @@ -20,7 +20,7 @@ import Testing let noisy = "debug: something logged\n" + self.sampleJSON + "\ntrailer" let snap = decodeHealthSnapshot(from: Data(noisy.utf8)) - #expect(snap?.providers["telegram"]?.probe?.elapsedMs == 800) + #expect(snap?.channels["telegram"]?.probe?.elapsedMs == 800) } @Test func failsWithoutBraces() async throws { diff --git a/apps/macos/Tests/ClawdbotIPCTests/HealthStoreStateTests.swift b/apps/macos/Tests/ClawdbotIPCTests/HealthStoreStateTests.swift index 9c2bb7ce8..a421f9358 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/HealthStoreStateTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/HealthStoreStateTests.swift @@ -3,12 +3,12 @@ import Testing @testable import Clawdbot @Suite struct HealthStoreStateTests { - @Test @MainActor func linkedProviderProbeFailureDegradesState() async throws { + @Test @MainActor func linkedChannelProbeFailureDegradesState() async throws { let snap = HealthSnapshot( ok: true, ts: 0, durationMs: 1, - providers: [ + channels: [ "whatsapp": .init( configured: true, linked: true, @@ -22,8 +22,8 @@ import Testing webhook: nil), lastProbeAt: 0), ], - providerOrder: ["whatsapp"], - providerLabels: ["whatsapp": "WhatsApp"], + channelOrder: ["whatsapp"], + channelLabels: ["whatsapp": "WhatsApp"], heartbeatSeconds: 60, sessions: .init(path: "/tmp/sessions.json", count: 0, recent: [])) @@ -34,7 +34,7 @@ import Testing case let .degraded(message): #expect(!message.isEmpty) default: - Issue.record("Expected degraded state when probe fails for linked provider") + Issue.record("Expected degraded state when probe fails for linked channel") } #expect(store.summaryLine.contains("probe degraded")) diff --git a/docs/concepts/agent-loop.md b/docs/concepts/agent-loop.md index 2b737b591..446b4c105 100644 --- a/docs/concepts/agent-loop.md +++ b/docs/concepts/agent-loop.md @@ -36,7 +36,7 @@ Short, exact flow of one agent run. - `assistant`: streamed deltas from pi-agent-core - `tool`: streamed tool events from pi-agent-core -## Chat provider handling +## Chat channel handling - Assistant deltas are buffered into chat `delta` messages. - A chat `final` is emitted on **lifecycle end/error**. diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 0cd8cfb4a..c8f851173 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -6,7 +6,7 @@ read_when: --- # Model providers -This page covers **LLM/model providers** (not chat providers like WhatsApp/Telegram). +This page covers **LLM/model providers** (not chat channels like WhatsApp/Telegram). For model selection rules, see [/concepts/models](/concepts/models). ## Quick rules diff --git a/docs/platforms/mac/bundled-gateway.md b/docs/platforms/mac/bundled-gateway.md index 20c8e1924..c8972e5b5 100644 --- a/docs/platforms/mac/bundled-gateway.md +++ b/docs/platforms/mac/bundled-gateway.md @@ -51,7 +51,7 @@ incompatible, update the global CLI to match the app version. ```bash clawdbot --version -CLAWDBOT_SKIP_PROVIDERS=1 \ +CLAWDBOT_SKIP_CHANNELS=1 \ CLAWDBOT_SKIP_CANVAS_HOST=1 \ clawdbot gateway --port 18999 --bind loopback ``` diff --git a/scripts/e2e/gateway-network-docker.sh b/scripts/e2e/gateway-network-docker.sh index 97f765298..19822f4cd 100644 --- a/scripts/e2e/gateway-network-docker.sh +++ b/scripts/e2e/gateway-network-docker.sh @@ -22,15 +22,15 @@ echo "Creating Docker network..." docker network create "$NET_NAME" >/dev/null echo "Starting gateway container..." -docker run --rm -d \ - --name "$GW_NAME" \ - --network "$NET_NAME" \ - -e "CLAWDBOT_GATEWAY_TOKEN=$TOKEN" \ - -e "CLAWDBOT_SKIP_PROVIDERS=1" \ - -e "CLAWDBOT_SKIP_GMAIL_WATCHER=1" \ - -e "CLAWDBOT_SKIP_CRON=1" \ - -e "CLAWDBOT_SKIP_CANVAS_HOST=1" \ - "$IMAGE_NAME" \ + docker run --rm -d \ + --name "$GW_NAME" \ + --network "$NET_NAME" \ + -e "CLAWDBOT_GATEWAY_TOKEN=$TOKEN" \ + -e "CLAWDBOT_SKIP_CHANNELS=1" \ + -e "CLAWDBOT_SKIP_GMAIL_WATCHER=1" \ + -e "CLAWDBOT_SKIP_CRON=1" \ + -e "CLAWDBOT_SKIP_CANVAS_HOST=1" \ + "$IMAGE_NAME" \ bash -lc "node dist/index.js gateway --port $PORT --bind lan --allow-unconfigured > /tmp/gateway-net-e2e.log 2>&1" echo "Waiting for gateway to come up..." diff --git a/src/auto-reply/reply/queue.ts b/src/auto-reply/reply/queue.ts index 6de41723d..edf8286dd 100644 --- a/src/auto-reply/reply/queue.ts +++ b/src/auto-reply/reply/queue.ts @@ -445,11 +445,11 @@ function buildCollectPrompt(items: FollowupRun[], summary?: string): string { /** * Checks if queued items have different routable originating channels. * - * Returns true if messages come from different providers (e.g., Slack + Telegram), + * Returns true if messages come from different channels (e.g., Slack + Telegram), * meaning they cannot be safely collected into one prompt without losing routing. * Also returns true for a mix of routable and non-routable channels. */ -function hasCrossProviderItems(items: FollowupRun[]): boolean { +function hasCrossChannelItems(items: FollowupRun[]): boolean { const keys = new Set(); let hasUnkeyed = false; @@ -499,33 +499,33 @@ export function scheduleFollowupDrain( if (forceIndividualCollect) { const next = queue.items.shift(); if (!next) break; - await runFollowup(next); - continue; - } + await runFollowup(next); + continue; + } - // Check if messages span multiple providers. - // If so, process individually to preserve per-message routing. - const isCrossProvider = hasCrossProviderItems(queue.items); + // Check if messages span multiple channels. + // If so, process individually to preserve per-message routing. + const isCrossChannel = hasCrossChannelItems(queue.items); - if (isCrossProvider) { - forceIndividualCollect = true; - // Process one at a time to preserve per-message routing info. - const next = queue.items.shift(); - if (!next) break; - await runFollowup(next); - continue; - } + if (isCrossChannel) { + forceIndividualCollect = true; + // Process one at a time to preserve per-message routing info. + const next = queue.items.shift(); + if (!next) break; + await runFollowup(next); + continue; + } - // Same-provider messages can be safely collected. - const items = queue.items.splice(0, queue.items.length); - const summary = buildSummaryPrompt(queue); - const run = items.at(-1)?.run ?? queue.lastRun; - if (!run) break; + // Same-channel messages can be safely collected. + const items = queue.items.splice(0, queue.items.length); + const summary = buildSummaryPrompt(queue); + const run = items.at(-1)?.run ?? queue.lastRun; + if (!run) break; - // Preserve originating channel from items when collecting same-provider. - const originatingChannel = items.find( - (i) => i.originatingChannel, - )?.originatingChannel; + // Preserve originating channel from items when collecting same-channel. + const originatingChannel = items.find( + (i) => i.originatingChannel, + )?.originatingChannel; const originatingTo = items.find( (i) => i.originatingTo, )?.originatingTo; diff --git a/src/cli/gateway.sigterm.test.ts b/src/cli/gateway.sigterm.test.ts index 5d722cf16..2502bd704 100644 --- a/src/cli/gateway.sigterm.test.ts +++ b/src/cli/gateway.sigterm.test.ts @@ -108,7 +108,7 @@ describe("gateway SIGTERM", () => { ...process.env, CLAWDBOT_STATE_DIR: stateDir, CLAWDBOT_CONFIG_PATH: configPath, - CLAWDBOT_SKIP_PROVIDERS: "1", + CLAWDBOT_SKIP_CHANNELS: "1", CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1", CLAWDBOT_SKIP_CANVAS_HOST: "1", // Avoid port collisions with other test processes that may also start a bridge server. diff --git a/src/commands/onboard-non-interactive.gateway-auth.test.ts b/src/commands/onboard-non-interactive.gateway-auth.test.ts index ced944154..e0ffe6f70 100644 --- a/src/commands/onboard-non-interactive.gateway-auth.test.ts +++ b/src/commands/onboard-non-interactive.gateway-auth.test.ts @@ -96,21 +96,21 @@ async function connectReq(params: { url: string; token?: string }) { describe("onboard (non-interactive): gateway auth", () => { it("writes gateway token auth into config and gateway enforces it", async () => { - const prev = { - home: process.env.HOME, - stateDir: process.env.CLAWDBOT_STATE_DIR, - configPath: process.env.CLAWDBOT_CONFIG_PATH, - skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - }; + const prev = { + home: process.env.HOME, + stateDir: process.env.CLAWDBOT_STATE_DIR, + configPath: process.env.CLAWDBOT_CONFIG_PATH, + skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, + skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, + skipCron: process.env.CLAWDBOT_SKIP_CRON, + skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + token: process.env.CLAWDBOT_GATEWAY_TOKEN, + }; - process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.CLAWDBOT_SKIP_CHANNELS = "1"; + process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; + process.env.CLAWDBOT_SKIP_CRON = "1"; + process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; delete process.env.CLAWDBOT_GATEWAY_TOKEN; const tempHome = await fs.mkdtemp( @@ -186,7 +186,7 @@ describe("onboard (non-interactive): gateway auth", () => { process.env.HOME = prev.home; process.env.CLAWDBOT_STATE_DIR = prev.stateDir; process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; - process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders; + process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels; process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; diff --git a/src/commands/onboard-non-interactive.lan-auto-token.test.ts b/src/commands/onboard-non-interactive.lan-auto-token.test.ts index b0b8ae009..4d2d35a29 100644 --- a/src/commands/onboard-non-interactive.lan-auto-token.test.ts +++ b/src/commands/onboard-non-interactive.lan-auto-token.test.ts @@ -106,21 +106,21 @@ describe("onboard (non-interactive): lan bind auto-token", () => { // Windows runner occasionally drops the temp config write in this flow; skip to keep CI green. return; } - const prev = { - home: process.env.HOME, - stateDir: process.env.CLAWDBOT_STATE_DIR, - configPath: process.env.CLAWDBOT_CONFIG_PATH, - skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - }; + const prev = { + home: process.env.HOME, + stateDir: process.env.CLAWDBOT_STATE_DIR, + configPath: process.env.CLAWDBOT_CONFIG_PATH, + skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, + skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, + skipCron: process.env.CLAWDBOT_SKIP_CRON, + skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + token: process.env.CLAWDBOT_GATEWAY_TOKEN, + }; - process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.CLAWDBOT_SKIP_CHANNELS = "1"; + process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; + process.env.CLAWDBOT_SKIP_CRON = "1"; + process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; delete process.env.CLAWDBOT_GATEWAY_TOKEN; const tempHome = await fs.mkdtemp( @@ -215,7 +215,7 @@ describe("onboard (non-interactive): lan bind auto-token", () => { process.env.HOME = prev.home; process.env.CLAWDBOT_STATE_DIR = prev.stateDir; process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; - process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders; + process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels; process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; diff --git a/src/commands/onboard-non-interactive.remote.test.ts b/src/commands/onboard-non-interactive.remote.test.ts index c932393d2..0cf950d9e 100644 --- a/src/commands/onboard-non-interactive.remote.test.ts +++ b/src/commands/onboard-non-interactive.remote.test.ts @@ -27,22 +27,22 @@ async function getFreePort(): Promise { describe("onboard (non-interactive): remote gateway config", () => { it("writes gateway.remote url/token and callGateway uses them", async () => { - const prev = { - home: process.env.HOME, - stateDir: process.env.CLAWDBOT_STATE_DIR, - configPath: process.env.CLAWDBOT_CONFIG_PATH, - skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - password: process.env.CLAWDBOT_GATEWAY_PASSWORD, - }; + const prev = { + home: process.env.HOME, + stateDir: process.env.CLAWDBOT_STATE_DIR, + configPath: process.env.CLAWDBOT_CONFIG_PATH, + skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, + skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, + skipCron: process.env.CLAWDBOT_SKIP_CRON, + skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + token: process.env.CLAWDBOT_GATEWAY_TOKEN, + password: process.env.CLAWDBOT_GATEWAY_PASSWORD, + }; - process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.CLAWDBOT_SKIP_CHANNELS = "1"; + process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; + process.env.CLAWDBOT_SKIP_CRON = "1"; + process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; delete process.env.CLAWDBOT_GATEWAY_TOKEN; delete process.env.CLAWDBOT_GATEWAY_PASSWORD; @@ -104,16 +104,16 @@ describe("onboard (non-interactive): remote gateway config", () => { expect(health?.ok).toBe(true); } finally { await server.close({ reason: "non-interactive remote test complete" }); - await fs.rm(tempHome, { recursive: true, force: true }); - process.env.HOME = prev.home; - process.env.CLAWDBOT_STATE_DIR = prev.stateDir; - process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; - process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; - process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; - process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token; - process.env.CLAWDBOT_GATEWAY_PASSWORD = prev.password; - } + await fs.rm(tempHome, { recursive: true, force: true }); + process.env.HOME = prev.home; + process.env.CLAWDBOT_STATE_DIR = prev.stateDir; + process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; + process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels; + process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; + process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; + process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; + process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token; + process.env.CLAWDBOT_GATEWAY_PASSWORD = prev.password; + } }, 60_000); }); diff --git a/src/daemon/service-audit.ts b/src/daemon/service-audit.ts index ff9e8405b..9430246f4 100644 --- a/src/daemon/service-audit.ts +++ b/src/daemon/service-audit.ts @@ -285,7 +285,7 @@ async function auditGatewayRuntime( issues.push({ code: SERVICE_AUDIT_CODES.gatewayRuntimeBun, message: - "Gateway service uses Bun; Bun is incompatible with WhatsApp + Telegram providers.", + "Gateway service uses Bun; Bun is incompatible with WhatsApp + Telegram channels.", detail: execPath, level: "recommended", }); diff --git a/src/gateway/gateway-cli-backend.live.test.ts b/src/gateway/gateway-cli-backend.live.test.ts index 45a7b1712..80bce867c 100644 --- a/src/gateway/gateway-cli-backend.live.test.ts +++ b/src/gateway/gateway-cli-backend.live.test.ts @@ -199,21 +199,21 @@ async function connectClient(params: { url: string; token: string }) { describeLive("gateway live (cli backend)", () => { it("runs the agent pipeline against the local CLI backend", async () => { - const previous = { - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + const previous = { + configPath: process.env.CLAWDBOT_CONFIG_PATH, + token: process.env.CLAWDBOT_GATEWAY_TOKEN, + skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, + skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, + skipCron: process.env.CLAWDBOT_SKIP_CRON, + skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, anthropicApiKey: process.env.ANTHROPIC_API_KEY, anthropicApiKeyOld: process.env.ANTHROPIC_API_KEY_OLD, }; - process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.CLAWDBOT_SKIP_CHANNELS = "1"; + process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; + process.env.CLAWDBOT_SKIP_CRON = "1"; + process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; delete process.env.ANTHROPIC_API_KEY; delete process.env.ANTHROPIC_API_KEY_OLD; @@ -444,9 +444,9 @@ describeLive("gateway live (cli backend)", () => { if (previous.token === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN; else process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token; - if (previous.skipProviders === undefined) - delete process.env.CLAWDBOT_SKIP_PROVIDERS; - else process.env.CLAWDBOT_SKIP_PROVIDERS = previous.skipProviders; + if (previous.skipChannels === undefined) + delete process.env.CLAWDBOT_SKIP_CHANNELS; + else process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels; if (previous.skipGmail === undefined) delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; else process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail; diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index 8d0a55ae0..2ddb4a3fb 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -352,7 +352,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { const previous = { configPath: process.env.CLAWDBOT_CONFIG_PATH, token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS, + skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, skipCron: process.env.CLAWDBOT_SKIP_CRON, skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, @@ -363,7 +363,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { let tempAgentDir: string | undefined; let tempStateDir: string | undefined; - process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; + process.env.CLAWDBOT_SKIP_CHANNELS = "1"; process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; process.env.CLAWDBOT_SKIP_CRON = "1"; process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; @@ -776,7 +776,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { process.env.CLAWDBOT_CONFIG_PATH = previous.configPath; process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token; - process.env.CLAWDBOT_SKIP_PROVIDERS = previous.skipProviders; + process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels; process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail; process.env.CLAWDBOT_SKIP_CRON = previous.skipCron; process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas; @@ -895,13 +895,13 @@ describeLive("gateway live (dev agent, profile keys)", () => { const previous = { configPath: process.env.CLAWDBOT_CONFIG_PATH, token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS, + skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, skipCron: process.env.CLAWDBOT_SKIP_CRON, skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, }; - process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; + process.env.CLAWDBOT_SKIP_CHANNELS = "1"; process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; process.env.CLAWDBOT_SKIP_CRON = "1"; process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; @@ -1035,7 +1035,7 @@ describeLive("gateway live (dev agent, profile keys)", () => { process.env.CLAWDBOT_CONFIG_PATH = previous.configPath; process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token; - process.env.CLAWDBOT_SKIP_PROVIDERS = previous.skipProviders; + process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels; process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail; process.env.CLAWDBOT_SKIP_CRON = previous.skipCron; process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas; diff --git a/src/gateway/gateway.tool-calling.mock-openai.test.ts b/src/gateway/gateway.tool-calling.mock-openai.test.ts index f8304a5cf..36661bbc3 100644 --- a/src/gateway/gateway.tool-calling.mock-openai.test.ts +++ b/src/gateway/gateway.tool-calling.mock-openai.test.ts @@ -271,15 +271,15 @@ async function connectClient(params: { url: string; token: string }) { describe("gateway (mock openai): tool calling", () => { it("runs a Read tool call end-to-end via gateway agent loop", async () => { - const prev = { - home: process.env.HOME, - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, - }; + const prev = { + home: process.env.HOME, + configPath: process.env.CLAWDBOT_CONFIG_PATH, + token: process.env.CLAWDBOT_GATEWAY_TOKEN, + skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, + skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, + skipCron: process.env.CLAWDBOT_SKIP_CRON, + skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + }; const originalFetch = globalThis.fetch; const openaiResponsesUrl = "https://api.openai.com/v1/responses"; @@ -321,14 +321,14 @@ describe("gateway (mock openai): tool calling", () => { // TypeScript: Bun's fetch typing includes extra properties; keep this test portable. (globalThis as unknown as { fetch: unknown }).fetch = fetchImpl; - const tempHome = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-gw-mock-home-"), - ); - process.env.HOME = tempHome; - process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + const tempHome = await fs.mkdtemp( + path.join(os.tmpdir(), "clawdbot-gw-mock-home-"), + ); + process.env.HOME = tempHome; + process.env.CLAWDBOT_SKIP_CHANNELS = "1"; + process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; + process.env.CLAWDBOT_SKIP_CRON = "1"; + process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; const token = `test-${randomUUID()}`; process.env.CLAWDBOT_GATEWAY_TOKEN = token; @@ -424,13 +424,13 @@ describe("gateway (mock openai): tool calling", () => { await server.close({ reason: "mock openai test complete" }); await fs.rm(tempHome, { recursive: true, force: true }); (globalThis as unknown as { fetch: unknown }).fetch = originalFetch; - process.env.HOME = prev.home; - process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; - process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token; - process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; - process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; - } + process.env.HOME = prev.home; + process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; + process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token; + process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels; + process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; + process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; + process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; + } }, 30_000); }); diff --git a/src/gateway/gateway.wizard.e2e.test.ts b/src/gateway/gateway.wizard.e2e.test.ts index 6520fd856..03a337bba 100644 --- a/src/gateway/gateway.wizard.e2e.test.ts +++ b/src/gateway/gateway.wizard.e2e.test.ts @@ -172,21 +172,21 @@ type WizardNextPayload = { describe("gateway wizard (e2e)", () => { it("runs wizard over ws and writes auth token config", async () => { - const prev = { - home: process.env.HOME, - stateDir: process.env.CLAWDBOT_STATE_DIR, - configPath: process.env.CLAWDBOT_CONFIG_PATH, - token: process.env.CLAWDBOT_GATEWAY_TOKEN, - skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS, - skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, - skipCron: process.env.CLAWDBOT_SKIP_CRON, - skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, - }; + const prev = { + home: process.env.HOME, + stateDir: process.env.CLAWDBOT_STATE_DIR, + configPath: process.env.CLAWDBOT_CONFIG_PATH, + token: process.env.CLAWDBOT_GATEWAY_TOKEN, + skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS, + skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, + skipCron: process.env.CLAWDBOT_SKIP_CRON, + skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, + }; - process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; - process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; - process.env.CLAWDBOT_SKIP_CRON = "1"; - process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; + process.env.CLAWDBOT_SKIP_CHANNELS = "1"; + process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; + process.env.CLAWDBOT_SKIP_CRON = "1"; + process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; delete process.env.CLAWDBOT_GATEWAY_TOKEN; const tempHome = await fs.mkdtemp( @@ -282,7 +282,7 @@ describe("gateway wizard (e2e)", () => { process.env.CLAWDBOT_STATE_DIR = prev.stateDir; process.env.CLAWDBOT_CONFIG_PATH = prev.configPath; process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token; - process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders; + process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels; process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail; process.env.CLAWDBOT_SKIP_CRON = prev.skipCron; process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas; diff --git a/src/gateway/server.reload.test.ts b/src/gateway/server.reload.test.ts index 61e31d2fe..35e6f7080 100644 --- a/src/gateway/server.reload.test.ts +++ b/src/gateway/server.reload.test.ts @@ -166,21 +166,21 @@ vi.mock("./config-reload.js", () => ({ installGatewayTestHooks(); describe("gateway hot reload", () => { - let prevSkipProviders: string | undefined; + let prevSkipChannels: string | undefined; let prevSkipGmail: string | undefined; beforeEach(() => { - prevSkipProviders = process.env.CLAWDBOT_SKIP_PROVIDERS; + prevSkipChannels = process.env.CLAWDBOT_SKIP_CHANNELS; prevSkipGmail = process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; - process.env.CLAWDBOT_SKIP_PROVIDERS = "0"; + process.env.CLAWDBOT_SKIP_CHANNELS = "0"; delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; }); afterEach(() => { - if (prevSkipProviders === undefined) { - delete process.env.CLAWDBOT_SKIP_PROVIDERS; + if (prevSkipChannels === undefined) { + delete process.env.CLAWDBOT_SKIP_CHANNELS; } else { - process.env.CLAWDBOT_SKIP_PROVIDERS = prevSkipProviders; + process.env.CLAWDBOT_SKIP_CHANNELS = prevSkipChannels; } if (prevSkipGmail === undefined) { delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; diff --git a/src/gateway/test-helpers.ts b/src/gateway/test-helpers.ts index 8bf6bb253..c322e8642 100644 --- a/src/gateway/test-helpers.ts +++ b/src/gateway/test-helpers.ts @@ -344,7 +344,7 @@ vi.mock("../commands/agent.js", () => ({ agentCommand, })); -process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; +process.env.CLAWDBOT_SKIP_CHANNELS = "1"; let previousHome: string | undefined; let tempHome: string | undefined; diff --git a/src/logging.ts b/src/logging.ts index db4602700..235d01a1f 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -430,9 +430,9 @@ const SUBSYSTEM_COLOR_OVERRIDES: Record< > = { "gmail-watcher": "blue", }; -const SUBSYSTEM_PREFIXES_TO_DROP = ["gateway", "providers"] as const; +const SUBSYSTEM_PREFIXES_TO_DROP = ["gateway", "channels", "providers"] as const; const SUBSYSTEM_MAX_SEGMENTS = 2; -const PROVIDER_SUBSYSTEM_PREFIXES = new Set(CHAT_CHANNEL_ORDER); +const CHANNEL_SUBSYSTEM_PREFIXES = new Set(CHAT_CHANNEL_ORDER); function pickSubsystemColor( color: ChalkInstance, @@ -461,7 +461,7 @@ function formatSubsystemForConsole(subsystem: string): string { parts.shift(); } if (parts.length === 0) return original; - if (PROVIDER_SUBSYSTEM_PREFIXES.has(parts[0])) { + if (CHANNEL_SUBSYSTEM_PREFIXES.has(parts[0])) { return parts[0]; } if (parts.length > SUBSYSTEM_MAX_SEGMENTS) { diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 842b4f0ee..782734f0e 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -249,14 +249,14 @@ export async function runOnboardingWizard( `Tailscale exposure: ${formatTailscale( quickstartGateway.tailscaleMode, )}`, - "Direct to chat providers.", + "Direct to chat channels.", ] : [ `Gateway port: ${DEFAULT_GATEWAY_PORT}`, "Gateway bind: Loopback (127.0.0.1)", "Gateway auth: Token (default)", "Tailscale exposure: Off", - "Direct to chat providers.", + "Direct to chat channels.", ]; await prompter.note(quickstartLines.join("\n"), "QuickStart"); } diff --git a/test/gateway.multi.e2e.test.ts b/test/gateway.multi.e2e.test.ts index 39e1995b9..2dd59d3c0 100644 --- a/test/gateway.multi.e2e.test.ts +++ b/test/gateway.multi.e2e.test.ts @@ -141,7 +141,7 @@ const spawnGatewayInstance = async (name: string): Promise => { CLAWDBOT_STATE_DIR: stateDir, CLAWDBOT_GATEWAY_TOKEN: "", CLAWDBOT_GATEWAY_PASSWORD: "", - CLAWDBOT_SKIP_PROVIDERS: "1", + CLAWDBOT_SKIP_CHANNELS: "1", CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1", CLAWDBOT_SKIP_CANVAS_HOST: "1", CLAWDBOT_ENABLE_BRIDGE_IN_TESTS: "1", diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 74640f9f9..7face4b37 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -21,7 +21,7 @@ import type { LogEntry, LogLevel, PresenceEntry, - ProvidersStatusSnapshot, + ChannelsStatusSnapshot, SessionsListResult, SkillStatusReport, StatusSummary, @@ -47,7 +47,7 @@ import { renderOverview } from "./views/overview"; import { renderSessions } from "./views/sessions"; import { renderSkills } from "./views/skills"; import { - loadProviders, + loadChannels, updateDiscordForm, updateIMessageForm, updateSlackForm, @@ -119,10 +119,10 @@ export type AppViewState = { configUiHints: Record; configForm: Record | null; configFormMode: "form" | "raw"; - providersLoading: boolean; - providersSnapshot: ProvidersStatusSnapshot | null; - providersError: string | null; - providersLastSuccess: number | null; + channelsLoading: boolean; + channelsSnapshot: ChannelsStatusSnapshot | null; + channelsError: string | null; + channelsLastSuccess: number | null; whatsappLoginMessage: string | null; whatsappLoginQrDataUrl: string | null; whatsappLoginConnected: boolean | null; @@ -299,7 +299,7 @@ export function renderApp(state: AppViewState) { sessionsCount, cronEnabled: state.cronStatus?.enabled ?? null, cronNext, - lastProvidersRefresh: state.providersLastSuccess, + lastChannelsRefresh: state.channelsLastSuccess, onSettingsChange: (next) => state.applySettings(next), onPasswordChange: (next) => (state.password = next), onSessionKeyChange: (next) => { @@ -320,10 +320,10 @@ export function renderApp(state: AppViewState) { ${state.tab === "connections" ? renderConnections({ connected: state.connected, - loading: state.providersLoading, - snapshot: state.providersSnapshot, - lastError: state.providersError, - lastSuccessAt: state.providersLastSuccess, + loading: state.channelsLoading, + snapshot: state.channelsSnapshot, + lastError: state.channelsError, + lastSuccessAt: state.channelsLastSuccess, whatsappMessage: state.whatsappLoginMessage, whatsappQrDataUrl: state.whatsappLoginQrDataUrl, whatsappConnected: state.whatsappLoginConnected, @@ -347,7 +347,7 @@ export function renderApp(state: AppViewState) { imessageForm: state.imessageForm, imessageSaving: state.imessageSaving, imessageStatus: state.imessageConfigStatus, - onRefresh: (probe) => loadProviders(state, probe), + onRefresh: (probe) => loadChannels(state, probe), onWhatsAppStart: (force) => state.handleWhatsAppStart(force), onWhatsAppWait: () => state.handleWhatsAppWait(), onWhatsAppLogout: () => state.handleWhatsAppLogout(), diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index d01715d85..02b7ed6a5 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -33,7 +33,7 @@ import type { LogEntry, LogLevel, PresenceEntry, - ProvidersStatusSnapshot, + ChannelsStatusSnapshot, SessionsListResult, SkillStatusReport, StatusSummary, @@ -63,7 +63,7 @@ import { updateConfigFormValue, } from "./controllers/config"; import { - loadProviders, + loadChannels, logoutWhatsApp, saveDiscordConfig, saveIMessageConfig, @@ -188,7 +188,7 @@ const DEFAULT_CRON_FORM: CronFormState = { payloadKind: "systemEvent", payloadText: "", deliver: false, - provider: "last", + channel: "last", to: "", timeoutSeconds: "", postToMainPrefix: "", @@ -247,10 +247,10 @@ export class ClawdbotApp extends LitElement { @state() configFormDirty = false; @state() configFormMode: "form" | "raw" = "form"; - @state() providersLoading = false; - @state() providersSnapshot: ProvidersStatusSnapshot | null = null; - @state() providersError: string | null = null; - @state() providersLastSuccess: number | null = null; + @state() channelsLoading = false; + @state() channelsSnapshot: ChannelsStatusSnapshot | null = null; + @state() channelsError: string | null = null; + @state() channelsLastSuccess: number | null = null; @state() whatsappLoginMessage: string | null = null; @state() whatsappLoginQrDataUrl: string | null = null; @state() whatsappLoginConnected: boolean | null = null; @@ -1026,7 +1026,7 @@ export class ClawdbotApp extends LitElement { async loadOverview() { await Promise.all([ - loadProviders(this, false), + loadChannels(this, false), loadPresence(this), loadSessions(this), loadCronStatus(this), @@ -1035,7 +1035,7 @@ export class ClawdbotApp extends LitElement { } private async loadConnections() { - await Promise.all([loadProviders(this, true), loadConfig(this)]); + await Promise.all([loadChannels(this, true), loadConfig(this)]); } async loadCron() { @@ -1147,47 +1147,47 @@ export class ClawdbotApp extends LitElement { async handleWhatsAppStart(force: boolean) { await startWhatsAppLogin(this, force); - await loadProviders(this, true); + await loadChannels(this, true); } async handleWhatsAppWait() { await waitWhatsAppLogin(this); - await loadProviders(this, true); + await loadChannels(this, true); } async handleWhatsAppLogout() { await logoutWhatsApp(this); - await loadProviders(this, true); + await loadChannels(this, true); } async handleTelegramSave() { await saveTelegramConfig(this); await loadConfig(this); - await loadProviders(this, true); + await loadChannels(this, true); } async handleDiscordSave() { await saveDiscordConfig(this); await loadConfig(this); - await loadProviders(this, true); + await loadChannels(this, true); } async handleSlackSave() { await saveSlackConfig(this); await loadConfig(this); - await loadProviders(this, true); + await loadChannels(this, true); } async handleSignalSave() { await saveSignalConfig(this); await loadConfig(this); - await loadProviders(this, true); + await loadChannels(this, true); } async handleIMessageSave() { await saveIMessageConfig(this); await loadConfig(this); - await loadProviders(this, true); + await loadChannels(this, true); } // Sidebar handlers for tool output viewing diff --git a/ui/src/ui/controllers/connections.ts b/ui/src/ui/controllers/connections.ts index 01f3ace38..2bff0b017 100644 --- a/ui/src/ui/controllers/connections.ts +++ b/ui/src/ui/controllers/connections.ts @@ -1,6 +1,6 @@ import type { GatewayBrowserClient } from "../gateway"; import { parseList } from "../format"; -import type { ConfigSnapshot, ProvidersStatusSnapshot } from "../types"; +import type { ChannelsStatusSnapshot, ConfigSnapshot } from "../types"; import { defaultDiscordActions, defaultSlackActions, @@ -18,10 +18,10 @@ import { export type ConnectionsState = { client: GatewayBrowserClient | null; connected: boolean; - providersLoading: boolean; - providersSnapshot: ProvidersStatusSnapshot | null; - providersError: string | null; - providersLastSuccess: number | null; + channelsLoading: boolean; + channelsSnapshot: ChannelsStatusSnapshot | null; + channelsError: string | null; + channelsLastSuccess: number | null; whatsappLoginMessage: string | null; whatsappLoginQrDataUrl: string | null; whatsappLoginConnected: boolean | null; @@ -48,22 +48,22 @@ export type ConnectionsState = { configSnapshot: ConfigSnapshot | null; }; -export async function loadProviders(state: ConnectionsState, probe: boolean) { +export async function loadChannels(state: ConnectionsState, probe: boolean) { if (!state.client || !state.connected) return; - if (state.providersLoading) return; - state.providersLoading = true; - state.providersError = null; + if (state.channelsLoading) return; + state.channelsLoading = true; + state.channelsError = null; try { - const res = (await state.client.request("providers.status", { + const res = (await state.client.request("channels.status", { probe, timeoutMs: 8000, - })) as ProvidersStatusSnapshot; - state.providersSnapshot = res; - state.providersLastSuccess = Date.now(); - const providers = res.providers as Record; - const telegram = providers.telegram as { tokenSource?: string | null }; - const discord = providers.discord as { tokenSource?: string | null } | null; - const slack = providers.slack as + })) as ChannelsStatusSnapshot; + state.channelsSnapshot = res; + state.channelsLastSuccess = Date.now(); + const channels = res.channels as Record; + const telegram = channels.telegram as { tokenSource?: string | null }; + const discord = channels.discord as { tokenSource?: string | null } | null; + const slack = channels.slack as | { botTokenSource?: string | null; appTokenSource?: string | null } | null; state.telegramTokenLocked = telegram?.tokenSource === "env"; @@ -71,9 +71,9 @@ export async function loadProviders(state: ConnectionsState, probe: boolean) { state.slackTokenLocked = slack?.botTokenSource === "env"; state.slackAppTokenLocked = slack?.appTokenSource === "env"; } catch (err) { - state.providersError = String(err); + state.channelsError = String(err); } finally { - state.providersLoading = false; + state.channelsLoading = false; } } @@ -119,7 +119,7 @@ export async function logoutWhatsApp(state: ConnectionsState) { if (!state.client || !state.connected || state.whatsappBusy) return; state.whatsappBusy = true; try { - await state.client.request("providers.logout", { provider: "whatsapp" }); + await state.client.request("channels.logout", { channel: "whatsapp" }); state.whatsappLoginMessage = "Logged out."; state.whatsappLoginQrDataUrl = null; state.whatsappLoginConnected = null; diff --git a/ui/src/ui/controllers/cron.ts b/ui/src/ui/controllers/cron.ts index d1c889e2b..dd7a142f0 100644 --- a/ui/src/ui/controllers/cron.ts +++ b/ui/src/ui/controllers/cron.ts @@ -73,7 +73,7 @@ export function buildCronPayload(form: CronFormState) { kind: "agentTurn"; message: string; deliver?: boolean; - provider?: + channel?: | "last" | "whatsapp" | "telegram" @@ -85,7 +85,7 @@ export function buildCronPayload(form: CronFormState) { timeoutSeconds?: number; } = { kind: "agentTurn", message }; if (form.deliver) payload.deliver = true; - if (form.provider) payload.provider = form.provider; + if (form.channel) payload.channel = form.channel; if (form.to.trim()) payload.to = form.to.trim(); const timeoutSeconds = toNumber(form.timeoutSeconds, 0); if (timeoutSeconds > 0) payload.timeoutSeconds = timeoutSeconds; diff --git a/ui/src/ui/navigation.ts b/ui/src/ui/navigation.ts index 0c1ee9e1e..2cf714210 100644 --- a/ui/src/ui/navigation.ts +++ b/ui/src/ui/navigation.ts @@ -161,7 +161,7 @@ export function subtitleForTab(tab: Tab) { case "overview": return "Gateway status, entry points, and a fast health read."; case "connections": - return "Link providers and keep transport settings in sync."; + return "Link channels and keep transport settings in sync."; case "instances": return "Presence beacons from connected clients and nodes."; case "sessions": diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 7a012a8c1..02e0431a7 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -1,13 +1,13 @@ -export type ProvidersStatusSnapshot = { +export type ChannelsStatusSnapshot = { ts: number; - providerOrder: string[]; - providerLabels: Record; - providers: Record; - providerAccounts: Record; - providerDefaultAccountId: Record; + channelOrder: string[]; + channelLabels: Record; + channels: Record; + channelAccounts: Record; + channelDefaultAccountId: Record; }; -export type ProviderAccountSnapshot = { +export type ChannelAccountSnapshot = { accountId: string; name?: string | null; enabled?: boolean | null; diff --git a/ui/src/ui/ui-types.ts b/ui/src/ui/ui-types.ts index 7140f47a3..51c9b7cdf 100644 --- a/ui/src/ui/ui-types.ts +++ b/ui/src/ui/ui-types.ts @@ -170,7 +170,7 @@ export type CronFormState = { payloadKind: "systemEvent" | "agentTurn"; payloadText: string; deliver: boolean; - provider: + channel: | "last" | "whatsapp" | "telegram" diff --git a/ui/src/ui/views/connections.ts b/ui/src/ui/views/connections.ts index 54579f183..dd276caec 100644 --- a/ui/src/ui/views/connections.ts +++ b/ui/src/ui/views/connections.ts @@ -2,10 +2,10 @@ import { html, nothing } from "lit"; import { formatAgo } from "../format"; import type { + ChannelAccountSnapshot, + ChannelsStatusSnapshot, DiscordStatus, IMessageStatus, - ProviderAccountSnapshot, - ProvidersStatusSnapshot, SignalStatus, SlackStatus, TelegramStatus, @@ -50,7 +50,7 @@ const slackActionOptions = [ export type ConnectionsProps = { connected: boolean; loading: boolean; - snapshot: ProvidersStatusSnapshot | null; + snapshot: ChannelsStatusSnapshot | null; lastError: string | null; lastSuccessAt: number | null; whatsappMessage: string | null; @@ -93,18 +93,18 @@ export type ConnectionsProps = { }; export function renderConnections(props: ConnectionsProps) { - const providers = props.snapshot?.providers as Record | null; - const whatsapp = (providers?.whatsapp ?? undefined) as + const channels = props.snapshot?.channels as Record | null; + const whatsapp = (channels?.whatsapp ?? undefined) as | WhatsAppStatus | undefined; - const telegram = (providers?.telegram ?? undefined) as + const telegram = (channels?.telegram ?? undefined) as | TelegramStatus | undefined; - const discord = (providers?.discord ?? null) as DiscordStatus | null; - const slack = (providers?.slack ?? null) as SlackStatus | null; - const signal = (providers?.signal ?? null) as SignalStatus | null; - const imessage = (providers?.imessage ?? null) as IMessageStatus | null; - const providerOrder: ProviderKey[] = [ + const discord = (channels?.discord ?? null) as DiscordStatus | null; + const slack = (channels?.slack ?? null) as SlackStatus | null; + const signal = (channels?.signal ?? null) as SignalStatus | null; + const imessage = (channels?.imessage ?? null) as IMessageStatus | null; + const channelOrder: ChannelKey[] = [ "whatsapp", "telegram", "discord", @@ -112,10 +112,10 @@ export function renderConnections(props: ConnectionsProps) { "signal", "imessage", ]; - const orderedProviders = providerOrder + const orderedChannels = channelOrder .map((key, index) => ({ key, - enabled: providerEnabled(key, props), + enabled: channelEnabled(key, props), order: index, })) .sort((a, b) => { @@ -125,15 +125,15 @@ export function renderConnections(props: ConnectionsProps) { return html`
- ${orderedProviders.map((provider) => - renderProvider(provider.key, props, { + ${orderedChannels.map((channel) => + renderChannel(channel.key, props, { whatsapp, telegram, discord, slack, signal, imessage, - providerAccounts: props.snapshot?.providerAccounts ?? null, + channelAccounts: props.snapshot?.channelAccounts ?? null, }), )}
@@ -142,7 +142,7 @@ export function renderConnections(props: ConnectionsProps) {
Connection health
-
Provider status snapshots from the gateway.
+
Channel status snapshots from the gateway.
${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}
@@ -168,7 +168,7 @@ function formatDuration(ms?: number | null) { return `${hr}h`; } -type ProviderKey = +type ChannelKey = | "whatsapp" | "telegram" | "discord" @@ -176,16 +176,16 @@ type ProviderKey = | "signal" | "imessage"; -function providerEnabled(key: ProviderKey, props: ConnectionsProps) { +function channelEnabled(key: ChannelKey, props: ConnectionsProps) { const snapshot = props.snapshot; - const providers = snapshot?.providers as Record | null; - if (!snapshot || !providers) return false; - const whatsapp = providers.whatsapp as WhatsAppStatus | undefined; - const telegram = providers.telegram as TelegramStatus | undefined; - const discord = (providers.discord ?? null) as DiscordStatus | null; - const slack = (providers.slack ?? null) as SlackStatus | null; - const signal = (providers.signal ?? null) as SignalStatus | null; - const imessage = (providers.imessage ?? null) as IMessageStatus | null; + const channels = snapshot?.channels as Record | null; + if (!snapshot || !channels) return false; + const whatsapp = channels.whatsapp as WhatsAppStatus | undefined; + const telegram = channels.telegram as TelegramStatus | undefined; + const discord = (channels.discord ?? null) as DiscordStatus | null; + const slack = (channels.slack ?? null) as SlackStatus | null; + const signal = (channels.signal ?? null) as SignalStatus | null; + const imessage = (channels.imessage ?? null) as IMessageStatus | null; switch (key) { case "whatsapp": return ( @@ -208,24 +208,24 @@ function providerEnabled(key: ProviderKey, props: ConnectionsProps) { } } -function getProviderAccountCount( - key: ProviderKey, - providerAccounts?: Record | null, +function getChannelAccountCount( + key: ChannelKey, + channelAccounts?: Record | null, ): number { - return providerAccounts?.[key]?.length ?? 0; + return channelAccounts?.[key]?.length ?? 0; } -function renderProviderAccountCount( - key: ProviderKey, - providerAccounts?: Record | null, +function renderChannelAccountCount( + key: ChannelKey, + channelAccounts?: Record | null, ) { - const count = getProviderAccountCount(key, providerAccounts); + const count = getChannelAccountCount(key, channelAccounts); if (count < 2) return nothing; return html``; } -function renderProvider( - key: ProviderKey, +function renderChannel( + key: ChannelKey, props: ConnectionsProps, data: { whatsapp?: WhatsAppStatus; @@ -234,12 +234,12 @@ function renderProvider( slack?: SlackStatus | null; signal?: SignalStatus | null; imessage?: IMessageStatus | null; - providerAccounts?: Record | null; + channelAccounts?: Record | null; }, ) { - const accountCountLabel = renderProviderAccountCount( + const accountCountLabel = renderChannelAccountCount( key, - data.providerAccounts, + data.channelAccounts, ); switch (key) { case "whatsapp": { @@ -345,10 +345,10 @@ function renderProvider( } case "telegram": { const telegram = data.telegram; - const telegramAccounts = data.providerAccounts?.telegram ?? []; + const telegramAccounts = data.channelAccounts?.telegram ?? []; const hasMultipleAccounts = telegramAccounts.length > 1; - const renderAccountCard = (account: ProviderAccountSnapshot) => { + const renderAccountCard = (account: ChannelAccountSnapshot) => { const probe = account.probe as { bot?: { username?: string } } | undefined; const botUsername = probe?.bot?.username; const label = account.name || account.accountId; diff --git a/ui/src/ui/views/cron.ts b/ui/src/ui/views/cron.ts index 881271882..3bd5a827c 100644 --- a/ui/src/ui/views/cron.ts +++ b/ui/src/ui/views/cron.ts @@ -168,9 +168,9 @@ export function renderCron(props: CronProps) { rows="4" > - ${props.form.payloadKind === "agentTurn" - ? html` -
+ ${props.form.payloadKind === "agentTurn" + ? html` +
-
-
Last Providers Refresh
+
Last Channels Refresh
- ${props.lastProvidersRefresh - ? formatAgo(props.lastProvidersRefresh) + ${props.lastChannelsRefresh + ? formatAgo(props.lastChannelsRefresh) : "n/a"}
diff --git a/vitest.config.ts b/vitest.config.ts index 87f83935f..d06c620c6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -47,7 +47,7 @@ export default defineConfig({ // Gateway server integration surfaces are intentionally validated via manual/e2e runs. "src/gateway/control-ui.ts", "src/gateway/server-bridge.ts", - "src/gateway/server-providers.ts", + "src/gateway/server-channels.ts", "src/gateway/server-methods/config.ts", "src/gateway/server-methods/send.ts", "src/gateway/server-methods/skills.ts", @@ -62,13 +62,13 @@ export default defineConfig({ // Interactive UIs/flows are intentionally validated via manual/e2e runs. "src/tui/**", "src/wizard/**", - // Provider surfaces are largely integration-tested (or manually validated). + // Channel surfaces are largely integration-tested (or manually validated). "src/discord/**", "src/imessage/**", "src/signal/**", "src/slack/**", "src/browser/**", - "src/providers/web/**", + "src/channels/web/**", "src/telegram/index.ts", "src/telegram/proxy.ts", "src/telegram/webhook-set.ts",