@chrysb/alphaclaw 0.4.5 → 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/README.md +21 -18
- package/lib/public/css/theme.css +29 -0
- package/lib/public/js/app.js +41 -2
- package/lib/public/js/components/badge.js +4 -0
- package/lib/public/js/components/doctor/findings-list.js +195 -0
- package/lib/public/js/components/doctor/fix-card-modal.js +186 -0
- package/lib/public/js/components/doctor/general-warning.js +37 -0
- package/lib/public/js/components/doctor/helpers.js +144 -0
- package/lib/public/js/components/doctor/index.js +538 -0
- package/lib/public/js/components/doctor/summary-cards.js +24 -0
- package/lib/public/js/lib/api.js +81 -0
- package/lib/server/commands.js +8 -4
- package/lib/server/db/doctor/index.js +529 -0
- package/lib/server/db/doctor/schema.js +69 -0
- package/lib/server/doctor/constants.js +43 -0
- package/lib/server/doctor/normalize.js +214 -0
- package/lib/server/doctor/prompt.js +89 -0
- package/lib/server/doctor/service.js +393 -0
- package/lib/server/doctor/workspace-fingerprint.js +126 -0
- package/lib/server/routes/doctor.js +124 -0
- package/lib/server/routes/system.js +21 -1
- package/lib/server/utils/json.js +56 -10
- package/lib/server.js +44 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
<h1 align="center">AlphaClaw</h1>
|
|
5
5
|
<p align="center">
|
|
6
|
-
<strong>The
|
|
6
|
+
<strong>The ultimate OpenClaw harness. Deploy in minutes. Stay running for months.</strong><br>
|
|
7
7
|
<strong>Observability. Reliability. Agent discipline. Zero SSH rescue missions.</strong>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT" /></a>
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
-
<p align="center">AlphaClaw wraps <a href="https://github.com/openclaw/openclaw">OpenClaw</a> with a convenient setup wizard, self-healing watchdog, Git-backed rollback, and full browser-based observability. Ships with anti-drift prompt hardening to keep your agent disciplined, and simplifies integrations (e.g. Google Workspace, Telegram Topics) so you can manage everything from one UI instead of config files.</p>
|
|
16
|
+
<p align="center">AlphaClaw wraps <a href="https://github.com/openclaw/openclaw">OpenClaw</a> with a convenient setup wizard, self-healing watchdog, Git-backed rollback, and full browser-based observability. Ships with anti-drift prompt hardening to keep your agent disciplined, and simplifies integrations (e.g. Google Workspace, Google Pub/Sub, Telegram Topics) so you can manage everything from one UI instead of config files.</p>
|
|
17
17
|
|
|
18
18
|
<p align="center"><em>First deploy to first message in under five minutes.</em></p>
|
|
19
19
|
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
- **Watchdog:** Crash detection, crash-loop recovery, auto-repair (`openclaw doctor --fix`), and Telegram/Discord notifications.
|
|
33
33
|
- **Channel Orchestration:** Telegram and Discord bot pairing, credential sync, and a guided wizard for splitting Telegram into multi-threaded topic groups as your usage grows.
|
|
34
34
|
- **Webhooks:** Named webhook endpoints with per-hook transform modules, request logging, and payload inspection.
|
|
35
|
-
- **Google Workspace:** OAuth integration for Gmail, Calendar, Drive, Docs, Sheets, Tasks, Contacts, and Meet.
|
|
35
|
+
- **Google Workspace:** OAuth integration for Gmail, Calendar, Drive, Docs, Sheets, Tasks, Contacts, and Meet, plus guided Gmail watch setup with Google Pub/Sub topic, subscription, and push endpoint handling.
|
|
36
|
+
- **File Explorer:** Browser-based workspace explorer with file visibility, inline edits, diff view, and Git-aware sync for quick fixes without SSH.
|
|
36
37
|
- **Prompt Hardening:** Ships anti-drift bootstrap prompts (`AGENTS.md`, `TOOLS.md`) injected into your agent's system prompt on every message — enforcing safe practices, commit discipline, and change summaries out of the box.
|
|
37
38
|
- **Git Sync:** Automatic hourly commits of your OpenClaw workspace to GitHub with configurable cron schedule. Combined with prompt hardening, every agent action is version-controlled and auditable.
|
|
38
39
|
- **Version Management:** In-place updates for both AlphaClaw and OpenClaw with changelog review and one-click apply.
|
|
@@ -81,13 +82,15 @@ CMD ["alphaclaw", "start"]
|
|
|
81
82
|
|
|
82
83
|
## Setup UI
|
|
83
84
|
|
|
84
|
-
| Tab | What it manages
|
|
85
|
-
| ------------- |
|
|
86
|
-
| **General** | Gateway status, channel health, pending pairings, Google Workspace, repo sync schedule, OpenClaw dashboard
|
|
87
|
-
| **
|
|
88
|
-
| **
|
|
89
|
-
| **
|
|
90
|
-
| **
|
|
85
|
+
| Tab | What it manages |
|
|
86
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
87
|
+
| **General** | Gateway status, channel health, pending pairings, Google Workspace, repo sync schedule, OpenClaw dashboard |
|
|
88
|
+
| **Browse** | File explorer for workspace visibility, inline edits, diff review, and Git-backed sync |
|
|
89
|
+
| **Usage** | Token summaries, per-session and per-agent cost and token breakdown |
|
|
90
|
+
| **Watchdog** | Health monitoring, crash-loop status, auto-repair toggle, notifications toggle, event log, live log tail |
|
|
91
|
+
| **Providers** | AI provider credentials (Anthropic, OpenAI, Gemini, Mistral, Voyage, Groq, Deepgram) and model selection |
|
|
92
|
+
| **Envars** | Environment variables — view, edit, add — with gateway restart prompts |
|
|
93
|
+
| **Webhooks** | Webhook endpoints, transform modules, request history, payload inspection, including Gmail watch delivery flows |
|
|
91
94
|
|
|
92
95
|
## CLI
|
|
93
96
|
|
|
@@ -121,14 +124,14 @@ graph TD
|
|
|
121
124
|
|
|
122
125
|
The built-in watchdog monitors gateway health and recovers from failures automatically.
|
|
123
126
|
|
|
124
|
-
| Capability
|
|
125
|
-
|
|
|
126
|
-
| **Health checks**
|
|
127
|
-
| **Crash detection**
|
|
128
|
-
| **Crash-loop detection**
|
|
129
|
-
| **Auto-repair**
|
|
130
|
-
| **Notifications**
|
|
131
|
-
| **Event log**
|
|
127
|
+
| Capability | Details |
|
|
128
|
+
| ------------------------ | -------------------------------------------------------------- |
|
|
129
|
+
| **Health checks** | Periodic `openclaw health` with configurable interval |
|
|
130
|
+
| **Crash detection** | Listens for gateway exit events |
|
|
131
|
+
| **Crash-loop detection** | Threshold-based (default: 3 crashes in 300s) |
|
|
132
|
+
| **Auto-repair** | Runs `openclaw doctor --fix --yes`, relaunches gateway |
|
|
133
|
+
| **Notifications** | Telegram and Discord alerts for crashes, repairs, and recovery |
|
|
134
|
+
| **Event log** | SQLite-backed incident history with API and UI access |
|
|
132
135
|
|
|
133
136
|
## Environment Variables
|
|
134
137
|
|
package/lib/public/css/theme.css
CHANGED
|
@@ -53,6 +53,14 @@ body::before {
|
|
|
53
53
|
color: var(--text-muted);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
.ac-small-heading {
|
|
57
|
+
font-size: 11px;
|
|
58
|
+
font-weight: 500;
|
|
59
|
+
letter-spacing: 0.08em;
|
|
60
|
+
text-transform: uppercase;
|
|
61
|
+
color: var(--text-muted);
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
/* Shared collapsible history rows (incidents, webhook requests). */
|
|
57
65
|
.ac-history-list {
|
|
58
66
|
display: flex;
|
|
@@ -398,6 +406,22 @@ textarea:focus {
|
|
|
398
406
|
}
|
|
399
407
|
}
|
|
400
408
|
|
|
409
|
+
@keyframes acStatusDotPulseInfo {
|
|
410
|
+
0%,
|
|
411
|
+
100% {
|
|
412
|
+
transform: scale(1);
|
|
413
|
+
box-shadow:
|
|
414
|
+
0 0 0 0 rgba(34, 211, 238, 0.16),
|
|
415
|
+
0 0 5px rgba(34, 211, 238, 0.22);
|
|
416
|
+
}
|
|
417
|
+
50% {
|
|
418
|
+
transform: scale(1.08);
|
|
419
|
+
box-shadow:
|
|
420
|
+
0 0 0 3px rgba(34, 211, 238, 0.1),
|
|
421
|
+
0 0 9px rgba(34, 211, 238, 0.34);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
401
425
|
.ac-status-dot {
|
|
402
426
|
width: 8px;
|
|
403
427
|
height: 8px;
|
|
@@ -409,6 +433,11 @@ textarea:focus {
|
|
|
409
433
|
animation: acStatusDotPulse 2.6s ease-in-out infinite;
|
|
410
434
|
}
|
|
411
435
|
|
|
436
|
+
.ac-status-dot--info {
|
|
437
|
+
background: #22d3ee;
|
|
438
|
+
animation: acStatusDotPulseInfo 2.6s ease-in-out infinite;
|
|
439
|
+
}
|
|
440
|
+
|
|
412
441
|
.ac-status-dot--healthy-offset {
|
|
413
442
|
animation-delay: 0.95s;
|
|
414
443
|
}
|
package/lib/public/js/app.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
fetchRestartStatus,
|
|
21
21
|
restartGateway,
|
|
22
22
|
fetchWatchdogStatus,
|
|
23
|
+
fetchDoctorStatus,
|
|
23
24
|
triggerWatchdogRepair,
|
|
24
25
|
updateOpenclaw,
|
|
25
26
|
} from "./lib/api.js";
|
|
@@ -44,6 +45,8 @@ import { WatchdogTab } from "./components/watchdog-tab.js";
|
|
|
44
45
|
import { FileViewer } from "./components/file-viewer/index.js";
|
|
45
46
|
import { AppSidebar } from "./components/sidebar.js";
|
|
46
47
|
import { UsageTab } from "./components/usage-tab/index.js";
|
|
48
|
+
import { DoctorTab } from "./components/doctor/index.js";
|
|
49
|
+
import { GeneralDoctorWarning } from "./components/doctor/general-warning.js";
|
|
47
50
|
import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";
|
|
48
51
|
const html = htm.bind(h);
|
|
49
52
|
const kDefaultUiTab = "general";
|
|
@@ -52,6 +55,8 @@ const kSidebarMinWidthPx = 180;
|
|
|
52
55
|
const kSidebarMaxWidthPx = 460;
|
|
53
56
|
const kBrowseLastPathUiSettingKey = "browseLastPath";
|
|
54
57
|
const kLastMenuRouteUiSettingKey = "lastMenuRoute";
|
|
58
|
+
const kDoctorWarningDismissedUntilUiSettingKey = "doctorWarningDismissedUntilMs";
|
|
59
|
+
const kOneWeekMs = 7 * 24 * 60 * 60 * 1000;
|
|
55
60
|
const kBrowseRestartRequiredRules = [
|
|
56
61
|
{ type: "file", path: "openclaw.json" },
|
|
57
62
|
{ type: "directory", path: "hooks/transforms" },
|
|
@@ -121,6 +126,8 @@ const RouteRedirect = ({ to }) => {
|
|
|
121
126
|
const GeneralTab = ({
|
|
122
127
|
statusData = null,
|
|
123
128
|
watchdogData = null,
|
|
129
|
+
doctorStatusData = null,
|
|
130
|
+
doctorWarningDismissedUntilMs = 0,
|
|
124
131
|
onRefreshStatuses = () => {},
|
|
125
132
|
onSwitchTab,
|
|
126
133
|
onNavigate,
|
|
@@ -133,11 +140,13 @@ const GeneralTab = ({
|
|
|
133
140
|
onOpenclawVersionActionComplete = () => {},
|
|
134
141
|
onOpenclawUpdate,
|
|
135
142
|
onRestartRequired = () => {},
|
|
143
|
+
onDismissDoctorWarning = () => {},
|
|
136
144
|
}) => {
|
|
137
145
|
const [dashboardLoading, setDashboardLoading] = useState(false);
|
|
138
146
|
const [repairingWatchdog, setRepairingWatchdog] = useState(false);
|
|
139
147
|
const status = statusData;
|
|
140
148
|
const watchdogStatus = watchdogData;
|
|
149
|
+
const doctorStatus = doctorStatusData;
|
|
141
150
|
const gatewayStatus = status?.gateway ?? null;
|
|
142
151
|
const channels = status?.channels ?? null;
|
|
143
152
|
const repo = status?.repo || null;
|
|
@@ -297,6 +306,12 @@ const GeneralTab = ({
|
|
|
297
306
|
onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
|
|
298
307
|
onOpenclawUpdate=${onOpenclawUpdate}
|
|
299
308
|
/>
|
|
309
|
+
<${GeneralDoctorWarning}
|
|
310
|
+
doctorStatus=${doctorStatus}
|
|
311
|
+
dismissedUntilMs=${doctorWarningDismissedUntilMs}
|
|
312
|
+
onOpenDoctor=${() => onSwitchTab("doctor")}
|
|
313
|
+
onDismiss=${onDismissDoctorWarning}
|
|
314
|
+
/>
|
|
300
315
|
<${Channels} channels=${channels} onSwitchTab=${onSwitchTab} onNavigate=${onNavigate} />
|
|
301
316
|
<${Pairings}
|
|
302
317
|
pending=${pending}
|
|
@@ -446,6 +461,10 @@ const App = () => {
|
|
|
446
461
|
}
|
|
447
462
|
return `/${kDefaultUiTab}`;
|
|
448
463
|
});
|
|
464
|
+
const [doctorWarningDismissedUntilMs, setDoctorWarningDismissedUntilMs] = useState(() => {
|
|
465
|
+
const settings = readUiSettings();
|
|
466
|
+
return Number(settings[kDoctorWarningDismissedUntilUiSettingKey] || 0);
|
|
467
|
+
});
|
|
449
468
|
const [isResizingSidebar, setIsResizingSidebar] = useState(false);
|
|
450
469
|
const [browsePreviewPath, setBrowsePreviewPath] = useState("");
|
|
451
470
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
@@ -464,13 +483,18 @@ const App = () => {
|
|
|
464
483
|
const sharedWatchdogPoll = usePolling(fetchWatchdogStatus, statusPollCadenceMs, {
|
|
465
484
|
enabled: onboarded === true,
|
|
466
485
|
});
|
|
486
|
+
const sharedDoctorPoll = usePolling(fetchDoctorStatus, statusPollCadenceMs, {
|
|
487
|
+
enabled: onboarded === true,
|
|
488
|
+
});
|
|
467
489
|
const sharedStatus = sharedStatusPoll.data || null;
|
|
468
490
|
const sharedWatchdogStatus = sharedWatchdogPoll.data?.status || null;
|
|
491
|
+
const sharedDoctorStatus = sharedDoctorPoll.data?.status || null;
|
|
469
492
|
const isAnyRestartRequired = restartRequired || browseRestartRequired;
|
|
470
493
|
const refreshSharedStatuses = useCallback(() => {
|
|
471
494
|
sharedStatusPoll.refresh();
|
|
472
495
|
sharedWatchdogPoll.refresh();
|
|
473
|
-
|
|
496
|
+
sharedDoctorPoll.refresh();
|
|
497
|
+
}, [sharedStatusPoll.refresh, sharedWatchdogPoll.refresh, sharedDoctorPoll.refresh]);
|
|
474
498
|
|
|
475
499
|
const closeMenu = useCallback((e) => {
|
|
476
500
|
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
@@ -762,6 +786,7 @@ const App = () => {
|
|
|
762
786
|
items: [
|
|
763
787
|
{ id: "watchdog", label: "Watchdog" },
|
|
764
788
|
{ id: "usage", label: "Usage" },
|
|
789
|
+
{ id: "doctor", label: "Doctor" },
|
|
765
790
|
],
|
|
766
791
|
},
|
|
767
792
|
{
|
|
@@ -810,6 +835,8 @@ const App = () => {
|
|
|
810
835
|
? "watchdog"
|
|
811
836
|
: location.startsWith("/usage")
|
|
812
837
|
? "usage"
|
|
838
|
+
: location.startsWith("/doctor")
|
|
839
|
+
? "doctor"
|
|
813
840
|
: location.startsWith("/envars")
|
|
814
841
|
? "envars"
|
|
815
842
|
: location.startsWith("/webhooks")
|
|
@@ -882,8 +909,9 @@ const App = () => {
|
|
|
882
909
|
settings.sidebarWidthPx = sidebarWidthPx;
|
|
883
910
|
settings[kBrowseLastPathUiSettingKey] = lastBrowsePath;
|
|
884
911
|
settings[kLastMenuRouteUiSettingKey] = lastMenuRoute;
|
|
912
|
+
settings[kDoctorWarningDismissedUntilUiSettingKey] = doctorWarningDismissedUntilMs;
|
|
885
913
|
writeUiSettings(settings);
|
|
886
|
-
}, [sidebarWidthPx, lastBrowsePath, lastMenuRoute]);
|
|
914
|
+
}, [sidebarWidthPx, lastBrowsePath, lastMenuRoute, doctorWarningDismissedUntilMs]);
|
|
887
915
|
|
|
888
916
|
const resizeSidebarWithClientX = useCallback((clientX) => {
|
|
889
917
|
const shellElement = appShellRef.current;
|
|
@@ -1039,6 +1067,8 @@ const App = () => {
|
|
|
1039
1067
|
<${GeneralTab}
|
|
1040
1068
|
statusData=${sharedStatus}
|
|
1041
1069
|
watchdogData=${sharedWatchdogStatus}
|
|
1070
|
+
doctorStatusData=${sharedDoctorStatus}
|
|
1071
|
+
doctorWarningDismissedUntilMs=${doctorWarningDismissedUntilMs}
|
|
1042
1072
|
onRefreshStatuses=${refreshSharedStatuses}
|
|
1043
1073
|
onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
|
|
1044
1074
|
onNavigate=${navigateToSubScreen}
|
|
@@ -1051,6 +1081,8 @@ const App = () => {
|
|
|
1051
1081
|
onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
|
|
1052
1082
|
onOpenclawUpdate=${handleOpenclawUpdate}
|
|
1053
1083
|
onRestartRequired=${setRestartRequired}
|
|
1084
|
+
onDismissDoctorWarning=${() =>
|
|
1085
|
+
setDoctorWarningDismissedUntilMs(Date.now() + kOneWeekMs)}
|
|
1054
1086
|
/>
|
|
1055
1087
|
</div>
|
|
1056
1088
|
</div>
|
|
@@ -1104,6 +1136,13 @@ const App = () => {
|
|
|
1104
1136
|
/>
|
|
1105
1137
|
</div>
|
|
1106
1138
|
</${Route}>
|
|
1139
|
+
<${Route} path="/doctor">
|
|
1140
|
+
<div class="pt-4">
|
|
1141
|
+
<${DoctorTab}
|
|
1142
|
+
isActive=${location === "/doctor"}
|
|
1143
|
+
/>
|
|
1144
|
+
</div>
|
|
1145
|
+
</${Route}>
|
|
1107
1146
|
<${Route} path="/envars">
|
|
1108
1147
|
<div class="pt-4">
|
|
1109
1148
|
<${Envars} onRestartRequired=${setRestartRequired} />
|
|
@@ -8,6 +8,10 @@ const kToneClasses = {
|
|
|
8
8
|
warning: "bg-yellow-500/10 text-yellow-500",
|
|
9
9
|
danger: "bg-red-500/10 text-red-400",
|
|
10
10
|
neutral: "bg-gray-500/10 text-gray-400",
|
|
11
|
+
info: "bg-blue-500/10 text-blue-400",
|
|
12
|
+
accent: "bg-purple-500/10 text-purple-400",
|
|
13
|
+
cyan: "bg-cyan-500/10 text-cyan-400",
|
|
14
|
+
secondary: "bg-indigo-500/10 text-indigo-300",
|
|
11
15
|
};
|
|
12
16
|
|
|
13
17
|
export const Badge = ({ tone = "neutral", children }) => html`
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { Badge } from "../badge.js";
|
|
4
|
+
import { ActionButton } from "../action-button.js";
|
|
5
|
+
import {
|
|
6
|
+
formatDoctorCategory,
|
|
7
|
+
getDoctorCategoryTone,
|
|
8
|
+
getDoctorPriorityTone,
|
|
9
|
+
} from "./helpers.js";
|
|
10
|
+
|
|
11
|
+
const html = htm.bind(h);
|
|
12
|
+
|
|
13
|
+
const renderEvidenceLine = (item = {}) => {
|
|
14
|
+
if (item?.path) return item.path;
|
|
15
|
+
if (item?.text) return item.text;
|
|
16
|
+
return JSON.stringify(item);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const DoctorFindingsList = ({
|
|
20
|
+
cards = [],
|
|
21
|
+
busyCardId = 0,
|
|
22
|
+
onAskAgentFix = () => {},
|
|
23
|
+
onUpdateStatus = () => {},
|
|
24
|
+
showRunMeta = false,
|
|
25
|
+
hideEmptyState = false,
|
|
26
|
+
}) => {
|
|
27
|
+
return html`
|
|
28
|
+
<div class="space-y-4">
|
|
29
|
+
${cards.length
|
|
30
|
+
? html`
|
|
31
|
+
<div class="space-y-3">
|
|
32
|
+
${cards.map(
|
|
33
|
+
(card) => html`
|
|
34
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
35
|
+
<div class="space-y-2">
|
|
36
|
+
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
37
|
+
<div class="space-y-2 min-w-0">
|
|
38
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
39
|
+
<${Badge} tone=${getDoctorPriorityTone(card.priority)}>
|
|
40
|
+
${card.priority}
|
|
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">
|
|
47
|
+
<${Badge} tone=${getDoctorCategoryTone(card.category)}>
|
|
48
|
+
${formatDoctorCategory(card.category)}
|
|
49
|
+
</${Badge}>
|
|
50
|
+
${
|
|
51
|
+
showRunMeta
|
|
52
|
+
? html`
|
|
53
|
+
<span class="text-xs text-gray-600"
|
|
54
|
+
>Run #${card.runId}</span
|
|
55
|
+
>
|
|
56
|
+
`
|
|
57
|
+
: null
|
|
58
|
+
}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
${
|
|
63
|
+
card.summary
|
|
64
|
+
? html`<p
|
|
65
|
+
class="text-xs text-gray-300 leading-5 pt-1"
|
|
66
|
+
>
|
|
67
|
+
${card.summary}
|
|
68
|
+
</p>`
|
|
69
|
+
: null
|
|
70
|
+
}
|
|
71
|
+
</div>
|
|
72
|
+
<details class="group rounded-lg border border-border bg-black/20">
|
|
73
|
+
<summary class="list-none cursor-pointer px-3 py-2.5 text-xs text-gray-400 group-open:border-b group-open:border-border">
|
|
74
|
+
<span class="inline-flex items-center gap-2">
|
|
75
|
+
<span
|
|
76
|
+
class="inline-block text-gray-500 transition-transform duration-200 group-open:rotate-90"
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
>▸</span
|
|
79
|
+
>
|
|
80
|
+
<span>Show recommendation and details</span>
|
|
81
|
+
</span>
|
|
82
|
+
</summary>
|
|
83
|
+
<div class="p-3 space-y-3">
|
|
84
|
+
<div>
|
|
85
|
+
<div class="ac-small-heading">
|
|
86
|
+
Recommendation
|
|
87
|
+
</div>
|
|
88
|
+
<p class="text-xs text-gray-200 mt-1 leading-5">
|
|
89
|
+
${card.recommendation}
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
${
|
|
93
|
+
Array.isArray(card.targetPaths) &&
|
|
94
|
+
card.targetPaths.length
|
|
95
|
+
? html`
|
|
96
|
+
<div>
|
|
97
|
+
<div class="ac-small-heading">
|
|
98
|
+
Target paths
|
|
99
|
+
</div>
|
|
100
|
+
<div class="mt-1 flex flex-wrap gap-1.5">
|
|
101
|
+
${card.targetPaths.map(
|
|
102
|
+
(targetPath) => html`
|
|
103
|
+
<span
|
|
104
|
+
class="text-[11px] px-2 py-1 rounded-md bg-black/30 border border-border font-mono text-gray-300"
|
|
105
|
+
>
|
|
106
|
+
${targetPath}
|
|
107
|
+
</span>
|
|
108
|
+
`,
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
`
|
|
113
|
+
: null
|
|
114
|
+
}
|
|
115
|
+
${
|
|
116
|
+
Array.isArray(card.evidence) && card.evidence.length
|
|
117
|
+
? html`
|
|
118
|
+
<div>
|
|
119
|
+
<div class="ac-small-heading">Evidence</div>
|
|
120
|
+
<div class="mt-1 space-y-1">
|
|
121
|
+
${card.evidence.map(
|
|
122
|
+
(item) => html`
|
|
123
|
+
<div class="text-xs text-gray-400">
|
|
124
|
+
${renderEvidenceLine(item)}
|
|
125
|
+
</div>
|
|
126
|
+
`,
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
`
|
|
131
|
+
: null
|
|
132
|
+
}
|
|
133
|
+
</div>
|
|
134
|
+
</details>
|
|
135
|
+
<div class="flex flex-wrap gap-2">
|
|
136
|
+
<${ActionButton}
|
|
137
|
+
onClick=${() => onAskAgentFix(card)}
|
|
138
|
+
loading=${busyCardId === card.id}
|
|
139
|
+
tone="primary"
|
|
140
|
+
idleLabel="Ask agent to fix"
|
|
141
|
+
loadingLabel="Sending..."
|
|
142
|
+
/>
|
|
143
|
+
${
|
|
144
|
+
card.status !== "fixed"
|
|
145
|
+
? html`
|
|
146
|
+
<${ActionButton}
|
|
147
|
+
onClick=${() => onUpdateStatus(card, "fixed")}
|
|
148
|
+
tone="secondary"
|
|
149
|
+
idleLabel="Mark fixed"
|
|
150
|
+
/>
|
|
151
|
+
`
|
|
152
|
+
: html`
|
|
153
|
+
<${ActionButton}
|
|
154
|
+
onClick=${() => onUpdateStatus(card, "open")}
|
|
155
|
+
tone="secondary"
|
|
156
|
+
idleLabel="Reopen"
|
|
157
|
+
/>
|
|
158
|
+
`
|
|
159
|
+
}
|
|
160
|
+
${
|
|
161
|
+
card.status !== "dismissed"
|
|
162
|
+
? html`
|
|
163
|
+
<${ActionButton}
|
|
164
|
+
onClick=${() =>
|
|
165
|
+
onUpdateStatus(card, "dismissed")}
|
|
166
|
+
tone="ghost"
|
|
167
|
+
idleLabel="Dismiss"
|
|
168
|
+
/>
|
|
169
|
+
`
|
|
170
|
+
: html`
|
|
171
|
+
<${ActionButton}
|
|
172
|
+
onClick=${() => onUpdateStatus(card, "open")}
|
|
173
|
+
tone="ghost"
|
|
174
|
+
idleLabel="Restore"
|
|
175
|
+
/>
|
|
176
|
+
`
|
|
177
|
+
}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
`,
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
`
|
|
184
|
+
: hideEmptyState
|
|
185
|
+
? null
|
|
186
|
+
: html`
|
|
187
|
+
<div class="ac-surface-inset rounded-xl p-4 space-y-1.5">
|
|
188
|
+
<p class="text-xs text-gray-300 leading-5">
|
|
189
|
+
No findings currently for this selection.
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
`}
|
|
193
|
+
</div>
|
|
194
|
+
`;
|
|
195
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ModalShell } from "../modal-shell.js";
|
|
5
|
+
import { ActionButton } from "../action-button.js";
|
|
6
|
+
import { Badge } from "../badge.js";
|
|
7
|
+
import {
|
|
8
|
+
fetchAgentSessions,
|
|
9
|
+
sendDoctorCardFix,
|
|
10
|
+
updateDoctorCardStatus,
|
|
11
|
+
} from "../../lib/api.js";
|
|
12
|
+
import { showToast } from "../toast.js";
|
|
13
|
+
import { getDoctorPriorityTone } from "./helpers.js";
|
|
14
|
+
|
|
15
|
+
const html = htm.bind(h);
|
|
16
|
+
|
|
17
|
+
export const DoctorFixCardModal = ({
|
|
18
|
+
visible = false,
|
|
19
|
+
card = null,
|
|
20
|
+
onClose = () => {},
|
|
21
|
+
onComplete = () => {},
|
|
22
|
+
}) => {
|
|
23
|
+
const [sessions, setSessions] = useState([]);
|
|
24
|
+
const [selectedSessionKey, setSelectedSessionKey] = useState("");
|
|
25
|
+
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
26
|
+
const [sending, setSending] = useState(false);
|
|
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]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!visible) return;
|
|
37
|
+
let active = true;
|
|
38
|
+
const loadSessions = async () => {
|
|
39
|
+
try {
|
|
40
|
+
setLoadingSessions(true);
|
|
41
|
+
setLoadError("");
|
|
42
|
+
const data = await fetchAgentSessions();
|
|
43
|
+
if (!active) return;
|
|
44
|
+
const nextSessions = Array.isArray(data?.sessions) ? data.sessions : [];
|
|
45
|
+
setSessions(nextSessions);
|
|
46
|
+
const preferredSession =
|
|
47
|
+
nextSessions.find((sessionRow) => {
|
|
48
|
+
const key = String(sessionRow?.key || "").toLowerCase();
|
|
49
|
+
return key === "agent:main:main";
|
|
50
|
+
}) ||
|
|
51
|
+
nextSessions.find((sessionRow) => {
|
|
52
|
+
const key = String(sessionRow?.key || "").toLowerCase();
|
|
53
|
+
return key.includes(":direct:") || key.includes(":group:");
|
|
54
|
+
}) ||
|
|
55
|
+
nextSessions[0] ||
|
|
56
|
+
null;
|
|
57
|
+
setSelectedSessionKey(String(preferredSession?.key || ""));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (!active) return;
|
|
60
|
+
setSessions([]);
|
|
61
|
+
setSelectedSessionKey("");
|
|
62
|
+
setLoadError(error.message || "Could not load agent sessions");
|
|
63
|
+
} finally {
|
|
64
|
+
if (active) setLoadingSessions(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
loadSessions();
|
|
68
|
+
return () => {
|
|
69
|
+
active = false;
|
|
70
|
+
};
|
|
71
|
+
}, [visible, card?.id]);
|
|
72
|
+
|
|
73
|
+
const selectedSession = useMemo(
|
|
74
|
+
() =>
|
|
75
|
+
sessions.find(
|
|
76
|
+
(sessionRow) => String(sessionRow?.key || "") === selectedSessionKey,
|
|
77
|
+
) || null,
|
|
78
|
+
[sessions, selectedSessionKey],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const handleSend = async () => {
|
|
82
|
+
if (!card?.id || sending) return;
|
|
83
|
+
try {
|
|
84
|
+
setSending(true);
|
|
85
|
+
await sendDoctorCardFix({
|
|
86
|
+
cardId: card.id,
|
|
87
|
+
sessionId: selectedSession?.sessionId || "",
|
|
88
|
+
replyChannel: selectedSession?.replyChannel || "",
|
|
89
|
+
replyTo: selectedSession?.replyTo || "",
|
|
90
|
+
prompt: promptText,
|
|
91
|
+
});
|
|
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
|
+
}
|
|
101
|
+
onComplete();
|
|
102
|
+
onClose();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
showToast(error.message || "Could not send Doctor fix request", "error");
|
|
105
|
+
} finally {
|
|
106
|
+
setSending(false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return html`
|
|
111
|
+
<${ModalShell}
|
|
112
|
+
visible=${visible}
|
|
113
|
+
onClose=${onClose}
|
|
114
|
+
panelClassName="bg-modal border border-border rounded-xl p-5 max-w-lg w-full space-y-4"
|
|
115
|
+
>
|
|
116
|
+
<div class="space-y-1">
|
|
117
|
+
<h2 class="text-base font-semibold">Ask agent to fix</h2>
|
|
118
|
+
<p class="text-sm text-gray-400">
|
|
119
|
+
Send this Doctor finding to one of your agent sessions as a focused fix request.
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="ac-surface-inset p-3 space-y-1">
|
|
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>
|
|
131
|
+
<div class="text-xs text-gray-400">${card?.recommendation || ""}</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="space-y-2">
|
|
134
|
+
<label class="text-xs text-gray-500">Send to session</label>
|
|
135
|
+
<select
|
|
136
|
+
value=${selectedSessionKey}
|
|
137
|
+
onChange=${(event) => setSelectedSessionKey(String(event.currentTarget?.value || ""))}
|
|
138
|
+
disabled=${loadingSessions || sending}
|
|
139
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500"
|
|
140
|
+
>
|
|
141
|
+
${sessions.map(
|
|
142
|
+
(sessionRow) => html`
|
|
143
|
+
<option value=${String(sessionRow?.key || "")}>
|
|
144
|
+
${String(sessionRow?.label || sessionRow?.key || "Session")}
|
|
145
|
+
</option>
|
|
146
|
+
`,
|
|
147
|
+
)}
|
|
148
|
+
</select>
|
|
149
|
+
${
|
|
150
|
+
loadingSessions
|
|
151
|
+
? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
|
|
152
|
+
: null
|
|
153
|
+
}
|
|
154
|
+
${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
|
|
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>
|
|
166
|
+
<div class="flex items-center justify-end gap-2">
|
|
167
|
+
<${ActionButton}
|
|
168
|
+
onClick=${onClose}
|
|
169
|
+
disabled=${sending}
|
|
170
|
+
tone="secondary"
|
|
171
|
+
size="md"
|
|
172
|
+
idleLabel="Cancel"
|
|
173
|
+
/>
|
|
174
|
+
<${ActionButton}
|
|
175
|
+
onClick=${handleSend}
|
|
176
|
+
disabled=${!selectedSession || loadingSessions || !!loadError || !String(promptText || "").trim()}
|
|
177
|
+
loading=${sending}
|
|
178
|
+
tone="primary"
|
|
179
|
+
size="md"
|
|
180
|
+
idleLabel="Send fix request"
|
|
181
|
+
loadingLabel="Sending..."
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
</${ModalShell}>
|
|
185
|
+
`;
|
|
186
|
+
};
|