@chrysb/alphaclaw 0.4.6-beta.0 → 0.4.6-beta.1
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/doctor/findings-list.js +5 -1
- package/lib/public/js/components/doctor/fix-card-modal.js +51 -9
- package/lib/public/js/components/doctor/helpers.js +6 -31
- package/lib/public/js/components/doctor/index.js +26 -24
- package/lib/public/js/lib/api.js +2 -0
- package/lib/server/doctor/service.js +4 -3
- package/lib/server/routes/doctor.js +1 -0
- package/lib/server/routes/system.js +14 -0
- package/lib/server.js +2 -0
- package/package.json +1 -1
|
@@ -35,11 +35,15 @@ export const DoctorFindingsList = ({
|
|
|
35
35
|
<div class="space-y-2">
|
|
36
36
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
37
37
|
<div class="space-y-2 min-w-0">
|
|
38
|
-
<h3 class="text-sm font-semibold text-gray-100">${card.title}</h3>
|
|
39
38
|
<div class="flex flex-wrap items-center gap-2">
|
|
40
39
|
<${Badge} tone=${getDoctorPriorityTone(card.priority)}>
|
|
41
40
|
${card.priority}
|
|
42
41
|
</${Badge}>
|
|
42
|
+
<h3 class="text-sm font-semibold text-gray-100">
|
|
43
|
+
${card.title}
|
|
44
|
+
</h3>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
43
47
|
<${Badge} tone=${getDoctorCategoryTone(card.category)}>
|
|
44
48
|
${formatDoctorCategory(card.category)}
|
|
45
49
|
</${Badge}>
|
|
@@ -3,8 +3,14 @@ import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { ModalShell } from "../modal-shell.js";
|
|
5
5
|
import { ActionButton } from "../action-button.js";
|
|
6
|
-
import {
|
|
6
|
+
import { Badge } from "../badge.js";
|
|
7
|
+
import {
|
|
8
|
+
fetchAgentSessions,
|
|
9
|
+
sendDoctorCardFix,
|
|
10
|
+
updateDoctorCardStatus,
|
|
11
|
+
} from "../../lib/api.js";
|
|
7
12
|
import { showToast } from "../toast.js";
|
|
13
|
+
import { getDoctorPriorityTone } from "./helpers.js";
|
|
8
14
|
|
|
9
15
|
const html = htm.bind(h);
|
|
10
16
|
|
|
@@ -19,6 +25,12 @@ export const DoctorFixCardModal = ({
|
|
|
19
25
|
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
20
26
|
const [sending, setSending] = useState(false);
|
|
21
27
|
const [loadError, setLoadError] = useState("");
|
|
28
|
+
const [promptText, setPromptText] = useState("");
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!visible) return;
|
|
32
|
+
setPromptText(String(card?.fixPrompt || ""));
|
|
33
|
+
}, [visible, card?.fixPrompt, card?.id]);
|
|
22
34
|
|
|
23
35
|
useEffect(() => {
|
|
24
36
|
if (!visible) return;
|
|
@@ -59,7 +71,10 @@ export const DoctorFixCardModal = ({
|
|
|
59
71
|
}, [visible, card?.id]);
|
|
60
72
|
|
|
61
73
|
const selectedSession = useMemo(
|
|
62
|
-
() =>
|
|
74
|
+
() =>
|
|
75
|
+
sessions.find(
|
|
76
|
+
(sessionRow) => String(sessionRow?.key || "") === selectedSessionKey,
|
|
77
|
+
) || null,
|
|
63
78
|
[sessions, selectedSessionKey],
|
|
64
79
|
);
|
|
65
80
|
|
|
@@ -72,8 +87,17 @@ export const DoctorFixCardModal = ({
|
|
|
72
87
|
sessionId: selectedSession?.sessionId || "",
|
|
73
88
|
replyChannel: selectedSession?.replyChannel || "",
|
|
74
89
|
replyTo: selectedSession?.replyTo || "",
|
|
90
|
+
prompt: promptText,
|
|
75
91
|
});
|
|
76
|
-
|
|
92
|
+
try {
|
|
93
|
+
await updateDoctorCardStatus({ cardId: card.id, status: "fixed" });
|
|
94
|
+
showToast("Doctor fix request sent and finding marked fixed", "success");
|
|
95
|
+
} catch (statusError) {
|
|
96
|
+
showToast(
|
|
97
|
+
statusError.message || "Doctor fix request sent, but could not mark the finding fixed",
|
|
98
|
+
"warning",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
77
101
|
onComplete();
|
|
78
102
|
onClose();
|
|
79
103
|
} catch (error) {
|
|
@@ -96,8 +120,14 @@ export const DoctorFixCardModal = ({
|
|
|
96
120
|
</p>
|
|
97
121
|
</div>
|
|
98
122
|
<div class="ac-surface-inset p-3 space-y-1">
|
|
99
|
-
<div class="
|
|
100
|
-
|
|
123
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
124
|
+
<${Badge} tone=${getDoctorPriorityTone(card?.priority || "P2")}>
|
|
125
|
+
${card?.priority || "P2"}
|
|
126
|
+
</${Badge}>
|
|
127
|
+
<div class="text-sm font-semibold text-gray-200 leading-5 min-w-0">
|
|
128
|
+
${card?.title || "Doctor finding"}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
101
131
|
<div class="text-xs text-gray-400">${card?.recommendation || ""}</div>
|
|
102
132
|
</div>
|
|
103
133
|
<div class="space-y-2">
|
|
@@ -116,11 +146,23 @@ export const DoctorFixCardModal = ({
|
|
|
116
146
|
`,
|
|
117
147
|
)}
|
|
118
148
|
</select>
|
|
119
|
-
${
|
|
120
|
-
|
|
121
|
-
|
|
149
|
+
${
|
|
150
|
+
loadingSessions
|
|
151
|
+
? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
|
|
152
|
+
: null
|
|
153
|
+
}
|
|
122
154
|
${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
|
|
123
155
|
</div>
|
|
156
|
+
<div class="space-y-2">
|
|
157
|
+
<label class="text-xs text-gray-500">Instructions</label>
|
|
158
|
+
<textarea
|
|
159
|
+
value=${promptText}
|
|
160
|
+
onInput=${(event) => setPromptText(String(event.currentTarget?.value || ""))}
|
|
161
|
+
disabled=${sending}
|
|
162
|
+
rows="8"
|
|
163
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500 font-mono leading-5"
|
|
164
|
+
></textarea>
|
|
165
|
+
</div>
|
|
124
166
|
<div class="flex items-center justify-end gap-2">
|
|
125
167
|
<${ActionButton}
|
|
126
168
|
onClick=${onClose}
|
|
@@ -131,7 +173,7 @@ export const DoctorFixCardModal = ({
|
|
|
131
173
|
/>
|
|
132
174
|
<${ActionButton}
|
|
133
175
|
onClick=${handleSend}
|
|
134
|
-
disabled=${!selectedSession || loadingSessions || !!loadError}
|
|
176
|
+
disabled=${!selectedSession || loadingSessions || !!loadError || !String(promptText || "").trim()}
|
|
135
177
|
loading=${sending}
|
|
136
178
|
tone="primary"
|
|
137
179
|
size="md"
|
|
@@ -81,40 +81,15 @@ export const getDoctorWarningMessage = (doctorStatus = null) => {
|
|
|
81
81
|
return "Doctor has not been run in the last week.";
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
export const
|
|
85
|
-
const deltaScore = Number(changeSummary?.deltaScore || 0);
|
|
84
|
+
export const getDoctorChangeLabel = (changeSummary = null) => {
|
|
86
85
|
const changedFilesCount = Number(changeSummary?.changedFilesCount || 0);
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
label: "Unknown",
|
|
92
|
-
tone: "neutral",
|
|
93
|
-
detail: "No prior baseline yet",
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
if (deltaScore >= 8 || changedFilesCount >= 8) {
|
|
97
|
-
return {
|
|
98
|
-
label: "High",
|
|
99
|
-
tone: "danger",
|
|
100
|
-
detail: `${changedFilesCount} changed file${changedFilesCount === 1 ? "" : "s"}`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
if (deltaScore >= 4 || changedFilesCount >= 4) {
|
|
104
|
-
return {
|
|
105
|
-
label: "Moderate",
|
|
106
|
-
tone: "warning",
|
|
107
|
-
detail: `${changedFilesCount} changed file${changedFilesCount === 1 ? "" : "s"}`,
|
|
108
|
-
};
|
|
86
|
+
const hasMeaningfulChanges = !!changeSummary?.hasMeaningfulChanges;
|
|
87
|
+
if (changedFilesCount === 0) {
|
|
88
|
+
return { text: "No changes since last run", meaningful: false };
|
|
109
89
|
}
|
|
110
90
|
return {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
detail: changedFilesCount
|
|
114
|
-
? `${changedFilesCount} changed file${changedFilesCount === 1 ? "" : "s"}`
|
|
115
|
-
: baselineSource === "initial_install"
|
|
116
|
-
? "Compared to initial install"
|
|
117
|
-
: "No detected changes",
|
|
91
|
+
text: `${changedFilesCount} change${changedFilesCount === 1 ? "" : "s"} since last run`,
|
|
92
|
+
meaningful: hasMeaningfulChanges,
|
|
118
93
|
};
|
|
119
94
|
};
|
|
120
95
|
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from "../../lib/api.js";
|
|
12
12
|
import { formatLocaleDateTime } from "../../lib/format.js";
|
|
13
13
|
import { ActionButton } from "../action-button.js";
|
|
14
|
-
import { Badge } from "../badge.js";
|
|
15
14
|
import { LoadingSpinner } from "../loading-spinner.js";
|
|
16
15
|
import { PageHeader } from "../page-header.js";
|
|
17
16
|
import { showToast } from "../toast.js";
|
|
@@ -21,7 +20,7 @@ import { DoctorFixCardModal } from "./fix-card-modal.js";
|
|
|
21
20
|
import {
|
|
22
21
|
buildDoctorRunMarkers,
|
|
23
22
|
buildDoctorStatusFilterOptions,
|
|
24
|
-
|
|
23
|
+
getDoctorChangeLabel,
|
|
25
24
|
getDoctorRunPillDetail,
|
|
26
25
|
shouldShowDoctorWarning,
|
|
27
26
|
} from "./helpers.js";
|
|
@@ -167,8 +166,8 @@ export const DoctorTab = ({ isActive = false }) => {
|
|
|
167
166
|
() => buildDoctorStatusFilterOptions(),
|
|
168
167
|
[],
|
|
169
168
|
);
|
|
170
|
-
const
|
|
171
|
-
() =>
|
|
169
|
+
const changeLabel = useMemo(
|
|
170
|
+
() => getDoctorChangeLabel(doctorStatus?.changeSummary || null),
|
|
172
171
|
[doctorStatus],
|
|
173
172
|
);
|
|
174
173
|
const canRunDoctor = useMemo(() => {
|
|
@@ -183,6 +182,7 @@ export const DoctorTab = ({ isActive = false }) => {
|
|
|
183
182
|
() => shouldShowDoctorWarning(doctorStatus, 0),
|
|
184
183
|
[doctorStatus],
|
|
185
184
|
);
|
|
185
|
+
const hasCompletedDoctorRun = !!doctorStatus?.lastRunAt;
|
|
186
186
|
const hasRuns = runs.length > 0;
|
|
187
187
|
const hasLoadedRuns = runsPoll.data !== null || runsPoll.error !== null;
|
|
188
188
|
const hasLoadedCards = cardsPoll.data !== null || cardsPoll.error !== null;
|
|
@@ -314,25 +314,27 @@ export const DoctorTab = ({ isActive = false }) => {
|
|
|
314
314
|
? html`
|
|
315
315
|
<${DoctorSummaryCards} cards=${openCards} />
|
|
316
316
|
<div class="space-y-2">
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
317
|
+
${hasCompletedDoctorRun
|
|
318
|
+
? html`
|
|
319
|
+
<div
|
|
320
|
+
class="bg-surface border border-border rounded-xl p-4 flex flex-wrap items-center justify-between gap-3"
|
|
321
|
+
>
|
|
322
|
+
<span class="text-xs text-gray-500">
|
|
323
|
+
Last run ·${" "}
|
|
324
|
+
<span class="text-gray-300">
|
|
325
|
+
${formatLocaleDateTime(doctorStatus?.lastRunAt, {
|
|
326
|
+
fallback: "Never",
|
|
327
|
+
})}
|
|
328
|
+
</span>
|
|
329
|
+
</span>
|
|
330
|
+
<span
|
|
331
|
+
class=${`text-xs ${changeLabel.meaningful ? "text-yellow-300" : "text-gray-500"}`}
|
|
332
|
+
>
|
|
333
|
+
${changeLabel.text}
|
|
334
|
+
</span>
|
|
335
|
+
</div>
|
|
336
|
+
`
|
|
337
|
+
: null}
|
|
336
338
|
${
|
|
337
339
|
showDoctorStaleBanner
|
|
338
340
|
? html`
|
|
@@ -376,7 +378,7 @@ export const DoctorTab = ({ isActive = false }) => {
|
|
|
376
378
|
setSelectedRunFilter(String(run.id || ""))}
|
|
377
379
|
>
|
|
378
380
|
<span class="font-medium">Run #${run.id}</span>
|
|
379
|
-
<span class="inline-flex items-center gap-1
|
|
381
|
+
<span class="inline-flex items-center gap-1">
|
|
380
382
|
${markers.map(
|
|
381
383
|
(marker) => html`
|
|
382
384
|
<span
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -261,6 +261,7 @@ export const sendDoctorCardFix = async ({
|
|
|
261
261
|
sessionId = "",
|
|
262
262
|
replyChannel = "",
|
|
263
263
|
replyTo = "",
|
|
264
|
+
prompt = "",
|
|
264
265
|
} = {}) => {
|
|
265
266
|
const res = await authFetch(
|
|
266
267
|
`/api/doctor/findings/${encodeURIComponent(String(cardId || ""))}/fix`,
|
|
@@ -271,6 +272,7 @@ export const sendDoctorCardFix = async ({
|
|
|
271
272
|
sessionId: String(sessionId || ""),
|
|
272
273
|
replyChannel: String(replyChannel || ""),
|
|
273
274
|
replyTo: String(replyTo || ""),
|
|
275
|
+
prompt: String(prompt || ""),
|
|
274
276
|
}),
|
|
275
277
|
},
|
|
276
278
|
);
|
|
@@ -331,12 +331,13 @@ const createDoctorService = ({
|
|
|
331
331
|
sessionId = "",
|
|
332
332
|
replyChannel = "",
|
|
333
333
|
replyTo = "",
|
|
334
|
+
prompt = "",
|
|
334
335
|
} = {}) => {
|
|
335
336
|
const card = getDoctorCard(cardId);
|
|
336
337
|
if (!card) throw new Error("Doctor card not found");
|
|
337
|
-
const
|
|
338
|
-
if (!
|
|
339
|
-
let command = `agent --agent main --message ${shellEscapeArg(
|
|
338
|
+
const resolvedPrompt = String(prompt || card.fixPrompt || "").trim();
|
|
339
|
+
if (!resolvedPrompt) throw new Error("Doctor card does not include a fix prompt");
|
|
340
|
+
let command = `agent --agent main --message ${shellEscapeArg(resolvedPrompt)}`;
|
|
340
341
|
const trimmedSessionId = String(sessionId || "").trim();
|
|
341
342
|
const trimmedReplyChannel = String(replyChannel || "").trim();
|
|
342
343
|
const trimmedReplyTo = String(replyTo || "").trim();
|
|
@@ -109,6 +109,7 @@ const registerDoctorRoutes = ({ app, requireAuth, doctorService }) => {
|
|
|
109
109
|
sessionId: req.body?.sessionId,
|
|
110
110
|
replyChannel: req.body?.replyChannel,
|
|
111
111
|
replyTo: req.body?.replyTo,
|
|
112
|
+
prompt: req.body?.prompt,
|
|
112
113
|
});
|
|
113
114
|
return res.json(result);
|
|
114
115
|
} catch (error) {
|
|
@@ -20,6 +20,7 @@ const registerSystemRoutes = ({
|
|
|
20
20
|
onExpectedGatewayRestart,
|
|
21
21
|
OPENCLAW_DIR,
|
|
22
22
|
restartRequiredState,
|
|
23
|
+
topicRegistry,
|
|
23
24
|
}) => {
|
|
24
25
|
let envRestartPending = false;
|
|
25
26
|
const kEnvVarsReservedForUserInput = new Set([
|
|
@@ -73,6 +74,19 @@ const registerSystemRoutes = ({
|
|
|
73
74
|
if (telegramMatch) {
|
|
74
75
|
return `Telegram ${telegramMatch[1]}`;
|
|
75
76
|
}
|
|
77
|
+
const telegramTopicMatch = key.match(/:telegram:group:([^:]+):topic:([^:]+)$/);
|
|
78
|
+
if (telegramTopicMatch) {
|
|
79
|
+
const [, groupId, topicId] = telegramTopicMatch;
|
|
80
|
+
let groupEntry = null;
|
|
81
|
+
try {
|
|
82
|
+
groupEntry = topicRegistry?.getGroup?.(groupId) || null;
|
|
83
|
+
} catch {}
|
|
84
|
+
const groupName = String(groupEntry?.name || "").trim();
|
|
85
|
+
const topicName = String(groupEntry?.topics?.[topicId]?.name || "").trim();
|
|
86
|
+
if (groupName && topicName) return `Telegram ${groupName} · ${topicName}`;
|
|
87
|
+
if (topicName) return `Telegram Topic ${topicName}`;
|
|
88
|
+
return `Telegram Topic ${topicId}`;
|
|
89
|
+
}
|
|
76
90
|
const directMatch = key.match(/:direct:([^:]+)$/);
|
|
77
91
|
if (directMatch) {
|
|
78
92
|
return `Direct ${directMatch[1]}`;
|
package/lib/server.js
CHANGED
|
@@ -43,6 +43,7 @@ const {
|
|
|
43
43
|
getSessionDetail,
|
|
44
44
|
getSessionTimeSeries,
|
|
45
45
|
} = require("./server/db/usage");
|
|
46
|
+
const topicRegistry = require("./server/topic-registry");
|
|
46
47
|
const {
|
|
47
48
|
initDoctorDb,
|
|
48
49
|
listDoctorRuns,
|
|
@@ -239,6 +240,7 @@ registerSystemRoutes({
|
|
|
239
240
|
onExpectedGatewayRestart: () => watchdog.onExpectedRestart(),
|
|
240
241
|
OPENCLAW_DIR: constants.OPENCLAW_DIR,
|
|
241
242
|
restartRequiredState,
|
|
243
|
+
topicRegistry,
|
|
242
244
|
});
|
|
243
245
|
registerBrowseRoutes({
|
|
244
246
|
app,
|