diff --git a/Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift b/Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift index 8f5d6a106..e2de6fdfc 100644 --- a/Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift +++ b/Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift @@ -34,8 +34,7 @@ extension AttributedString { var ranges: [Range] = [] for wordRange in wordRanges { if let lastRange = ranges.last, - self[lastRange].characters.count + self[wordRange].characters.count <= maxLength - { + self[lastRange].characters.count + self[wordRange].characters.count <= maxLength { ranges[ranges.count - 1] = lastRange.lowerBound..= 2 else { throw CommanderProgramError.missingSubcommand(command: "swabble") } - let sub = path[1] - switch sub { - case "serve": - var cmd = ServeCommand(parsed: parsed) - try await cmd.run() - case "transcribe": - var cmd = TranscribeCommand(parsed: parsed) - try await cmd.run() - case "test-hook": - var cmd = TestHookCommand(parsed: parsed) - try await cmd.run() - case "mic": - guard path.count >= 3 else { throw CommanderProgramError.missingSubcommand(command: "mic") } - let micSub = path[2] - if micSub == "list" { - var cmd = MicList(parsed: parsed) - try await cmd.run() - } else if micSub == "set" { - var cmd = MicSet(parsed: parsed) - try await cmd.run() - } else { - throw CommanderProgramError.unknownSubcommand(command: "mic", name: micSub) - } - case "service": - guard path.count >= 3 else { throw CommanderProgramError.missingSubcommand(command: "service") } - let svcSub = path[2] - switch svcSub { - case "install": - var cmd = ServiceInstall() - try await cmd.run() - case "uninstall": - var cmd = ServiceUninstall() - try await cmd.run() - case "status": - var cmd = ServiceStatus() - try await cmd.run() - default: - throw CommanderProgramError.unknownSubcommand(command: "service", name: svcSub) - } - case "doctor": - var cmd = DoctorCommand(parsed: parsed) - try await cmd.run() - case "setup": - var cmd = SetupCommand(parsed: parsed) - try await cmd.run() - case "health": - var cmd = HealthCommand(parsed: parsed) - try await cmd.run() - case "tail-log": - var cmd = TailLogCommand(parsed: parsed) - try await cmd.run() - case "start": - var cmd = StartCommand() - try await cmd.run() - case "stop": - var cmd = StopCommand() - try await cmd.run() - case "restart": - var cmd = RestartCommand() - try await cmd.run() - case "status": - var cmd = StatusCommand() - try await cmd.run() - default: - throw CommanderProgramError.unknownSubcommand(command: "swabble", name: sub) - } + try await dispatchSwabble(parsed: parsed, path: path) default: throw CommanderProgramError.unknownCommand(first) } } +@available(macOS 26.0, *) +@MainActor +private func dispatchSwabble(parsed: ParsedValues, path: [String]) async throws { + let sub = try subcommand(path, index: 1, command: "swabble") + switch sub { + case "mic": + try await dispatchMic(parsed: parsed, path: path) + case "service": + try await dispatchService(path: path) + default: + let handlers = swabbleHandlers(parsed: parsed) + guard let handler = handlers[sub] else { + throw CommanderProgramError.unknownSubcommand(command: "swabble", name: sub) + } + try await handler() + } +} + +@available(macOS 26.0, *) +@MainActor +private func swabbleHandlers(parsed: ParsedValues) -> [String: () async throws -> Void] { + [ + "serve": { + var cmd = ServeCommand(parsed: parsed) + try await cmd.run() + }, + "transcribe": { + var cmd = TranscribeCommand(parsed: parsed) + try await cmd.run() + }, + "test-hook": { + var cmd = TestHookCommand(parsed: parsed) + try await cmd.run() + }, + "doctor": { + var cmd = DoctorCommand(parsed: parsed) + try await cmd.run() + }, + "setup": { + var cmd = SetupCommand(parsed: parsed) + try await cmd.run() + }, + "health": { + var cmd = HealthCommand(parsed: parsed) + try await cmd.run() + }, + "tail-log": { + var cmd = TailLogCommand(parsed: parsed) + try await cmd.run() + }, + "start": { + var cmd = StartCommand() + try await cmd.run() + }, + "stop": { + var cmd = StopCommand() + try await cmd.run() + }, + "restart": { + var cmd = RestartCommand() + try await cmd.run() + }, + "status": { + var cmd = StatusCommand() + try await cmd.run() + } + ] +} + +@available(macOS 26.0, *) +@MainActor +private func dispatchMic(parsed: ParsedValues, path: [String]) async throws { + let micSub = try subcommand(path, index: 2, command: "mic") + switch micSub { + case "list": + var cmd = MicList(parsed: parsed) + try await cmd.run() + case "set": + var cmd = MicSet(parsed: parsed) + try await cmd.run() + default: + throw CommanderProgramError.unknownSubcommand(command: "mic", name: micSub) + } +} + +@available(macOS 26.0, *) +@MainActor +private func dispatchService(path: [String]) async throws { + let svcSub = try subcommand(path, index: 2, command: "service") + switch svcSub { + case "install": + var cmd = ServiceInstall() + try await cmd.run() + case "uninstall": + var cmd = ServiceUninstall() + try await cmd.run() + case "status": + var cmd = ServiceStatus() + try await cmd.run() + default: + throw CommanderProgramError.unknownSubcommand(command: "service", name: svcSub) + } +} + +private func subcommand(_ path: [String], index: Int, command: String) throws -> String { + guard path.count > index else { + throw CommanderProgramError.missingSubcommand(command: command) + } + return path[index] +} + if #available(macOS 26.0, *) { let exitCode = await runCLI() exit(exitCode) diff --git a/apps/macos/Tests/ClawdbotIPCTests/ConnectionsSettingsSmokeTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ConnectionsSettingsSmokeTests.swift index 6e451d13c..e7778f9da 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 = 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, +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 @@ import Testing "lastError": "not configured", "probe": ["ok": false, "error": "imsg not found (imsg)"], "lastProbeAt": 1_700_000_050_000, - ]), - ], - channelAccounts: [:], - channelDefaultAccountId: [ - "whatsapp": "default", - "telegram": "default", - "signal": "default", + ]), + ], + channelAccounts: [:], + channelDefaultAccountId: [ + "whatsapp": "default", + "telegram": "default", + "signal": "default", "imessage": "default", ]) @@ -93,23 +93,23 @@ import Testing let view = ConnectionsSettings(store: store) _ = view.body - } + } - @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, + @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 @@ import Testing "cliPath": "imsg", "probe": ["ok": false, "error": "imsg not found (imsg)"], "lastProbeAt": 1_700_000_200_000, - ]), - ], - channelAccounts: [:], - channelDefaultAccountId: [ - "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 2d7672563..25881774e 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/HealthDecodeTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/HealthDecodeTests.swift @@ -2,9 +2,9 @@ import Foundation import Testing @testable import Clawdbot - @Suite struct HealthDecodeTests { - private let sampleJSON: String = // minimal but complete payload - """ +@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}]}} """ diff --git a/docs/tools/browser.md b/docs/tools/browser.md index 7a1b147a9..b7b7149f4 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -204,6 +204,8 @@ Inspection: - `clawdbot browser snapshot` - `clawdbot browser snapshot --format aria --limit 200` - `clawdbot browser snapshot --interactive --compact --depth 6` +- `clawdbot browser snapshot --efficient` +- `clawdbot browser snapshot --labels` - `clawdbot browser snapshot --selector "#main" --interactive` - `clawdbot browser snapshot --frame "iframe#main" --interactive` - `clawdbot browser console --level error` @@ -260,9 +262,11 @@ Notes: - `snapshot`: - `--format ai` (default when Playwright is installed): returns an AI snapshot with numeric refs (`aria-ref=""`). - `--format aria`: returns the accessibility tree (no refs; inspection only). + - `--efficient` (or `--mode efficient`): compact role snapshot preset (interactive + compact + depth + lower maxChars). - Role snapshot options (`--interactive`, `--compact`, `--depth`, `--selector`) force a role-based snapshot with refs like `ref=e12`. - `--frame "