fix(macos): hide Restart Gateway when remote
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
### Fixes
|
||||
- macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.
|
||||
- macOS menu: add a Talk Mode action alongside the Open Dashboard/Chat/Canvas entries.
|
||||
- macOS Debug: hide “Restart Gateway” when the app won’t start a local gateway (remote mode / attach-only).
|
||||
- macOS Talk Mode: orb overlay refresh, ElevenLabs request logging, API key status in settings, and auto-select first voice when none is configured.
|
||||
- iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).
|
||||
- macOS menu: device list now uses `node.list` (devices only; no agent/tool presence entries).
|
||||
|
||||
@@ -16,6 +16,10 @@ enum CommandResolver {
|
||||
RuntimeLocator.resolve(searchPaths: self.preferredPaths())
|
||||
}
|
||||
|
||||
static func runtimeResolution(searchPaths: [String]?) -> Result<RuntimeResolution, RuntimeResolutionError> {
|
||||
RuntimeLocator.resolve(searchPaths: searchPaths ?? self.preferredPaths())
|
||||
}
|
||||
|
||||
static func makeRuntimeCommand(
|
||||
runtime: RuntimeResolution,
|
||||
entrypoint: String,
|
||||
@@ -152,8 +156,8 @@ enum CommandResolver {
|
||||
return paths
|
||||
}
|
||||
|
||||
static func findExecutable(named name: String) -> String? {
|
||||
for dir in self.preferredPaths() {
|
||||
static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? {
|
||||
for dir in (searchPaths ?? self.preferredPaths()) {
|
||||
let candidate = (dir as NSString).appendingPathComponent(name)
|
||||
if FileManager.default.isExecutableFile(atPath: candidate) {
|
||||
return candidate
|
||||
@@ -162,8 +166,14 @@ enum CommandResolver {
|
||||
return nil
|
||||
}
|
||||
|
||||
static func clawdisExecutable() -> String? {
|
||||
self.findExecutable(named: self.helperName)
|
||||
static func clawdisExecutable(searchPaths: [String]? = nil) -> String? {
|
||||
self.findExecutable(named: self.helperName, searchPaths: searchPaths)
|
||||
}
|
||||
|
||||
static func projectClawdisExecutable(projectRoot: URL? = nil) -> String? {
|
||||
let root = projectRoot ?? self.projectRoot()
|
||||
let candidate = root.appendingPathComponent("node_modules/.bin").appendingPathComponent(self.helperName).path
|
||||
return FileManager.default.isExecutableFile(atPath: candidate) ? candidate : nil
|
||||
}
|
||||
|
||||
static func nodeCliPath() -> String? {
|
||||
@@ -171,17 +181,18 @@ enum CommandResolver {
|
||||
return FileManager.default.isReadableFile(atPath: candidate) ? candidate : nil
|
||||
}
|
||||
|
||||
static func hasAnyClawdisInvoker() -> Bool {
|
||||
if self.clawdisExecutable() != nil { return true }
|
||||
if self.findExecutable(named: "pnpm") != nil { return true }
|
||||
if self.findExecutable(named: "node") != nil, self.nodeCliPath() != nil { return true }
|
||||
static func hasAnyClawdisInvoker(searchPaths: [String]? = nil) -> Bool {
|
||||
if self.clawdisExecutable(searchPaths: searchPaths) != nil { return true }
|
||||
if self.findExecutable(named: "pnpm", searchPaths: searchPaths) != nil { return true }
|
||||
if self.findExecutable(named: "node", searchPaths: searchPaths) != nil, self.nodeCliPath() != nil { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
static func clawdisNodeCommand(
|
||||
subcommand: String,
|
||||
extraArgs: [String] = [],
|
||||
defaults: UserDefaults = .standard) -> [String]
|
||||
defaults: UserDefaults = .standard,
|
||||
searchPaths: [String]? = nil) -> [String]
|
||||
{
|
||||
let settings = self.connectionSettings(defaults: defaults)
|
||||
if settings.mode == .remote, let ssh = self.sshNodeCommand(
|
||||
@@ -192,25 +203,29 @@ enum CommandResolver {
|
||||
return ssh
|
||||
}
|
||||
|
||||
let runtimeResult = self.runtimeResolution()
|
||||
let runtimeResult = self.runtimeResolution(searchPaths: searchPaths)
|
||||
|
||||
switch runtimeResult {
|
||||
case let .success(runtime):
|
||||
if let clawdisPath = self.clawdisExecutable() {
|
||||
let root = self.projectRoot()
|
||||
if let clawdisPath = self.projectClawdisExecutable(projectRoot: root) {
|
||||
return [clawdisPath, subcommand] + extraArgs
|
||||
}
|
||||
|
||||
if let entry = self.gatewayEntrypoint(in: self.projectRoot()) {
|
||||
if let entry = self.gatewayEntrypoint(in: root) {
|
||||
return self.makeRuntimeCommand(
|
||||
runtime: runtime,
|
||||
entrypoint: entry,
|
||||
subcommand: subcommand,
|
||||
extraArgs: extraArgs)
|
||||
}
|
||||
if let pnpm = self.findExecutable(named: "pnpm") {
|
||||
if let pnpm = self.findExecutable(named: "pnpm", searchPaths: searchPaths) {
|
||||
// Use --silent to avoid pnpm lifecycle banners that would corrupt JSON outputs.
|
||||
return [pnpm, "--silent", "clawdis", subcommand] + extraArgs
|
||||
}
|
||||
if let clawdisPath = self.clawdisExecutable(searchPaths: searchPaths) {
|
||||
return [clawdisPath, subcommand] + extraArgs
|
||||
}
|
||||
|
||||
let missingEntry = """
|
||||
clawdis entrypoint missing (looked for dist/index.js or bin/clawdis.js); run pnpm build.
|
||||
@@ -226,9 +241,10 @@ enum CommandResolver {
|
||||
static func clawdisCommand(
|
||||
subcommand: String,
|
||||
extraArgs: [String] = [],
|
||||
defaults: UserDefaults = .standard) -> [String]
|
||||
defaults: UserDefaults = .standard,
|
||||
searchPaths: [String]? = nil) -> [String]
|
||||
{
|
||||
self.clawdisNodeCommand(subcommand: subcommand, extraArgs: extraArgs, defaults: defaults)
|
||||
self.clawdisNodeCommand(subcommand: subcommand, extraArgs: extraArgs, defaults: defaults, searchPaths: searchPaths)
|
||||
}
|
||||
|
||||
// MARK: - SSH helpers
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import AppKit
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct DebugSettings: View {
|
||||
@Bindable var state: AppState
|
||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||
private let labelColumnWidth: CGFloat = 140
|
||||
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
|
||||
@@ -36,6 +38,10 @@ struct DebugSettings: View {
|
||||
@State private var canvasEvalResult: String?
|
||||
@State private var canvasSnapshotPath: String?
|
||||
|
||||
init(state: AppState = AppStateStore.shared) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
@@ -194,7 +200,9 @@ struct DebugSettings: View {
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2)))
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Restart Gateway") { DebugActions.restartGateway() }
|
||||
if self.canRestartGateway {
|
||||
Button("Restart Gateway") { DebugActions.restartGateway() }
|
||||
}
|
||||
Button("Clear log") { GatewayProcessManager.shared.clearLog() }
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
@@ -762,6 +770,10 @@ struct DebugSettings: View {
|
||||
CommandResolver.connectionSettings().mode == .remote
|
||||
}
|
||||
|
||||
private var canRestartGateway: Bool {
|
||||
self.state.connectionMode == .local && !self.attachExistingGatewayOnly
|
||||
}
|
||||
|
||||
private func configURL() -> URL {
|
||||
FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".clawdis")
|
||||
@@ -902,7 +914,7 @@ private struct PlainSettingsGroupBoxStyle: GroupBoxStyle {
|
||||
#if DEBUG
|
||||
struct DebugSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DebugSettings()
|
||||
DebugSettings(state: .preview)
|
||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||
}
|
||||
}
|
||||
@@ -910,7 +922,7 @@ struct DebugSettings_Previews: PreviewProvider {
|
||||
@MainActor
|
||||
extension DebugSettings {
|
||||
static func exerciseForTesting() async {
|
||||
let view = DebugSettings()
|
||||
let view = DebugSettings(state: .preview)
|
||||
view.modelsCount = 3
|
||||
view.modelsLoading = false
|
||||
view.modelsError = "Failed to load models"
|
||||
|
||||
@@ -209,10 +209,12 @@ struct MenuContent: View {
|
||||
Label("Send Test Notification", systemImage: "bell")
|
||||
}
|
||||
Divider()
|
||||
Button {
|
||||
DebugActions.restartGateway()
|
||||
} label: {
|
||||
Label("Restart Gateway", systemImage: "arrow.clockwise")
|
||||
if self.state.connectionMode == .local, !AppStateStore.attachExistingGatewayOnly {
|
||||
Button {
|
||||
DebugActions.restartGateway()
|
||||
} label: {
|
||||
Label("Restart Gateway", systemImage: "arrow.clockwise")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
DebugActions.restartApp()
|
||||
|
||||
@@ -57,7 +57,7 @@ struct SettingsRootView: View {
|
||||
.tag(SettingsTab.permissions)
|
||||
|
||||
if self.state.debugPaneEnabled {
|
||||
DebugSettings()
|
||||
DebugSettings(state: self.state)
|
||||
.tabItem { Label("Debug", systemImage: "ant") }
|
||||
.tag(SettingsTab.debug)
|
||||
}
|
||||
|
||||
@@ -52,12 +52,17 @@ import Testing
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
|
||||
try self.makeExec(at: scriptPath)
|
||||
|
||||
let cmd = CommandResolver.clawdisCommand(subcommand: "rpc", defaults: defaults)
|
||||
let cmd = CommandResolver.clawdisCommand(
|
||||
subcommand: "rpc",
|
||||
defaults: defaults,
|
||||
searchPaths: [tmp.appendingPathComponent("node_modules/.bin").path])
|
||||
|
||||
#expect(cmd.count >= 3)
|
||||
#expect(cmd[0] == nodePath.path)
|
||||
#expect(cmd[1] == scriptPath.path)
|
||||
#expect(cmd[2] == "rpc")
|
||||
if cmd.count >= 3 {
|
||||
#expect(cmd[0] == nodePath.path)
|
||||
#expect(cmd[1] == scriptPath.path)
|
||||
#expect(cmd[2] == "rpc")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func fallsBackToPnpm() async throws {
|
||||
|
||||
@@ -43,7 +43,8 @@ struct ConnectionsSettingsSmokeTests {
|
||||
elapsedMs: 120,
|
||||
bot: ProvidersStatusSnapshot.TelegramBot(id: 123, username: "clawdisbot"),
|
||||
webhook: ProvidersStatusSnapshot.TelegramWebhook(url: "https://example.com/hook", hasCustomCert: false)),
|
||||
lastProbeAt: 1_700_000_050_000))
|
||||
lastProbeAt: 1_700_000_050_000),
|
||||
discord: nil)
|
||||
|
||||
store.whatsappLoginMessage = "Scan QR"
|
||||
store.whatsappLoginQrDataUrl =
|
||||
@@ -92,7 +93,8 @@ struct ConnectionsSettingsSmokeTests {
|
||||
elapsedMs: 120,
|
||||
bot: nil,
|
||||
webhook: nil),
|
||||
lastProbeAt: 1_700_000_100_000))
|
||||
lastProbeAt: 1_700_000_100_000),
|
||||
discord: nil)
|
||||
|
||||
let view = ConnectionsSettings(store: store)
|
||||
_ = view.body
|
||||
|
||||
Reference in New Issue
Block a user