chore: fix swiftlint after split
This commit is contained in:
@@ -62,7 +62,7 @@ final class AppState: ObservableObject {
|
||||
@Published var isWorking: Bool = false
|
||||
@Published var earBoostActive: Bool = false
|
||||
|
||||
private var earBoostTask: Task<Void, Never>? = nil
|
||||
private var earBoostTask: Task<Void, Never>?
|
||||
|
||||
init() {
|
||||
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
|
||||
|
||||
@@ -23,15 +23,15 @@ struct ClawdisApp: App {
|
||||
earBoostActive: self.state.earBoostActive,
|
||||
relayStatus: self.relayManager.status)
|
||||
}
|
||||
.menuBarExtraStyle(.menu)
|
||||
.menuBarExtraAccess(isPresented: self.$isMenuPresented) { item in
|
||||
self.statusItem = item
|
||||
self.applyStatusItemAppearance(paused: self.state.isPaused)
|
||||
}
|
||||
.onChange(of: self.state.isPaused) { _, paused in
|
||||
self.applyStatusItemAppearance(paused: paused)
|
||||
self.relayManager.setActive(!paused)
|
||||
}
|
||||
.menuBarExtraStyle(.menu)
|
||||
.menuBarExtraAccess(isPresented: self.$isMenuPresented) { item in
|
||||
self.statusItem = item
|
||||
self.applyStatusItemAppearance(paused: self.state.isPaused)
|
||||
}
|
||||
.onChange(of: self.state.isPaused) { _, paused in
|
||||
self.applyStatusItemAppearance(paused: paused)
|
||||
self.relayManager.setActive(!paused)
|
||||
}
|
||||
|
||||
Settings {
|
||||
SettingsRootView(state: self.state)
|
||||
@@ -90,10 +90,10 @@ private struct MenuContent: View {
|
||||
|
||||
private func statusColor(_ status: RelayProcessManager.Status) -> Color {
|
||||
switch status {
|
||||
case .running: return .green
|
||||
case .starting, .restarting: return .orange
|
||||
case .failed: return .red
|
||||
case .stopped: return .secondary
|
||||
case .running: .green
|
||||
case .starting, .restarting: .orange
|
||||
case .failed: .red
|
||||
case .stopped: .secondary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,17 +257,17 @@ private struct CritterStatusLabel: View {
|
||||
private var relayNeedsAttention: Bool {
|
||||
switch self.relayStatus {
|
||||
case .failed, .stopped:
|
||||
return !self.isPaused
|
||||
!self.isPaused
|
||||
case .starting, .restarting, .running:
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private var relayBadgeColor: Color {
|
||||
switch self.relayStatus {
|
||||
case .failed: return .red
|
||||
case .stopped: return .orange
|
||||
default: return .clear
|
||||
case .failed: .red
|
||||
case .stopped: .orange
|
||||
default: .clear
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,8 +279,8 @@ enum CritterIconRenderer {
|
||||
blink: CGFloat,
|
||||
legWiggle: CGFloat = 0,
|
||||
earWiggle: CGFloat = 0,
|
||||
earScale: CGFloat = 1
|
||||
) -> NSImage {
|
||||
earScale: CGFloat = 1) -> NSImage
|
||||
{
|
||||
let image = NSImage(size: size)
|
||||
image.lockFocus()
|
||||
defer { image.unlockFocus() }
|
||||
|
||||
@@ -120,7 +120,8 @@ struct OnboardingView: View {
|
||||
self.onboardingCard {
|
||||
self.featureRow(
|
||||
title: "Owns the TCC prompts",
|
||||
subtitle: "Requests Notifications, Accessibility, and Screen Recording so your agents stay unblocked.",
|
||||
subtitle: "Requests Notifications, Accessibility, and Screen Recording "
|
||||
+ "so your agents stay unblocked.",
|
||||
systemImage: "lock.shield")
|
||||
self.featureRow(
|
||||
title: "Native notifications",
|
||||
@@ -128,7 +129,8 @@ struct OnboardingView: View {
|
||||
systemImage: "bell.and.waveform")
|
||||
self.featureRow(
|
||||
title: "Privileged helpers",
|
||||
subtitle: "Runs screenshots or shell actions from the `clawdis-mac` CLI with the right permissions.",
|
||||
subtitle: "Runs screenshots or shell actions from the `clawdis-mac` CLI "
|
||||
+ "with the right permissions.",
|
||||
systemImage: "terminal")
|
||||
}
|
||||
}
|
||||
@@ -232,7 +234,8 @@ struct OnboardingView: View {
|
||||
Spacer()
|
||||
}
|
||||
Text(
|
||||
"You can pause from the menu bar anytime. Settings keeps a \"Show onboarding\" button if you need to revisit.")
|
||||
"You can pause from the menu bar anytime. Settings keeps a \"Show onboarding\" "
|
||||
+ "button if you need to revisit.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
@@ -247,7 +250,8 @@ struct OnboardingView: View {
|
||||
self.onboardingCard {
|
||||
self.featureRow(
|
||||
title: "Run the dashboard",
|
||||
subtitle: "Use the CLI helper from your scripts, and reopen onboarding from Settings if you add a new user.",
|
||||
subtitle: "Use the CLI helper from your scripts, and reopen onboarding from "
|
||||
+ "Settings if you add a new user.",
|
||||
systemImage: "checkmark.seal")
|
||||
self.featureRow(
|
||||
title: "Test a notification",
|
||||
@@ -273,10 +277,10 @@ struct OnboardingView: View {
|
||||
.disabled(true)
|
||||
|
||||
if self.currentPage > 0 {
|
||||
Button(action: { self.handleBack() }) {
|
||||
Button(action: self.handleBack, label: {
|
||||
Label("Back", systemImage: "chevron.left")
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
})
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(0.8)
|
||||
|
||||
@@ -23,7 +23,8 @@ enum PermissionManager {
|
||||
|
||||
case .notDetermined:
|
||||
if interactive {
|
||||
let granted = (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||
let granted = await (try? center.requestAuthorization(options: [.alert, .sound, .badge])) ??
|
||||
false
|
||||
let updated = await center.notificationSettings()
|
||||
results[cap] = granted && (updated.authorizationStatus == .authorized || updated
|
||||
.authorizationStatus == .provisional)
|
||||
|
||||
@@ -46,7 +46,7 @@ final class RelayProcessManager: ObservableObject {
|
||||
private var recentCrashes: [Date] = []
|
||||
|
||||
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "relay")
|
||||
private let logLimit = 20_000 // characters to keep in-memory
|
||||
private let logLimit = 20000 // characters to keep in-memory
|
||||
private let maxCrashes = 3
|
||||
private let crashWindow: TimeInterval = 120 // seconds
|
||||
|
||||
@@ -98,8 +98,8 @@ final class RelayProcessManager: ObservableObject {
|
||||
.name(command.first ?? "clawdis"),
|
||||
arguments: Arguments(Array(command.dropFirst())),
|
||||
environment: self.makeEnvironment(),
|
||||
workingDirectory: FilePath(cwd)
|
||||
) { execution, stdin, stdout, stderr in
|
||||
workingDirectory: FilePath(cwd))
|
||||
{ execution, stdin, stdout, stderr in
|
||||
self.didStart(execution)
|
||||
async let out: Void = self.stream(output: stdout, label: "stdout")
|
||||
async let err: Void = self.stream(output: stderr, label: "stderr")
|
||||
@@ -122,12 +122,10 @@ final class RelayProcessManager: ObservableObject {
|
||||
}
|
||||
|
||||
private func handleTermination(status: TerminationStatus) async {
|
||||
let code: Int32 = {
|
||||
switch status {
|
||||
case let .exited(exitCode): return exitCode
|
||||
case let .unhandledException(sig): return -Int32(sig)
|
||||
}
|
||||
}()
|
||||
let code: Int32 = switch status {
|
||||
case let .exited(exitCode): exitCode
|
||||
case let .unhandledException(sig): -Int32(sig)
|
||||
}
|
||||
|
||||
self.execution = nil
|
||||
if self.stopping || !self.desiredActive {
|
||||
@@ -161,7 +159,7 @@ final class RelayProcessManager: ObservableObject {
|
||||
}
|
||||
self.appendLog("[relay] failed: \(message)\n")
|
||||
self.logger.error("relay failed: \(message, privacy: .public)")
|
||||
if self.desiredActive && !self.shouldGiveUpAfterCrashes() {
|
||||
if self.desiredActive, !self.shouldGiveUpAfterCrashes() {
|
||||
self.status = .restarting
|
||||
self.recentCrashes.append(Date())
|
||||
self.startIfNeeded()
|
||||
@@ -200,7 +198,8 @@ final class RelayProcessManager: ObservableObject {
|
||||
// Keep it simple: rely on system-installed clawdis/warelay.
|
||||
// Default to `clawdis relay`; users can provide an override via env if needed.
|
||||
if let override = ProcessInfo.processInfo.environment["CLAWDIS_RELAY_CMD"],
|
||||
!override.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
!override.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
{
|
||||
return override.split(separator: " ").map(String.init)
|
||||
}
|
||||
|
||||
@@ -257,7 +256,8 @@ final class RelayProcessManager: ObservableObject {
|
||||
|
||||
private func defaultProjectRoot() -> URL {
|
||||
if let stored = UserDefaults.standard.string(forKey: Defaults.projectRootPath),
|
||||
let url = self.expandPath(stored) {
|
||||
let url = self.expandPath(stored)
|
||||
{
|
||||
return url
|
||||
}
|
||||
let fallback = FileManager.default.homeDirectoryForCurrentUser
|
||||
@@ -275,7 +275,7 @@ final class RelayProcessManager: ObservableObject {
|
||||
func projectRootPath() -> String {
|
||||
UserDefaults.standard.string(forKey: Defaults.projectRootPath)
|
||||
?? FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent("Projects/clawdis").path
|
||||
.appendingPathComponent("Projects/clawdis").path
|
||||
}
|
||||
|
||||
private func expandPath(_ path: String) -> URL? {
|
||||
|
||||
@@ -115,11 +115,9 @@ extension [String] {
|
||||
fileprivate func dedupedPreserveOrder() -> [String] {
|
||||
var seen = Set<String>()
|
||||
var result: [String] = []
|
||||
for item in self {
|
||||
if !seen.contains(item) {
|
||||
seen.insert(item)
|
||||
result.append(item)
|
||||
}
|
||||
for item in self where !seen.contains(item) {
|
||||
seen.insert(item)
|
||||
result.append(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ private enum InstallMethod: Equatable {
|
||||
let .npm(_, binary),
|
||||
let .go(_, binary),
|
||||
let .pnpm(_, _, binary):
|
||||
return binary
|
||||
binary
|
||||
case .gitClone:
|
||||
return nil
|
||||
nil
|
||||
case .mcporter:
|
||||
return "mcporter"
|
||||
"mcporter"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,80 +57,70 @@ struct ToolsSettings: View {
|
||||
url: URL(string: "https://github.com/steipete/mcporter")!,
|
||||
description: "MCP runtime/CLI to discover servers, run tools, and sync configs across AI clients.",
|
||||
method: .npm(package: "mcporter", binary: "mcporter"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "peekaboo",
|
||||
name: "Peekaboo",
|
||||
url: URL(string: "https://github.com/steipete/Peekaboo")!,
|
||||
description: "Lightning-fast macOS screenshots with AI vision helpers for step-by-step automation.",
|
||||
method: .brew(formula: "steipete/tap/peekaboo", binary: "peekaboo"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "camsnap",
|
||||
name: "camsnap",
|
||||
url: URL(string: "https://github.com/steipete/camsnap")!,
|
||||
description: "One command to grab frames, clips, or motion alerts from RTSP/ONVIF cameras.",
|
||||
method: .brew(formula: "steipete/tap/camsnap", binary: "camsnap"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "oracle",
|
||||
name: "oracle",
|
||||
url: URL(string: "https://github.com/steipete/oracle")!,
|
||||
description: "Runs OpenAI-ready agent workflows from the CLI with session replay and browser control.",
|
||||
method: .npm(package: "@steipete/oracle", binary: "oracle"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "eightctl",
|
||||
name: "eightctl",
|
||||
url: URL(string: "https://github.com/steipete/eightctl")!,
|
||||
description: "Control Eight Sleep Pods (temp, alarms, schedules, metrics) from scripts or cron.",
|
||||
method: .go(module: "github.com/steipete/eightctl/cmd/eightctl@latest", binary: "eightctl"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "imsg",
|
||||
name: "imsg",
|
||||
url: URL(string: "https://github.com/steipete/imsg")!,
|
||||
description: "CLI for macOS Messages: read/tail chats and send iMessage/SMS with attachments.",
|
||||
method: .go(module: "github.com/steipete/imsg/cmd/imsg@latest", binary: "imsg"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "spotify-player",
|
||||
name: "spotify-player",
|
||||
url: URL(string: "https://github.com/aome510/spotify-player")!,
|
||||
description: "Terminal Spotify client to queue, search, and control playback without leaving chat.",
|
||||
method: .brew(formula: "spotify_player", binary: "spotify_player"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "openhue-cli",
|
||||
name: "OpenHue CLI",
|
||||
url: URL(string: "https://github.com/openhue/openhue-cli")!,
|
||||
description: "Control Philips Hue lights from scripts—scenes, dimming, and automations.",
|
||||
method: .brew(formula: "openhue/cli/openhue-cli", binary: "openhue"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "openai-whisper",
|
||||
name: "OpenAI Whisper",
|
||||
url: URL(string: "https://github.com/openai/whisper")!,
|
||||
description: "On-device speech-to-text for quick note taking or voicemail transcription.",
|
||||
method: .brew(formula: "openai-whisper", binary: "whisper"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "gemini-cli",
|
||||
name: "Gemini CLI",
|
||||
url: URL(string: "https://github.com/google-gemini/gemini-cli")!,
|
||||
description: "Google Gemini models from the terminal for fast Q&A and web-grounded summaries.",
|
||||
method: .brew(formula: "gemini-cli", binary: "gemini"),
|
||||
kind: .tool
|
||||
),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "bird",
|
||||
name: "bird",
|
||||
@@ -139,10 +129,8 @@ struct ToolsSettings: View {
|
||||
method: .pnpm(
|
||||
repoPath: "\(NSHomeDirectory())/Projects/bird",
|
||||
script: "binary",
|
||||
binary: "bird"
|
||||
),
|
||||
kind: .tool
|
||||
),
|
||||
binary: "bird"),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "agent-tools",
|
||||
name: "agent-tools",
|
||||
@@ -150,10 +138,8 @@ struct ToolsSettings: View {
|
||||
description: "Collection of utilities and scripts tuned for autonomous agents and MCP clients.",
|
||||
method: .gitClone(
|
||||
url: "https://github.com/badlogic/agent-tools.git",
|
||||
destination: "\(NSHomeDirectory())/agent-tools"
|
||||
),
|
||||
kind: .tool
|
||||
),
|
||||
destination: "\(NSHomeDirectory())/agent-tools"),
|
||||
kind: .tool),
|
||||
ToolEntry(
|
||||
id: "gmail-mcp",
|
||||
name: "Gmail MCP",
|
||||
@@ -162,10 +148,8 @@ struct ToolsSettings: View {
|
||||
method: .mcporter(
|
||||
name: "gmail",
|
||||
command: "npx -y @gongrzhe/server-gmail-autoauth-mcp",
|
||||
summary: "Adds Gmail MCP via mcporter (stdio transport, auto-auth)."
|
||||
),
|
||||
kind: .mcp
|
||||
),
|
||||
summary: "Adds Gmail MCP via mcporter (stdio transport, auto-auth)."),
|
||||
kind: .mcp),
|
||||
ToolEntry(
|
||||
id: "google-calendar-mcp",
|
||||
name: "Google Calendar MCP",
|
||||
@@ -174,10 +158,8 @@ struct ToolsSettings: View {
|
||||
method: .mcporter(
|
||||
name: "google-calendar",
|
||||
command: "npx -y @cocal/google-calendar-mcp",
|
||||
summary: "Adds Google Calendar MCP via mcporter (stdio transport)."
|
||||
),
|
||||
kind: .mcp
|
||||
),
|
||||
summary: "Adds Google Calendar MCP via mcporter (stdio transport)."),
|
||||
kind: .mcp),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
@@ -188,8 +170,8 @@ struct ToolsSettings: View {
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
section(for: .tool, title: "CLI Tools")
|
||||
section(for: .mcp, title: "MCP Servers")
|
||||
self.section(for: .tool, title: "CLI Tools")
|
||||
self.section(for: .mcp, title: "MCP Servers")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +180,7 @@ struct ToolsSettings: View {
|
||||
}
|
||||
|
||||
private func section(for kind: ToolEntry.Kind, title: String) -> some View {
|
||||
let filtered = tools.filter { $0.kind == kind }
|
||||
let filtered = self.tools.filter { $0.kind == kind }
|
||||
return VStack(alignment: .leading, spacing: 10) {
|
||||
Text(title)
|
||||
.font(.callout.weight(.semibold))
|
||||
@@ -212,8 +194,7 @@ struct ToolsSettings: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.secondary.opacity(0.15), lineWidth: 1)
|
||||
)
|
||||
.stroke(Color.secondary.opacity(0.15), lineWidth: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,15 +212,15 @@ private struct ToolRow: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Link(tool.name, destination: tool.url)
|
||||
Link(self.tool.name, destination: self.tool.url)
|
||||
.font(.headline)
|
||||
Text(tool.description)
|
||||
Text(self.tool.description)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
Spacer()
|
||||
actionButton
|
||||
self.actionButton
|
||||
}
|
||||
|
||||
if let statusMessage, !statusMessage.isEmpty {
|
||||
@@ -248,12 +229,12 @@ private struct ToolRow: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.onAppear { refresh() }
|
||||
.onAppear { self.refresh() }
|
||||
}
|
||||
|
||||
private var actionButton: some View {
|
||||
VStack {
|
||||
switch state {
|
||||
switch self.state {
|
||||
case .installed:
|
||||
Label("Installed", systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
@@ -261,12 +242,12 @@ private struct ToolRow: View {
|
||||
case .installing:
|
||||
ProgressView().controlSize(.small)
|
||||
case .failed:
|
||||
Button("Retry") { install() }
|
||||
Button("Retry") { self.install() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
case .checking:
|
||||
ProgressView().controlSize(.small)
|
||||
case .notInstalled:
|
||||
Button("Install") { install() }
|
||||
Button("Install") { self.install() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
@@ -274,21 +255,21 @@ private struct ToolRow: View {
|
||||
|
||||
private func refresh() {
|
||||
Task {
|
||||
state = .checking
|
||||
let installed = await ToolInstaller.isInstalled(tool.method)
|
||||
self.state = .checking
|
||||
let installed = await ToolInstaller.isInstalled(self.tool.method)
|
||||
await MainActor.run {
|
||||
state = installed ? .installed : .notInstalled
|
||||
self.state = installed ? .installed : .notInstalled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func install() {
|
||||
Task {
|
||||
state = .installing
|
||||
let result = await ToolInstaller.install(tool.method)
|
||||
self.state = .installing
|
||||
let result = await ToolInstaller.install(self.tool.method)
|
||||
await MainActor.run {
|
||||
statusMessage = result.message
|
||||
state = result.installed ? .installed : .failed(result.message)
|
||||
self.statusMessage = result.message
|
||||
self.state = result.installed ? .installed : .failed(result.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,30 +286,30 @@ private enum ToolInstaller {
|
||||
static func isInstalled(_ method: InstallMethod) async -> Bool {
|
||||
switch method {
|
||||
case let .brew(formula, _):
|
||||
return await shellSucceeds("brew list --versions \(formula)")
|
||||
return await self.shellSucceeds("brew list --versions \(formula)")
|
||||
case let .npm(_, binary),
|
||||
let .go(_, binary),
|
||||
let .pnpm(_, _, binary):
|
||||
return await commandExists(binary)
|
||||
return await self.commandExists(binary)
|
||||
case let .gitClone(_, destination):
|
||||
return FileManager.default.fileExists(atPath: destination)
|
||||
case let .mcporter(name, _, _):
|
||||
guard await commandExists("mcporter") else { return false }
|
||||
return await shellSucceeds("mcporter config get \(name) --json")
|
||||
guard await self.commandExists("mcporter") else { return false }
|
||||
return await self.shellSucceeds("mcporter config get \(name) --json")
|
||||
}
|
||||
}
|
||||
|
||||
static func install(_ method: InstallMethod) async -> InstallResult {
|
||||
switch method {
|
||||
case let .brew(formula, _):
|
||||
return await runInstall("brew install \(formula)")
|
||||
return await self.runInstall("brew install \(formula)")
|
||||
case let .npm(package, _):
|
||||
return await runInstall("npm install -g \(package)")
|
||||
return await self.runInstall("npm install -g \(package)")
|
||||
case let .go(module, _):
|
||||
return await runInstall("GO111MODULE=on go install \(module)")
|
||||
return await self.runInstall("GO111MODULE=on go install \(module)")
|
||||
case let .pnpm(repoPath, script, _):
|
||||
let cmd = "cd \(escape(repoPath)) && pnpm install && pnpm run \(script)"
|
||||
return await runInstall(cmd)
|
||||
return await self.runInstall(cmd)
|
||||
case let .gitClone(url, destination):
|
||||
let cmd = """
|
||||
if [ -d \(escape(destination)) ]; then
|
||||
@@ -337,19 +318,19 @@ private enum ToolInstaller {
|
||||
git clone \(url) \(escape(destination))
|
||||
fi
|
||||
"""
|
||||
return await runInstall(cmd)
|
||||
return await self.runInstall(cmd)
|
||||
case let .mcporter(name, command, summary):
|
||||
let cmd = """
|
||||
mcporter config add \(name) --command "\(command)" --transport stdio --scope home --description "\(summary)"
|
||||
"""
|
||||
return await runInstall(cmd)
|
||||
return await self.runInstall(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static func commandExists(_ binary: String) async -> Bool {
|
||||
await shellSucceeds("command -v \(binary)")
|
||||
await self.shellSucceeds("command -v \(binary)")
|
||||
}
|
||||
|
||||
private static func shellSucceeds(_ command: String) async -> Bool {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
enum LaunchdManager {
|
||||
private static func runLaunchctl(_ args: [String]) {
|
||||
|
||||
@@ -103,7 +103,10 @@ final class VoiceWakeTester {
|
||||
domain: "VoiceWakeTester",
|
||||
code: 3,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Missing mic/speech privacy strings. Rebuild the mac app (scripts/restart-mac.sh) to include usage descriptions.",
|
||||
NSLocalizedDescriptionKey: """
|
||||
Missing mic/speech privacy strings. Rebuild the mac app (scripts/restart-mac.sh) \
|
||||
to include usage descriptions.
|
||||
""",
|
||||
])
|
||||
}
|
||||
|
||||
@@ -256,7 +259,8 @@ struct VoiceWakeSettings: View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
SettingsToggleRow(
|
||||
title: "Enable Voice Wake",
|
||||
subtitle: "Listen for a wake phrase (e.g. \"Claude\") before running voice commands. Voice recognition runs fully on-device.",
|
||||
subtitle: "Listen for a wake phrase (e.g. \"Claude\") before running voice commands. "
|
||||
+ "Voice recognition runs fully on-device.",
|
||||
binding: self.$state.swabbleEnabled)
|
||||
.disabled(!voiceWakeSupported)
|
||||
|
||||
@@ -314,7 +318,8 @@ struct VoiceWakeSettings: View {
|
||||
.stroke(Color.secondary.opacity(0.25), lineWidth: 1))
|
||||
|
||||
Text(
|
||||
"Clawdis reacts when any trigger appears in a transcription. Keep them short to avoid false positives.")
|
||||
"Clawdis reacts when any trigger appears in a transcription. "
|
||||
+ "Keep them short to avoid false positives.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -69,7 +69,7 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler,
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
private func loadPage() {
|
||||
let messagesJSON = self.initialMessagesJSON.replacingOccurrences(of: "</script>", with: "<\\/script>")
|
||||
@@ -112,7 +112,13 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler,
|
||||
"{}"
|
||||
}
|
||||
|
||||
let html = """
|
||||
let html = self.makeHTML(importMapJSON: importMapJSON, messagesJSON: messagesJSON)
|
||||
self.webView.loadHTMLString(html, baseURL: webChatURL)
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
private func makeHTML(importMapJSON: String, messagesJSON: String) -> String {
|
||||
"""
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -156,7 +162,7 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler,
|
||||
class NativeTransport {
|
||||
async *run(messages, userMessage, cfg, signal) {
|
||||
const result = await window.__clawdisSend({ type: 'chat', payload: { text: userMessage.content?.[0]?.text ?? '', sessionKey: '\(
|
||||
sessionKey)' } });
|
||||
self.sessionKey)' } });
|
||||
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
||||
const assistant = {
|
||||
role: 'assistant',
|
||||
@@ -225,9 +231,10 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler,
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.webView.loadHTMLString(html, baseURL: webChatURL)
|
||||
}
|
||||
|
||||
// swiftlint:enable line_length
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
webView.evaluateJavaScript("document.body.innerText") { result, error in
|
||||
if let error {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ClawdisIPC
|
||||
import Foundation
|
||||
import OSLog
|
||||
import ClawdisIPC
|
||||
|
||||
@objc protocol ClawdisXPCProtocol {
|
||||
func handle(_ data: Data, withReply reply: @escaping @Sendable (Data?, Error?) -> Void)
|
||||
|
||||
@@ -37,6 +37,7 @@ struct ClawdisCLI {
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
private static func parseCommandLine() throws -> Request {
|
||||
var args = Array(CommandLine.arguments.dropFirst())
|
||||
guard let command = args.first else { throw CLIError.help }
|
||||
@@ -126,6 +127,8 @@ struct ClawdisCLI {
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable cyclomatic_complexity
|
||||
|
||||
private static func send(request: Request) async throws -> Response {
|
||||
let conn = NSXPCConnection(machServiceName: serviceName)
|
||||
let interface = NSXPCInterface(with: ClawdisXPCProtocol.self)
|
||||
|
||||
Reference in New Issue
Block a user