chore: format swift/ts and fix gateway lint

This commit is contained in:
Peter Steinberger
2025-12-09 17:11:25 +00:00
parent b6bd39660f
commit 8d888b426f
18 changed files with 205 additions and 121 deletions

View File

@@ -26,11 +26,14 @@ final class AppState: ObservableObject {
}
@Published var onboardingSeen: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") } }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") }
}
}
@Published var debugPaneEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } }
didSet {
self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") }
}
}
@Published var swabbleEnabled: Bool {
@@ -63,7 +66,9 @@ final class AppState: ObservableObject {
}
@Published var iconAnimationsEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } }
didSet { self.ifNotPreview { UserDefaults.standard.set(
self.iconAnimationsEnabled,
forKey: iconAnimationsEnabledKey) } }
}
@Published var showDockIcon: Bool {
@@ -98,19 +103,27 @@ final class AppState: ObservableObject {
}
@Published var voiceWakeAdditionalLocaleIDs: [String] {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) } }
didSet { self.ifNotPreview { UserDefaults.standard.set(
self.voiceWakeAdditionalLocaleIDs,
forKey: voiceWakeAdditionalLocalesKey) } }
}
@Published var voiceWakeForwardEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) } }
didSet { self.ifNotPreview { UserDefaults.standard.set(
self.voiceWakeForwardEnabled,
forKey: voiceWakeForwardEnabledKey) } }
}
@Published var voiceWakeForwardCommand: String {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) } }
didSet { self.ifNotPreview { UserDefaults.standard.set(
self.voiceWakeForwardCommand,
forKey: voiceWakeForwardCommandKey) } }
}
@Published var voicePushToTalkEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } }
didSet { self.ifNotPreview { UserDefaults.standard.set(
self.voicePushToTalkEnabled,
forKey: voicePushToTalkEnabledKey) } }
}
@Published var iconOverride: IconOverrideSelection {
@@ -131,7 +144,9 @@ final class AppState: ObservableObject {
}
@Published var connectionMode: ConnectionMode {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) } }
didSet {
self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) }
}
}
@Published var webChatEnabled: Bool {

View File

@@ -1,7 +1,7 @@
import ClawdisProtocol
import Foundation
import OSLog
import SwiftUI
import ClawdisProtocol
struct ControlHeartbeatEvent: Codable {
let ts: Double
@@ -14,7 +14,7 @@ struct ControlHeartbeatEvent: Codable {
}
struct ControlAgentEvent: Codable, Sendable, Identifiable {
var id: String { "\(runId)-\(seq)" }
var id: String { "\(self.runId)-\(self.seq)" }
let runId: String
let seq: Int
let stream: String
@@ -173,7 +173,8 @@ final class ControlChannel: ObservableObject {
if let data = evt.payload?.value,
JSONSerialization.isValidJSONObject(data),
let blob = try? JSONSerialization.data(withJSONObject: data),
let agent = try? JSONDecoder().decode(AgentEvent.self, from: blob) {
let agent = try? JSONDecoder().decode(AgentEvent.self, from: blob)
{
Task { @MainActor in
AgentEventStore.shared.append(ControlAgentEvent(
runId: agent.runid,

View File

@@ -108,7 +108,8 @@ enum DebugActionError: LocalizedError {
var errorDescription: String? {
switch self {
case let .message(text): text
case let .message(text):
text
}
}
}

View File

@@ -1,6 +1,6 @@
import ClawdisProtocol
import Foundation
import OSLog
import ClawdisProtocol
struct GatewayEvent: Codable {
let type: String
@@ -27,7 +27,7 @@ private actor GatewayChannelActor {
private var shouldReconnect = true
private var lastSeq: Int?
private var lastTick: Date?
private var tickIntervalMs: Double = 30_000
private var tickIntervalMs: Double = 30000
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
@@ -88,7 +88,8 @@ private actor GatewayChannelActor {
let type = obj["type"] as? String else { return false }
if type == "hello-ok" {
if let policy = obj["policy"] as? [String: Any],
let tick = policy["tickIntervalMs"] as? Double {
let tick = policy["tickIntervalMs"] as? Double
{
self.tickIntervalMs = tick
}
self.lastTick = Date()

View File

@@ -296,7 +296,9 @@ struct GeneralSettings: View {
.disabled(self.relayInstalling)
}
Text(self.relayInstallMessage ?? "Installs the global \"clawdis\" package and expects the gateway on port 18789.")
Text(self
.relayInstallMessage ??
"Installs the global \"clawdis\" package and expects the gateway on port 18789.")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)

View File

@@ -151,15 +151,15 @@ final class HealthStore: ObservableObject {
/// Short, human-friendly detail for the last failure, used in the UI.
var detailLine: String? {
if let error = self.lastError, !error.isEmpty {
let lower = error.lowercased()
if lower.contains("connection refused") {
return "The gateway control port (127.0.0.1:18789) isnt listening — restart Clawdis to bring it back."
}
if lower.contains("timeout") {
return "Timed out waiting for the control server; the relay may be crashed or still starting."
}
return error
if let error = self.lastError, !error.isEmpty {
let lower = error.lowercased()
if lower.contains("connection refused") {
return "The gateway control port (127.0.0.1:18789) isnt listening — restart Clawdis to bring it back."
}
if lower.contains("timeout") {
return "Timed out waiting for the control server; the relay may be crashed or still starting."
}
return error
}
return nil
}

View File

@@ -1,7 +1,7 @@
import ClawdisProtocol
import Cocoa
import Foundation
import OSLog
import ClawdisProtocol
struct InstanceInfo: Identifiable, Codable {
let id: String
@@ -79,7 +79,8 @@ final class InstancesStore: ObservableObject {
case let .event(evt) where evt.event == "presence":
if let payload = evt.payload?.value as? [String: Any],
let presence = payload["presence"],
let presenceData = try? JSONSerialization.data(withJSONObject: presence) {
let presenceData = try? JSONSerialization.data(withJSONObject: presence)
{
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(presenceData) }
}
default:
@@ -104,7 +105,8 @@ final class InstancesStore: ObservableObject {
switch frame {
case let .helloOk(hello):
if JSONSerialization.isValidJSONObject(hello.snapshot.presence),
let data = try? JSONEncoder().encode(hello.snapshot.presence) {
let data = try? JSONEncoder().encode(hello.snapshot.presence)
{
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(data) }
}
default:
@@ -323,7 +325,7 @@ extension InstancesStore {
mode: "remote",
reason: "preview",
text: "Relay node · tunnel ok",
ts: Date().timeIntervalSince1970 * 1000 - 45_000),
ts: Date().timeIntervalSince1970 * 1000 - 45000),
]) -> InstancesStore {
let store = InstancesStore(isPreview: true)
store.instances = instances

View File

@@ -181,7 +181,8 @@ struct OnboardingView: View {
self.onboardingPage {
Text("Install the relay")
.font(.largeTitle.weight(.semibold))
Text("Clawdis now runs the WebSocket gateway from the global \"clawdis\" package. Install/update it here and well check Node for you.")
Text(
"Clawdis now runs the WebSocket gateway from the global \"clawdis\" package. Install/update it here and well check Node for you.")
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)

View File

@@ -132,9 +132,8 @@ struct PermissionsSettings_Previews: PreviewProvider {
.speechRecognition: false,
],
refresh: {},
showOnboarding: {}
)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
showOnboarding: {})
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -1,5 +1,5 @@
import Foundation
import ClawdisIPC
import Foundation
// Lightweight SemVer helper (major.minor.patch only) for relay compatibility checks.
struct Semver: Comparable, CustomStringConvertible, Sendable {

View File

@@ -126,7 +126,7 @@ extension SessionRow {
id: "global",
key: "global",
kind: .global,
updatedAt: Date().addingTimeInterval(-86_400),
updatedAt: Date().addingTimeInterval(-86400),
sessionId: nil,
thinkingLevel: nil,
verboseLevel: nil,

View File

@@ -101,7 +101,14 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate {
private func loadWebChat(baseEndpoint: URL) {
var comps = URLComponents(url: baseEndpoint.appendingPathComponent("webchat/"), resolvingAgainstBaseURL: false)
comps?.queryItems = [URLQueryItem(name: "session", value: self.sessionKey)]
var items = [URLQueryItem(name: "session", value: self.sessionKey)]
if let hostName = Host.current().localizedName ?? Host.current().name {
items.append(URLQueryItem(name: "host", value: hostName))
}
if let ip = Self.primaryIPv4Address() {
items.append(URLQueryItem(name: "ip", value: ip))
}
comps?.queryItems = items
guard let url = comps?.url else {
self.showError("invalid webchat url")
return
@@ -197,6 +204,37 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate {
}
}
extension WebChatWindowController {
/// Returns the first non-loopback IPv4 address, skipping link-local (169.254.x.x).
fileprivate static func primaryIPv4Address() -> String? {
var ifaddr: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddr) == 0, let first = ifaddr else { return nil }
defer { freeifaddrs(ifaddr) }
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let addrFamily = ptr.pointee.ifa_addr.pointee.sa_family
if (flags & IFF_UP) == 0 || (flags & IFF_LOOPBACK) != 0 { continue }
if addrFamily == UInt8(AF_INET) {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
if getnameinfo(
ptr.pointee.ifa_addr,
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
&hostname,
socklen_t(hostname.count),
nil,
0,
NI_NUMERICHOST) == 0
{
let ip = String(cString: hostname)
if !ip.hasPrefix("169.254") { return ip }
}
}
}
return nil
}
}
// MARK: - Manager
@MainActor

View File

@@ -28,8 +28,8 @@ public struct Hello: Codable {
caps: [String]?,
auth: [String: AnyCodable]?,
locale: String?,
useragent: String?
) {
useragent: String?)
{
self.type = type
self.minprotocol = minprotocol
self.maxprotocol = maxprotocol
@@ -39,6 +39,7 @@ public struct Hello: Codable {
self.locale = locale
self.useragent = useragent
}
private enum CodingKeys: String, CodingKey {
case type
case minprotocol = "minProtocol"
@@ -65,8 +66,8 @@ public struct HelloOk: Codable {
server: [String: AnyCodable],
features: [String: AnyCodable],
snapshot: Snapshot,
policy: [String: AnyCodable]
) {
policy: [String: AnyCodable])
{
self.type = type
self._protocol = _protocol
self.server = server
@@ -74,6 +75,7 @@ public struct HelloOk: Codable {
self.snapshot = snapshot
self.policy = policy
}
private enum CodingKeys: String, CodingKey {
case type
case _protocol = "protocol"
@@ -94,13 +96,14 @@ public struct HelloError: Codable {
type: String,
reason: String,
expectedprotocol: Int?,
minclient: String?
) {
minclient: String?)
{
self.type = type
self.reason = reason
self.expectedprotocol = expectedprotocol
self.minclient = minclient
}
private enum CodingKeys: String, CodingKey {
case type
case reason
@@ -119,13 +122,14 @@ public struct RequestFrame: Codable {
type: String,
id: String,
method: String,
params: AnyCodable?
) {
params: AnyCodable?)
{
self.type = type
self.id = id
self.method = method
self.params = params
}
private enum CodingKeys: String, CodingKey {
case type
case id
@@ -146,14 +150,15 @@ public struct ResponseFrame: Codable {
id: String,
ok: Bool,
payload: AnyCodable?,
error: [String: AnyCodable]?
) {
error: [String: AnyCodable]?)
{
self.type = type
self.id = id
self.ok = ok
self.payload = payload
self.error = error
}
private enum CodingKeys: String, CodingKey {
case type
case id
@@ -175,14 +180,15 @@ public struct EventFrame: Codable {
event: String,
payload: AnyCodable?,
seq: Int?,
stateversion: [String: AnyCodable]?
) {
stateversion: [String: AnyCodable]?)
{
self.type = type
self.event = event
self.payload = payload
self.seq = seq
self.stateversion = stateversion
}
private enum CodingKeys: String, CodingKey {
case type
case event
@@ -214,8 +220,8 @@ public struct PresenceEntry: Codable {
tags: [String]?,
text: String?,
ts: Int,
instanceid: String?
) {
instanceid: String?)
{
self.host = host
self.ip = ip
self.version = version
@@ -227,6 +233,7 @@ public struct PresenceEntry: Codable {
self.ts = ts
self.instanceid = instanceid
}
private enum CodingKeys: String, CodingKey {
case host
case ip
@@ -247,11 +254,12 @@ public struct StateVersion: Codable {
public init(
presence: Int,
health: Int
) {
health: Int)
{
self.presence = presence
self.health = health
}
private enum CodingKeys: String, CodingKey {
case presence
case health
@@ -268,13 +276,14 @@ public struct Snapshot: Codable {
presence: [PresenceEntry],
health: AnyCodable,
stateversion: StateVersion,
uptimems: Int
) {
uptimems: Int)
{
self.presence = presence
self.health = health
self.stateversion = stateversion
self.uptimems = uptimems
}
private enum CodingKeys: String, CodingKey {
case presence
case health
@@ -295,14 +304,15 @@ public struct ErrorShape: Codable {
message: String,
details: AnyCodable?,
retryable: Bool?,
retryafterms: Int?
) {
retryafterms: Int?)
{
self.code = code
self.message = message
self.details = details
self.retryable = retryable
self.retryafterms = retryafterms
}
private enum CodingKeys: String, CodingKey {
case code
case message
@@ -324,14 +334,15 @@ public struct AgentEvent: Codable {
seq: Int,
stream: String,
ts: Int,
data: [String: AnyCodable]
) {
data: [String: AnyCodable])
{
self.runid = runid
self.seq = seq
self.stream = stream
self.ts = ts
self.data = data
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case seq
@@ -353,14 +364,15 @@ public struct SendParams: Codable {
message: String,
mediaurl: String?,
provider: String?,
idempotencykey: String
) {
idempotencykey: String)
{
self.to = to
self.message = message
self.mediaurl = mediaurl
self.provider = provider
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case to
case message
@@ -386,8 +398,8 @@ public struct AgentParams: Codable {
thinking: String?,
deliver: Bool?,
timeout: Int?,
idempotencykey: String
) {
idempotencykey: String)
{
self.message = message
self.to = to
self.sessionid = sessionid
@@ -396,6 +408,7 @@ public struct AgentParams: Codable {
self.timeout = timeout
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case message
case to
@@ -411,10 +424,11 @@ public struct TickEvent: Codable {
public let ts: Int
public init(
ts: Int
) {
ts: Int)
{
self.ts = ts
}
private enum CodingKeys: String, CodingKey {
case ts
}
@@ -426,11 +440,12 @@ public struct ShutdownEvent: Codable {
public init(
reason: String,
restartexpectedms: Int?
) {
restartexpectedms: Int?)
{
self.reason = reason
self.restartexpectedms = restartexpectedms
}
private enum CodingKeys: String, CodingKey {
case reason
case restartexpectedms = "restartExpectedMs"
@@ -454,17 +469,17 @@ public enum GatewayFrame: Codable {
}
switch type {
case "hello":
self = .hello(try Self.decodePayload(Hello.self, from: raw))
self = try .hello(Self.decodePayload(Hello.self, from: raw))
case "hello-ok":
self = .helloOk(try Self.decodePayload(HelloOk.self, from: raw))
self = try .helloOk(Self.decodePayload(HelloOk.self, from: raw))
case "hello-error":
self = .helloError(try Self.decodePayload(HelloError.self, from: raw))
self = try .helloError(Self.decodePayload(HelloError.self, from: raw))
case "req":
self = .req(try Self.decodePayload(RequestFrame.self, from: raw))
self = try .req(Self.decodePayload(RequestFrame.self, from: raw))
case "res":
self = .res(try Self.decodePayload(ResponseFrame.self, from: raw))
self = try .res(Self.decodePayload(ResponseFrame.self, from: raw))
case "event":
self = .event(try Self.decodePayload(EventFrame.self, from: raw))
self = try .event(Self.decodePayload(EventFrame.self, from: raw))
default:
self = .unknown(type: type, raw: raw)
}
@@ -472,23 +487,21 @@ public enum GatewayFrame: Codable {
public func encode(to encoder: Encoder) throws {
switch self {
case .hello(let v): try v.encode(to: encoder)
case .helloOk(let v): try v.encode(to: encoder)
case .helloError(let v): try v.encode(to: encoder)
case .req(let v): try v.encode(to: encoder)
case .res(let v): try v.encode(to: encoder)
case .event(let v): try v.encode(to: encoder)
case .unknown(_, let raw):
case let .hello(v): try v.encode(to: encoder)
case let .helloOk(v): try v.encode(to: encoder)
case let .helloError(v): try v.encode(to: encoder)
case let .req(v): try v.encode(to: encoder)
case let .res(v): try v.encode(to: encoder)
case let .event(v): try v.encode(to: encoder)
case let .unknown(_, raw):
var container = encoder.singleValueContainer()
try container.encode(raw)
}
}
private static func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
let data = try JSONSerialization.data(withJSONObject: raw)
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
}

View File

@@ -624,7 +624,7 @@ export async function getReplyFromConfig(
systemLines.push(...queued);
if (isNewSession) {
const summary = await buildProviderSummary(cfg);
if (summary) systemLines.unshift(summary);
if (summary.length > 0) systemLines.unshift(...summary);
}
if (systemLines.length > 0) {
const block = systemLines.map((l) => `System: ${l}`).join("\n");

View File

@@ -1,9 +1,10 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>(
"node:child_process",
);
const actual =
await vi.importActual<typeof import("node:child_process")>(
"node:child_process",
);
return {
...actual,
execFileSync: vi.fn(),
@@ -14,8 +15,8 @@ import { execFileSync } from "node:child_process";
import {
forceFreePort,
listPortListeners,
parseLsofOutput,
type PortProcess,
parseLsofOutput,
} from "./program.js";
describe("gateway --force helpers", () => {

View File

@@ -116,10 +116,7 @@ export function buildProgram() {
'clawdis send --to +15555550123 --message "Hi" --json',
"Send via your web session and print JSON result.",
],
[
"clawdis gateway --port 18789",
"Run the WebSocket Gateway locally.",
],
["clawdis gateway --port 18789", "Run the WebSocket Gateway locally."],
[
"clawdis gateway --force",
"Kill anything bound to the default gateway port, then start it.",

View File

@@ -15,11 +15,11 @@ import {
} from "../infra/system-presence.js";
import { logError } from "../logger.js";
import { getResolvedLoggerSettings } from "../logging.js";
import { defaultRuntime } from "../runtime.js";
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
import { sendMessageWhatsApp } from "../web/outbound.js";
import { defaultRuntime } from "../runtime.js";
import { monitorTelegramProvider } from "../telegram/monitor.js";
import { sendMessageTelegram } from "../telegram/send.js";
import { sendMessageWhatsApp } from "../web/outbound.js";
import {
ErrorCodes,
type ErrorShape,
@@ -450,32 +450,41 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
const message = params.message.trim();
const provider = (params.provider ?? "whatsapp").toLowerCase();
try {
const result =
provider === "telegram"
? await sendMessageTelegram(to, message, {
mediaUrl: params.mediaUrl,
verbose: isVerbose(),
})
: await sendMessageWhatsApp(to, message, {
mediaUrl: params.mediaUrl,
verbose: isVerbose(),
});
const payload =
provider === "telegram"
? {
runId: idem,
messageId: result.messageId,
chatId: result.chatId,
provider,
}
: {
runId: idem,
messageId: result.messageId,
toJid: result.toJid ?? `${to}@s.whatsapp.net`,
provider,
};
dedupe.set(`send:${idem}`, { ts: Date.now(), ok: true, payload });
respond(true, payload, undefined);
if (provider === "telegram") {
const result = await sendMessageTelegram(to, message, {
mediaUrl: params.mediaUrl,
verbose: isVerbose(),
});
const payload = {
runId: idem,
messageId: result.messageId,
chatId: result.chatId,
provider,
};
dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined);
} else {
const result = await sendMessageWhatsApp(to, message, {
mediaUrl: params.mediaUrl,
verbose: isVerbose(),
});
const payload = {
runId: idem,
messageId: result.messageId,
toJid: result.toJid ?? `${to}@s.whatsapp.net`,
provider,
};
dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined);
}
} catch (err) {
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
dedupe.set(`send:${idem}`, { ts: Date.now(), ok: false, error });
@@ -594,7 +603,9 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
void startProviders();
} else {
defaultRuntime.log("gateway: skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)");
defaultRuntime.log(
"gateway: skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)",
);
}
return {

View File

@@ -21,7 +21,9 @@ export async function buildProviderSummary(
const { e164 } = readWebSelfId();
lines.push(
webLinked
? chalk.green(`WhatsApp: linked${e164 ? ` as ${e164}` : ""} (auth ${authAge})`)
? chalk.green(
`WhatsApp: linked${e164 ? ` as ${e164}` : ""} (auth ${authAge})`,
)
: chalk.red("WhatsApp: not linked"),
);