ci: fix swiftformat and bun CI

main
Peter Steinberger 2025-12-18 08:55:47 +01:00
parent 2f21b94a76
commit 5c705ab675
20 changed files with 259 additions and 93 deletions

View File

@ -149,4 +149,3 @@ struct AnthropicAuthControls: View {
} }
} }
} }

View File

@ -26,7 +26,11 @@ final class CanvasManager {
try self.showDetailed(sessionKey: sessionKey, target: path, placement: placement).directory try self.showDetailed(sessionKey: sessionKey, target: path, placement: placement).directory
} }
func showDetailed(sessionKey: String, target: String? = nil, placement: CanvasPlacement? = nil) throws -> CanvasShowResult { func showDetailed(
sessionKey: String,
target: String? = nil,
placement: CanvasPlacement? = nil) throws -> CanvasShowResult
{
Self.logger.debug( Self.logger.debug(
"showDetailed start session=\(sessionKey, privacy: .public) target=\(target ?? "", privacy: .public) placement=\(placement != nil)") "showDetailed start session=\(sessionKey, privacy: .public) target=\(target ?? "", privacy: .public) placement=\(placement != nil)")
let anchorProvider = self.defaultAnchorProvider ?? Self.mouseAnchorProvider let anchorProvider = self.defaultAnchorProvider ?? Self.mouseAnchorProvider
@ -177,7 +181,8 @@ final class CanvasManager {
private static func localStatus(sessionDir: URL, target: String) -> CanvasShowStatus { private static func localStatus(sessionDir: URL, target: String) -> CanvasShowStatus {
let fm = FileManager.default let fm = FileManager.default
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first.map(String.init) ?? trimmed let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first
.map(String.init) ?? trimmed
var path = withoutQuery var path = withoutQuery
if path.hasPrefix("/") { path.removeFirst() } if path.hasPrefix("/") { path.removeFirst() }
path = path.removingPercentEncoding ?? path path = path.removingPercentEncoding ?? path

View File

@ -222,11 +222,10 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|| trimmed.hasPrefix(Self.builtinPrefix + "/") || trimmed.hasPrefix(Self.builtinPrefix + "/")
else { return nil } else { return nil }
let relative: String let relative = if trimmed == Self.builtinPrefix || trimmed == Self.builtinPrefix + "/" {
if trimmed == Self.builtinPrefix || trimmed == Self.builtinPrefix + "/" { "index.html"
relative = "index.html"
} else { } else {
relative = String(trimmed.dropFirst((Self.builtinPrefix + "/").count)) String(trimmed.dropFirst((Self.builtinPrefix + "/").count))
} }
if relative.isEmpty { return self.html("Not Found", title: "Canvas: 404") } if relative.isEmpty { return self.html("Not Found", title: "Canvas: 404") }

View File

@ -160,7 +160,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
// Only auto-reload when we are showing local canvas content. // Only auto-reload when we are showing local canvas content.
guard webView.url?.scheme == CanvasScheme.scheme else { return } guard webView.url?.scheme == CanvasScheme.scheme else { return }
// Avoid reloading the built-in A2UI shell due to filesystem noise (it does not depend on session files). // Avoid reloading the built-in A2UI shell due to filesystem noise (it does not depend on session
// files).
let path = webView.url?.path ?? "" let path = webView.url?.path ?? ""
if path.hasPrefix("/__clawdis__/a2ui") { return } if path.hasPrefix("/__clawdis__/a2ui") { return }
if path == "/" || path.isEmpty { if path == "/" || path.isEmpty {
@ -200,7 +201,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
@MainActor deinit { @MainActor deinit {
self.webView.configuration.userContentController.removeScriptMessageHandler(forName: CanvasA2UIActionMessageHandler.messageName) self.webView.configuration.userContentController
.removeScriptMessageHandler(forName: CanvasA2UIActionMessageHandler.messageName)
self.watcher.stop() self.watcher.stop()
} }
@ -501,7 +503,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
if self.webView.url?.scheme == CanvasScheme.scheme { if self.webView.url?.scheme == CanvasScheme.scheme {
Task { await DeepLinkHandler.shared.handle(url: url) } Task { await DeepLinkHandler.shared.handle(url: url) }
} else { } else {
canvasWindowLogger.debug("ignoring deep link from non-canvas page \(url.absoluteString, privacy: .public)") canvasWindowLogger
.debug("ignoring deep link from non-canvas page \(url.absoluteString, privacy: .public)")
} }
decisionHandler(.cancel) decisionHandler(.cancel)
return return
@ -639,8 +642,10 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
canvasWindowLogger.info("A2UI action \(name, privacy: .public) session=\(self.sessionKey, privacy: .public)") canvasWindowLogger.info("A2UI action \(name, privacy: .public) session=\(self.sessionKey, privacy: .public)")
let surfaceId = (userAction["surfaceId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "main" let surfaceId = (userAction["surfaceId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let sourceComponentId = (userAction["sourceComponentId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "-" .nonEmpty ?? "main"
let sourceComponentId = (userAction["sourceComponentId"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "-"
let host = Self.sanitizeTagValue(InstanceIdentity.displayName) let host = Self.sanitizeTagValue(InstanceIdentity.displayName)
let instanceId = InstanceIdentity.instanceId.lowercased() let instanceId = InstanceIdentity.instanceId.lowercased()
let contextJSON = Self.compactJSON(userAction["context"]) let contextJSON = Self.compactJSON(userAction["context"])

View File

@ -237,8 +237,13 @@ enum ControlRequestHandler {
logger.info("canvas show start session=\(session, privacy: .public) path=\(path ?? "", privacy: .public)") logger.info("canvas show start session=\(session, privacy: .public) path=\(path ?? "", privacy: .public)")
do { do {
logger.info("canvas show awaiting CanvasManager") logger.info("canvas show awaiting CanvasManager")
let res = try await CanvasManager.shared.showDetailed(sessionKey: session, target: path, placement: placement) let res = try await CanvasManager.shared.showDetailed(
logger.info("canvas show done dir=\(res.directory, privacy: .public) status=\(String(describing: res.status), privacy: .public)") sessionKey: session,
target: path,
placement: placement)
logger
.info(
"canvas show done dir=\(res.directory, privacy: .public) status=\(String(describing: res.status), privacy: .public)")
let payload = try? JSONEncoder().encode(res) let payload = try? JSONEncoder().encode(res)
return Response(ok: true, message: res.directory, payload: payload) return Response(ok: true, message: res.directory, payload: payload)
} catch { } catch {
@ -277,17 +282,21 @@ enum ControlRequestHandler {
} }
} }
private static func handleCanvasA2UI(session: String, command: CanvasA2UICommand, jsonl: String?) async -> Response { private static func handleCanvasA2UI(
session: String,
command: CanvasA2UICommand,
jsonl: String?) async -> Response
{
guard self.canvasEnabled() else { return Response(ok: false, message: "Canvas disabled by user") } guard self.canvasEnabled() else { return Response(ok: false, message: "Canvas disabled by user") }
do { do {
// Ensure the Canvas is visible without forcing a navigation/reload. // Ensure the Canvas is visible without forcing a navigation/reload.
_ = try await CanvasManager.shared.show(sessionKey: session, path: nil) _ = try await CanvasManager.shared.show(sessionKey: session, path: nil)
// Wait for the in-page A2UI bridge. If it doesn't appear, force-load the bundled A2UI shell once. // Wait for the in-page A2UI bridge. If it doesn't appear, force-load the bundled A2UI shell once.
var ready = await Self.waitForCanvasA2UI(session: session, requireBuiltinPath: false, timeoutMs: 2_000) var ready = await Self.waitForCanvasA2UI(session: session, requireBuiltinPath: false, timeoutMs: 2000)
if !ready { if !ready {
_ = try await CanvasManager.shared.show(sessionKey: session, path: "/__clawdis__/a2ui/") _ = try await CanvasManager.shared.show(sessionKey: session, path: "/__clawdis__/a2ui/")
ready = await Self.waitForCanvasA2UI(session: session, requireBuiltinPath: true, timeoutMs: 5_000) ready = await Self.waitForCanvasA2UI(session: session, requireBuiltinPath: true, timeoutMs: 5000)
} }
guard ready else { return Response(ok: false, message: "A2UI not ready") } guard ready else { return Response(ok: false, message: "A2UI not ready") }
@ -397,7 +406,8 @@ enum ControlRequestHandler {
let found = dict.keys.sorted().joined(separator: ", ") let found = dict.keys.sorted().joined(separator: ", ")
throw NSError(domain: "A2UI", code: 3, userInfo: [ throw NSError(domain: "A2UI", code: 3, userInfo: [
NSLocalizedDescriptionKey: """ NSLocalizedDescriptionKey: """
A2UI JSONL line \(item.lineNumber): expected exactly one of \(allowed.sorted().joined(separator: ", ")); found: \(found) A2UI JSONL line \(item.lineNumber): expected exactly one of \(allowed.sorted()
.joined(separator: ", ")); found: \(found)
""", """,
]) ])
} }
@ -441,7 +451,7 @@ enum ControlRequestHandler {
let data = try await GatewayConnection.shared.request( let data = try await GatewayConnection.shared.request(
method: "node.list", method: "node.list",
params: [:], params: [:],
timeoutMs: 10_000) timeoutMs: 10000)
let payload = try JSONDecoder().decode(GatewayNodeListPayload.self, from: data) let payload = try JSONDecoder().decode(GatewayNodeListPayload.self, from: data)
let result = self.buildNodeListResult(payload: payload) let result = self.buildNodeListResult(payload: payload)
@ -460,7 +470,7 @@ enum ControlRequestHandler {
let data = try await GatewayConnection.shared.request( let data = try await GatewayConnection.shared.request(
method: "node.describe", method: "node.describe",
params: ["nodeId": AnyCodable(nodeId)], params: ["nodeId": AnyCodable(nodeId)],
timeoutMs: 10_000) timeoutMs: 10000)
return Response(ok: true, payload: data) return Response(ok: true, payload: data)
} catch { } catch {
return Response(ok: false, message: error.localizedDescription) return Response(ok: false, message: error.localizedDescription)
@ -526,7 +536,7 @@ enum ControlRequestHandler {
let data = try await GatewayConnection.shared.request( let data = try await GatewayConnection.shared.request(
method: "node.invoke", method: "node.invoke",
params: params, params: params,
timeoutMs: 30_000) timeoutMs: 30000)
return Response(ok: true, payload: data) return Response(ok: true, payload: data)
} catch { } catch {
logger.error("node invoke failed: \(error.localizedDescription, privacy: .public)") logger.error("node invoke failed: \(error.localizedDescription, privacy: .public)")

View File

@ -12,9 +12,9 @@ enum DeviceModelCatalog {
let family = (deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let family = (deviceFamily ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let model = (modelIdentifier ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let model = (modelIdentifier ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let friendlyName = model.isEmpty ? nil : modelIdentifierToName[model] let friendlyName = model.isEmpty ? nil : self.modelIdentifierToName[model]
let symbol = symbolFor(modelIdentifier: model, friendlyName: friendlyName) let symbol = self.symbolFor(modelIdentifier: model, friendlyName: friendlyName)
?? fallbackSymbol(for: family, modelIdentifier: model) ?? self.fallbackSymbol(for: family, modelIdentifier: model)
let title = if let friendlyName, !friendlyName.isEmpty { let title = if let friendlyName, !friendlyName.isEmpty {
friendlyName friendlyName
@ -47,7 +47,9 @@ enum DeviceModelCatalog {
if lower.hasPrefix("macbook") || lower.hasPrefix("macbookpro") || lower.hasPrefix("macbookair") { if lower.hasPrefix("macbook") || lower.hasPrefix("macbookpro") || lower.hasPrefix("macbookair") {
return "laptopcomputer" return "laptopcomputer"
} }
if lower.hasPrefix("imac") || lower.hasPrefix("macmini") || lower.hasPrefix("macpro") || lower.hasPrefix("macstudio") { if lower.hasPrefix("imac") || lower.hasPrefix("macmini") || lower.hasPrefix("macpro") || lower
.hasPrefix("macstudio")
{
return "desktopcomputer" return "desktopcomputer"
} }
@ -84,8 +86,12 @@ enum DeviceModelCatalog {
private static func loadModelIdentifierToName() -> [String: String] { private static func loadModelIdentifierToName() -> [String: String] {
var combined: [String: String] = [:] var combined: [String: String] = [:]
combined.merge(loadMapping(resourceName: "ios-device-identifiers"), uniquingKeysWith: { current, _ in current }) combined.merge(
combined.merge(loadMapping(resourceName: "mac-device-identifiers"), uniquingKeysWith: { current, _ in current }) self.loadMapping(resourceName: "ios-device-identifiers"),
uniquingKeysWith: { current, _ in current })
combined.merge(
self.loadMapping(resourceName: "mac-device-identifiers"),
uniquingKeysWith: { current, _ in current })
return combined return combined
} }
@ -128,10 +134,10 @@ enum DeviceModelCatalog {
var normalizedName: String? { var normalizedName: String? {
switch self { switch self {
case .string(let s): case let .string(s):
let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed return trimmed.isEmpty ? nil : trimmed
case .stringArray(let arr): case let .stringArray(arr):
let values = arr let values = arr
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty } .filter { !$0.isEmpty }

View File

@ -42,11 +42,11 @@ actor GatewayConnection {
typealias Config = (url: URL, token: String?) typealias Config = (url: URL, token: String?)
enum Method: String, Sendable { enum Method: String, Sendable {
case agent = "agent" case agent
case status = "status" case status
case setHeartbeats = "set-heartbeats" case setHeartbeats = "set-heartbeats"
case systemEvent = "system-event" case systemEvent = "system-event"
case health = "health" case health
case chatHistory = "chat.history" case chatHistory = "chat.history"
case chatSend = "chat.send" case chatSend = "chat.send"
case chatAbort = "chat.abort" case chatAbort = "chat.abort"

View File

@ -31,4 +31,3 @@ struct GatewayDecodingError: LocalizedError, Sendable {
var errorDescription: String? { "\(self.method): \(self.message)" } var errorDescription: String? { "\(self.method): \(self.message)" }
} }

View File

@ -6,4 +6,3 @@ extension String {
return trimmed.isEmpty ? nil : trimmed return trimmed.isEmpty ? nil : trimmed
} }
} }

View File

@ -18,8 +18,8 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
init( init(
beginAction: @escaping @Sendable () async -> Void = { await VoicePushToTalk.shared.begin() }, beginAction: @escaping @Sendable () async -> Void = { await VoicePushToTalk.shared.begin() },
endAction: @escaping @Sendable () async -> Void = { await VoicePushToTalk.shared.end() } endAction: @escaping @Sendable () async -> Void = { await VoicePushToTalk.shared.end() })
) { {
self.beginAction = beginAction self.beginAction = beginAction
self.endAction = endAction self.endAction = endAction
} }

View File

@ -102,4 +102,3 @@ final class WebChatManager {
// Keep panel controller cached so reopening doesn't re-bootstrap. // Keep panel controller cached so reopening doesn't re-bootstrap.
} }
} }

View File

@ -25,7 +25,7 @@ struct MacGatewayChatTransport: ClawdisChatTransport, Sendable {
"sessionKey": AnyCodable(sessionKey), "sessionKey": AnyCodable(sessionKey),
"runId": AnyCodable(runId), "runId": AnyCodable(runId),
], ],
timeoutMs: 10_000) timeoutMs: 10000)
} }
func listSessions(limit: Int?) async throws -> ClawdisChatSessionsListResponse { func listSessions(limit: Int?) async throws -> ClawdisChatSessionsListResponse {
@ -39,7 +39,7 @@ struct MacGatewayChatTransport: ClawdisChatTransport, Sendable {
let data = try await GatewayConnection.shared.request( let data = try await GatewayConnection.shared.request(
method: "sessions.list", method: "sessions.list",
params: params, params: params,
timeoutMs: 15_000) timeoutMs: 15000)
return try JSONDecoder().decode(ClawdisChatSessionsListResponse.self, from: data) return try JSONDecoder().decode(ClawdisChatSessionsListResponse.self, from: data)
} }

View File

@ -259,6 +259,7 @@ struct ClawdisCLI {
return ParsedCLIRequest( return ParsedCLIRequest(
request: .nodeInvoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON), request: .nodeInvoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON),
kind: .generic) kind: .generic)
default: default:
throw CLIError.help throw CLIError.help
} }
@ -450,10 +451,13 @@ struct ClawdisCLI {
if let message = response.message, !message.isEmpty { if let message = response.message, !message.isEmpty {
FileHandle.standardOutput.write(Data((message + "\n").utf8)) FileHandle.standardOutput.write(Data((message + "\n").utf8))
} }
if let payload = response.payload, let info = try? JSONDecoder().decode(CanvasShowResult.self, from: payload) { if let payload = response.payload, let info = try? JSONDecoder().decode(
FileHandle.standardOutput.write(Data(("STATUS:\(info.status.rawValue)\n").utf8)) CanvasShowResult.self,
from: payload)
{
FileHandle.standardOutput.write(Data("STATUS:\(info.status.rawValue)\n".utf8))
if let url = info.url, !url.isEmpty { if let url = info.url, !url.isEmpty {
FileHandle.standardOutput.write(Data(("URL:\(url)\n").utf8)) FileHandle.standardOutput.write(Data("URL:\(url)\n".utf8))
} }
} }
return return
@ -515,7 +519,7 @@ struct ClawdisCLI {
let version = (n.version ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let version = (n.version ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if !platform.isEmpty || !version.isEmpty { if !platform.isEmpty || !version.isEmpty {
let pv = [platform.isEmpty ? nil : platform, version.isEmpty ? nil : version] let pv = [platform.isEmpty ? nil : platform, version.isEmpty ? nil : version]
.compactMap { $0 } .compactMap(\.self)
.joined(separator: " ") .joined(separator: " ")
if !pv.isEmpty { print(" platform: \(pv)") } if !pv.isEmpty { print(" platform: \(pv)") }
} }
@ -571,7 +575,9 @@ struct ClawdisCLI {
print(parts.joined(separator: " · ")) print(parts.joined(separator: " · "))
if !commands.isEmpty { if !commands.isEmpty {
print("Commands:") print("Commands:")
for c in commands { print("- \(c)") } for c in commands {
print("- \(c)")
}
} }
return return
} }

View File

@ -420,6 +420,10 @@ public struct NodePairRequestParams: Codable {
public let displayname: String? public let displayname: String?
public let platform: String? public let platform: String?
public let version: String? public let version: String?
public let devicefamily: String?
public let modelidentifier: String?
public let caps: [String]?
public let commands: [String]?
public let remoteip: String? public let remoteip: String?
public init( public init(
@ -427,12 +431,20 @@ public struct NodePairRequestParams: Codable {
displayname: String?, displayname: String?,
platform: String?, platform: String?,
version: String?, version: String?,
devicefamily: String?,
modelidentifier: String?,
caps: [String]?,
commands: [String]?,
remoteip: String? remoteip: String?
) { ) {
self.nodeid = nodeid self.nodeid = nodeid
self.displayname = displayname self.displayname = displayname
self.platform = platform self.platform = platform
self.version = version self.version = version
self.devicefamily = devicefamily
self.modelidentifier = modelidentifier
self.caps = caps
self.commands = commands
self.remoteip = remoteip self.remoteip = remoteip
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
@ -440,6 +452,10 @@ public struct NodePairRequestParams: Codable {
case displayname = "displayName" case displayname = "displayName"
case platform case platform
case version case version
case devicefamily = "deviceFamily"
case modelidentifier = "modelIdentifier"
case caps
case commands
case remoteip = "remoteIp" case remoteip = "remoteIp"
} }
} }
@ -493,6 +509,19 @@ public struct NodePairVerifyParams: Codable {
public struct NodeListParams: Codable { public struct NodeListParams: Codable {
} }
public struct NodeDescribeParams: Codable {
public let nodeid: String
public init(
nodeid: String
) {
self.nodeid = nodeid
}
private enum CodingKeys: String, CodingKey {
case nodeid = "nodeId"
}
}
public struct NodeInvokeParams: Codable { public struct NodeInvokeParams: Codable {
public let nodeid: String public let nodeid: String
public let command: String public let command: String
@ -837,6 +866,23 @@ public struct ChatSendParams: Codable {
} }
} }
public struct ChatAbortParams: Codable {
public let sessionkey: String
public let runid: String
public init(
sessionkey: String,
runid: String
) {
self.sessionkey = sessionkey
self.runid = runid
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case runid = "runId"
}
}
public struct ChatEvent: Codable { public struct ChatEvent: Codable {
public let runid: String public let runid: String
public let sessionkey: String public let sessionkey: String

View File

@ -51,6 +51,14 @@
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
}, },
"deviceFamily": {
"minLength": 1,
"type": "string"
},
"modelIdentifier": {
"minLength": 1,
"type": "string"
},
"mode": { "mode": {
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
@ -185,6 +193,14 @@
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
}, },
"deviceFamily": {
"minLength": 1,
"type": "string"
},
"modelIdentifier": {
"minLength": 1,
"type": "string"
},
"mode": { "mode": {
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
@ -538,6 +554,14 @@
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
}, },
"deviceFamily": {
"minLength": 1,
"type": "string"
},
"modelIdentifier": {
"minLength": 1,
"type": "string"
},
"mode": { "mode": {
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
@ -617,6 +641,14 @@
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
}, },
"deviceFamily": {
"minLength": 1,
"type": "string"
},
"modelIdentifier": {
"minLength": 1,
"type": "string"
},
"mode": { "mode": {
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
@ -860,6 +892,28 @@
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
}, },
"deviceFamily": {
"minLength": 1,
"type": "string"
},
"modelIdentifier": {
"minLength": 1,
"type": "string"
},
"caps": {
"type": "array",
"items": {
"minLength": 1,
"type": "string"
}
},
"commands": {
"type": "array",
"items": {
"minLength": 1,
"type": "string"
}
},
"remoteIp": { "remoteIp": {
"minLength": 1, "minLength": 1,
"type": "string" "type": "string"
@ -923,6 +977,19 @@
"type": "object", "type": "object",
"properties": {} "properties": {}
}, },
"NodeDescribeParams": {
"additionalProperties": false,
"type": "object",
"properties": {
"nodeId": {
"minLength": 1,
"type": "string"
}
},
"required": [
"nodeId"
]
},
"NodeInvokeParams": { "NodeInvokeParams": {
"additionalProperties": false, "additionalProperties": false,
"type": "object", "type": "object",
@ -1773,7 +1840,7 @@
}, },
"limit": { "limit": {
"minimum": 1, "minimum": 1,
"maximum": 500, "maximum": 1000,
"type": "integer" "type": "integer"
} }
}, },
@ -1818,6 +1885,24 @@
"idempotencyKey" "idempotencyKey"
] ]
}, },
"ChatAbortParams": {
"additionalProperties": false,
"type": "object",
"properties": {
"sessionKey": {
"minLength": 1,
"type": "string"
},
"runId": {
"minLength": 1,
"type": "string"
}
},
"required": [
"sessionKey",
"runId"
]
},
"ChatEvent": { "ChatEvent": {
"additionalProperties": false, "additionalProperties": false,
"type": "object", "type": "object",
@ -1844,6 +1929,10 @@
"const": "final", "const": "final",
"type": "string" "type": "string"
}, },
{
"const": "aborted",
"type": "string"
},
{ {
"const": "error", "const": "error",
"type": "string" "type": "string"

View File

@ -38,6 +38,7 @@
"dependencies": { "dependencies": {
"@grammyjs/transformer-throttler": "^1.2.1", "@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4", "@homebridge/ciao": "^1.3.4",
"@mariozechner/pi-agent-core": "^0.21.0",
"@mariozechner/pi-ai": "^0.21.0", "@mariozechner/pi-ai": "^0.21.0",
"@mariozechner/pi-coding-agent": "^0.21.0", "@mariozechner/pi-coding-agent": "^0.21.0",
"@sinclair/typebox": "^0.34.41", "@sinclair/typebox": "^0.34.41",

View File

@ -2419,6 +2419,11 @@ describe("gateway server", () => {
}); });
}); });
const sendResP = onceMessage(
ws,
(o) => o.type === "res" && o.id === "send-mismatch-1",
10_000,
);
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "req", type: "req",
@ -2435,6 +2440,11 @@ describe("gateway server", () => {
await agentStartedP; await agentStartedP;
const abortResP = onceMessage(
ws,
(o) => o.type === "res" && o.id === "abort-mismatch-1",
10_000,
);
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "req", type: "req",
@ -2444,14 +2454,15 @@ describe("gateway server", () => {
}), }),
); );
const abortRes = await onceMessage( const abortRes = await abortResP;
ws,
(o) => o.type === "res" && o.id === "abort-mismatch-1",
10_000,
);
expect(abortRes.ok).toBe(false); expect(abortRes.ok).toBe(false);
expect(abortRes.error?.code).toBe("INVALID_REQUEST"); expect(abortRes.error?.code).toBe("INVALID_REQUEST");
const abortRes2P = onceMessage(
ws,
(o) => o.type === "res" && o.id === "abort-mismatch-2",
10_000,
);
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "req", type: "req",
@ -2461,23 +2472,15 @@ describe("gateway server", () => {
}), }),
); );
const abortRes2 = await onceMessage( const abortRes2 = await abortRes2P;
ws,
(o) => o.type === "res" && o.id === "abort-mismatch-2",
10_000,
);
expect(abortRes2.ok).toBe(true); expect(abortRes2.ok).toBe(true);
const sendRes = await onceMessage( const sendRes = await sendResP;
ws,
(o) => o.type === "res" && o.id === "send-mismatch-1",
10_000,
);
expect(sendRes.ok).toBe(true); expect(sendRes.ok).toBe(true);
ws.close(); ws.close();
await server.close(); await server.close();
}); }, 15_000);
test("chat.abort is a no-op after chat.send completes", async () => { test("chat.abort is a no-op after chat.send completes", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-gw-")); const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-gw-"));