Skip to content

Commit 1518ae3

Browse files
committed
fix(appintents): stable IDs, Shortcut auto‑activate, Spotlight VM indexing
- Prevent ActionNotFound by adding static action IDs - Activate/foreground UTM when starting a VM via Shortcuts - Index VM entities (IndexedEntity) and reindex on launch/changes for Spotlight free‑text resolution feat(Intents): activate UTM before starting VM via Shortcut feat(spotlight): index VM entities (IndexedEntity) and reindex on launch/changes to fix Spotlight free‑text resolution
1 parent 3de3c4a commit 1518ae3

File tree

5 files changed

+98
-3
lines changed

5 files changed

+98
-3
lines changed

Intents/UTMActionIntent.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
//
1616

1717
import AppIntents
18+
#if os(macOS)
19+
import AppKit
20+
#endif
1821

1922
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
2023
struct UTMStatusActionIntent: UTMIntent {
24+
static var id: String = "UTMStatusActionIntent"
2125
static let title: LocalizedStringResource = "Get Virtual Machine Status"
2226
static let description = IntentDescription("Get the status of a virtual machine.")
2327
static var parameterSummary: some ParameterSummary {
@@ -38,6 +42,7 @@ struct UTMStatusActionIntent: UTMIntent {
3842

3943
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
4044
struct UTMStartActionIntent: UTMIntent {
45+
static var id: String = "UTMStartActionIntent"
4146
static let title: LocalizedStringResource = "Start Virtual Machine"
4247
static let description = IntentDescription("Start a virtual machine.")
4348
static var parameterSummary: some ParameterSummary {
@@ -78,7 +83,12 @@ struct UTMStartActionIntent: UTMIntent {
7883
}
7984
options.insert(.bootDisposibleMode)
8085
}
86+
#if os(macOS)
87+
// Ensure the app comes to the foreground before presenting VM UI
88+
NSApp.activate(ignoringOtherApps: true)
89+
#endif
8190
data.run(vm: boxed, options: options)
91+
// For platforms that support foreground continuation, request it (no-op on older SDKs).
8292
if !vm.isHeadless {
8393
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *), systemContext.currentMode.canContinueInForeground {
8494
try await continueInForeground(alwaysConfirm: false)
@@ -90,6 +100,7 @@ struct UTMStartActionIntent: UTMIntent {
90100

91101
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
92102
struct UTMStopActionIntent: UTMIntent {
103+
static var id: String = "UTMStopActionIntent"
93104
static let title: LocalizedStringResource = "Stop Virtual Machine"
94105
static let description = IntentDescription("Stop a virtual machine.")
95106
static var parameterSummary: some ParameterSummary {
@@ -128,6 +139,7 @@ extension UTMVirtualMachineStopMethod: AppEnum {
128139

129140
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
130141
struct UTMPauseActionIntent: UTMIntent {
142+
static var id: String = "UTMPauseActionIntent"
131143
static let title: LocalizedStringResource = "Pause Virtual Machine"
132144
static let description = IntentDescription("Pause a virtual machine.")
133145
static var parameterSummary: some ParameterSummary {
@@ -157,6 +169,7 @@ struct UTMPauseActionIntent: UTMIntent {
157169

158170
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
159171
struct UTMResumeActionIntent: UTMIntent {
172+
static var id: String = "UTMResumeActionIntent"
160173
static let title: LocalizedStringResource = "Resume Virtual Machine"
161174
static let description = IntentDescription("Resume a virtual machine.")
162175
static var parameterSummary: some ParameterSummary {
@@ -172,7 +185,7 @@ struct UTMResumeActionIntent: UTMIntent {
172185
@MainActor
173186
func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
174187
try await vm.resume()
175-
if vm.isHeadless {
188+
if !vm.isHeadless {
176189
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *), systemContext.currentMode.canContinueInForeground {
177190
try await continueInForeground(alwaysConfirm: false)
178191
}
@@ -183,6 +196,7 @@ struct UTMResumeActionIntent: UTMIntent {
183196

184197
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
185198
struct UTMRestartActionIntent: UTMIntent {
199+
static var id: String = "UTMRestartActionIntent"
186200
static let title: LocalizedStringResource = "Restart Virtual Machine"
187201
static let description = IntentDescription("Restart a virtual machine.")
188202
static var parameterSummary: some ParameterSummary {
@@ -198,7 +212,7 @@ struct UTMRestartActionIntent: UTMIntent {
198212
@MainActor
199213
func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
200214
try await vm.restart()
201-
if vm.isHeadless {
215+
if !vm.isHeadless {
202216
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *), systemContext.currentMode.canContinueInForeground {
203217
try await continueInForeground(alwaysConfirm: false)
204218
}

Intents/UTMEntityIndexing.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// Copyright © 2025 osy. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import Foundation
18+
import AppIntents
19+
import CoreSpotlight
20+
21+
enum UTMEntityIndexer {
22+
/// Rebuild the Spotlight index for all VM entities (macOS 15+/iOS 18+).
23+
@MainActor
24+
static func reindexAll(with data: UTMData) async {
25+
guard #available(iOS 18, macOS 15, tvOS 18, watchOS 11, *) else {
26+
return
27+
}
28+
let entities: [UTMVirtualMachineEntity] = data.virtualMachines.map { UTMVirtualMachineEntity(from: $0) }
29+
do {
30+
let index = CSSearchableIndex.default()
31+
try await index.deleteAppEntities(ofType: UTMVirtualMachineEntity.self)
32+
if !entities.isEmpty {
33+
try await index.indexAppEntities(entities)
34+
logger.debug("[Indexing] Indexed \(entities.count) VM entities for Spotlight")
35+
} else {
36+
logger.debug("[Indexing] Cleared VM entity index (no entities)")
37+
}
38+
} catch {
39+
logger.error("[Indexing] Failed to (re)index VM entities: \(error.localizedDescription)")
40+
}
41+
}
42+
}
43+

Intents/UTMInputIntent.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ private let kDelayNs: UInt64 = 20000000
2020

2121
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
2222
struct UTMSendScanCodeIntent: UTMIntent {
23+
static var id: String = "UTMSendScanCodeIntent"
2324
static let title: LocalizedStringResource = "Send Scan Code"
2425
static let description = IntentDescription("Send a sequence of raw keyboard scan codes to the virtual machine. Only supported on QEMU backend.")
2526
static var parameterSummary: some ParameterSummary {
@@ -63,6 +64,7 @@ struct UTMSendScanCodeIntent: UTMIntent {
6364

6465
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
6566
struct UTMSendKeystrokesIntent: UTMIntent {
67+
static var id: String = "UTMSendKeystrokesIntent"
6668
static let title: LocalizedStringResource = "Send Keystrokes"
6769
static let description = IntentDescription("Send text as a sequence of keystrokes to the virtual machine. Only supported on QEMU backend.")
6870
static var parameterSummary: some ParameterSummary {
@@ -154,6 +156,7 @@ struct UTMSendKeystrokesIntent: UTMIntent {
154156

155157
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
156158
struct UTMMouseClickIntent: UTMIntent {
159+
static var id: String = "UTMMouseClickIntent"
157160
static let title: LocalizedStringResource = "Send Mouse Click"
158161
static let description = IntentDescription("Send a mouse position and click to the virtual machine. Only supported on QEMU backend.")
159162
static var parameterSummary: some ParameterSummary {

Intents/UTMVirtualMachineEntity.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//
1616

1717
import AppIntents
18+
import CoreSpotlight
1819

1920
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
2021
struct UTMVirtualMachineEntity: AppEntity {
@@ -63,7 +64,18 @@ struct UTMVirtualMachineEntity: AppEntity {
6364

6465
@available(iOS 18, macOS 15, *)
6566
extension UTMVirtualMachineEntity: IndexedEntity {
66-
67+
// Spotlight searchable attributes for this VM entity
68+
var attributeSet: CSSearchableItemAttributeSet {
69+
let attrs = CSSearchableItemAttributeSet()
70+
attrs.title = name
71+
attrs.contentDescription = description
72+
if let iconURL {
73+
attrs.thumbnailURL = iconURL
74+
}
75+
// Light keywords for common queries
76+
attrs.keywords = ["vm", "virtual machine", String(describing: state)]
77+
return attrs
78+
}
6779
}
6880

6981
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)

Platform/macOS/UTMApp.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import SwiftUI
1818
import AppIntents
19+
import CoreSpotlight
1920

2021
struct UTMApp: App {
2122
let data: UTMData
@@ -35,12 +36,16 @@ struct UTMApp: App {
3536
.onAppear {
3637
appDelegate.data = data
3738
NSApp.scriptingDelegate = appDelegate
39+
Task { await reindexVms() }
3840
}
3941
.onReceive(.vmSessionError) { notification in
4042
if let message = notification.userInfo?["Message"] as? String {
4143
data.showErrorAlert(message: message)
4244
}
4345
}
46+
.onChange(of: data.virtualMachines) { _ in
47+
Task { await reindexVms() }
48+
}
4449
}
4550

4651
@SceneBuilder
@@ -81,4 +86,22 @@ struct UTMApp: App {
8186
return oldBody
8287
}
8388
}
89+
90+
@MainActor
91+
private func reindexVms() async {
92+
guard #available(macOS 15, *) else { return }
93+
let entities = data.virtualMachines.map { UTMVirtualMachineEntity(from: $0) }
94+
do {
95+
let index = CSSearchableIndex.default()
96+
try await index.deleteAppEntities(ofType: UTMVirtualMachineEntity.self)
97+
if !entities.isEmpty {
98+
try await index.indexAppEntities(entities)
99+
logger.debug("[Indexing] Indexed \(entities.count) VM entities for Spotlight")
100+
} else {
101+
logger.debug("[Indexing] Cleared VM entity index (no entities)")
102+
}
103+
} catch {
104+
logger.error("[Indexing] Failed to (re)index VM entities: \(error.localizedDescription)")
105+
}
106+
}
84107
}

0 commit comments

Comments
 (0)