@chrysb/alphaclaw 0.7.2-beta.5 → 0.7.2-beta.7
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/lib/public/css/explorer.css +16 -2
- package/lib/public/css/shell.css +7 -1
- package/lib/public/js/components/file-viewer/use-file-viewer.js +0 -1
- package/lib/public/js/components/icons.js +55 -0
- package/lib/public/js/components/nodes-tab/connected-nodes/index.js +337 -360
- package/lib/public/js/components/nodes-tab/connected-nodes/use-connected-nodes-card.js +266 -0
- package/lib/public/js/components/nodes-tab/index.js +1 -1
- package/lib/public/js/components/nodes-tab/setup-wizard/index.js +123 -178
- package/lib/public/js/components/nodes-tab/setup-wizard/use-setup-wizard.js +57 -52
- package/lib/public/js/components/nodes-tab/use-nodes-tab.js +4 -1
- package/lib/public/js/components/sidebar.js +58 -28
- package/lib/public/js/lib/api.js +31 -1
- package/lib/public/js/lib/app-navigation.js +1 -1
- package/lib/server/routes/nodes.js +92 -7
- package/package.json +1 -1
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
4
3
|
import { ActionButton } from "../../action-button.js";
|
|
5
4
|
import { Badge } from "../../badge.js";
|
|
5
|
+
import { ConfirmDialog } from "../../confirm-dialog.js";
|
|
6
6
|
import { ComputerLineIcon, FileCopyLineIcon } from "../../icons.js";
|
|
7
7
|
import { LoadingSpinner } from "../../loading-spinner.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { readUiSettings, updateUiSettings } from "../../../lib/ui-settings.js";
|
|
11
|
-
import { showToast } from "../../toast.js";
|
|
8
|
+
import { OverflowMenu, OverflowMenuItem } from "../../overflow-menu.js";
|
|
9
|
+
import { useConnectedNodesCard } from "./use-connected-nodes-card.js";
|
|
12
10
|
|
|
13
11
|
const html = htm.bind(h);
|
|
14
|
-
const kBrowserCheckTimeoutMs = 10000;
|
|
15
|
-
const kBrowserPollIntervalMs = 10000;
|
|
16
|
-
const kBrowserAttachStateByNodeKey = "nodesBrowserAttachStateByNode";
|
|
17
12
|
|
|
18
13
|
const escapeDoubleQuotes = (value) => String(value || "").replace(/"/g, '\\"');
|
|
19
14
|
|
|
@@ -22,7 +17,9 @@ const buildReconnectCommand = ({ node, connectInfo, maskToken = false }) => {
|
|
|
22
17
|
const port = Number(connectInfo?.gatewayPort) || 3000;
|
|
23
18
|
const token = String(connectInfo?.gatewayToken || "").trim();
|
|
24
19
|
const tlsFlag = connectInfo?.tls === true ? "--tls" : "";
|
|
25
|
-
const displayName = String(
|
|
20
|
+
const displayName = String(
|
|
21
|
+
node?.displayName || node?.nodeId || "My Node",
|
|
22
|
+
).trim();
|
|
26
23
|
const tokenValue = maskToken ? "****" : token;
|
|
27
24
|
|
|
28
25
|
return [
|
|
@@ -63,236 +60,99 @@ const getBrowserStatusLabel = (status) => {
|
|
|
63
60
|
return "Not connected";
|
|
64
61
|
};
|
|
65
62
|
|
|
66
|
-
const withTimeout = async (promise, timeoutMs = kBrowserCheckTimeoutMs) => {
|
|
67
|
-
let timeoutId = null;
|
|
68
|
-
try {
|
|
69
|
-
return await Promise.race([
|
|
70
|
-
promise,
|
|
71
|
-
new Promise((_, reject) => {
|
|
72
|
-
timeoutId = setTimeout(() => {
|
|
73
|
-
reject(new Error("Browser check timed out"));
|
|
74
|
-
}, timeoutMs);
|
|
75
|
-
}),
|
|
76
|
-
]);
|
|
77
|
-
} finally {
|
|
78
|
-
if (timeoutId) {
|
|
79
|
-
clearTimeout(timeoutId);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const readBrowserAttachStateByNode = () => {
|
|
85
|
-
const uiSettings = readUiSettings();
|
|
86
|
-
const attachState = uiSettings?.[kBrowserAttachStateByNodeKey];
|
|
87
|
-
if (!attachState || typeof attachState !== "object" || Array.isArray(attachState)) {
|
|
88
|
-
return {};
|
|
89
|
-
}
|
|
90
|
-
return attachState;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const writeBrowserAttachStateByNode = (nextState = {}) => {
|
|
94
|
-
updateUiSettings((currentSettings) => {
|
|
95
|
-
const nextSettings =
|
|
96
|
-
currentSettings && typeof currentSettings === "object" ? currentSettings : {};
|
|
97
|
-
return {
|
|
98
|
-
...nextSettings,
|
|
99
|
-
[kBrowserAttachStateByNodeKey]:
|
|
100
|
-
nextState && typeof nextState === "object" ? nextState : {},
|
|
101
|
-
};
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
|
|
105
63
|
export const ConnectedNodesCard = ({
|
|
106
64
|
nodes = [],
|
|
107
65
|
pending = [],
|
|
108
66
|
loading = false,
|
|
109
67
|
error = "",
|
|
110
68
|
connectInfo = null,
|
|
69
|
+
onRefreshNodes = async () => {},
|
|
111
70
|
}) => {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const handleCheckNodeBrowser = useCallback(async (nodeId, { silent = false } = {}) => {
|
|
131
|
-
const normalizedNodeId = String(nodeId || "").trim();
|
|
132
|
-
if (!normalizedNodeId || browserCheckInFlightNodeIdRef.current) return;
|
|
133
|
-
browserCheckInFlightNodeIdRef.current = normalizedNodeId;
|
|
134
|
-
if (!silent) {
|
|
135
|
-
setCheckingBrowserNodeId(normalizedNodeId);
|
|
136
|
-
}
|
|
137
|
-
setBrowserErrorByNodeId((prev) => ({
|
|
138
|
-
...prev,
|
|
139
|
-
[normalizedNodeId]: "",
|
|
140
|
-
}));
|
|
141
|
-
try {
|
|
142
|
-
const result = await withTimeout(
|
|
143
|
-
fetchNodeBrowserStatusForNode(normalizedNodeId, "user"),
|
|
144
|
-
);
|
|
145
|
-
const status = result?.status && typeof result.status === "object" ? result.status : null;
|
|
146
|
-
setBrowserStatusByNodeId((prev) => ({
|
|
147
|
-
...prev,
|
|
148
|
-
[normalizedNodeId]: status,
|
|
149
|
-
}));
|
|
150
|
-
} catch (error) {
|
|
151
|
-
const message = error.message || "Could not check node browser status";
|
|
152
|
-
setBrowserErrorByNodeId((prev) => ({
|
|
153
|
-
...prev,
|
|
154
|
-
[normalizedNodeId]: message,
|
|
155
|
-
}));
|
|
156
|
-
if (!silent) {
|
|
157
|
-
showToast(message, "error");
|
|
158
|
-
}
|
|
159
|
-
} finally {
|
|
160
|
-
browserCheckInFlightNodeIdRef.current = "";
|
|
161
|
-
if (!silent) {
|
|
162
|
-
setCheckingBrowserNodeId("");
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}, []);
|
|
166
|
-
|
|
167
|
-
const setBrowserAttachStateForNode = useCallback((nodeId, enabled) => {
|
|
168
|
-
const normalizedNodeId = String(nodeId || "").trim();
|
|
169
|
-
if (!normalizedNodeId) return;
|
|
170
|
-
setBrowserAttachStateByNodeId((prevState) => {
|
|
171
|
-
const nextState = {
|
|
172
|
-
...(prevState && typeof prevState === "object" ? prevState : {}),
|
|
173
|
-
[normalizedNodeId]: enabled === true,
|
|
174
|
-
};
|
|
175
|
-
writeBrowserAttachStateByNode(nextState);
|
|
176
|
-
return nextState;
|
|
177
|
-
});
|
|
178
|
-
}, []);
|
|
179
|
-
|
|
180
|
-
const handleAttachNodeBrowser = useCallback(async (nodeId) => {
|
|
181
|
-
const normalizedNodeId = String(nodeId || "").trim();
|
|
182
|
-
if (!normalizedNodeId) return;
|
|
183
|
-
setBrowserAttachStateForNode(normalizedNodeId, true);
|
|
184
|
-
await handleCheckNodeBrowser(normalizedNodeId);
|
|
185
|
-
}, [handleCheckNodeBrowser, setBrowserAttachStateForNode]);
|
|
186
|
-
|
|
187
|
-
const handleDetachNodeBrowser = useCallback((nodeId) => {
|
|
188
|
-
const normalizedNodeId = String(nodeId || "").trim();
|
|
189
|
-
if (!normalizedNodeId) return;
|
|
190
|
-
setBrowserAttachStateForNode(normalizedNodeId, false);
|
|
191
|
-
setBrowserStatusByNodeId((prevState) => {
|
|
192
|
-
const nextState = { ...(prevState || {}) };
|
|
193
|
-
delete nextState[normalizedNodeId];
|
|
194
|
-
return nextState;
|
|
195
|
-
});
|
|
196
|
-
setBrowserErrorByNodeId((prevState) => {
|
|
197
|
-
const nextState = { ...(prevState || {}) };
|
|
198
|
-
delete nextState[normalizedNodeId];
|
|
199
|
-
return nextState;
|
|
200
|
-
});
|
|
201
|
-
}, [setBrowserAttachStateForNode]);
|
|
202
|
-
|
|
203
|
-
useEffect(() => {
|
|
204
|
-
if (checkingBrowserNodeId) return;
|
|
205
|
-
const pollableNodeIds = nodes
|
|
206
|
-
.map((node) => ({
|
|
207
|
-
nodeId: String(node?.nodeId || "").trim(),
|
|
208
|
-
connected: node?.connected === true,
|
|
209
|
-
browserCapable: isBrowserCapableNode(node),
|
|
210
|
-
}))
|
|
211
|
-
.filter(
|
|
212
|
-
(entry) =>
|
|
213
|
-
entry.nodeId &&
|
|
214
|
-
entry.connected &&
|
|
215
|
-
entry.browserCapable &&
|
|
216
|
-
browserAttachStateByNodeId?.[entry.nodeId] === true,
|
|
217
|
-
)
|
|
218
|
-
.map((entry) => entry.nodeId);
|
|
219
|
-
if (!pollableNodeIds.length) return;
|
|
220
|
-
|
|
221
|
-
let active = true;
|
|
222
|
-
const poll = async () => {
|
|
223
|
-
if (!active || browserCheckInFlightNodeIdRef.current) return;
|
|
224
|
-
const pollIndex = browserPollCursorRef.current % pollableNodeIds.length;
|
|
225
|
-
browserPollCursorRef.current += 1;
|
|
226
|
-
const nextNodeId = pollableNodeIds[pollIndex];
|
|
227
|
-
await handleCheckNodeBrowser(nextNodeId, { silent: true });
|
|
228
|
-
};
|
|
229
|
-
poll();
|
|
230
|
-
const timer = setInterval(poll, kBrowserPollIntervalMs);
|
|
231
|
-
return () => {
|
|
232
|
-
active = false;
|
|
233
|
-
clearInterval(timer);
|
|
234
|
-
};
|
|
235
|
-
}, [browserAttachStateByNodeId, handleCheckNodeBrowser, nodes]);
|
|
71
|
+
const state = useConnectedNodesCard({ nodes, onRefreshNodes });
|
|
72
|
+
const {
|
|
73
|
+
browserStatusByNodeId,
|
|
74
|
+
browserErrorByNodeId,
|
|
75
|
+
checkingBrowserNodeId,
|
|
76
|
+
browserAttachStateByNodeId,
|
|
77
|
+
menuOpenNodeId,
|
|
78
|
+
removeDialogNode,
|
|
79
|
+
removingNodeId,
|
|
80
|
+
handleCopyText,
|
|
81
|
+
handleCheckNodeBrowser,
|
|
82
|
+
handleAttachNodeBrowser,
|
|
83
|
+
handleDetachNodeBrowser,
|
|
84
|
+
handleOpenNodeMenu,
|
|
85
|
+
handleRemoveNode,
|
|
86
|
+
setMenuOpenNodeId,
|
|
87
|
+
setRemoveDialogNode,
|
|
88
|
+
} = state;
|
|
236
89
|
|
|
237
90
|
return html`
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
${loading
|
|
248
|
-
? html`
|
|
249
|
-
<div class="bg-surface border border-border rounded-xl p-4">
|
|
250
|
-
<div class="flex items-center gap-3 text-sm text-gray-400">
|
|
251
|
-
<${LoadingSpinner} className="h-4 w-4" />
|
|
252
|
-
<span>Loading nodes...</span>
|
|
91
|
+
<div class="space-y-3">
|
|
92
|
+
${pending.length
|
|
93
|
+
? html`
|
|
94
|
+
<div
|
|
95
|
+
class="bg-surface border border-yellow-500/40 rounded-xl px-4 py-3 text-xs text-yellow-300"
|
|
96
|
+
>
|
|
97
|
+
${pending.length} pending node${pending.length === 1 ? "" : "s"}
|
|
98
|
+
waiting for approval.
|
|
253
99
|
</div>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
100
|
+
`
|
|
101
|
+
: null}
|
|
102
|
+
${loading
|
|
257
103
|
? html`
|
|
258
|
-
<div class="bg-surface border border-border rounded-xl p-4
|
|
259
|
-
|
|
104
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
105
|
+
<div class="flex items-center gap-3 text-sm text-gray-400">
|
|
106
|
+
<${LoadingSpinner} className="h-4 w-4" />
|
|
107
|
+
<span>Loading nodes...</span>
|
|
108
|
+
</div>
|
|
260
109
|
</div>
|
|
261
110
|
`
|
|
262
|
-
:
|
|
111
|
+
: error
|
|
263
112
|
? html`
|
|
264
113
|
<div
|
|
265
|
-
class="bg-surface border border-border rounded-xl
|
|
114
|
+
class="bg-surface border border-border rounded-xl p-4 text-xs text-red-400"
|
|
266
115
|
>
|
|
267
|
-
|
|
268
|
-
<${ComputerLineIcon} className="h-12 w-12 text-cyan-400" />
|
|
269
|
-
<div class="space-y-2">
|
|
270
|
-
<h2 class="font-semibold text-lg text-gray-100">
|
|
271
|
-
No connected nodes yet
|
|
272
|
-
</h2>
|
|
273
|
-
<p class="text-xs text-gray-400 leading-5">
|
|
274
|
-
Connect a Mac, iOS, Android, or headless node to run
|
|
275
|
-
system and browser commands through this gateway.
|
|
276
|
-
</p>
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
116
|
+
${error}
|
|
279
117
|
</div>
|
|
280
118
|
`
|
|
281
|
-
:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
119
|
+
: !nodes.length
|
|
120
|
+
? html`
|
|
121
|
+
<div
|
|
122
|
+
class="bg-surface border border-border rounded-xl px-6 py-10 min-h-[26rem] flex flex-col items-center justify-center text-center"
|
|
123
|
+
>
|
|
124
|
+
<div class="max-w-md w-full flex flex-col items-center gap-4">
|
|
125
|
+
<${ComputerLineIcon} className="h-12 w-12 text-cyan-400" />
|
|
126
|
+
<div class="space-y-2">
|
|
127
|
+
<h2 class="font-semibold text-lg text-gray-100">
|
|
128
|
+
No connected nodes yet
|
|
129
|
+
</h2>
|
|
130
|
+
<p class="text-xs text-gray-400 leading-5">
|
|
131
|
+
Connect a Mac, iOS, Android, or headless node to run
|
|
132
|
+
system and browser commands through this gateway.
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
`
|
|
138
|
+
: html`
|
|
139
|
+
<div class="space-y-2">
|
|
140
|
+
${nodes.map((node) => {
|
|
285
141
|
const nodeId = String(node?.nodeId || "").trim();
|
|
286
142
|
const browserStatus = browserStatusByNodeId[nodeId] || null;
|
|
287
143
|
const browserError = browserErrorByNodeId[nodeId] || "";
|
|
288
144
|
const checkingBrowser = checkingBrowserNodeId === nodeId;
|
|
289
145
|
const canCheckBrowser =
|
|
290
146
|
node?.connected && isBrowserCapableNode(node) && nodeId;
|
|
291
|
-
const browserAttachEnabled =
|
|
292
|
-
|
|
147
|
+
const browserAttachEnabled =
|
|
148
|
+
browserAttachStateByNodeId?.[nodeId] === true;
|
|
149
|
+
const hasBrowserCheckResult =
|
|
150
|
+
!!browserStatus || !!browserError;
|
|
293
151
|
const browserAttached = browserStatus?.running === true;
|
|
294
152
|
const showResolvingSpinner =
|
|
295
|
-
browserAttachEnabled &&
|
|
153
|
+
browserAttachEnabled &&
|
|
154
|
+
!hasBrowserCheckResult &&
|
|
155
|
+
!checkingBrowser;
|
|
296
156
|
const showBrowserCheckButton =
|
|
297
157
|
canCheckBrowser &&
|
|
298
158
|
browserAttachEnabled &&
|
|
@@ -300,164 +160,281 @@ export const ConnectedNodesCard = ({
|
|
|
300
160
|
hasBrowserCheckResult &&
|
|
301
161
|
!browserAttached;
|
|
302
162
|
return html`
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
163
|
+
<div
|
|
164
|
+
class="bg-surface border border-border rounded-xl p-4 space-y-2"
|
|
165
|
+
>
|
|
166
|
+
<div class="flex items-center justify-between gap-2">
|
|
167
|
+
<div class="min-w-0 space-y-1">
|
|
168
|
+
<div class="flex items-center gap-2 min-w-0 mb-2">
|
|
169
|
+
<div class="text-sm font-medium truncate">
|
|
170
|
+
${node?.displayName ||
|
|
171
|
+
node?.nodeId ||
|
|
172
|
+
"Unnamed node"}
|
|
173
|
+
</div>
|
|
174
|
+
${nodeId
|
|
175
|
+
? html`
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
class="shrink-0 inline-flex items-center gap-1 text-[11px] text-gray-500 hover:text-gray-300"
|
|
179
|
+
onclick=${() =>
|
|
180
|
+
handleCopyText(nodeId, {
|
|
181
|
+
successMessage: "Device ID copied",
|
|
182
|
+
errorMessage:
|
|
183
|
+
"Could not copy device ID",
|
|
184
|
+
})}
|
|
185
|
+
>
|
|
186
|
+
<${FileCopyLineIcon}
|
|
187
|
+
className="w-3.5 h-3.5"
|
|
188
|
+
/>
|
|
189
|
+
<span>Copy device id</span>
|
|
190
|
+
</button>
|
|
191
|
+
`
|
|
192
|
+
: null}
|
|
193
|
+
</div>
|
|
308
194
|
</div>
|
|
309
|
-
<div class="
|
|
310
|
-
${node
|
|
195
|
+
<div class="flex items-center gap-1.5">
|
|
196
|
+
${renderNodeStatusBadge(node)}
|
|
197
|
+
${node?.paired
|
|
198
|
+
? html`
|
|
199
|
+
<${OverflowMenu}
|
|
200
|
+
open=${menuOpenNodeId === nodeId}
|
|
201
|
+
ariaLabel="Open node actions"
|
|
202
|
+
title="Open node actions"
|
|
203
|
+
onClose=${() => setMenuOpenNodeId("")}
|
|
204
|
+
onToggle=${() => handleOpenNodeMenu(nodeId)}
|
|
205
|
+
>
|
|
206
|
+
<${OverflowMenuItem}
|
|
207
|
+
className="text-red-300 hover:text-red-200"
|
|
208
|
+
onClick=${() => {
|
|
209
|
+
setMenuOpenNodeId("");
|
|
210
|
+
setRemoveDialogNode(node);
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
Remove device
|
|
214
|
+
</${OverflowMenuItem}>
|
|
215
|
+
</${OverflowMenu}>
|
|
216
|
+
`
|
|
217
|
+
: null}
|
|
311
218
|
</div>
|
|
312
219
|
</div>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
220
|
+
<div class="flex flex-wrap gap-2 text-[11px]">
|
|
221
|
+
<div class="ac-surface-inset rounded-lg px-2.5 py-1">
|
|
222
|
+
<span class="text-gray-500">platform: </span>
|
|
223
|
+
<code>${node?.platform || "unknown"}</code>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="ac-surface-inset rounded-lg px-2.5 py-1">
|
|
226
|
+
<span class="text-gray-500">version: </span>
|
|
227
|
+
<code>${node?.version || "unknown"}</code>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="ac-surface-inset rounded-lg px-2.5 py-1">
|
|
230
|
+
<span class="text-gray-500">capabilities: </span>
|
|
231
|
+
<code
|
|
232
|
+
>${Array.isArray(node?.caps)
|
|
233
|
+
? node.caps.join(", ")
|
|
234
|
+
: "none"}</code
|
|
235
|
+
>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
${canCheckBrowser
|
|
239
|
+
? html`
|
|
240
|
+
<div class="space-y-2">
|
|
241
|
+
<div
|
|
242
|
+
class="ac-surface-inset rounded-lg px-3 py-2 space-y-2"
|
|
243
|
+
>
|
|
244
|
+
<div
|
|
245
|
+
class="flex items-start justify-between gap-2"
|
|
246
|
+
>
|
|
247
|
+
<div class="space-y-0.5">
|
|
248
|
+
<div class="text-sm font-medium">
|
|
249
|
+
Browser
|
|
250
|
+
</div>
|
|
251
|
+
${browserAttachEnabled
|
|
252
|
+
? html`
|
|
253
|
+
<div
|
|
254
|
+
class="text-[11px] text-gray-500"
|
|
255
|
+
>
|
|
256
|
+
profile: <code>user</code>
|
|
257
|
+
</div>
|
|
258
|
+
`
|
|
259
|
+
: html`
|
|
260
|
+
<div
|
|
261
|
+
class="text-[11px] text-gray-500"
|
|
262
|
+
>
|
|
263
|
+
Attach is disabled until you click
|
|
264
|
+
${" "}
|
|
265
|
+
<code>Attach</code>
|
|
266
|
+
${" "} (prevents control prompts
|
|
267
|
+
when opening this tab).
|
|
268
|
+
</div>
|
|
269
|
+
`}
|
|
270
|
+
</div>
|
|
271
|
+
<div class="flex items-start gap-2">
|
|
272
|
+
${browserStatus
|
|
273
|
+
? html`
|
|
349
274
|
<span class="inline-flex mt-0.5">
|
|
350
275
|
<${Badge} tone=${getBrowserStatusTone(browserStatus)}
|
|
351
276
|
>${getBrowserStatusLabel(browserStatus)}</${Badge}
|
|
352
277
|
>
|
|
353
278
|
</span>
|
|
354
279
|
`
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
`
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
280
|
+
: null}
|
|
281
|
+
${showResolvingSpinner
|
|
282
|
+
? html`
|
|
283
|
+
<${LoadingSpinner}
|
|
284
|
+
className="h-3.5 w-3.5"
|
|
285
|
+
/>
|
|
286
|
+
`
|
|
287
|
+
: null}
|
|
288
|
+
${checkingBrowser
|
|
289
|
+
? html`
|
|
290
|
+
<${LoadingSpinner}
|
|
291
|
+
className="h-3.5 w-3.5"
|
|
292
|
+
/>
|
|
293
|
+
`
|
|
294
|
+
: null}
|
|
295
|
+
${canCheckBrowser && !browserAttachEnabled
|
|
296
|
+
? html`
|
|
297
|
+
<${ActionButton}
|
|
298
|
+
onClick=${() =>
|
|
299
|
+
handleAttachNodeBrowser(nodeId)}
|
|
300
|
+
idleLabel="Attach"
|
|
301
|
+
tone="primary"
|
|
302
|
+
size="sm"
|
|
303
|
+
/>
|
|
304
|
+
`
|
|
305
|
+
: null}
|
|
306
|
+
${showBrowserCheckButton
|
|
307
|
+
? html`
|
|
308
|
+
<${ActionButton}
|
|
309
|
+
onClick=${() =>
|
|
310
|
+
handleCheckNodeBrowser(nodeId)}
|
|
311
|
+
idleLabel="Check"
|
|
312
|
+
tone="secondary"
|
|
313
|
+
size="sm"
|
|
314
|
+
/>
|
|
315
|
+
`
|
|
316
|
+
: null}
|
|
317
|
+
</div>
|
|
386
318
|
</div>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
<
|
|
393
|
-
|
|
319
|
+
${browserStatus
|
|
320
|
+
? html`
|
|
321
|
+
<div
|
|
322
|
+
class="flex items-center justify-between gap-2"
|
|
323
|
+
>
|
|
324
|
+
<div
|
|
325
|
+
class="flex flex-wrap gap-2 text-[11px] text-gray-500"
|
|
326
|
+
>
|
|
327
|
+
<span
|
|
328
|
+
>driver:
|
|
329
|
+
<code
|
|
330
|
+
>${browserStatus?.driver ||
|
|
331
|
+
"unknown"}</code
|
|
332
|
+
></span
|
|
333
|
+
>
|
|
334
|
+
<span
|
|
335
|
+
>transport:
|
|
336
|
+
<code
|
|
337
|
+
>${browserStatus?.transport ||
|
|
338
|
+
"unknown"}</code
|
|
339
|
+
></span
|
|
340
|
+
>
|
|
341
|
+
</div>
|
|
342
|
+
${browserAttachEnabled
|
|
343
|
+
? html`
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onclick=${() =>
|
|
347
|
+
handleDetachNodeBrowser(
|
|
348
|
+
nodeId,
|
|
349
|
+
)}
|
|
350
|
+
class="shrink-0 text-[11px] text-gray-500 hover:text-gray-300"
|
|
351
|
+
>
|
|
352
|
+
Detach
|
|
353
|
+
</button>
|
|
354
|
+
`
|
|
355
|
+
: null}
|
|
394
356
|
</div>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
`
|
|
406
|
-
: null}
|
|
407
|
-
</div>
|
|
408
|
-
`
|
|
409
|
-
: null}
|
|
410
|
-
${browserError
|
|
411
|
-
? html`<div class="text-[11px] text-red-400">${browserError}</div>`
|
|
412
|
-
: null}
|
|
413
|
-
</div>
|
|
414
|
-
</div>
|
|
415
|
-
`
|
|
416
|
-
: null}
|
|
417
|
-
${node?.paired && !node?.connected && connectInfo
|
|
418
|
-
? html`
|
|
419
|
-
<div class="border-t border-border pt-2 space-y-2">
|
|
420
|
-
<div class="text-[11px] text-gray-500">
|
|
421
|
-
Reconnect command
|
|
357
|
+
`
|
|
358
|
+
: null}
|
|
359
|
+
${browserError
|
|
360
|
+
? html`<div
|
|
361
|
+
class="text-[11px] text-red-400"
|
|
362
|
+
>
|
|
363
|
+
${browserError}
|
|
364
|
+
</div>`
|
|
365
|
+
: null}
|
|
366
|
+
</div>
|
|
422
367
|
</div>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
368
|
+
`
|
|
369
|
+
: null}
|
|
370
|
+
${node?.paired && !node?.connected && connectInfo
|
|
371
|
+
? html`
|
|
372
|
+
<div
|
|
373
|
+
class="border-t border-border pt-2 space-y-2"
|
|
374
|
+
>
|
|
375
|
+
<div class="text-[11px] text-gray-500">
|
|
376
|
+
Reconnect command
|
|
377
|
+
</div>
|
|
378
|
+
<div class="flex items-center gap-2">
|
|
379
|
+
<input
|
|
380
|
+
type="text"
|
|
381
|
+
readonly
|
|
382
|
+
value=${buildReconnectCommand({
|
|
383
|
+
node,
|
|
384
|
+
connectInfo,
|
|
385
|
+
maskToken: true,
|
|
386
|
+
})}
|
|
387
|
+
class="flex-1 min-w-0 bg-black/30 border border-border rounded-lg px-2 py-1.5 text-[11px] font-mono text-gray-300"
|
|
388
|
+
/>
|
|
389
|
+
<${ActionButton}
|
|
390
|
+
onClick=${() =>
|
|
391
|
+
handleCopyText(
|
|
392
|
+
buildReconnectCommand({
|
|
393
|
+
node,
|
|
394
|
+
connectInfo,
|
|
395
|
+
maskToken: false,
|
|
396
|
+
}),
|
|
397
|
+
{
|
|
398
|
+
successMessage:
|
|
399
|
+
"Connection command copied",
|
|
400
|
+
errorMessage:
|
|
401
|
+
"Could not copy connection command",
|
|
402
|
+
},
|
|
403
|
+
)}
|
|
404
|
+
tone="secondary"
|
|
405
|
+
size="sm"
|
|
406
|
+
iconOnly=${true}
|
|
407
|
+
idleIcon=${FileCopyLineIcon}
|
|
408
|
+
idleIconClassName="w-3.5 h-3.5"
|
|
409
|
+
ariaLabel="Copy reconnect command"
|
|
410
|
+
title="Copy reconnect command"
|
|
411
|
+
/>
|
|
412
|
+
</div>
|
|
451
413
|
</div>
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
414
|
+
`
|
|
415
|
+
: null}
|
|
416
|
+
</div>
|
|
417
|
+
`;
|
|
418
|
+
})}
|
|
419
|
+
</div>
|
|
420
|
+
`}
|
|
421
|
+
</div>
|
|
422
|
+
<${ConfirmDialog}
|
|
423
|
+
visible=${!!removeDialogNode}
|
|
424
|
+
title="Remove device?"
|
|
425
|
+
message=${removeDialogNode?.connected
|
|
426
|
+
? "This device is currently connected. Removing it will disconnect and remove the paired device from this gateway (equivalent to running openclaw devices remove for this device id). The device can reconnect and pair again later."
|
|
427
|
+
: "This removes the paired device from this gateway (equivalent to running openclaw devices remove for this device id). The device can reconnect and pair again later."}
|
|
428
|
+
confirmLabel="Remove device"
|
|
429
|
+
confirmLoadingLabel="Removing..."
|
|
430
|
+
confirmTone="warning"
|
|
431
|
+
confirmLoading=${Boolean(removingNodeId)}
|
|
432
|
+
confirmDisabled=${Boolean(removingNodeId)}
|
|
433
|
+
onCancel=${() => {
|
|
434
|
+
if (removingNodeId) return;
|
|
435
|
+
setRemoveDialogNode(null);
|
|
436
|
+
}}
|
|
437
|
+
onConfirm=${handleRemoveNode}
|
|
438
|
+
/>
|
|
439
|
+
`;
|
|
463
440
|
};
|