@chrysb/alphaclaw 0.7.2-beta.3 → 0.7.2-beta.5
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/js/components/nodes-tab/connected-nodes/index.js +164 -25
- package/lib/public/js/components/nodes-tab/index.js +2 -0
- package/lib/public/js/components/nodes-tab/setup-wizard/index.js +35 -4
- package/lib/public/js/components/nodes-tab/setup-wizard/use-setup-wizard.js +58 -4
- package/lib/public/js/components/nodes-tab/use-nodes-tab.js +14 -2
- package/package.json +2 -2
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
|
-
import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
4
4
|
import { ActionButton } from "../../action-button.js";
|
|
5
5
|
import { Badge } from "../../badge.js";
|
|
6
6
|
import { ComputerLineIcon, FileCopyLineIcon } from "../../icons.js";
|
|
7
7
|
import { LoadingSpinner } from "../../loading-spinner.js";
|
|
8
8
|
import { copyTextToClipboard } from "../../../lib/clipboard.js";
|
|
9
9
|
import { fetchNodeBrowserStatusForNode } from "../../../lib/api.js";
|
|
10
|
+
import { readUiSettings, updateUiSettings } from "../../../lib/ui-settings.js";
|
|
10
11
|
import { showToast } from "../../toast.js";
|
|
11
12
|
|
|
12
13
|
const html = htm.bind(h);
|
|
13
14
|
const kBrowserCheckTimeoutMs = 10000;
|
|
15
|
+
const kBrowserPollIntervalMs = 10000;
|
|
16
|
+
const kBrowserAttachStateByNodeKey = "nodesBrowserAttachStateByNode";
|
|
14
17
|
|
|
15
18
|
const escapeDoubleQuotes = (value) => String(value || "").replace(/"/g, '\\"');
|
|
16
19
|
|
|
@@ -78,6 +81,27 @@ const withTimeout = async (promise, timeoutMs = kBrowserCheckTimeoutMs) => {
|
|
|
78
81
|
}
|
|
79
82
|
};
|
|
80
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
|
+
|
|
81
105
|
export const ConnectedNodesCard = ({
|
|
82
106
|
nodes = [],
|
|
83
107
|
pending = [],
|
|
@@ -88,7 +112,11 @@ export const ConnectedNodesCard = ({
|
|
|
88
112
|
const [browserStatusByNodeId, setBrowserStatusByNodeId] = useState({});
|
|
89
113
|
const [browserErrorByNodeId, setBrowserErrorByNodeId] = useState({});
|
|
90
114
|
const [checkingBrowserNodeId, setCheckingBrowserNodeId] = useState("");
|
|
91
|
-
const
|
|
115
|
+
const [browserAttachStateByNodeId, setBrowserAttachStateByNodeId] = useState(() =>
|
|
116
|
+
readBrowserAttachStateByNode(),
|
|
117
|
+
);
|
|
118
|
+
const browserPollCursorRef = useRef(0);
|
|
119
|
+
const browserCheckInFlightNodeIdRef = useRef("");
|
|
92
120
|
|
|
93
121
|
const handleCopyCommand = async (command) => {
|
|
94
122
|
const copied = await copyTextToClipboard(command);
|
|
@@ -99,10 +127,13 @@ export const ConnectedNodesCard = ({
|
|
|
99
127
|
showToast("Could not copy connection command", "error");
|
|
100
128
|
};
|
|
101
129
|
|
|
102
|
-
const handleCheckNodeBrowser = async (nodeId, { silent = false } = {}) => {
|
|
130
|
+
const handleCheckNodeBrowser = useCallback(async (nodeId, { silent = false } = {}) => {
|
|
103
131
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
104
|
-
if (!normalizedNodeId ||
|
|
105
|
-
|
|
132
|
+
if (!normalizedNodeId || browserCheckInFlightNodeIdRef.current) return;
|
|
133
|
+
browserCheckInFlightNodeIdRef.current = normalizedNodeId;
|
|
134
|
+
if (!silent) {
|
|
135
|
+
setCheckingBrowserNodeId(normalizedNodeId);
|
|
136
|
+
}
|
|
106
137
|
setBrowserErrorByNodeId((prev) => ({
|
|
107
138
|
...prev,
|
|
108
139
|
[normalizedNodeId]: "",
|
|
@@ -126,22 +157,82 @@ export const ConnectedNodesCard = ({
|
|
|
126
157
|
showToast(message, "error");
|
|
127
158
|
}
|
|
128
159
|
} finally {
|
|
129
|
-
|
|
160
|
+
browserCheckInFlightNodeIdRef.current = "";
|
|
161
|
+
if (!silent) {
|
|
162
|
+
setCheckingBrowserNodeId("");
|
|
163
|
+
}
|
|
130
164
|
}
|
|
131
|
-
};
|
|
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]);
|
|
132
202
|
|
|
133
203
|
useEffect(() => {
|
|
134
204
|
if (checkingBrowserNodeId) return;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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]);
|
|
145
236
|
|
|
146
237
|
return html`
|
|
147
238
|
<div class="space-y-3">
|
|
@@ -197,9 +288,17 @@ export const ConnectedNodesCard = ({
|
|
|
197
288
|
const checkingBrowser = checkingBrowserNodeId === nodeId;
|
|
198
289
|
const canCheckBrowser =
|
|
199
290
|
node?.connected && isBrowserCapableNode(node) && nodeId;
|
|
291
|
+
const browserAttachEnabled = browserAttachStateByNodeId?.[nodeId] === true;
|
|
200
292
|
const hasBrowserCheckResult = !!browserStatus || !!browserError;
|
|
293
|
+
const browserAttached = browserStatus?.running === true;
|
|
294
|
+
const showResolvingSpinner =
|
|
295
|
+
browserAttachEnabled && !hasBrowserCheckResult && !checkingBrowser;
|
|
201
296
|
const showBrowserCheckButton =
|
|
202
|
-
canCheckBrowser &&
|
|
297
|
+
canCheckBrowser &&
|
|
298
|
+
browserAttachEnabled &&
|
|
299
|
+
!checkingBrowser &&
|
|
300
|
+
hasBrowserCheckResult &&
|
|
301
|
+
!browserAttached;
|
|
203
302
|
return html`
|
|
204
303
|
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
205
304
|
<div class="flex items-center justify-between gap-2">
|
|
@@ -228,9 +327,21 @@ export const ConnectedNodesCard = ({
|
|
|
228
327
|
<div class="flex items-start justify-between gap-2">
|
|
229
328
|
<div class="space-y-0.5">
|
|
230
329
|
<div class="text-sm font-medium">Browser</div>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
330
|
+
${browserAttachEnabled
|
|
331
|
+
? html`
|
|
332
|
+
<div class="text-[11px] text-gray-500">
|
|
333
|
+
profile: <code>user</code>
|
|
334
|
+
</div>
|
|
335
|
+
`
|
|
336
|
+
: html`
|
|
337
|
+
<div class="text-[11px] text-gray-500">
|
|
338
|
+
Attach is disabled until you click
|
|
339
|
+
${" "}
|
|
340
|
+
<code>Attach</code>
|
|
341
|
+
${" "}
|
|
342
|
+
(prevents control prompts when opening this tab).
|
|
343
|
+
</div>
|
|
344
|
+
`}
|
|
234
345
|
</div>
|
|
235
346
|
<div class="flex items-start gap-2">
|
|
236
347
|
${browserStatus
|
|
@@ -242,11 +353,26 @@ export const ConnectedNodesCard = ({
|
|
|
242
353
|
</span>
|
|
243
354
|
`
|
|
244
355
|
: null}
|
|
356
|
+
${showResolvingSpinner
|
|
357
|
+
? html`
|
|
358
|
+
<${LoadingSpinner} className="h-3.5 w-3.5" />
|
|
359
|
+
`
|
|
360
|
+
: null}
|
|
245
361
|
${checkingBrowser
|
|
246
362
|
? html`
|
|
247
363
|
<${LoadingSpinner} className="h-3.5 w-3.5" />
|
|
248
364
|
`
|
|
249
365
|
: null}
|
|
366
|
+
${canCheckBrowser && !browserAttachEnabled
|
|
367
|
+
? html`
|
|
368
|
+
<${ActionButton}
|
|
369
|
+
onClick=${() => handleAttachNodeBrowser(nodeId)}
|
|
370
|
+
idleLabel="Attach"
|
|
371
|
+
tone="primary"
|
|
372
|
+
size="sm"
|
|
373
|
+
/>
|
|
374
|
+
`
|
|
375
|
+
: null}
|
|
250
376
|
${showBrowserCheckButton
|
|
251
377
|
? html`
|
|
252
378
|
<${ActionButton}
|
|
@@ -261,10 +387,23 @@ export const ConnectedNodesCard = ({
|
|
|
261
387
|
</div>
|
|
262
388
|
${browserStatus
|
|
263
389
|
? html`
|
|
264
|
-
<div class="flex
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
390
|
+
<div class="flex items-center justify-between gap-2">
|
|
391
|
+
<div class="flex flex-wrap gap-2 text-[11px] text-gray-500">
|
|
392
|
+
<span>driver: <code>${browserStatus?.driver || "unknown"}</code></span>
|
|
393
|
+
<span>transport: <code>${browserStatus?.transport || "unknown"}</code></span>
|
|
394
|
+
</div>
|
|
395
|
+
${browserAttachEnabled
|
|
396
|
+
? html`
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
onclick=${() =>
|
|
400
|
+
handleDetachNodeBrowser(nodeId)}
|
|
401
|
+
class="shrink-0 text-[11px] text-gray-500 hover:text-gray-300"
|
|
402
|
+
>
|
|
403
|
+
Detach
|
|
404
|
+
</button>
|
|
405
|
+
`
|
|
406
|
+
: null}
|
|
268
407
|
</div>
|
|
269
408
|
`
|
|
270
409
|
: null}
|
|
@@ -2,7 +2,7 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
3
|
import { ModalShell } from "../../modal-shell.js";
|
|
4
4
|
import { ActionButton } from "../../action-button.js";
|
|
5
|
-
import { CloseIcon } from "../../icons.js";
|
|
5
|
+
import { CloseIcon, FileCopyLineIcon } from "../../icons.js";
|
|
6
6
|
import { copyTextToClipboard } from "../../../lib/clipboard.js";
|
|
7
7
|
import { showToast } from "../../toast.js";
|
|
8
8
|
import { useSetupWizard } from "./use-setup-wizard.js";
|
|
@@ -23,8 +23,9 @@ const renderCommandBlock = ({ command = "", onCopy = () => {} }) => html`
|
|
|
23
23
|
<button
|
|
24
24
|
type="button"
|
|
25
25
|
onclick=${onCopy}
|
|
26
|
-
class="text-xs px-2 py-1 rounded-lg ac-btn-ghost"
|
|
26
|
+
class="text-xs px-2 py-1 rounded-lg ac-btn-ghost inline-flex items-center gap-1.5"
|
|
27
27
|
>
|
|
28
|
+
<${FileCopyLineIcon} className="w-3.5 h-3.5" />
|
|
28
29
|
Copy
|
|
29
30
|
</button>
|
|
30
31
|
</div>
|
|
@@ -57,6 +58,7 @@ export const NodesSetupWizard = ({
|
|
|
57
58
|
onClose,
|
|
58
59
|
});
|
|
59
60
|
const isFinalStep = state.step === kWizardSteps.length - 1;
|
|
61
|
+
const canApproveSelectedNode = state.selectedNode?.paired === false;
|
|
60
62
|
|
|
61
63
|
return html`
|
|
62
64
|
<${ModalShell}
|
|
@@ -125,6 +127,13 @@ export const NodesSetupWizard = ({
|
|
|
125
127
|
onCopy: () =>
|
|
126
128
|
copyAndToast(state.connectCommand || "", "command"),
|
|
127
129
|
})}
|
|
130
|
+
<div class="rounded-lg border border-border bg-black/20 px-3 py-2 text-xs text-gray-400">
|
|
131
|
+
${state.pendingSelectableNodes.length
|
|
132
|
+
? `${state.pendingSelectableNodes.length} pending node${state.pendingSelectableNodes.length === 1
|
|
133
|
+
? ""
|
|
134
|
+
: "s"} detected. Continue to Step 3 to approve.`
|
|
135
|
+
: "Pairing requests will show up here. Checks every 3s."}
|
|
136
|
+
</div>
|
|
128
137
|
</div>
|
|
129
138
|
`
|
|
130
139
|
: null}
|
|
@@ -135,6 +144,13 @@ export const NodesSetupWizard = ({
|
|
|
135
144
|
<div class="text-xs text-gray-500">
|
|
136
145
|
Select the node to approve after you run the connect command.
|
|
137
146
|
</div>
|
|
147
|
+
<div class="text-xs text-gray-500">
|
|
148
|
+
This list refreshes automatically every
|
|
149
|
+
${" "}
|
|
150
|
+
<code>${Math.round(state.nodeDiscoveryPollIntervalMs / 1000)}s</code>
|
|
151
|
+
${" "}
|
|
152
|
+
while this step is open.
|
|
153
|
+
</div>
|
|
138
154
|
<div class="flex items-center gap-2">
|
|
139
155
|
<select
|
|
140
156
|
value=${state.selectedNodeId}
|
|
@@ -149,7 +165,10 @@ export const NodesSetupWizard = ({
|
|
|
149
165
|
${state.selectableNodes.map(
|
|
150
166
|
(entry) => html`
|
|
151
167
|
<option value=${entry.nodeId}>
|
|
152
|
-
${entry.displayName} (${entry.nodeId.slice(0, 12)}...)
|
|
168
|
+
${entry.displayName} (${entry.nodeId.slice(0, 12)}...)${entry.paired ===
|
|
169
|
+
false
|
|
170
|
+
? " · pending approval"
|
|
171
|
+
: " · already paired"}
|
|
153
172
|
</option>
|
|
154
173
|
`,
|
|
155
174
|
)}
|
|
@@ -161,6 +180,17 @@ export const NodesSetupWizard = ({
|
|
|
161
180
|
size="sm"
|
|
162
181
|
/>
|
|
163
182
|
</div>
|
|
183
|
+
${state.selectedNodeId && !canApproveSelectedNode
|
|
184
|
+
? html`
|
|
185
|
+
<div class="text-xs text-yellow-300">
|
|
186
|
+
This node is already paired. Pick a
|
|
187
|
+
${" "}
|
|
188
|
+
<code>pending approval</code>
|
|
189
|
+
${" "}
|
|
190
|
+
node to continue.
|
|
191
|
+
</div>
|
|
192
|
+
`
|
|
193
|
+
: null}
|
|
164
194
|
</div>
|
|
165
195
|
`
|
|
166
196
|
: null}
|
|
@@ -234,7 +264,8 @@ export const NodesSetupWizard = ({
|
|
|
234
264
|
tone="primary"
|
|
235
265
|
size="md"
|
|
236
266
|
className="w-full justify-center"
|
|
237
|
-
disabled=${state.step === 2 &&
|
|
267
|
+
disabled=${state.step === 2 &&
|
|
268
|
+
(!state.selectedNodeId || !canApproveSelectedNode)}
|
|
238
269
|
/>
|
|
239
270
|
`}
|
|
240
271
|
</div>
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "https://esm.sh/preact/hooks";
|
|
2
8
|
import {
|
|
3
9
|
approveNode,
|
|
4
10
|
fetchNodeConnectInfo,
|
|
@@ -6,6 +12,8 @@ import {
|
|
|
6
12
|
} from "../../../lib/api.js";
|
|
7
13
|
import { showToast } from "../../toast.js";
|
|
8
14
|
|
|
15
|
+
const kNodeDiscoveryPollIntervalMs = 3000;
|
|
16
|
+
|
|
9
17
|
export const useSetupWizard = ({
|
|
10
18
|
visible = false,
|
|
11
19
|
nodes = [],
|
|
@@ -21,6 +29,7 @@ export const useSetupWizard = ({
|
|
|
21
29
|
const [selectedNodeId, setSelectedNodeId] = useState("");
|
|
22
30
|
const [approvingNodeId, setApprovingNodeId] = useState("");
|
|
23
31
|
const [configuring, setConfiguring] = useState(false);
|
|
32
|
+
const refreshInFlightRef = useRef(false);
|
|
24
33
|
|
|
25
34
|
useEffect(() => {
|
|
26
35
|
if (!visible) return;
|
|
@@ -46,7 +55,10 @@ export const useSetupWizard = ({
|
|
|
46
55
|
}, [visible]);
|
|
47
56
|
|
|
48
57
|
const selectableNodes = useMemo(() => {
|
|
49
|
-
const all = [
|
|
58
|
+
const all = [
|
|
59
|
+
...pending.map((entry) => ({ ...entry, pendingApproval: true })),
|
|
60
|
+
...nodes.map((entry) => ({ ...entry, pendingApproval: false })),
|
|
61
|
+
];
|
|
50
62
|
const seen = new Set();
|
|
51
63
|
const unique = [];
|
|
52
64
|
for (const entry of all) {
|
|
@@ -56,13 +68,18 @@ export const useSetupWizard = ({
|
|
|
56
68
|
unique.push({
|
|
57
69
|
nodeId,
|
|
58
70
|
displayName: String(entry?.displayName || entry?.name || nodeId),
|
|
59
|
-
paired: entry?.paired !== false,
|
|
71
|
+
paired: entry?.pendingApproval ? false : entry?.paired !== false,
|
|
60
72
|
connected: entry?.connected === true,
|
|
61
73
|
});
|
|
62
74
|
}
|
|
63
75
|
return unique;
|
|
64
76
|
}, [nodes, pending]);
|
|
65
77
|
|
|
78
|
+
const pendingSelectableNodes = useMemo(
|
|
79
|
+
() => selectableNodes.filter((entry) => entry.paired === false),
|
|
80
|
+
[selectableNodes],
|
|
81
|
+
);
|
|
82
|
+
|
|
66
83
|
const selectedNode = useMemo(
|
|
67
84
|
() =>
|
|
68
85
|
selectableNodes.find(
|
|
@@ -93,9 +110,44 @@ export const useSetupWizard = ({
|
|
|
93
110
|
}, [connectInfo, displayName]);
|
|
94
111
|
|
|
95
112
|
const refreshNodeList = useCallback(async () => {
|
|
96
|
-
|
|
113
|
+
if (refreshInFlightRef.current) return;
|
|
114
|
+
refreshInFlightRef.current = true;
|
|
115
|
+
try {
|
|
116
|
+
await refreshNodes();
|
|
117
|
+
} finally {
|
|
118
|
+
refreshInFlightRef.current = false;
|
|
119
|
+
}
|
|
97
120
|
}, [refreshNodes]);
|
|
98
121
|
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!visible || (step !== 1 && step !== 2)) return;
|
|
124
|
+
let active = true;
|
|
125
|
+
const poll = async () => {
|
|
126
|
+
if (!active) return;
|
|
127
|
+
try {
|
|
128
|
+
await refreshNodeList();
|
|
129
|
+
} catch {}
|
|
130
|
+
};
|
|
131
|
+
poll();
|
|
132
|
+
const timer = setInterval(poll, kNodeDiscoveryPollIntervalMs);
|
|
133
|
+
return () => {
|
|
134
|
+
active = false;
|
|
135
|
+
clearInterval(timer);
|
|
136
|
+
};
|
|
137
|
+
}, [refreshNodeList, step, visible]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!visible || step !== 2) return;
|
|
141
|
+
const hasSelected = selectableNodes.some(
|
|
142
|
+
(entry) => entry.nodeId === String(selectedNodeId || "").trim(),
|
|
143
|
+
);
|
|
144
|
+
if (hasSelected) return;
|
|
145
|
+
const preferredNode =
|
|
146
|
+
pendingSelectableNodes.find((entry) => entry.paired === false) || selectableNodes[0];
|
|
147
|
+
if (!preferredNode) return;
|
|
148
|
+
setSelectedNodeId(preferredNode.nodeId);
|
|
149
|
+
}, [pendingSelectableNodes, selectableNodes, selectedNodeId, step, visible]);
|
|
150
|
+
|
|
99
151
|
const approveSelectedNode = useCallback(async () => {
|
|
100
152
|
const nodeId = String(selectedNodeId || "").trim();
|
|
101
153
|
if (!nodeId || approvingNodeId) return false;
|
|
@@ -150,10 +202,12 @@ export const useSetupWizard = ({
|
|
|
150
202
|
setSelectedNodeId,
|
|
151
203
|
selectedNode,
|
|
152
204
|
selectableNodes,
|
|
205
|
+
pendingSelectableNodes,
|
|
153
206
|
approvingNodeId,
|
|
154
207
|
configuring,
|
|
155
208
|
connectCommand,
|
|
156
209
|
refreshNodeList,
|
|
210
|
+
nodeDiscoveryPollIntervalMs: kNodeDiscoveryPollIntervalMs,
|
|
157
211
|
approveSelectedNode,
|
|
158
212
|
applyGatewayNodeRouting,
|
|
159
213
|
completeWizard,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
1
|
+
import { useCallback, useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
2
2
|
import { fetchNodeConnectInfo } from "../../lib/api.js";
|
|
3
3
|
import { showToast } from "../toast.js";
|
|
4
4
|
import { useConnectedNodes } from "./connected-nodes/user-connected-nodes.js";
|
|
@@ -7,6 +7,7 @@ export const useNodesTab = () => {
|
|
|
7
7
|
const connectedNodesState = useConnectedNodes({ enabled: true });
|
|
8
8
|
const [wizardVisible, setWizardVisible] = useState(false);
|
|
9
9
|
const [connectInfo, setConnectInfo] = useState(null);
|
|
10
|
+
const [refreshingNodes, setRefreshingNodes] = useState(false);
|
|
10
11
|
|
|
11
12
|
useEffect(() => {
|
|
12
13
|
fetchNodeConnectInfo()
|
|
@@ -18,19 +19,30 @@ export const useNodesTab = () => {
|
|
|
18
19
|
});
|
|
19
20
|
}, []);
|
|
20
21
|
|
|
22
|
+
const refreshNodes = useCallback(async () => {
|
|
23
|
+
if (refreshingNodes) return;
|
|
24
|
+
setRefreshingNodes(true);
|
|
25
|
+
try {
|
|
26
|
+
await connectedNodesState.refresh();
|
|
27
|
+
} finally {
|
|
28
|
+
setRefreshingNodes(false);
|
|
29
|
+
}
|
|
30
|
+
}, [connectedNodesState.refresh, refreshingNodes]);
|
|
31
|
+
|
|
21
32
|
return {
|
|
22
33
|
state: {
|
|
23
34
|
wizardVisible,
|
|
24
35
|
nodes: connectedNodesState.nodes,
|
|
25
36
|
pending: connectedNodesState.pending,
|
|
26
37
|
loadingNodes: connectedNodesState.loading,
|
|
38
|
+
refreshingNodes,
|
|
27
39
|
nodesError: connectedNodesState.error,
|
|
28
40
|
connectInfo,
|
|
29
41
|
},
|
|
30
42
|
actions: {
|
|
31
43
|
openWizard: () => setWizardVisible(true),
|
|
32
44
|
closeWizard: () => setWizardVisible(false),
|
|
33
|
-
refreshNodes
|
|
45
|
+
refreshNodes,
|
|
34
46
|
},
|
|
35
47
|
};
|
|
36
48
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrysb/alphaclaw",
|
|
3
|
-
"version": "0.7.2-beta.
|
|
3
|
+
"version": "0.7.2-beta.5",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"express": "^4.21.0",
|
|
33
33
|
"http-proxy": "^1.18.1",
|
|
34
|
-
"openclaw": "2026.3.
|
|
34
|
+
"openclaw": "2026.3.13",
|
|
35
35
|
"ws": "^8.19.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|