fix(macos): hide Restart Gateway when remote
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
### Fixes
|
### 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: 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 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.
|
- 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).
|
- 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).
|
- 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())
|
RuntimeLocator.resolve(searchPaths: self.preferredPaths())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func runtimeResolution(searchPaths: [String]?) -> Result<RuntimeResolution, RuntimeResolutionError> {
|
||||||
|
RuntimeLocator.resolve(searchPaths: searchPaths ?? self.preferredPaths())
|
||||||
|
}
|
||||||
|
|
||||||
static func makeRuntimeCommand(
|
static func makeRuntimeCommand(
|
||||||
runtime: RuntimeResolution,
|
runtime: RuntimeResolution,
|
||||||
entrypoint: String,
|
entrypoint: String,
|
||||||
@@ -152,8 +156,8 @@ enum CommandResolver {
|
|||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
static func findExecutable(named name: String) -> String? {
|
static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? {
|
||||||
for dir in self.preferredPaths() {
|
for dir in (searchPaths ?? self.preferredPaths()) {
|
||||||
let candidate = (dir as NSString).appendingPathComponent(name)
|
let candidate = (dir as NSString).appendingPathComponent(name)
|
||||||
if FileManager.default.isExecutableFile(atPath: candidate) {
|
if FileManager.default.isExecutableFile(atPath: candidate) {
|
||||||
return candidate
|
return candidate
|
||||||
@@ -162,8 +166,14 @@ enum CommandResolver {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func clawdisExecutable() -> String? {
|
static func clawdisExecutable(searchPaths: [String]? = nil) -> String? {
|
||||||
self.findExecutable(named: self.helperName)
|
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? {
|
static func nodeCliPath() -> String? {
|
||||||
@@ -171,17 +181,18 @@ enum CommandResolver {
|
|||||||
return FileManager.default.isReadableFile(atPath: candidate) ? candidate : nil
|
return FileManager.default.isReadableFile(atPath: candidate) ? candidate : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func hasAnyClawdisInvoker() -> Bool {
|
static func hasAnyClawdisInvoker(searchPaths: [String]? = nil) -> Bool {
|
||||||
if self.clawdisExecutable() != nil { return true }
|
if self.clawdisExecutable(searchPaths: searchPaths) != nil { return true }
|
||||||
if self.findExecutable(named: "pnpm") != nil { return true }
|
if self.findExecutable(named: "pnpm", searchPaths: searchPaths) != nil { return true }
|
||||||
if self.findExecutable(named: "node") != nil, self.nodeCliPath() != nil { return true }
|
if self.findExecutable(named: "node", searchPaths: searchPaths) != nil, self.nodeCliPath() != nil { return true }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
static func clawdisNodeCommand(
|
static func clawdisNodeCommand(
|
||||||
subcommand: String,
|
subcommand: String,
|
||||||
extraArgs: [String] = [],
|
extraArgs: [String] = [],
|
||||||
defaults: UserDefaults = .standard) -> [String]
|
defaults: UserDefaults = .standard,
|
||||||
|
searchPaths: [String]? = nil) -> [String]
|
||||||
{
|
{
|
||||||
let settings = self.connectionSettings(defaults: defaults)
|
let settings = self.connectionSettings(defaults: defaults)
|
||||||
if settings.mode == .remote, let ssh = self.sshNodeCommand(
|
if settings.mode == .remote, let ssh = self.sshNodeCommand(
|
||||||
@@ -192,25 +203,29 @@ enum CommandResolver {
|
|||||||
return ssh
|
return ssh
|
||||||
}
|
}
|
||||||
|
|
||||||
let runtimeResult = self.runtimeResolution()
|
let runtimeResult = self.runtimeResolution(searchPaths: searchPaths)
|
||||||
|
|
||||||
switch runtimeResult {
|
switch runtimeResult {
|
||||||
case let .success(runtime):
|
case let .success(runtime):
|
||||||
if let clawdisPath = self.clawdisExecutable() {
|
let root = self.projectRoot()
|
||||||
|
if let clawdisPath = self.projectClawdisExecutable(projectRoot: root) {
|
||||||
return [clawdisPath, subcommand] + extraArgs
|
return [clawdisPath, subcommand] + extraArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
if let entry = self.gatewayEntrypoint(in: self.projectRoot()) {
|
if let entry = self.gatewayEntrypoint(in: root) {
|
||||||
return self.makeRuntimeCommand(
|
return self.makeRuntimeCommand(
|
||||||
runtime: runtime,
|
runtime: runtime,
|
||||||
entrypoint: entry,
|
entrypoint: entry,
|
||||||
subcommand: subcommand,
|
subcommand: subcommand,
|
||||||
extraArgs: extraArgs)
|
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.
|
// Use --silent to avoid pnpm lifecycle banners that would corrupt JSON outputs.
|
||||||
return [pnpm, "--silent", "clawdis", subcommand] + extraArgs
|
return [pnpm, "--silent", "clawdis", subcommand] + extraArgs
|
||||||
}
|
}
|
||||||
|
if let clawdisPath = self.clawdisExecutable(searchPaths: searchPaths) {
|
||||||
|
return [clawdisPath, subcommand] + extraArgs
|
||||||
|
}
|
||||||
|
|
||||||
let missingEntry = """
|
let missingEntry = """
|
||||||
clawdis entrypoint missing (looked for dist/index.js or bin/clawdis.js); run pnpm build.
|
clawdis entrypoint missing (looked for dist/index.js or bin/clawdis.js); run pnpm build.
|
||||||
@@ -226,9 +241,10 @@ enum CommandResolver {
|
|||||||
static func clawdisCommand(
|
static func clawdisCommand(
|
||||||
subcommand: String,
|
subcommand: String,
|
||||||
extraArgs: [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
|
// MARK: - SSH helpers
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
import Observation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct DebugSettings: View {
|
struct DebugSettings: View {
|
||||||
|
@Bindable var state: AppState
|
||||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||||
private let labelColumnWidth: CGFloat = 140
|
private let labelColumnWidth: CGFloat = 140
|
||||||
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
|
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
|
||||||
@@ -36,6 +38,10 @@ struct DebugSettings: View {
|
|||||||
@State private var canvasEvalResult: String?
|
@State private var canvasEvalResult: String?
|
||||||
@State private var canvasSnapshotPath: String?
|
@State private var canvasSnapshotPath: String?
|
||||||
|
|
||||||
|
init(state: AppState = AppStateStore.shared) {
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
@@ -194,7 +200,9 @@ struct DebugSettings: View {
|
|||||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2)))
|
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2)))
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Button("Restart Gateway") { DebugActions.restartGateway() }
|
if self.canRestartGateway {
|
||||||
|
Button("Restart Gateway") { DebugActions.restartGateway() }
|
||||||
|
}
|
||||||
Button("Clear log") { GatewayProcessManager.shared.clearLog() }
|
Button("Clear log") { GatewayProcessManager.shared.clearLog() }
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
@@ -762,6 +770,10 @@ struct DebugSettings: View {
|
|||||||
CommandResolver.connectionSettings().mode == .remote
|
CommandResolver.connectionSettings().mode == .remote
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var canRestartGateway: Bool {
|
||||||
|
self.state.connectionMode == .local && !self.attachExistingGatewayOnly
|
||||||
|
}
|
||||||
|
|
||||||
private func configURL() -> URL {
|
private func configURL() -> URL {
|
||||||
FileManager.default.homeDirectoryForCurrentUser
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdis")
|
.appendingPathComponent(".clawdis")
|
||||||
@@ -902,7 +914,7 @@ private struct PlainSettingsGroupBoxStyle: GroupBoxStyle {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct DebugSettings_Previews: PreviewProvider {
|
struct DebugSettings_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
DebugSettings()
|
DebugSettings(state: .preview)
|
||||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -910,7 +922,7 @@ struct DebugSettings_Previews: PreviewProvider {
|
|||||||
@MainActor
|
@MainActor
|
||||||
extension DebugSettings {
|
extension DebugSettings {
|
||||||
static func exerciseForTesting() async {
|
static func exerciseForTesting() async {
|
||||||
let view = DebugSettings()
|
let view = DebugSettings(state: .preview)
|
||||||
view.modelsCount = 3
|
view.modelsCount = 3
|
||||||
view.modelsLoading = false
|
view.modelsLoading = false
|
||||||
view.modelsError = "Failed to load models"
|
view.modelsError = "Failed to load models"
|
||||||
|
|||||||
@@ -209,10 +209,12 @@ struct MenuContent: View {
|
|||||||
Label("Send Test Notification", systemImage: "bell")
|
Label("Send Test Notification", systemImage: "bell")
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Button {
|
if self.state.connectionMode == .local, !AppStateStore.attachExistingGatewayOnly {
|
||||||
DebugActions.restartGateway()
|
Button {
|
||||||
} label: {
|
DebugActions.restartGateway()
|
||||||
Label("Restart Gateway", systemImage: "arrow.clockwise")
|
} label: {
|
||||||
|
Label("Restart Gateway", systemImage: "arrow.clockwise")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
DebugActions.restartApp()
|
DebugActions.restartApp()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ struct SettingsRootView: View {
|
|||||||
.tag(SettingsTab.permissions)
|
.tag(SettingsTab.permissions)
|
||||||
|
|
||||||
if self.state.debugPaneEnabled {
|
if self.state.debugPaneEnabled {
|
||||||
DebugSettings()
|
DebugSettings(state: self.state)
|
||||||
.tabItem { Label("Debug", systemImage: "ant") }
|
.tabItem { Label("Debug", systemImage: "ant") }
|
||||||
.tag(SettingsTab.debug)
|
.tag(SettingsTab.debug)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,17 @@ import Testing
|
|||||||
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
|
||||||
try self.makeExec(at: scriptPath)
|
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.count >= 3)
|
||||||
#expect(cmd[0] == nodePath.path)
|
if cmd.count >= 3 {
|
||||||
#expect(cmd[1] == scriptPath.path)
|
#expect(cmd[0] == nodePath.path)
|
||||||
#expect(cmd[2] == "rpc")
|
#expect(cmd[1] == scriptPath.path)
|
||||||
|
#expect(cmd[2] == "rpc")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func fallsBackToPnpm() async throws {
|
@Test func fallsBackToPnpm() async throws {
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ struct ConnectionsSettingsSmokeTests {
|
|||||||
elapsedMs: 120,
|
elapsedMs: 120,
|
||||||
bot: ProvidersStatusSnapshot.TelegramBot(id: 123, username: "clawdisbot"),
|
bot: ProvidersStatusSnapshot.TelegramBot(id: 123, username: "clawdisbot"),
|
||||||
webhook: ProvidersStatusSnapshot.TelegramWebhook(url: "https://example.com/hook", hasCustomCert: false)),
|
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.whatsappLoginMessage = "Scan QR"
|
||||||
store.whatsappLoginQrDataUrl =
|
store.whatsappLoginQrDataUrl =
|
||||||
@@ -92,7 +93,8 @@ struct ConnectionsSettingsSmokeTests {
|
|||||||
elapsedMs: 120,
|
elapsedMs: 120,
|
||||||
bot: nil,
|
bot: nil,
|
||||||
webhook: nil),
|
webhook: nil),
|
||||||
lastProbeAt: 1_700_000_100_000))
|
lastProbeAt: 1_700_000_100_000),
|
||||||
|
discord: nil)
|
||||||
|
|
||||||
let view = ConnectionsSettings(store: store)
|
let view = ConnectionsSettings(store: store)
|
||||||
_ = view.body
|
_ = view.body
|
||||||
|
|||||||
Reference in New Issue
Block a user