@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.
@@ -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 { fetchAgentSessions, sendDoctorCardFix } from "../../lib/api.js";
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
- () => sessions.find((sessionRow) => String(sessionRow?.key || "") === selectedSessionKey) || null,
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
- showToast("Doctor fix request sent to your agent", "success");
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="text-xs text-gray-500 uppercase tracking-wide">${card?.priority || "P2"}</div>
100
- <div class="text-sm text-gray-200">${card?.title || "Doctor finding"}</div>
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
- ${loadingSessions
120
- ? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
121
- : null}
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 getDoctorDriftRisk = (changeSummary = null) => {
85
- const deltaScore = Number(changeSummary?.deltaScore || 0);
84
+ export const getDoctorChangeLabel = (changeSummary = null) => {
86
85
  const changedFilesCount = Number(changeSummary?.changedFilesCount || 0);
87
- const hasBaseline = !!changeSummary?.hasBaseline;
88
- const baselineSource = String(changeSummary?.baselineSource || "none");
89
- if (!hasBaseline) {
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
- label: "Low",
112
- tone: "info",
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
- getDoctorDriftRisk,
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 driftRisk = useMemo(
171
- () => getDoctorDriftRisk(doctorStatus?.changeSummary || null),
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
- <div
318
- class="bg-surface border border-border rounded-xl p-4 flex flex-wrap items-center justify-between gap-3"
319
- >
320
- <div class="flex flex-wrap items-center gap-2 text-xs text-gray-500">
321
- <${Badge} tone=${driftRisk.tone}
322
- >${driftRisk.label} drift risk${
323
- driftRisk.detail ? ` · ${driftRisk.detail}` : ""
324
- }</${Badge}
325
- >
326
- </div>
327
- <div class="flex items-center gap-3 text-xs text-gray-500">
328
- <span>Last run</span>
329
- <span class="text-gray-300">
330
- ${formatLocaleDateTime(doctorStatus?.lastRunAt, {
331
- fallback: "Never",
332
- })}
333
- </span>
334
- </div>
335
- </div>
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.5">
381
+ <span class="inline-flex items-center gap-1">
380
382
  ${markers.map(
381
383
  (marker) => html`
382
384
  <span
@@ -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 prompt = String(card.fixPrompt || "").trim();
338
- if (!prompt) throw new Error("Doctor card does not include a fix prompt");
339
- let command = `agent --agent main --message ${shellEscapeArg(prompt)}`;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.6-beta.0",
3
+ "version": "0.4.6-beta.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },