@chrysb/alphaclaw 0.9.9 → 0.9.10
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/dist/app.bundle.js +1424 -1398
- package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +4 -5
- package/lib/public/js/components/agents-tab/agent-pairing-section.js +11 -5
- package/lib/public/js/components/cron-tab/cron-helpers.js +13 -1
- package/lib/public/js/components/cron-tab/use-cron-tab.js +4 -3
- package/lib/public/js/components/general/index.js +6 -1
- package/lib/public/js/components/general/use-general-tab.js +17 -20
- package/lib/public/js/components/models-tab/index.js +5 -1
- package/lib/public/js/components/models-tab/model-picker.js +52 -0
- package/lib/public/js/components/onboarding/use-welcome-pairing.js +4 -1
- package/lib/public/js/components/pairings.js +75 -4
- package/lib/public/js/hooks/usePolling.js +46 -13
- package/lib/public/js/lib/model-config.js +4 -0
- package/lib/server/agents/channels.js +53 -9
- package/lib/server/commands.js +4 -1
- package/lib/server/constants.js +5 -0
- package/lib/server/cost-utils.js +9 -0
- package/lib/server/cron-service.js +12 -1
- package/lib/server/db/doctor/index.js +9 -0
- package/lib/server/db/usage/index.js +13 -0
- package/lib/server/db/watchdog/index.js +13 -1
- package/lib/server/db/webhooks/index.js +13 -1
- package/lib/server/init/register-server-routes.js +2 -0
- package/lib/server/internal-files-migration.js +11 -1
- package/lib/server/model-catalog-cache.js +85 -6
- package/lib/server/onboarding/github.js +79 -2
- package/lib/server/openclaw-version.js +2 -1
- package/lib/server/routes/models.js +26 -0
- package/lib/server/routes/pairings.js +106 -15
- package/lib/server/utils/command-output.js +11 -0
- package/lib/setup/gitignore +2 -0
- package/package.json +2 -2
- package/patches/openclaw+2026.4.21.patch +13 -0
- package/patches/openclaw+2026.4.15.patch +0 -13
|
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "preact/hooks";
|
|
|
2
2
|
import { useModels } from "../../models-tab/use-models.js";
|
|
3
3
|
import {
|
|
4
4
|
buildProviderHasAuth,
|
|
5
|
+
buildSyntheticModelEntry,
|
|
5
6
|
getModelCatalogProvider,
|
|
6
7
|
getModelsTabAuthProvider,
|
|
7
8
|
getProviderSortIndex,
|
|
@@ -51,10 +52,8 @@ export const useModelCard = ({
|
|
|
51
52
|
Object.keys(configuredModels || {})
|
|
52
53
|
.map(
|
|
53
54
|
(modelKey) =>
|
|
54
|
-
resolveCatalogModel(catalog, modelKey) ||
|
|
55
|
-
|
|
56
|
-
label: modelKey,
|
|
57
|
-
},
|
|
55
|
+
resolveCatalogModel(catalog, modelKey) ||
|
|
56
|
+
buildSyntheticModelEntry(modelKey),
|
|
58
57
|
)
|
|
59
58
|
.filter((model) => {
|
|
60
59
|
const provider = getModelsTabAuthProvider(model.key);
|
|
@@ -75,7 +74,7 @@ export const useModelCard = ({
|
|
|
75
74
|
const effectiveModelEntry = useMemo(
|
|
76
75
|
() =>
|
|
77
76
|
resolveCatalogModel(catalog, effectiveModel) ||
|
|
78
|
-
(effectiveModel ?
|
|
77
|
+
(effectiveModel ? buildSyntheticModelEntry(effectiveModel) : null),
|
|
79
78
|
[catalog, effectiveModel],
|
|
80
79
|
);
|
|
81
80
|
|
|
@@ -241,12 +241,14 @@ export const AgentPairingSection = ({ agent = {} }) => {
|
|
|
241
241
|
},
|
|
242
242
|
3000,
|
|
243
243
|
{
|
|
244
|
-
enabled:
|
|
244
|
+
enabled: ownedAccounts.length > 0,
|
|
245
245
|
cacheKey: `/api/pairings?agent=${encodeURIComponent(agentId)}`,
|
|
246
|
+
dedupeInFlight: true,
|
|
246
247
|
},
|
|
247
248
|
);
|
|
248
249
|
|
|
249
250
|
const pending = pairingsPoll.data || [];
|
|
251
|
+
const showPairings = hasUnpaired || pending.length > 0 || pairingStatusRefreshing;
|
|
250
252
|
|
|
251
253
|
const refreshAfterPairingAction = useCallback(() => {
|
|
252
254
|
setPairingStatusRefreshing(true);
|
|
@@ -262,7 +264,7 @@ export const AgentPairingSection = ({ agent = {} }) => {
|
|
|
262
264
|
}
|
|
263
265
|
pairingDelayedRefreshTimerRefs.current = [];
|
|
264
266
|
const refresh = () => {
|
|
265
|
-
pairingsPoll.refresh();
|
|
267
|
+
pairingsPoll.refresh({ force: true });
|
|
266
268
|
loadBindings();
|
|
267
269
|
announcePairingsChanged(agentId);
|
|
268
270
|
};
|
|
@@ -273,10 +275,12 @@ export const AgentPairingSection = ({ agent = {} }) => {
|
|
|
273
275
|
|
|
274
276
|
const handleApprove = async (id, channel, accountId = "") => {
|
|
275
277
|
try {
|
|
276
|
-
await approvePairing(id, channel, accountId);
|
|
278
|
+
const result = await approvePairing(id, channel, accountId);
|
|
279
|
+
if (!result.ok) throw new Error(result.error || "Could not approve pairing");
|
|
277
280
|
refreshAfterPairingAction();
|
|
278
281
|
} catch (err) {
|
|
279
282
|
showToast(err.message || "Could not approve pairing", "error");
|
|
283
|
+
throw err;
|
|
280
284
|
}
|
|
281
285
|
};
|
|
282
286
|
|
|
@@ -286,6 +290,7 @@ export const AgentPairingSection = ({ agent = {} }) => {
|
|
|
286
290
|
refreshAfterPairingAction();
|
|
287
291
|
} catch (err) {
|
|
288
292
|
showToast(err.message || "Could not reject pairing", "error");
|
|
293
|
+
throw err;
|
|
289
294
|
}
|
|
290
295
|
};
|
|
291
296
|
|
|
@@ -298,13 +303,14 @@ export const AgentPairingSection = ({ agent = {} }) => {
|
|
|
298
303
|
`;
|
|
299
304
|
}
|
|
300
305
|
|
|
301
|
-
if (!
|
|
306
|
+
if (!showPairings) return null;
|
|
302
307
|
|
|
303
308
|
return html`
|
|
304
309
|
<${Pairings}
|
|
305
310
|
pending=${pending}
|
|
306
311
|
channels=${ownedChannelsStatus}
|
|
307
|
-
visible=${
|
|
312
|
+
visible=${showPairings}
|
|
313
|
+
pollingInFlight=${pairingsPoll.isPolling}
|
|
308
314
|
statusRefreshing=${pairingStatusRefreshing}
|
|
309
315
|
onApprove=${handleApprove}
|
|
310
316
|
onReject=${handleReject}
|
|
@@ -7,6 +7,18 @@ import {
|
|
|
7
7
|
|
|
8
8
|
export const kAllCronJobsRouteKey = "__all__";
|
|
9
9
|
|
|
10
|
+
export const readCronJobPrompt = (job = {}) => {
|
|
11
|
+
const payload = job?.payload && typeof job.payload === "object" ? job.payload : {};
|
|
12
|
+
const kind = String(payload?.kind || "").trim();
|
|
13
|
+
if (kind === "systemEvent" && typeof payload.text === "string") {
|
|
14
|
+
return payload.text;
|
|
15
|
+
}
|
|
16
|
+
if (kind === "agentTurn" && typeof payload.message === "string") {
|
|
17
|
+
return payload.message;
|
|
18
|
+
}
|
|
19
|
+
return "";
|
|
20
|
+
};
|
|
21
|
+
|
|
10
22
|
const kWeekdayLabelByCronValue = {
|
|
11
23
|
"0": "Sun",
|
|
12
24
|
"1": "Mon",
|
|
@@ -388,7 +400,7 @@ export const buildCronOptimizationWarnings = (jobs = [], bulkRunsByJobId = {}) =
|
|
|
388
400
|
const warnings = [];
|
|
389
401
|
jobs.forEach((job) => {
|
|
390
402
|
const jobId = String(job?.id || "");
|
|
391
|
-
const prompt =
|
|
403
|
+
const prompt = readCronJobPrompt(job).toLowerCase();
|
|
392
404
|
const deliveryMode = String(job?.delivery?.mode || "").toLowerCase();
|
|
393
405
|
if (
|
|
394
406
|
deliveryMode === "none" &&
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "../../lib/api.js";
|
|
23
23
|
import { readUiSettings, writeUiSettings } from "../../lib/ui-settings.js";
|
|
24
24
|
import { showToast } from "../toast.js";
|
|
25
|
-
import { kAllCronJobsRouteKey } from "./cron-helpers.js";
|
|
25
|
+
import { kAllCronJobsRouteKey, readCronJobPrompt } from "./cron-helpers.js";
|
|
26
26
|
|
|
27
27
|
const kDefaultListPanelWidthPx = 372;
|
|
28
28
|
const kListPanelMinWidthPx = 220;
|
|
@@ -178,6 +178,7 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
178
178
|
() => jobs.find((job) => String(job?.id || "") === selectedJobId) || null,
|
|
179
179
|
[jobs, selectedJobId],
|
|
180
180
|
);
|
|
181
|
+
const selectedJobPrompt = readCronJobPrompt(selectedJob);
|
|
181
182
|
|
|
182
183
|
useEffect(() => {
|
|
183
184
|
if (!selectedJobId) {
|
|
@@ -186,11 +187,11 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
186
187
|
setRoutingDraft(kRoutingDefaults);
|
|
187
188
|
return;
|
|
188
189
|
}
|
|
189
|
-
const prompt =
|
|
190
|
+
const prompt = selectedJobPrompt;
|
|
190
191
|
setPromptValue(prompt);
|
|
191
192
|
setSavedPromptValue(prompt);
|
|
192
193
|
setRoutingDraft(readRoutingDraftFromJob(selectedJob));
|
|
193
|
-
}, [selectedJobId,
|
|
194
|
+
}, [selectedJobId, selectedJobPrompt]);
|
|
194
195
|
|
|
195
196
|
useEffect(() => {
|
|
196
197
|
if (!selectedJobId) return;
|
|
@@ -62,6 +62,10 @@ export const GeneralTab = ({
|
|
|
62
62
|
Array.isArray(state.pending) &&
|
|
63
63
|
state.pending.length === 0 &&
|
|
64
64
|
hasWhatsAppAwaitingPairing;
|
|
65
|
+
const showPairings =
|
|
66
|
+
state.hasUnpaired ||
|
|
67
|
+
(Array.isArray(state.pending) && state.pending.length > 0) ||
|
|
68
|
+
state.pairingStatusRefreshing;
|
|
65
69
|
|
|
66
70
|
return html`
|
|
67
71
|
<div class="space-y-4">
|
|
@@ -117,7 +121,8 @@ export const GeneralTab = ({
|
|
|
117
121
|
<${Pairings}
|
|
118
122
|
pending=${state.pending}
|
|
119
123
|
channels=${state.channels}
|
|
120
|
-
visible=${
|
|
124
|
+
visible=${showPairings}
|
|
125
|
+
pollingInFlight=${state.pairingsPolling}
|
|
121
126
|
statusRefreshing=${state.pairingStatusRefreshing}
|
|
122
127
|
onApprove=${actions.handleApprove}
|
|
123
128
|
onReject=${actions.handleReject}
|
|
@@ -56,6 +56,9 @@ export const useGeneralTab = ({
|
|
|
56
56
|
}
|
|
57
57
|
return info.status !== "paired";
|
|
58
58
|
});
|
|
59
|
+
const hasConfiguredPairingChannel = ALL_CHANNELS.some((channel) =>
|
|
60
|
+
Boolean(channels?.[channel]),
|
|
61
|
+
);
|
|
59
62
|
|
|
60
63
|
const pairingsPoll = usePolling(
|
|
61
64
|
async () => {
|
|
@@ -64,8 +67,9 @@ export const useGeneralTab = ({
|
|
|
64
67
|
},
|
|
65
68
|
3000,
|
|
66
69
|
{
|
|
67
|
-
enabled:
|
|
70
|
+
enabled: hasConfiguredPairingChannel && gatewayStatus === "running",
|
|
68
71
|
cacheKey: "/api/pairings",
|
|
72
|
+
dedupeInFlight: true,
|
|
69
73
|
},
|
|
70
74
|
);
|
|
71
75
|
const pending = pairingsPoll.data || [];
|
|
@@ -86,25 +90,10 @@ export const useGeneralTab = ({
|
|
|
86
90
|
);
|
|
87
91
|
const devicePending = devicePoll.data || [];
|
|
88
92
|
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
if (!isActive) return;
|
|
91
|
-
pairingsPoll.refresh();
|
|
92
|
-
if (shouldPollDevices) {
|
|
93
|
-
devicePoll.refresh();
|
|
94
|
-
}
|
|
95
|
-
}, [
|
|
96
|
-
devicePoll.refresh,
|
|
97
|
-
isActive,
|
|
98
|
-
onRefreshStatuses,
|
|
99
|
-
pairingsPoll.refresh,
|
|
100
|
-
devicePollingEnabled,
|
|
101
|
-
shouldPollDevices,
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
93
|
useEffect(() => {
|
|
105
94
|
if (!restartSignal || !isActive) return;
|
|
106
95
|
onRefreshStatuses();
|
|
107
|
-
pairingsPoll.refresh();
|
|
96
|
+
pairingsPoll.refresh({ force: true });
|
|
108
97
|
if (shouldPollDevices) {
|
|
109
98
|
devicePoll.refresh();
|
|
110
99
|
}
|
|
@@ -164,7 +153,7 @@ export const useGeneralTab = ({
|
|
|
164
153
|
pairingRefreshTimerRef.current = null;
|
|
165
154
|
}, 2800);
|
|
166
155
|
onRefreshStatuses();
|
|
167
|
-
pairingsPoll.refresh();
|
|
156
|
+
pairingsPoll.refresh({ force: true });
|
|
168
157
|
setTimeout(() => {
|
|
169
158
|
onRefreshStatuses();
|
|
170
159
|
pairingsPoll.refresh();
|
|
@@ -208,8 +197,14 @@ export const useGeneralTab = ({
|
|
|
208
197
|
};
|
|
209
198
|
|
|
210
199
|
const handleApprove = async (id, channel, accountId = "") => {
|
|
211
|
-
|
|
212
|
-
|
|
200
|
+
try {
|
|
201
|
+
const result = await approvePairing(id, channel, accountId);
|
|
202
|
+
if (!result.ok) throw new Error(result.error || "Could not approve pairing");
|
|
203
|
+
refreshAfterPairingAction();
|
|
204
|
+
} catch (err) {
|
|
205
|
+
showToast(err.message || "Could not approve pairing", "error");
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
213
208
|
};
|
|
214
209
|
|
|
215
210
|
const handleReject = async (id, channel, accountId = "") => {
|
|
@@ -218,6 +213,7 @@ export const useGeneralTab = ({
|
|
|
218
213
|
refreshAfterPairingAction();
|
|
219
214
|
} catch (err) {
|
|
220
215
|
showToast(err.message || "Could not reject pairing", "error");
|
|
216
|
+
throw err;
|
|
221
217
|
}
|
|
222
218
|
};
|
|
223
219
|
|
|
@@ -276,6 +272,7 @@ export const useGeneralTab = ({
|
|
|
276
272
|
hasUnpaired,
|
|
277
273
|
openclawVersion,
|
|
278
274
|
pending,
|
|
275
|
+
pairingsPolling: pairingsPoll.isPolling,
|
|
279
276
|
pairingStatusRefreshing,
|
|
280
277
|
repairingWatchdog,
|
|
281
278
|
repo,
|
|
@@ -10,6 +10,7 @@ import { Badge } from "../badge.js";
|
|
|
10
10
|
import { useModels } from "./use-models.js";
|
|
11
11
|
import {
|
|
12
12
|
buildProviderHasAuth,
|
|
13
|
+
buildSyntheticModelEntry,
|
|
13
14
|
getModelCatalogProvider,
|
|
14
15
|
getModelsTabAuthProvider,
|
|
15
16
|
getProviderSortIndex,
|
|
@@ -111,12 +112,14 @@ export const Models = ({ onRestartRequired = () => {}, agentId, embedded = false
|
|
|
111
112
|
const configuredModelEntries = useMemo(
|
|
112
113
|
() =>
|
|
113
114
|
Object.keys(configuredModels).map((key) => {
|
|
114
|
-
const catalogEntry =
|
|
115
|
+
const catalogEntry =
|
|
116
|
+
catalog.find((m) => m.key === key) || buildSyntheticModelEntry(key);
|
|
115
117
|
const provider = getModelsTabAuthProvider(key);
|
|
116
118
|
const hasAuth = !!providerHasAuth[provider];
|
|
117
119
|
return {
|
|
118
120
|
key,
|
|
119
121
|
label: catalogEntry?.label || key,
|
|
122
|
+
provider: catalogEntry?.provider || provider,
|
|
120
123
|
isPrimary: key === primary,
|
|
121
124
|
hasAuth,
|
|
122
125
|
};
|
|
@@ -216,6 +219,7 @@ export const Models = ({ onRestartRequired = () => {}, agentId, embedded = false
|
|
|
216
219
|
<${SearchableModelPicker}
|
|
217
220
|
options=${pickerModels}
|
|
218
221
|
popularModels=${popularPickerModels}
|
|
222
|
+
configuredOptions=${configuredModelEntries}
|
|
219
223
|
placeholder="Add model..."
|
|
220
224
|
onSelect=${(modelKey) => {
|
|
221
225
|
addModel(modelKey);
|
|
@@ -36,6 +36,33 @@ const formatProviderSectionLabel = (provider) =>
|
|
|
36
36
|
|
|
37
37
|
const normalizeSearch = (value) => String(value || "").trim().toLowerCase();
|
|
38
38
|
|
|
39
|
+
const titleCaseToken = (value) => {
|
|
40
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
41
|
+
if (!normalized) return "";
|
|
42
|
+
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const formatAnthropicModelLabel = (modelId) => {
|
|
46
|
+
const match = /^claude-(opus|sonnet|haiku)-(\d+)[-.](\d+)$/i.exec(
|
|
47
|
+
String(modelId || "").trim(),
|
|
48
|
+
);
|
|
49
|
+
if (!match) return "";
|
|
50
|
+
return `Claude ${titleCaseToken(match[1])} ${match[2]}.${match[3]}`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const buildSyntheticModelEntry = (modelKey) => {
|
|
54
|
+
const key = String(modelKey || "").trim();
|
|
55
|
+
const provider = getModelProvider(key);
|
|
56
|
+
const modelId = key.includes("/") ? key.split("/").slice(1).join("/") : key;
|
|
57
|
+
const anthropicLabel =
|
|
58
|
+
provider === "anthropic" ? formatAnthropicModelLabel(modelId) : "";
|
|
59
|
+
return {
|
|
60
|
+
key,
|
|
61
|
+
provider,
|
|
62
|
+
label: anthropicLabel || key,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
39
66
|
export const getModelDisplayLabel = (model) => model?.featuredLabel || model?.label || model?.key;
|
|
40
67
|
|
|
41
68
|
const buildModelSearchText = (model) =>
|
|
@@ -67,6 +94,7 @@ export const buildProviderHasAuth = ({
|
|
|
67
94
|
export const SearchableModelPicker = ({
|
|
68
95
|
options = [],
|
|
69
96
|
popularModels = [],
|
|
97
|
+
configuredOptions = [],
|
|
70
98
|
placeholder = "Add model...",
|
|
71
99
|
onSelect = () => {},
|
|
72
100
|
disabled = false,
|
|
@@ -84,6 +112,15 @@ export const SearchableModelPicker = ({
|
|
|
84
112
|
: options,
|
|
85
113
|
[options, normalizedQuery],
|
|
86
114
|
);
|
|
115
|
+
const matchingConfiguredOptions = useMemo(
|
|
116
|
+
() =>
|
|
117
|
+
normalizedQuery
|
|
118
|
+
? configuredOptions.filter((option) =>
|
|
119
|
+
buildModelSearchText(option).includes(normalizedQuery),
|
|
120
|
+
)
|
|
121
|
+
: [],
|
|
122
|
+
[configuredOptions, normalizedQuery],
|
|
123
|
+
);
|
|
87
124
|
const groupedOptions = useMemo(() => {
|
|
88
125
|
const groups = [];
|
|
89
126
|
const showPopularGroup = !normalizedQuery;
|
|
@@ -198,6 +235,21 @@ export const SearchableModelPicker = ({
|
|
|
198
235
|
</div>
|
|
199
236
|
`,
|
|
200
237
|
)
|
|
238
|
+
: matchingConfiguredOptions.length > 0
|
|
239
|
+
? html`
|
|
240
|
+
<div class="px-3 py-3 text-xs text-fg-muted">
|
|
241
|
+
${matchingConfiguredOptions.length === 1
|
|
242
|
+
? html`
|
|
243
|
+
Already added above:
|
|
244
|
+
<span class="text-body">
|
|
245
|
+
${getModelDisplayLabel(
|
|
246
|
+
matchingConfiguredOptions[0],
|
|
247
|
+
)}
|
|
248
|
+
</span>
|
|
249
|
+
`
|
|
250
|
+
: `${matchingConfiguredOptions.length} matching models are already added above.`}
|
|
251
|
+
</div>
|
|
252
|
+
`
|
|
201
253
|
: html`
|
|
202
254
|
<div class="px-3 py-3 text-xs text-fg-muted">
|
|
203
255
|
No models match that search.
|
|
@@ -20,7 +20,10 @@ export const useWelcomePairing = ({
|
|
|
20
20
|
return allPending.filter((p) => p.channel === selectedPairingChannel);
|
|
21
21
|
},
|
|
22
22
|
1000,
|
|
23
|
-
{
|
|
23
|
+
{
|
|
24
|
+
enabled: isPairingStep && !!selectedPairingChannel,
|
|
25
|
+
dedupeInFlight: true,
|
|
26
|
+
},
|
|
24
27
|
);
|
|
25
28
|
const pairingChannels = pairingStatusPoll.data?.channels || {};
|
|
26
29
|
const canFinishPairing = isChannelPaired(pairingChannels, selectedPairingChannel);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { h } from 'preact';
|
|
2
|
-
import { useState } from 'preact/hooks';
|
|
2
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
3
3
|
import htm from 'htm';
|
|
4
4
|
import { ActionButton } from './action-button.js';
|
|
5
|
+
import { LoadingSpinner } from './loading-spinner.js';
|
|
5
6
|
const html = htm.bind(h);
|
|
6
7
|
|
|
7
8
|
export const PairingRow = ({ p, onApprove, onReject }) => {
|
|
@@ -66,6 +67,13 @@ const ALL_CHANNELS = ['telegram', 'discord', 'slack', 'whatsapp'];
|
|
|
66
67
|
|
|
67
68
|
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
68
69
|
|
|
70
|
+
const getPairingKey = (p) => {
|
|
71
|
+
const channel = String(p?.channel || "").trim().toLowerCase();
|
|
72
|
+
const accountId = String(p?.accountId || "").trim() || "default";
|
|
73
|
+
const id = String(p?.id || p?.code || "").trim();
|
|
74
|
+
return channel && id ? `${channel}\u0000${accountId}\u0000${id}` : "";
|
|
75
|
+
};
|
|
76
|
+
|
|
69
77
|
export function Pairings({
|
|
70
78
|
pending,
|
|
71
79
|
channels,
|
|
@@ -73,7 +81,52 @@ export function Pairings({
|
|
|
73
81
|
onApprove,
|
|
74
82
|
onReject,
|
|
75
83
|
statusRefreshing = false,
|
|
84
|
+
pollingInFlight = false,
|
|
76
85
|
}) {
|
|
86
|
+
const [hiddenPairingKeys, setHiddenPairingKeys] = useState(() => new Set());
|
|
87
|
+
const pendingList = Array.isArray(pending) ? pending : [];
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
setHiddenPairingKeys((current) => {
|
|
91
|
+
if (current.size === 0) return current;
|
|
92
|
+
const pendingKeys = new Set(
|
|
93
|
+
pendingList.map(getPairingKey).filter(Boolean),
|
|
94
|
+
);
|
|
95
|
+
const next = new Set();
|
|
96
|
+
for (const key of current) {
|
|
97
|
+
if (pendingKeys.has(key)) {
|
|
98
|
+
next.add(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return next.size === current.size ? current : next;
|
|
102
|
+
});
|
|
103
|
+
}, [pending]);
|
|
104
|
+
|
|
105
|
+
const hidePairing = (p) => {
|
|
106
|
+
const key = getPairingKey(p);
|
|
107
|
+
if (!key) return;
|
|
108
|
+
setHiddenPairingKeys((current) => {
|
|
109
|
+
if (current.has(key)) return current;
|
|
110
|
+
const next = new Set(current);
|
|
111
|
+
next.add(key);
|
|
112
|
+
return next;
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleApprove = async (p) => {
|
|
117
|
+
await onApprove(p.id, p.channel, p.accountId);
|
|
118
|
+
hidePairing(p);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleReject = async (p) => {
|
|
122
|
+
await onReject(p.id, p.channel, p.accountId);
|
|
123
|
+
hidePairing(p);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const visiblePending = pendingList.filter(
|
|
127
|
+
(p) => !hiddenPairingKeys.has(getPairingKey(p)),
|
|
128
|
+
);
|
|
129
|
+
|
|
77
130
|
if (!visible) return null;
|
|
78
131
|
|
|
79
132
|
const unpaired = ALL_CHANNELS
|
|
@@ -95,12 +148,30 @@ export function Pairings({
|
|
|
95
148
|
? unpaired.join(' or ')
|
|
96
149
|
: unpaired.slice(0, -1).join(', ') + ', or ' + unpaired[unpaired.length - 1];
|
|
97
150
|
|
|
151
|
+
if (unpaired.length === 0 && visiblePending.length === 0) return null;
|
|
152
|
+
|
|
98
153
|
return html`
|
|
99
154
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
100
|
-
<
|
|
101
|
-
|
|
155
|
+
<div class="flex items-center justify-between gap-3 mb-3">
|
|
156
|
+
<h2 class="card-label">Pending Pairings</h2>
|
|
157
|
+
${pollingInFlight
|
|
158
|
+
? html`
|
|
159
|
+
<div class="inline-flex items-center text-fg-muted" aria-label="Pairings refresh in progress">
|
|
160
|
+
<${LoadingSpinner} className="h-3.5 w-3.5 text-fg-muted" />
|
|
161
|
+
</div>
|
|
162
|
+
`
|
|
163
|
+
: null}
|
|
164
|
+
</div>
|
|
165
|
+
${visiblePending.length > 0
|
|
102
166
|
? html`<div>
|
|
103
|
-
${
|
|
167
|
+
${visiblePending.map((p) => html`
|
|
168
|
+
<${PairingRow}
|
|
169
|
+
key=${getPairingKey(p) || p.id}
|
|
170
|
+
p=${p}
|
|
171
|
+
onApprove=${() => handleApprove(p)}
|
|
172
|
+
onReject=${() => handleReject(p)}
|
|
173
|
+
/>
|
|
174
|
+
`)}
|
|
104
175
|
</div>`
|
|
105
176
|
: statusRefreshing
|
|
106
177
|
? html`<div class="text-center py-4 space-y-2">
|
|
@@ -8,6 +8,7 @@ export const usePolling = (
|
|
|
8
8
|
enabled = true,
|
|
9
9
|
pauseWhenHidden = true,
|
|
10
10
|
cacheKey = "",
|
|
11
|
+
dedupeInFlight = false,
|
|
11
12
|
} = {},
|
|
12
13
|
) => {
|
|
13
14
|
const normalizedCacheKey = String(cacheKey || "");
|
|
@@ -15,23 +16,55 @@ export const usePolling = (
|
|
|
15
16
|
normalizedCacheKey ? getCached(normalizedCacheKey) : null,
|
|
16
17
|
);
|
|
17
18
|
const [error, setError] = useState(null);
|
|
19
|
+
const [isPolling, setIsPolling] = useState(false);
|
|
18
20
|
const fetcherRef = useRef(fetcher);
|
|
21
|
+
const inFlightRefreshRef = useRef(null);
|
|
22
|
+
const activeRefreshCountRef = useRef(0);
|
|
23
|
+
const nextRefreshIdRef = useRef(0);
|
|
24
|
+
const latestRefreshIdRef = useRef(0);
|
|
19
25
|
fetcherRef.current = fetcher;
|
|
20
26
|
|
|
21
|
-
const refresh = useCallback(async () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const refresh = useCallback(async ({ force = false } = {}) => {
|
|
28
|
+
if (dedupeInFlight && inFlightRefreshRef.current && !force) {
|
|
29
|
+
return inFlightRefreshRef.current;
|
|
30
|
+
}
|
|
31
|
+
const refreshId = nextRefreshIdRef.current + 1;
|
|
32
|
+
nextRefreshIdRef.current = refreshId;
|
|
33
|
+
latestRefreshIdRef.current = refreshId;
|
|
34
|
+
activeRefreshCountRef.current += 1;
|
|
35
|
+
setIsPolling(true);
|
|
36
|
+
const refreshPromise = Promise.resolve().then(async () => {
|
|
37
|
+
try {
|
|
38
|
+
const result = await fetcherRef.current();
|
|
39
|
+
if (latestRefreshIdRef.current === refreshId) {
|
|
40
|
+
if (normalizedCacheKey) {
|
|
41
|
+
setCached(normalizedCacheKey, result);
|
|
42
|
+
}
|
|
43
|
+
setData(result);
|
|
44
|
+
setError(null);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (latestRefreshIdRef.current === refreshId) {
|
|
49
|
+
setError(err);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
} finally {
|
|
53
|
+
activeRefreshCountRef.current = Math.max(
|
|
54
|
+
0,
|
|
55
|
+
activeRefreshCountRef.current - 1,
|
|
56
|
+
);
|
|
57
|
+
setIsPolling(activeRefreshCountRef.current > 0);
|
|
58
|
+
if (inFlightRefreshRef.current === refreshPromise) {
|
|
59
|
+
inFlightRefreshRef.current = null;
|
|
60
|
+
}
|
|
26
61
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} catch (err) {
|
|
31
|
-
setError(err);
|
|
32
|
-
return null;
|
|
62
|
+
});
|
|
63
|
+
if (dedupeInFlight) {
|
|
64
|
+
inFlightRefreshRef.current = refreshPromise;
|
|
33
65
|
}
|
|
34
|
-
|
|
66
|
+
return refreshPromise;
|
|
67
|
+
}, [dedupeInFlight, normalizedCacheKey]);
|
|
35
68
|
|
|
36
69
|
useEffect(() => {
|
|
37
70
|
if (!normalizedCacheKey) return;
|
|
@@ -63,5 +96,5 @@ export const usePolling = (
|
|
|
63
96
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
64
97
|
}, [enabled, pauseWhenHidden, refresh]);
|
|
65
98
|
|
|
66
|
-
return { data, error, refresh };
|
|
99
|
+
return { data, error, refresh, isPolling };
|
|
67
100
|
};
|
|
@@ -9,6 +9,10 @@ export const getAuthProviderFromModelProvider = (provider) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export const kFeaturedModelDefs = [
|
|
12
|
+
{
|
|
13
|
+
label: "Opus 4.7",
|
|
14
|
+
preferredKeys: ["anthropic/claude-opus-4-7"],
|
|
15
|
+
},
|
|
12
16
|
{
|
|
13
17
|
label: "Opus 4.6",
|
|
14
18
|
preferredKeys: ["anthropic/claude-opus-4-6"],
|
|
@@ -38,6 +38,41 @@ const createChannelsDomain = ({
|
|
|
38
38
|
}) => {
|
|
39
39
|
let createChannelAccountInProgress = false;
|
|
40
40
|
|
|
41
|
+
const formatClawResultOutput = (result) =>
|
|
42
|
+
[result?.stderr, result?.stdout].filter(Boolean).join("\n").trim();
|
|
43
|
+
|
|
44
|
+
const isConfigMutationConflictResult = (result) =>
|
|
45
|
+
/ConfigMutationConflictError|config changed since last load/i.test(
|
|
46
|
+
formatClawResultOutput(result),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const waitForRetry = (delayMs) =>
|
|
50
|
+
new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
51
|
+
|
|
52
|
+
const clawCmdWithConfigConflictRetry = async (
|
|
53
|
+
command,
|
|
54
|
+
options,
|
|
55
|
+
{ label = "command", delaysMs = [250, 750] } = {},
|
|
56
|
+
) => {
|
|
57
|
+
for (let attempt = 0; attempt <= delaysMs.length; attempt += 1) {
|
|
58
|
+
const result = await clawCmd(command, options);
|
|
59
|
+
if (result?.ok || !isConfigMutationConflictResult(result)) {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
if (attempt >= delaysMs.length) {
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
const delayMs = Number(delaysMs[attempt] || 0);
|
|
66
|
+
console.warn(
|
|
67
|
+
`[alphaclaw] Retrying openclaw ${label} after config mutation conflict`,
|
|
68
|
+
);
|
|
69
|
+
if (delayMs > 0) {
|
|
70
|
+
await waitForRetry(delayMs);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ok: false, stdout: "", stderr: "Command retry exhausted" };
|
|
74
|
+
};
|
|
75
|
+
|
|
41
76
|
const getChannelAccountToken = ({
|
|
42
77
|
provider: rawProvider,
|
|
43
78
|
accountId: rawAccountId,
|
|
@@ -266,10 +301,14 @@ const createChannelsDomain = ({
|
|
|
266
301
|
? `--app-token ${shellEscapeArg(appToken)}`
|
|
267
302
|
: "",
|
|
268
303
|
].filter(Boolean);
|
|
269
|
-
const addResult = await
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
304
|
+
const addResult = await clawCmdWithConfigConflictRetry(
|
|
305
|
+
addArgs.join(" "),
|
|
306
|
+
{
|
|
307
|
+
quiet: true,
|
|
308
|
+
timeoutMs: 30000,
|
|
309
|
+
},
|
|
310
|
+
{ label: "channels add" },
|
|
311
|
+
);
|
|
273
312
|
if (!addResult?.ok) {
|
|
274
313
|
throw new Error(
|
|
275
314
|
addResult?.stderr ||
|
|
@@ -323,9 +362,10 @@ const createChannelsDomain = ({
|
|
|
323
362
|
saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
|
|
324
363
|
onProgress({ phase: "binding", label: "Binding agent..." });
|
|
325
364
|
const bindSpec = buildBindingSpec({ provider, accountId });
|
|
326
|
-
const bindResult = await
|
|
365
|
+
const bindResult = await clawCmdWithConfigConflictRetry(
|
|
327
366
|
`agents bind --agent ${shellEscapeArg(agentId)} --bind ${shellEscapeArg(bindSpec)}`,
|
|
328
367
|
{ quiet: true, timeoutMs: 30000 },
|
|
368
|
+
{ label: "agents bind" },
|
|
329
369
|
);
|
|
330
370
|
if (!bindResult?.ok) {
|
|
331
371
|
throw new Error(
|
|
@@ -885,10 +925,14 @@ const createChannelsDomain = ({
|
|
|
885
925
|
name ? `--name ${shellEscapeArg(name)}` : "",
|
|
886
926
|
`--token ${shellEscapeArg(ownerNumber)}`,
|
|
887
927
|
].filter(Boolean);
|
|
888
|
-
const addResult = await
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
928
|
+
const addResult = await clawCmdWithConfigConflictRetry(
|
|
929
|
+
addArgs.join(" "),
|
|
930
|
+
{
|
|
931
|
+
quiet: true,
|
|
932
|
+
timeoutMs: 30000,
|
|
933
|
+
},
|
|
934
|
+
{ label: "channels add" },
|
|
935
|
+
);
|
|
892
936
|
if (!addResult?.ok) {
|
|
893
937
|
throw new Error(
|
|
894
938
|
addResult?.stderr ||
|