diff --git a/CHANGELOG.md b/CHANGELOG.md index be1e8040463..13808f21703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - Codex: add Computer Use setup for Codex-mode agents, including `/codex computer-use status/install`, marketplace discovery, optional auto-install, and fail-closed MCP server checks before Codex-mode turns start. Fixes #72094. (#71842) Thanks @pash-openai. +- Apps: consume Peekaboo 3.0.0-beta4 and ElevenLabsKit 0.1.1, align Swabble on Commander 0.2.2, and refresh macOS/iOS SwiftPM resolutions against the released dependency graph. Thanks @Blaizzy. - Plugin SDK: expose shared channel route normalization, parser-driven target resolution, raw-target compact keys, parsed-target types, and route comparison helpers through `openclaw/plugin-sdk/channel-route`, switch native approval origin matching onto that route contract with optional delivery and match-only target normalization, and retire the internal channel-route shim behind dated compatibility aliases for legacy key/comparable-target helpers. Thanks @vincentkoc. - Docs/Codex: document how Codex Computer Use, direct `cua-driver mcp`, and OpenClaw.app's PeekabooBridge fit together so desktop-control setup choices are clearer. Thanks @pash-openai and @trycua. - Matrix/streaming: stream tool-progress updates into live Matrix preview edits by default when preview streaming is active, with `streaming.preview.toolProgress: false` to keep answer previews while hiding interim tool lines. Thanks @gumadeiras. diff --git a/Swabble/Package.resolved b/Swabble/Package.resolved index 24de6ea3a7c..23cff7debed 100644 --- a/Swabble/Package.resolved +++ b/Swabble/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "c0677e232394b5f6b0191b6dbb5bae553d55264f65ae725cd03a8ffdfda9cdd3", + "originHash" : "e6910acc97de62dc423c0a391985c1c2f28207951e356081539abde41f9ffc72", "pins" : [ { "identity" : "commander", "kind" : "remoteSourceControl", "location" : "https://github.com/steipete/Commander.git", "state" : { - "revision" : "9e349575c8e3c6745e81fe19e5bb5efa01b078ce", - "version" : "0.2.1" + "revision" : "ae2ce746b386ff94b26648cfe5625cfa8d02639b", + "version" : "0.2.2" } }, { diff --git a/Swabble/Package.swift b/Swabble/Package.swift index 9f5a0003619..79a2eb75711 100644 --- a/Swabble/Package.swift +++ b/Swabble/Package.swift @@ -13,7 +13,7 @@ let package = Package( .executable(name: "swabble", targets: ["SwabbleCLI"]), ], dependencies: [ - .package(url: "https://github.com/steipete/Commander.git", exact: "0.2.1"), + .package(url: "https://github.com/steipete/Commander.git", exact: "0.2.2"), .package(url: "https://github.com/apple/swift-testing", from: "0.99.0"), ], targets: [ @@ -43,7 +43,6 @@ let package = Package( ], swiftSettings: [ .enableUpcomingFeature("StrictConcurrency"), - .enableExperimentalFeature("SwiftTesting"), ]), .testTarget( name: "swabbleTests", diff --git a/Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift b/Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift index e2de6fdfce5..f69475c201b 100644 --- a/Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift +++ b/Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift @@ -45,6 +45,15 @@ extension AttributedString { } return ranges.compactMap { range in + guard #available(macOS 26.0, iOS 26.0, *) else { + return AttributedString(self[range].characters) + } + return self.sentenceWithAudioTimeRange(range) + } + } + + @available(macOS 26.0, iOS 26.0, *) + private func sentenceWithAudioTimeRange(_ range: Range) -> AttributedString? { let audioTimeRanges = self[range].runs.filter { !String(self[$0.range].characters) .trimmingCharacters(in: .whitespacesAndNewlines).isEmpty @@ -57,6 +66,5 @@ extension AttributedString { start: start, end: end) return AttributedString(self[range].characters, attributes: attributes) - } } } diff --git a/Swabble/Sources/SwabbleCore/Support/OutputFormat.swift b/Swabble/Sources/SwabbleCore/Support/OutputFormat.swift index 84047c7284b..2c1935e1529 100644 --- a/Swabble/Sources/SwabbleCore/Support/OutputFormat.swift +++ b/Swabble/Sources/SwabbleCore/Support/OutputFormat.swift @@ -17,29 +17,35 @@ public enum OutputFormat: String { case .txt: return String(transcript.characters) case .srt: - func format(_ timeInterval: TimeInterval) -> String { - let ms = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1000) - let s = Int(timeInterval) % 60 - let m = (Int(timeInterval) / 60) % 60 - let h = Int(timeInterval) / 60 / 60 - return String(format: "%0.2d:%0.2d:%0.2d,%0.3d", h, m, s, ms) - } - - return transcript.sentences(maxLength: maxLength).compactMap { (sentence: AttributedString) -> ( - CMTimeRange, - String)? in - guard let timeRange = sentence.audioTimeRange else { return nil } - return (timeRange, String(sentence.characters)) - }.enumerated().map { index, run in - let (timeRange, text) = run - return """ - - \(index + 1) - \(format(timeRange.start.seconds)) --> \(format(timeRange.end.seconds)) - \(text.trimmingCharacters(in: .whitespacesAndNewlines)) - - """ - }.joined().trimmingCharacters(in: .whitespacesAndNewlines) + guard #available(macOS 26.0, iOS 26.0, *) else { return "" } + return self.srtText(for: transcript, maxLength: maxLength) } } + + @available(macOS 26.0, iOS 26.0, *) + private func srtText(for transcript: AttributedString, maxLength: Int) -> String { + func format(_ timeInterval: TimeInterval) -> String { + let ms = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1000) + let s = Int(timeInterval) % 60 + let m = (Int(timeInterval) / 60) % 60 + let h = Int(timeInterval) / 60 / 60 + return String(format: "%0.2d:%0.2d:%0.2d,%0.3d", h, m, s, ms) + } + + return transcript.sentences(maxLength: maxLength).compactMap { (sentence: AttributedString) -> ( + CMTimeRange, + String)? in + guard let timeRange = sentence.audioTimeRange else { return nil } + return (timeRange, String(sentence.characters)) + }.enumerated().map { index, run in + let (timeRange, text) = run + return """ + + \(index + 1) + \(format(timeRange.start.seconds)) --> \(format(timeRange.end.seconds)) + \(text.trimmingCharacters(in: .whitespacesAndNewlines)) + + """ + }.joined().trimmingCharacters(in: .whitespacesAndNewlines) + } } diff --git a/Swabble/Sources/swabble/Commands/TranscribeCommand.swift b/Swabble/Sources/swabble/Commands/TranscribeCommand.swift index 1bedca3fc0a..32f3f14a4b4 100644 --- a/Swabble/Sources/swabble/Commands/TranscribeCommand.swift +++ b/Swabble/Sources/swabble/Commands/TranscribeCommand.swift @@ -5,6 +5,7 @@ import Speech import Swabble @MainActor +@available(macOS 26.0, *) struct TranscribeCommand: ParsableCommand { @Argument(help: "Path to audio/video file") var inputFile: String = "" @Option(name: .long("locale"), help: "Locale identifier", parsing: .singleValue) var locale: String = Locale.current diff --git a/Swabble/Tests/SwabbleKitTests/WakeWordGateTests.swift b/Swabble/Tests/SwabbleKitTests/WakeWordGateTests.swift index 13be4550d4d..f055a56fc2d 100644 --- a/Swabble/Tests/SwabbleKitTests/WakeWordGateTests.swift +++ b/Swabble/Tests/SwabbleKitTests/WakeWordGateTests.swift @@ -1,9 +1,9 @@ import Foundation import SwabbleKit -import Testing +import XCTest -@Suite struct WakeWordGateTests { - @Test func matchRequiresGapAfterTrigger() { +final class WakeWordGateTests: XCTestCase { + func testMatchRequiresGapAfterTrigger() { let transcript = "hey clawd do thing" let segments = makeSegments( transcript: transcript, @@ -14,10 +14,10 @@ import Testing ("thing", 0.5, 0.1), ]) let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3) - #expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config) == nil) + XCTAssertNil(WakeWordGate.match(transcript: transcript, segments: segments, config: config)) } - @Test func matchAllowsGapAndExtractsCommand() { + func testMatchAllowsGapAndExtractsCommand() { let transcript = "hey clawd do thing" let segments = makeSegments( transcript: transcript, @@ -29,10 +29,10 @@ import Testing ]) let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3) let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config) - #expect(match?.command == "do thing") + XCTAssertEqual(match?.command, "do thing") } - @Test func matchHandlesMultiWordTriggers() { + func testMatchHandlesMultiWordTriggers() { let transcript = "hey clawd do it" let segments = makeSegments( transcript: transcript, @@ -44,10 +44,10 @@ import Testing ]) let config = WakeWordGateConfig(triggers: ["hey clawd"], minPostTriggerGap: 0.3) let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config) - #expect(match?.command == "do it") + XCTAssertEqual(match?.command, "do it") } - @Test func matchPrefersMostSpecificTriggerWhenOverlapping() { + func testMatchPrefersMostSpecificTriggerWhenOverlapping() { let transcript = "hey clawd do it" let segments = makeSegments( transcript: transcript, @@ -59,10 +59,10 @@ import Testing ]) let config = WakeWordGateConfig(triggers: ["clawd", "hey clawd"], minPostTriggerGap: 0.3) let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config) - #expect(match?.trigger == "hey clawd") + XCTAssertEqual(match?.trigger, "hey clawd") } - @Test func commandTextHandlesForeignRangeIndices() { + func testCommandTextHandlesForeignRangeIndices() { let transcript = "hey clawd do thing" let other = "do thing" let foreignRange = other.range(of: "do") @@ -78,7 +78,7 @@ import Testing segments: segments, triggerEndTime: 0.3) - #expect(command == "do thing") + XCTAssertEqual(command, "do thing") } } diff --git a/Swabble/Tests/swabbleTests/ConfigTests.swift b/Swabble/Tests/swabbleTests/ConfigTests.swift index e0833db7fe7..8e8c4b1e7ec 100644 --- a/Swabble/Tests/swabbleTests/ConfigTests.swift +++ b/Swabble/Tests/swabbleTests/ConfigTests.swift @@ -1,23 +1,22 @@ import Foundation -import Testing @testable import Swabble +import XCTest -@Test -func configRoundTrip() throws { - var cfg = SwabbleConfig() - cfg.wake.word = "robot" - let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") - defer { try? FileManager.default.removeItem(at: url) } +final class ConfigTests: XCTestCase { + func testConfigRoundTrip() throws { + var cfg = SwabbleConfig() + cfg.wake.word = "robot" + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json") + defer { try? FileManager.default.removeItem(at: url) } - try ConfigLoader.save(cfg, at: url) - let loaded = try ConfigLoader.load(at: url) - #expect(loaded.wake.word == "robot") - #expect(loaded.hook.prefix.contains("Voice swabble")) -} + try ConfigLoader.save(cfg, at: url) + let loaded = try ConfigLoader.load(at: url) + XCTAssertEqual(loaded.wake.word, "robot") + XCTAssertTrue(loaded.hook.prefix.contains("Voice swabble")) + } -@Test -func configMissingThrows() { - #expect(throws: ConfigError.missingConfig) { - _ = try ConfigLoader.load(at: FileManager.default.temporaryDirectory.appendingPathComponent("nope.json")) + func testConfigMissingThrows() { + XCTAssertThrowsError( + try ConfigLoader.load(at: FileManager.default.temporaryDirectory.appendingPathComponent("nope.json"))) } } diff --git a/apps/macos/Package.resolved b/apps/macos/Package.resolved index ac41f58cd57..d495eb24b48 100644 --- a/apps/macos/Package.resolved +++ b/apps/macos/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "77c5e32a542e4c2ca3c7fff037abaa02066ea47cb2c2afc17927eda5a56aa5c0", + "originHash" : "45e1ade868f67cf9cac4811c3b8c8b7dab7cef3f932ddebac6e292fdf9d6973c", "pins" : [ { "identity" : "axorcist", "kind" : "remoteSourceControl", "location" : "https://github.com/steipete/AXorcist.git", "state" : { - "revision" : "c75d06f7f93e264a9786edc2b78c04973061cb2f", - "version" : "0.1.0" + "revision" : "289d403c53ce26cdd2fcb2bfb8672cd43adc2b43", + "version" : "0.1.2" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/steipete/Commander.git", "state" : { - "revision" : "9e349575c8e3c6745e81fe19e5bb5efa01b078ce", - "version" : "0.2.1" + "revision" : "ae2ce746b386ff94b26648cfe5625cfa8d02639b", + "version" : "0.2.2" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/steipete/ElevenLabsKit", "state" : { - "revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d", - "version" : "0.1.0" + "revision" : "0f1e4c039bd0e22b03c0cb7f43c00c1865858f0b", + "version" : "0.1.1" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/steipete/Peekaboo.git", "state" : { - "branch" : "main", - "revision" : "461bc2e1ae4bfd6757eb003e528e8e26d55e06e9" + "revision" : "bb57c83935ebc27aae69a23042a9f9fe6ca8e404", + "version" : "3.0.0-beta4" } }, { diff --git a/apps/macos/Package.swift b/apps/macos/Package.swift index 2eb8b1743ff..01500452c99 100644 --- a/apps/macos/Package.swift +++ b/apps/macos/Package.swift @@ -19,7 +19,7 @@ let package = Package( .package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.4.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.10.1"), .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.0"), - .package(url: "https://github.com/steipete/Peekaboo.git", branch: "main"), + .package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.0.0-beta4"), .package(path: "../shared/OpenClawKit"), .package(path: "../../Swabble"), ], diff --git a/apps/shared/OpenClawKit/Package.swift b/apps/shared/OpenClawKit/Package.swift index 5c8132d2c9b..702b6e052c9 100644 --- a/apps/shared/OpenClawKit/Package.swift +++ b/apps/shared/OpenClawKit/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library(name: "OpenClawChatUI", targets: ["OpenClawChatUI"]), ], dependencies: [ - .package(url: "https://github.com/steipete/ElevenLabsKit", exact: "0.1.0"), + .package(url: "https://github.com/steipete/ElevenLabsKit", exact: "0.1.1"), .package(url: "https://github.com/gonzalezreal/textual", exact: "0.3.1"), ], targets: [ diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift index e892e0a2409..0f0b2191d77 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift @@ -141,6 +141,7 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda "maxBufferedBytes": 1, "tickIntervalMs": 30_000, ], + "auth": [:], ] if let auth { payload["auth"] = auth diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift index 1903d917860..af2ef5d11e4 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift @@ -12,6 +12,25 @@ private struct TalkConfigContractFixture: Decodable { let payloadValid: Bool let expectedSelection: ExpectedSelection? let talk: [String: AnyCodable] + + var gatewayResponseTalk: [String: AnyCodable] { + guard let expectedSelection else { return self.talk } + var config: [String: AnyCodable] = [:] + if let voiceId = expectedSelection.voiceId { + config["voiceId"] = AnyCodable(voiceId) + } + if let apiKey = expectedSelection.apiKey { + config["apiKey"] = AnyCodable(apiKey) + } + var response = self.talk + response["provider"] = AnyCodable(expectedSelection.provider) + response["providers"] = AnyCodable([expectedSelection.provider: config]) + response["resolved"] = AnyCodable([ + "provider": AnyCodable(expectedSelection.provider), + "config": AnyCodable(config), + ]) + return response + } } struct ExpectedSelection: Decodable { @@ -53,7 +72,7 @@ struct TalkConfigContractTests { @Test func selectionFixtures() throws { for fixture in try TalkConfigContractFixtureLoader.load().selectionCases { let selection = TalkConfigParsing.selectProviderConfig( - fixture.talk, + fixture.gatewayResponseTalk, defaultProvider: fixture.defaultProvider) if let expected = fixture.expectedSelection { #expect(selection != nil) diff --git a/test-fixtures/talk-config-contract.json b/test-fixtures/talk-config-contract.json index 6b64ccf58bc..0276a3164ad 100644 --- a/test-fixtures/talk-config-contract.json +++ b/test-fixtures/talk-config-contract.json @@ -84,7 +84,7 @@ "payloadValid": true, "expectedSelection": { "provider": "elevenlabs", - "normalizedPayload": false, + "normalizedPayload": true, "voiceId": "voice-legacy", "apiKey": "xxxxx" },