palmier 0.8.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +13 -0
- package/README.md +11 -11
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/gemini.js +3 -2
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/shared-prompt.d.ts +0 -3
- package/dist/agents/shared-prompt.js +0 -3
- package/dist/app-registry.d.ts +10 -0
- package/dist/app-registry.js +44 -0
- package/dist/commands/info.d.ts +0 -3
- package/dist/commands/info.js +0 -5
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +2 -11
- package/dist/commands/pair.d.ts +1 -4
- package/dist/commands/pair.js +1 -12
- package/dist/commands/restart.d.ts +0 -3
- package/dist/commands/restart.js +0 -3
- package/dist/commands/run.d.ts +1 -14
- package/dist/commands/run.js +18 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +33 -27
- package/dist/config.d.ts +0 -8
- package/dist/config.js +0 -8
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +6 -21
- package/dist/event-queues.js +6 -21
- package/dist/events.d.ts +0 -6
- package/dist/events.js +1 -9
- package/dist/index.js +0 -1
- package/dist/mcp-handler.js +1 -2
- package/dist/mcp-tools.d.ts +0 -3
- package/dist/mcp-tools.js +14 -18
- package/dist/nats-client.d.ts +0 -3
- package/dist/nats-client.js +1 -4
- package/dist/pending-requests.d.ts +4 -18
- package/dist/pending-requests.js +4 -18
- package/dist/platform/index.d.ts +1 -4
- package/dist/platform/index.js +1 -4
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/platform.d.ts +1 -4
- package/dist/platform/windows.d.ts +2 -5
- package/dist/platform/windows.js +19 -39
- package/dist/pwa/assets/index-B0F9mtid.css +1 -0
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-CF-N8Di6.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-BpM3fNCn.js → web-Z1623me-.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.d.ts +0 -6
- package/dist/rpc-handler.js +19 -48
- package/dist/spawn-command.d.ts +10 -25
- package/dist/spawn-command.js +7 -15
- package/dist/task.d.ts +6 -64
- package/dist/task.js +7 -70
- package/dist/transports/http-transport.d.ts +0 -4
- package/dist/transports/http-transport.js +6 -28
- package/dist/transports/nats-transport.d.ts +0 -4
- package/dist/transports/nats-transport.js +3 -9
- package/dist/types.d.ts +3 -7
- package/dist/update-checker.d.ts +1 -4
- package/dist/update-checker.js +2 -5
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +170 -20
- package/palmier-server/pwa/src/App.tsx +15 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +282 -473
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -25
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +12 -4
- package/palmier-server/pwa/src/components/TaskForm.tsx +230 -33
- package/palmier-server/pwa/src/components/TasksView.tsx +5 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +66 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- package/palmier-server/pwa/src/pages/PairHost.tsx +18 -2
- package/palmier-server/pwa/src/types.ts +1 -1
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +47 -6
- package/src/agents/agent.ts +0 -4
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/gemini.ts +3 -2
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/shared-prompt.ts +0 -3
- package/src/app-registry.ts +52 -0
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +1 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +31 -27
- package/src/config.ts +0 -8
- package/src/device-capabilities.ts +4 -3
- package/src/event-queues.ts +6 -21
- package/src/events.ts +1 -9
- package/src/index.ts +0 -1
- package/src/mcp-handler.ts +1 -2
- package/src/mcp-tools.ts +14 -20
- package/src/nats-client.ts +1 -4
- package/src/pending-requests.ts +4 -18
- package/src/platform/index.ts +1 -4
- package/src/platform/linux.ts +9 -20
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +20 -48
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +6 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/task-parsing.test.ts +2 -3
- package/test/windows-xml.test.ts +11 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +0 -120
- package/dist/pwa/assets/index-bLTn8zBj.css +0 -1
|
@@ -1,101 +1,9 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { useNavigate } from "react-router-dom";
|
|
4
|
-
import { Capacitor
|
|
5
|
-
import { App as
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
interface LocationPermissionResult {
|
|
9
|
-
fine: boolean;
|
|
10
|
-
background: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface LocationPermissionPlugin {
|
|
14
|
-
request(): Promise<LocationPermissionResult>;
|
|
15
|
-
check(): Promise<LocationPermissionResult>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface NotificationListenerResult {
|
|
19
|
-
enabled: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface NotificationListenerPlugin {
|
|
23
|
-
request(): Promise<NotificationListenerResult>;
|
|
24
|
-
check(): Promise<NotificationListenerResult>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const LocationPermission = Capacitor.isNativePlatform()
|
|
28
|
-
? registerPlugin<LocationPermissionPlugin>("LocationPermission")
|
|
29
|
-
: null;
|
|
30
|
-
|
|
31
|
-
interface SmsPermissionResult {
|
|
32
|
-
granted: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface SmsPermissionPlugin {
|
|
36
|
-
request(): Promise<SmsPermissionResult>;
|
|
37
|
-
check(): Promise<SmsPermissionResult>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface ContactsPermissionResult {
|
|
41
|
-
granted: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface ContactsPermissionPlugin {
|
|
45
|
-
request(): Promise<ContactsPermissionResult>;
|
|
46
|
-
check(): Promise<ContactsPermissionResult>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface CalendarPermissionResult {
|
|
50
|
-
granted: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface CalendarPermissionPlugin {
|
|
54
|
-
request(): Promise<CalendarPermissionResult>;
|
|
55
|
-
check(): Promise<CalendarPermissionResult>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface DndAccessResult {
|
|
59
|
-
enabled: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface DndAccessPlugin {
|
|
63
|
-
request(): Promise<DndAccessResult>;
|
|
64
|
-
check(): Promise<DndAccessResult>;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface FullScreenIntentResult {
|
|
68
|
-
granted: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface FullScreenIntentPlugin {
|
|
72
|
-
request(): Promise<FullScreenIntentResult>;
|
|
73
|
-
check(): Promise<FullScreenIntentResult>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const NotificationListener = Capacitor.isNativePlatform()
|
|
77
|
-
? registerPlugin<NotificationListenerPlugin>("NotificationListener")
|
|
78
|
-
: null;
|
|
79
|
-
|
|
80
|
-
const SmsPermission = Capacitor.isNativePlatform()
|
|
81
|
-
? registerPlugin<SmsPermissionPlugin>("SmsPermission")
|
|
82
|
-
: null;
|
|
83
|
-
|
|
84
|
-
const ContactsPermission = Capacitor.isNativePlatform()
|
|
85
|
-
? registerPlugin<ContactsPermissionPlugin>("ContactsPermission")
|
|
86
|
-
: null;
|
|
87
|
-
|
|
88
|
-
const CalendarPermission = Capacitor.isNativePlatform()
|
|
89
|
-
? registerPlugin<CalendarPermissionPlugin>("CalendarPermission")
|
|
90
|
-
: null;
|
|
91
|
-
|
|
92
|
-
const DndAccess = Capacitor.isNativePlatform()
|
|
93
|
-
? registerPlugin<DndAccessPlugin>("DndAccess")
|
|
94
|
-
: null;
|
|
95
|
-
|
|
96
|
-
const FullScreenIntent = Capacitor.isNativePlatform()
|
|
97
|
-
? registerPlugin<FullScreenIntentPlugin>("FullScreenIntent")
|
|
98
|
-
: null;
|
|
4
|
+
import { Capacitor } from "@capacitor/core";
|
|
5
|
+
import { App as CapacitorApp } from "@capacitor/app";
|
|
6
|
+
import { Device, type PermissionType } from "../native/Device";
|
|
99
7
|
import { useHostStore } from "../contexts/HostStoreContext";
|
|
100
8
|
import { useMediaQuery } from "../hooks/useMediaQuery";
|
|
101
9
|
import { confirmLeaveDraft } from "../draftGuard";
|
|
@@ -104,6 +12,50 @@ import { confirmLeaveDraft } from "../draftGuard";
|
|
|
104
12
|
const isLanMode = !!(window as any).__PALMIER_SERVE__;
|
|
105
13
|
const isNative = Capacitor.isNativePlatform();
|
|
106
14
|
|
|
15
|
+
type CapabilityGroup = "Messaging" | "Data" | "Device";
|
|
16
|
+
|
|
17
|
+
const CAPABILITY_GROUPS: CapabilityGroup[] = ["Device", "Data", "Messaging"];
|
|
18
|
+
|
|
19
|
+
interface CapabilityDefinition {
|
|
20
|
+
/** Server-side capability name used in device.capability.{enable,disable} RPCs. */
|
|
21
|
+
capability: string;
|
|
22
|
+
/** Label shown in the drawer toggle. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Drawer section this capability is grouped under. */
|
|
25
|
+
group: CapabilityGroup;
|
|
26
|
+
/** Runtime or settings permission to request before enabling. */
|
|
27
|
+
permission?: PermissionType;
|
|
28
|
+
/** True for capabilities that display a full-screen UI (alarm). */
|
|
29
|
+
needsFullScreenIntent?: boolean;
|
|
30
|
+
/** Override RPC methods; location uses device.location.{enable,disable} instead. */
|
|
31
|
+
enableMethod?: string;
|
|
32
|
+
disableMethod?: string;
|
|
33
|
+
enableParams?(fcmToken: string): Record<string, unknown>;
|
|
34
|
+
disableParams?(): Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CAPABILITIES: CapabilityDefinition[] = [
|
|
38
|
+
{ capability: "sms-read", label: "Read SMS", group: "Messaging", permission: "smsRead" },
|
|
39
|
+
{ capability: "sms-send", label: "Send SMS", group: "Messaging", permission: "smsSend" },
|
|
40
|
+
{ capability: "send-email", label: "Send Email", group: "Messaging", permission: "postNotifications" },
|
|
41
|
+
{ capability: "notifications", label: "Read Notifications", group: "Data", permission: "notificationListener" },
|
|
42
|
+
{ capability: "contacts", label: "Manage Contacts", group: "Data", permission: "contacts" },
|
|
43
|
+
{ capability: "calendar", label: "Manage Calendar", group: "Data", permission: "calendar" },
|
|
44
|
+
{
|
|
45
|
+
capability: "location",
|
|
46
|
+
label: "Get Location",
|
|
47
|
+
group: "Device",
|
|
48
|
+
permission: "location",
|
|
49
|
+
enableMethod: "device.location.enable",
|
|
50
|
+
disableMethod: "device.location.disable",
|
|
51
|
+
enableParams: (fcmToken) => ({ fcmToken }),
|
|
52
|
+
disableParams: () => ({}),
|
|
53
|
+
},
|
|
54
|
+
{ capability: "battery", label: "Read Battery Status", group: "Device" },
|
|
55
|
+
{ capability: "dnd", label: "Set Ringer Mode", group: "Device", permission: "dnd" },
|
|
56
|
+
{ capability: "alarm", label: "Trigger Alarms", group: "Device", needsFullScreenIntent: true },
|
|
57
|
+
];
|
|
58
|
+
|
|
107
59
|
interface HostMenuProps {
|
|
108
60
|
daemonVersion?: string | null;
|
|
109
61
|
capabilityTokens?: Record<string, string | null>;
|
|
@@ -122,294 +74,170 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
122
74
|
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
123
75
|
const [renameValue, setRenameValue] = useState("");
|
|
124
76
|
const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(null);
|
|
125
|
-
const [
|
|
126
|
-
const [
|
|
127
|
-
const [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
77
|
+
const [togglingCapability, setTogglingCapability] = useState<string | null>(null);
|
|
78
|
+
const [confirmingSwitchCapability, setConfirmingSwitchCapability] = useState<CapabilityDefinition | null>(null);
|
|
79
|
+
const [confirmingDisableCapability, setConfirmingDisableCapability] = useState<CapabilityDefinition | null>(null);
|
|
80
|
+
/**
|
|
81
|
+
* Permission types the installed APK understands. Null while loading; an empty
|
|
82
|
+
* set means the native plugin doesn't expose a discovery method (pre-Device
|
|
83
|
+
* plugin build) — in that case we don't pre-filter the UI and rely on per-call
|
|
84
|
+
* {supported: false} from the native side as the fallback.
|
|
85
|
+
*/
|
|
86
|
+
const [supportedPerms, setSupportedPerms] = useState<Set<PermissionType> | null>(null);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!isNative || !Device) {
|
|
90
|
+
setSupportedPerms(new Set());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
Device.getSupportedPermissions()
|
|
94
|
+
.then(({ types }) => setSupportedPerms(new Set(types)))
|
|
95
|
+
.catch(() => setSupportedPerms(new Set())); // old APK: fall back to per-call supported flag
|
|
96
|
+
}, []);
|
|
134
97
|
|
|
135
98
|
// Capability enabled = this device's client token matches the registered device for that capability
|
|
136
|
-
function
|
|
137
|
-
return !!(activeClientToken && capabilityTokens?.[
|
|
99
|
+
function isCapabilityEnabled(capability: string): boolean {
|
|
100
|
+
return !!(activeClientToken && capabilityTokens?.[capability] === activeClientToken);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A capability is shown when native either explicitly supports its permission, or
|
|
105
|
+
* can't advertise support (empty set = old APK or web) — in the latter case the
|
|
106
|
+
* toggle still works because the per-call `supported` flag guards at tap time.
|
|
107
|
+
*/
|
|
108
|
+
function isCapabilityVisible(definition: CapabilityDefinition): boolean {
|
|
109
|
+
if (!supportedPerms) return false;
|
|
110
|
+
if (supportedPerms.size === 0) return true;
|
|
111
|
+
if (definition.permission && !supportedPerms.has(definition.permission)) return false;
|
|
112
|
+
if (definition.needsFullScreenIntent && !supportedPerms.has("fullScreenIntent")) return false;
|
|
113
|
+
return true;
|
|
138
114
|
}
|
|
139
|
-
const locationEnabled = isCapEnabled("location");
|
|
140
|
-
const notificationListenerEnabled = isCapEnabled("notifications");
|
|
141
|
-
const smsEnabled = isCapEnabled("sms");
|
|
142
|
-
const contactsEnabled = isCapEnabled("contacts");
|
|
143
|
-
const calendarEnabled = isCapEnabled("calendar");
|
|
144
|
-
const dndEnabled = isCapEnabled("dnd");
|
|
145
|
-
const alarmEnabled = isCapEnabled("alert");
|
|
146
|
-
const batteryEnabled = isCapEnabled("battery");
|
|
147
|
-
const emailEnabled = isCapEnabled("email");
|
|
148
115
|
|
|
149
116
|
/** Update local capability tokens state after a toggle change */
|
|
150
|
-
function
|
|
117
|
+
function setCapabilityEnabled(capability: string, enabled: boolean) {
|
|
151
118
|
const updated: Record<string, string | null> = {};
|
|
152
119
|
for (const [k, v] of Object.entries(capabilityTokens ?? {})) updated[k] = v ?? null;
|
|
153
|
-
updated[
|
|
120
|
+
updated[capability] = enabled ? (activeClientToken ?? null) : null;
|
|
154
121
|
onCapabilityTokensChange?.(updated);
|
|
155
122
|
}
|
|
156
123
|
|
|
157
|
-
//
|
|
124
|
+
// If the OS location permission is revoked while the app is backgrounded,
|
|
125
|
+
// disable the capability on the host so agents don't keep pinging for fixes.
|
|
158
126
|
useEffect(() => {
|
|
159
|
-
if (!isNative || !
|
|
127
|
+
if (!isNative || !Device || !request) return;
|
|
128
|
+
|
|
129
|
+
const locationEnabled = isCapabilityEnabled("location");
|
|
130
|
+
if (!locationEnabled) return;
|
|
160
131
|
|
|
161
132
|
function syncPermissionState() {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!fine) {
|
|
165
|
-
// Permission revoked — disable on host
|
|
133
|
+
Device!.checkPermission({ type: "location" }).then(({ granted }) => {
|
|
134
|
+
if (!granted) {
|
|
166
135
|
request!("device.location.disable").then(() => {
|
|
167
|
-
|
|
136
|
+
setCapabilityEnabled("location", false);
|
|
168
137
|
}).catch(() => {});
|
|
169
138
|
}
|
|
170
139
|
});
|
|
171
140
|
}
|
|
172
141
|
|
|
173
142
|
syncPermissionState();
|
|
174
|
-
|
|
175
|
-
const listener = CapApp.addListener("resume", () => {
|
|
176
|
-
syncPermissionState();
|
|
177
|
-
});
|
|
178
|
-
|
|
143
|
+
const listener = CapacitorApp.addListener("resume", syncPermissionState);
|
|
179
144
|
return () => { listener.then((h) => h.remove()); };
|
|
180
|
-
}, [
|
|
181
|
-
|
|
182
|
-
async function handleNotificationListenerToggle() {
|
|
183
|
-
if (!NotificationListener || !request) return;
|
|
184
|
-
setTogglingNotificationListener(true);
|
|
185
|
-
try {
|
|
186
|
-
if (notificationListenerEnabled) {
|
|
187
|
-
await Preferences.set({ key: "notificationListenerEnabled", value: "false" });
|
|
188
|
-
await request("device.capability.disable", { capability: "notifications" });
|
|
189
|
-
setCapEnabled("notifications", false);
|
|
190
|
-
} else {
|
|
191
|
-
const { enabled: systemEnabled } = await NotificationListener.check();
|
|
192
|
-
if (!systemEnabled) {
|
|
193
|
-
const result = await NotificationListener.request();
|
|
194
|
-
if (!result.enabled) return;
|
|
195
|
-
}
|
|
196
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
197
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
198
|
-
await Preferences.set({ key: "notificationListenerEnabled", value: "true" });
|
|
199
|
-
await request("device.capability.enable", { capability: "notifications", fcmToken });
|
|
200
|
-
setCapEnabled("notifications", true);
|
|
201
|
-
}
|
|
202
|
-
} catch (err) {
|
|
203
|
-
console.error("Failed to toggle notification listener:", err);
|
|
204
|
-
} finally {
|
|
205
|
-
setTogglingNotificationListener(false);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function handleSmsToggle() {
|
|
210
|
-
if (!SmsPermission || !request) return;
|
|
211
|
-
setTogglingSms(true);
|
|
212
|
-
try {
|
|
213
|
-
if (smsEnabled) {
|
|
214
|
-
await Preferences.set({ key: "smsListenerEnabled", value: "false" });
|
|
215
|
-
await request("device.capability.disable", { capability: "sms" });
|
|
216
|
-
setCapEnabled("sms", false);
|
|
217
|
-
} else {
|
|
218
|
-
const { granted } = await SmsPermission.check();
|
|
219
|
-
if (!granted) {
|
|
220
|
-
const result = await SmsPermission.request();
|
|
221
|
-
if (!result.granted) return;
|
|
222
|
-
}
|
|
223
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
224
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
225
|
-
await Preferences.set({ key: "smsListenerEnabled", value: "true" });
|
|
226
|
-
await request("device.capability.enable", { capability: "sms", fcmToken });
|
|
227
|
-
setCapEnabled("sms", true);
|
|
228
|
-
}
|
|
229
|
-
} catch (err) {
|
|
230
|
-
console.error("Failed to toggle SMS access:", err);
|
|
231
|
-
} finally {
|
|
232
|
-
setTogglingSms(false);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async function handleContactsToggle() {
|
|
237
|
-
if (!ContactsPermission || !request) return;
|
|
238
|
-
setTogglingContacts(true);
|
|
239
|
-
try {
|
|
240
|
-
if (contactsEnabled) {
|
|
241
|
-
await Preferences.set({ key: "contactsAccessEnabled", value: "false" });
|
|
242
|
-
await request("device.capability.disable", { capability: "contacts" });
|
|
243
|
-
setCapEnabled("contacts", false);
|
|
244
|
-
} else {
|
|
245
|
-
const { granted } = await ContactsPermission.check();
|
|
246
|
-
if (!granted) {
|
|
247
|
-
const result = await ContactsPermission.request();
|
|
248
|
-
if (!result.granted) return;
|
|
249
|
-
}
|
|
250
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
251
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
252
|
-
await Preferences.set({ key: "contactsAccessEnabled", value: "true" });
|
|
253
|
-
await request("device.capability.enable", { capability: "contacts", fcmToken });
|
|
254
|
-
setCapEnabled("contacts", true);
|
|
255
|
-
}
|
|
256
|
-
} catch (err) {
|
|
257
|
-
console.error("Failed to toggle contacts access:", err);
|
|
258
|
-
} finally {
|
|
259
|
-
setTogglingContacts(false);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function handleCalendarToggle() {
|
|
264
|
-
if (!CalendarPermission || !request) return;
|
|
265
|
-
setTogglingCalendar(true);
|
|
266
|
-
try {
|
|
267
|
-
if (calendarEnabled) {
|
|
268
|
-
await Preferences.set({ key: "calendarAccessEnabled", value: "false" });
|
|
269
|
-
await request("device.capability.disable", { capability: "calendar" });
|
|
270
|
-
setCapEnabled("calendar", false);
|
|
271
|
-
} else {
|
|
272
|
-
const { granted } = await CalendarPermission.check();
|
|
273
|
-
if (!granted) {
|
|
274
|
-
const result = await CalendarPermission.request();
|
|
275
|
-
if (!result.granted) return;
|
|
276
|
-
}
|
|
277
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
278
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
279
|
-
await Preferences.set({ key: "calendarAccessEnabled", value: "true" });
|
|
280
|
-
await request("device.capability.enable", { capability: "calendar", fcmToken });
|
|
281
|
-
setCapEnabled("calendar", true);
|
|
282
|
-
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.error("Failed to toggle calendar access:", err);
|
|
285
|
-
} finally {
|
|
286
|
-
setTogglingCalendar(false);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
145
|
+
}, [capabilityTokens, activeClientToken]);
|
|
289
146
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
304
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
305
|
-
await request("device.capability.enable", { capability: "dnd", fcmToken });
|
|
306
|
-
setCapEnabled("dnd", true);
|
|
307
|
-
}
|
|
308
|
-
} catch (err) {
|
|
309
|
-
console.error("Failed to toggle DND access:", err);
|
|
310
|
-
} finally {
|
|
311
|
-
setTogglingDnd(false);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/** Ensure full-screen intent permission is granted (needed for alert + email). */
|
|
316
|
-
async function ensureFullScreenIntent(): Promise<boolean> {
|
|
317
|
-
if (!FullScreenIntent) return true;
|
|
318
|
-
const { granted } = await FullScreenIntent.check();
|
|
319
|
-
if (granted) return true;
|
|
320
|
-
const result = await FullScreenIntent.request();
|
|
321
|
-
return result.granted;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function handleAlarmToggle() {
|
|
147
|
+
// Mirror the server-derived enabled set into native as the local kill-switch.
|
|
148
|
+
// Toggling below writes through immediately; this useEffect catches host-initiated
|
|
149
|
+
// changes (e.g. a capability revoked on another device) on the next render.
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!isNative || !Device) return;
|
|
152
|
+
const enabledCapabilities = CAPABILITIES
|
|
153
|
+
.map((definition) => definition.capability)
|
|
154
|
+
.filter((capability) => capabilityTokens?.[capability] === activeClientToken);
|
|
155
|
+
Device.setEnabledCapabilities({ capabilities: enabledCapabilities }).catch(() => {});
|
|
156
|
+
}, [capabilityTokens, activeClientToken]);
|
|
157
|
+
|
|
158
|
+
async function toggleCapability(definition: CapabilityDefinition, bypassConfirmation = false) {
|
|
325
159
|
if (!request) return;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
} else {
|
|
332
|
-
if (!await ensureFullScreenIntent()) return;
|
|
333
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
334
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
335
|
-
await request("device.capability.enable", { capability: "alert", fcmToken });
|
|
336
|
-
setCapEnabled("alert", true);
|
|
337
|
-
}
|
|
338
|
-
} catch (err) {
|
|
339
|
-
console.error("Failed to toggle alert access:", err);
|
|
340
|
-
} finally {
|
|
341
|
-
setTogglingAlarm(false);
|
|
160
|
+
const enabled = isCapabilityEnabled(definition.capability);
|
|
161
|
+
|
|
162
|
+
if (enabled && !bypassConfirmation) {
|
|
163
|
+
setConfirmingDisableCapability(definition);
|
|
164
|
+
return;
|
|
342
165
|
}
|
|
343
|
-
}
|
|
344
166
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
await request("device.capability.disable", { capability: "email" });
|
|
351
|
-
setCapEnabled("email", false);
|
|
352
|
-
} else {
|
|
353
|
-
if (!await ensureFullScreenIntent()) return;
|
|
354
|
-
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
355
|
-
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
356
|
-
await request("device.capability.enable", { capability: "email", fcmToken });
|
|
357
|
-
setCapEnabled("email", true);
|
|
358
|
-
}
|
|
359
|
-
} catch (err) {
|
|
360
|
-
console.error("Failed to toggle email access:", err);
|
|
361
|
-
} finally {
|
|
362
|
-
setTogglingEmail(false);
|
|
167
|
+
const ownedByOther = !enabled && !!capabilityTokens?.[definition.capability]
|
|
168
|
+
&& capabilityTokens[definition.capability] !== activeClientToken;
|
|
169
|
+
if (ownedByOther && !bypassConfirmation) {
|
|
170
|
+
setConfirmingSwitchCapability(definition);
|
|
171
|
+
return;
|
|
363
172
|
}
|
|
364
|
-
}
|
|
365
173
|
|
|
366
|
-
|
|
367
|
-
if (!request) return;
|
|
368
|
-
setTogglingBattery(true);
|
|
174
|
+
setTogglingCapability(definition.capability);
|
|
369
175
|
try {
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
await request("device.capability.enable", { capability: "battery", fcmToken });
|
|
377
|
-
setCapEnabled("battery", true);
|
|
176
|
+
if (enabled) {
|
|
177
|
+
const method = definition.disableMethod ?? "device.capability.disable";
|
|
178
|
+
const params = definition.disableParams?.() ?? { capability: definition.capability };
|
|
179
|
+
await request(method, params);
|
|
180
|
+
setCapabilityEnabled(definition.capability, false);
|
|
181
|
+
return;
|
|
378
182
|
}
|
|
379
|
-
} catch (err) {
|
|
380
|
-
console.error("Failed to toggle battery access:", err);
|
|
381
|
-
} finally {
|
|
382
|
-
setTogglingBattery(false);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
183
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (LocationPermission) {
|
|
395
|
-
const result = await LocationPermission.request();
|
|
396
|
-
if (!result.fine) {
|
|
184
|
+
if (Device && definition.capability === "send-email") {
|
|
185
|
+
// Verify a mailto: handler exists before enabling — silent PackageManager
|
|
186
|
+
// lookup. If the APK predates this method, allow enabling and let the
|
|
187
|
+
// user discover the missing-app failure at first use.
|
|
188
|
+
try {
|
|
189
|
+
const result = await Device.hasEmailClient();
|
|
190
|
+
if (result.supported && !result.available) {
|
|
191
|
+
alert("No email app is installed on this device. Install one (e.g. Gmail) before enabling Sending Email.");
|
|
397
192
|
return;
|
|
398
193
|
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Older APK without this method — fall through.
|
|
399
196
|
}
|
|
197
|
+
}
|
|
400
198
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
199
|
+
if (Device && definition.permission) {
|
|
200
|
+
const check = await Device.checkPermission({ type: definition.permission });
|
|
201
|
+
if (!check.supported) {
|
|
202
|
+
console.warn(`Native build does not support permission '${definition.permission}'`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!check.granted) {
|
|
206
|
+
const result = await Device.requestPermission({ type: definition.permission });
|
|
207
|
+
if (!result.granted) return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (Device && definition.needsFullScreenIntent) {
|
|
211
|
+
const check = await Device.checkPermission({ type: "fullScreenIntent" });
|
|
212
|
+
if (!check.supported) {
|
|
213
|
+
console.warn("Native build does not support fullScreenIntent");
|
|
404
214
|
return;
|
|
405
215
|
}
|
|
406
|
-
|
|
407
|
-
|
|
216
|
+
if (!check.granted) {
|
|
217
|
+
const result = await Device.requestPermission({ type: "fullScreenIntent" });
|
|
218
|
+
if (!result.granted) return;
|
|
219
|
+
}
|
|
408
220
|
}
|
|
221
|
+
|
|
222
|
+
if (!Device) return;
|
|
223
|
+
const { token: fcmToken } = await Device.getFcmToken();
|
|
224
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
225
|
+
|
|
226
|
+
// Whitelist the capability natively before enabling on the host, so an FCM
|
|
227
|
+
// from the host can't arrive in the gap where our useEffect hasn't synced yet.
|
|
228
|
+
const enabledNow = CAPABILITIES
|
|
229
|
+
.map((c) => c.capability)
|
|
230
|
+
.filter((name) => name === definition.capability || capabilityTokens?.[name] === activeClientToken);
|
|
231
|
+
await Device.setEnabledCapabilities({ capabilities: enabledNow });
|
|
232
|
+
|
|
233
|
+
const method = definition.enableMethod ?? "device.capability.enable";
|
|
234
|
+
const params = definition.enableParams?.(fcmToken) ?? { capability: definition.capability, fcmToken };
|
|
235
|
+
await request(method, params);
|
|
236
|
+
setCapabilityEnabled(definition.capability, true);
|
|
409
237
|
} catch (err) {
|
|
410
|
-
console.error(
|
|
238
|
+
console.error(`Failed to toggle ${definition.capability}:`, err);
|
|
411
239
|
} finally {
|
|
412
|
-
|
|
240
|
+
setTogglingCapability(null);
|
|
413
241
|
}
|
|
414
242
|
}
|
|
415
243
|
const drawerRef = useRef<HTMLDivElement>(null);
|
|
@@ -471,20 +299,17 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
471
299
|
|
|
472
300
|
const drawerContent = (
|
|
473
301
|
<>
|
|
474
|
-
|
|
475
|
-
<
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
</button>
|
|
486
|
-
)}
|
|
487
|
-
</div>
|
|
302
|
+
{!isDesktop && (
|
|
303
|
+
<button
|
|
304
|
+
className="drawer-close-btn"
|
|
305
|
+
onClick={close}
|
|
306
|
+
aria-label="Close menu"
|
|
307
|
+
>
|
|
308
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
309
|
+
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
310
|
+
</svg>
|
|
311
|
+
</button>
|
|
312
|
+
)}
|
|
488
313
|
|
|
489
314
|
{!isLanMode && pairedHosts.length > 0 && (
|
|
490
315
|
<div className="drawer-section">
|
|
@@ -592,121 +417,41 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
592
417
|
</div>
|
|
593
418
|
</>)}
|
|
594
419
|
|
|
595
|
-
{isNative && (
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
aria-checked={smsEnabled}
|
|
631
|
-
>
|
|
632
|
-
<span className="toggle-switch-thumb" />
|
|
633
|
-
</button>
|
|
634
|
-
</label>
|
|
635
|
-
<label className="drawer-toggle">
|
|
636
|
-
<span className="drawer-toggle-label">Contacts Access</span>
|
|
637
|
-
<button
|
|
638
|
-
className={`toggle-switch ${contactsEnabled ? "toggle-switch-on" : ""}`}
|
|
639
|
-
onClick={handleContactsToggle}
|
|
640
|
-
disabled={togglingContacts}
|
|
641
|
-
role="switch"
|
|
642
|
-
aria-checked={contactsEnabled}
|
|
643
|
-
>
|
|
644
|
-
<span className="toggle-switch-thumb" />
|
|
645
|
-
</button>
|
|
646
|
-
</label>
|
|
647
|
-
<label className="drawer-toggle">
|
|
648
|
-
<span className="drawer-toggle-label">Calendar Access</span>
|
|
649
|
-
<button
|
|
650
|
-
className={`toggle-switch ${calendarEnabled ? "toggle-switch-on" : ""}`}
|
|
651
|
-
onClick={handleCalendarToggle}
|
|
652
|
-
disabled={togglingCalendar}
|
|
653
|
-
role="switch"
|
|
654
|
-
aria-checked={calendarEnabled}
|
|
655
|
-
>
|
|
656
|
-
<span className="toggle-switch-thumb" />
|
|
657
|
-
</button>
|
|
658
|
-
</label>
|
|
659
|
-
<label className="drawer-toggle">
|
|
660
|
-
<span className="drawer-toggle-label">Do Not Disturb Control</span>
|
|
661
|
-
<button
|
|
662
|
-
className={`toggle-switch ${dndEnabled ? "toggle-switch-on" : ""}`}
|
|
663
|
-
onClick={handleDndToggle}
|
|
664
|
-
disabled={togglingDnd}
|
|
665
|
-
role="switch"
|
|
666
|
-
aria-checked={dndEnabled}
|
|
667
|
-
>
|
|
668
|
-
<span className="toggle-switch-thumb" />
|
|
669
|
-
</button>
|
|
670
|
-
</label>
|
|
671
|
-
<label className="drawer-toggle">
|
|
672
|
-
<span className="drawer-toggle-label">Alert Access</span>
|
|
673
|
-
<button
|
|
674
|
-
className={`toggle-switch ${alarmEnabled ? "toggle-switch-on" : ""}`}
|
|
675
|
-
onClick={handleAlarmToggle}
|
|
676
|
-
disabled={togglingAlarm}
|
|
677
|
-
role="switch"
|
|
678
|
-
aria-checked={alarmEnabled}
|
|
679
|
-
>
|
|
680
|
-
<span className="toggle-switch-thumb" />
|
|
681
|
-
</button>
|
|
682
|
-
</label>
|
|
683
|
-
<label className="drawer-toggle">
|
|
684
|
-
<span className="drawer-toggle-label">Battery Access</span>
|
|
685
|
-
<button
|
|
686
|
-
className={`toggle-switch ${batteryEnabled ? "toggle-switch-on" : ""}`}
|
|
687
|
-
onClick={handleBatteryToggle}
|
|
688
|
-
disabled={togglingBattery}
|
|
689
|
-
role="switch"
|
|
690
|
-
aria-checked={batteryEnabled}
|
|
691
|
-
>
|
|
692
|
-
<span className="toggle-switch-thumb" />
|
|
693
|
-
</button>
|
|
694
|
-
</label>
|
|
695
|
-
<label className="drawer-toggle">
|
|
696
|
-
<span className="drawer-toggle-label">Email Access</span>
|
|
697
|
-
<button
|
|
698
|
-
className={`toggle-switch ${emailEnabled ? "toggle-switch-on" : ""}`}
|
|
699
|
-
onClick={handleEmailToggle}
|
|
700
|
-
disabled={togglingEmail}
|
|
701
|
-
role="switch"
|
|
702
|
-
aria-checked={emailEnabled}
|
|
703
|
-
>
|
|
704
|
-
<span className="toggle-switch-thumb" />
|
|
705
|
-
</button>
|
|
706
|
-
</label>
|
|
707
|
-
</div>
|
|
708
|
-
</>
|
|
709
|
-
)}
|
|
420
|
+
{isNative && (() => {
|
|
421
|
+
const visibleGroups = CAPABILITY_GROUPS
|
|
422
|
+
.map((group) => ({ group, items: CAPABILITIES.filter((definition) => definition.group === group && isCapabilityVisible(definition)) }))
|
|
423
|
+
.filter((g) => g.items.length > 0);
|
|
424
|
+
if (visibleGroups.length === 0) return null;
|
|
425
|
+
return (
|
|
426
|
+
<>
|
|
427
|
+
<div className="drawer-divider" />
|
|
428
|
+
<div className="drawer-section">
|
|
429
|
+
<h3 className="drawer-section-label">Host capabilities on this device</h3>
|
|
430
|
+
{visibleGroups.map(({ group, items }, index) => (
|
|
431
|
+
<div key={group} className={index > 0 ? "drawer-toggle-group drawer-toggle-group-divided" : "drawer-toggle-group"}>
|
|
432
|
+
{items.map((definition) => {
|
|
433
|
+
const enabled = isCapabilityEnabled(definition.capability);
|
|
434
|
+
return (
|
|
435
|
+
<label key={definition.capability} className="drawer-toggle">
|
|
436
|
+
<span className="drawer-toggle-label">{definition.label}</span>
|
|
437
|
+
<button
|
|
438
|
+
className={`toggle-switch ${enabled ? "toggle-switch-on" : ""}`}
|
|
439
|
+
onClick={() => toggleCapability(definition)}
|
|
440
|
+
disabled={togglingCapability === definition.capability}
|
|
441
|
+
role="switch"
|
|
442
|
+
aria-checked={enabled}
|
|
443
|
+
>
|
|
444
|
+
<span className="toggle-switch-thumb" />
|
|
445
|
+
</button>
|
|
446
|
+
</label>
|
|
447
|
+
);
|
|
448
|
+
})}
|
|
449
|
+
</div>
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
</>
|
|
453
|
+
);
|
|
454
|
+
})()}
|
|
710
455
|
|
|
711
456
|
<div className="drawer-footer">
|
|
712
457
|
{daemonVersion && (
|
|
@@ -752,6 +497,66 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
752
497
|
document.body
|
|
753
498
|
);
|
|
754
499
|
|
|
500
|
+
const disableCapabilityModal = confirmingDisableCapability && createPortal(
|
|
501
|
+
<div className="confirm-modal-overlay" onClick={() => setConfirmingDisableCapability(null)}>
|
|
502
|
+
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
503
|
+
<h2 className="confirm-modal-title">Disable {confirmingDisableCapability.label}?</h2>
|
|
504
|
+
<p className="confirm-modal-message">
|
|
505
|
+
Agents running on the host will no longer be able to use {confirmingDisableCapability.label} until it is re-enabled on a device.
|
|
506
|
+
</p>
|
|
507
|
+
<div className="confirm-modal-actions">
|
|
508
|
+
<button
|
|
509
|
+
className="btn btn-secondary"
|
|
510
|
+
onClick={() => setConfirmingDisableCapability(null)}
|
|
511
|
+
>
|
|
512
|
+
Cancel
|
|
513
|
+
</button>
|
|
514
|
+
<button
|
|
515
|
+
className="btn btn-danger"
|
|
516
|
+
onClick={() => {
|
|
517
|
+
const definition = confirmingDisableCapability;
|
|
518
|
+
setConfirmingDisableCapability(null);
|
|
519
|
+
toggleCapability(definition, true);
|
|
520
|
+
}}
|
|
521
|
+
>
|
|
522
|
+
Disable
|
|
523
|
+
</button>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>,
|
|
527
|
+
document.body
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const switchCapabilityModal = confirmingSwitchCapability && createPortal(
|
|
531
|
+
<div className="confirm-modal-overlay" onClick={() => setConfirmingSwitchCapability(null)}>
|
|
532
|
+
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
533
|
+
<h2 className="confirm-modal-title">Switch {confirmingSwitchCapability.label} to this device?</h2>
|
|
534
|
+
<p className="confirm-modal-message">
|
|
535
|
+
{confirmingSwitchCapability.label} is currently enabled on another device. Switching will make this device the one the host uses for {confirmingSwitchCapability.label}, and it will be disabled on the other device.
|
|
536
|
+
</p>
|
|
537
|
+
<div className="confirm-modal-actions">
|
|
538
|
+
<button
|
|
539
|
+
className="btn btn-secondary"
|
|
540
|
+
onClick={() => setConfirmingSwitchCapability(null)}
|
|
541
|
+
>
|
|
542
|
+
Cancel
|
|
543
|
+
</button>
|
|
544
|
+
<button
|
|
545
|
+
className="btn btn-primary"
|
|
546
|
+
onClick={() => {
|
|
547
|
+
const definition = confirmingSwitchCapability;
|
|
548
|
+
setConfirmingSwitchCapability(null);
|
|
549
|
+
toggleCapability(definition, true);
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
Switch
|
|
553
|
+
</button>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
</div>,
|
|
557
|
+
document.body
|
|
558
|
+
);
|
|
559
|
+
|
|
755
560
|
// Desktop: persistent inline sidebar
|
|
756
561
|
if (isDesktop) {
|
|
757
562
|
return (
|
|
@@ -760,6 +565,8 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
760
565
|
{drawerContent}
|
|
761
566
|
</div>
|
|
762
567
|
{deleteModal}
|
|
568
|
+
{switchCapabilityModal}
|
|
569
|
+
{disableCapabilityModal}
|
|
763
570
|
</>
|
|
764
571
|
);
|
|
765
572
|
}
|
|
@@ -795,6 +602,8 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
795
602
|
)}
|
|
796
603
|
|
|
797
604
|
{deleteModal}
|
|
605
|
+
{switchCapabilityModal}
|
|
606
|
+
{disableCapabilityModal}
|
|
798
607
|
</>
|
|
799
608
|
);
|
|
800
609
|
}
|