@chrysb/alphaclaw 0.7.0 → 0.7.2-beta.0
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/css/cron.css +26 -17
- package/lib/public/css/explorer.css +12 -0
- package/lib/public/css/theme.css +14 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +17 -12
- package/lib/public/js/components/cron-tab/cron-job-list.js +11 -1
- package/lib/public/js/components/cron-tab/cron-overview.js +2 -1
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +65 -49
- package/lib/public/js/components/cron-tab/index.js +16 -2
- package/lib/public/js/components/icons.js +11 -0
- package/lib/public/js/components/routes/watchdog-route.js +1 -1
- package/lib/public/js/components/sidebar.js +14 -2
- package/lib/public/js/components/update-modal.js +173 -0
- package/lib/public/js/components/watchdog-tab/console/index.js +115 -0
- package/lib/public/js/components/watchdog-tab/console/use-console.js +137 -0
- package/lib/public/js/components/watchdog-tab/helpers.js +106 -0
- package/lib/public/js/components/watchdog-tab/incidents/index.js +56 -0
- package/lib/public/js/components/watchdog-tab/incidents/use-incidents.js +33 -0
- package/lib/public/js/components/watchdog-tab/index.js +84 -0
- package/lib/public/js/components/watchdog-tab/resource-bar.js +76 -0
- package/lib/public/js/components/watchdog-tab/resources/index.js +85 -0
- package/lib/public/js/components/watchdog-tab/resources/use-resources.js +13 -0
- package/lib/public/js/components/watchdog-tab/settings/index.js +44 -0
- package/lib/public/js/components/watchdog-tab/settings/use-settings.js +117 -0
- package/lib/public/js/components/watchdog-tab/terminal/index.js +20 -0
- package/lib/public/js/components/watchdog-tab/terminal/use-terminal.js +263 -0
- package/lib/public/js/components/watchdog-tab/use-watchdog-tab.js +55 -0
- package/lib/public/js/lib/api.js +75 -0
- package/lib/server/constants.js +3 -0
- package/lib/server/init/register-server-routes.js +240 -0
- package/lib/server/init/runtime-init.js +44 -0
- package/lib/server/init/server-lifecycle.js +55 -0
- package/lib/server/routes/system.js +98 -0
- package/lib/server/routes/watchdog.js +62 -0
- package/lib/server/watchdog-terminal-ws.js +114 -0
- package/lib/server/watchdog-terminal.js +262 -0
- package/lib/server.js +89 -215
- package/package.json +3 -2
- package/lib/public/js/components/watchdog-tab.js +0 -535
package/lib/public/css/cron.css
CHANGED
|
@@ -400,23 +400,6 @@
|
|
|
400
400
|
justify-content: center;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
.cron-calendar-expand-btn {
|
|
404
|
-
width: 22px;
|
|
405
|
-
height: 22px;
|
|
406
|
-
border-radius: 6px;
|
|
407
|
-
border: 1px solid var(--border);
|
|
408
|
-
background: rgba(255, 255, 255, 0.03);
|
|
409
|
-
color: var(--text-dim);
|
|
410
|
-
display: inline-flex;
|
|
411
|
-
align-items: center;
|
|
412
|
-
justify-content: center;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.cron-calendar-expand-btn:hover {
|
|
416
|
-
color: var(--text);
|
|
417
|
-
border-color: rgba(148, 163, 184, 0.5);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
403
|
.cron-calendar-grid-wrap {
|
|
421
404
|
position: relative;
|
|
422
405
|
}
|
|
@@ -464,6 +447,7 @@
|
|
|
464
447
|
border-left: 1px solid var(--border);
|
|
465
448
|
border-bottom: 1px solid var(--border);
|
|
466
449
|
padding: 5px;
|
|
450
|
+
position: relative;
|
|
467
451
|
display: flex;
|
|
468
452
|
flex-direction: column;
|
|
469
453
|
gap: 4px;
|
|
@@ -473,6 +457,29 @@
|
|
|
473
457
|
background: rgba(99, 235, 255, 0.04);
|
|
474
458
|
}
|
|
475
459
|
|
|
460
|
+
.cron-calendar-now-indicator {
|
|
461
|
+
position: absolute;
|
|
462
|
+
left: 5px;
|
|
463
|
+
right: 5px;
|
|
464
|
+
height: 2px;
|
|
465
|
+
border-radius: 999px;
|
|
466
|
+
background: rgba(248, 113, 113, 0.95);
|
|
467
|
+
box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.18), 0 0 8px rgba(239, 68, 68, 0.55);
|
|
468
|
+
pointer-events: none;
|
|
469
|
+
z-index: 1;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.cron-calendar-now-indicator-dot {
|
|
473
|
+
position: absolute;
|
|
474
|
+
left: -3px;
|
|
475
|
+
top: 50%;
|
|
476
|
+
width: 6px;
|
|
477
|
+
height: 6px;
|
|
478
|
+
border-radius: 999px;
|
|
479
|
+
background: rgba(248, 113, 113, 0.98);
|
|
480
|
+
transform: translateY(-50%);
|
|
481
|
+
}
|
|
482
|
+
|
|
476
483
|
.cron-calendar-slot-chip {
|
|
477
484
|
font-size: 11px;
|
|
478
485
|
line-height: 1.2;
|
|
@@ -485,6 +492,8 @@
|
|
|
485
492
|
width: 100%;
|
|
486
493
|
max-width: 100%;
|
|
487
494
|
overflow: hidden;
|
|
495
|
+
position: relative;
|
|
496
|
+
z-index: 2;
|
|
488
497
|
}
|
|
489
498
|
|
|
490
499
|
.cron-calendar-slot-overflow {
|
|
@@ -1158,6 +1158,18 @@
|
|
|
1158
1158
|
background: rgba(255, 255, 255, 0.04);
|
|
1159
1159
|
}
|
|
1160
1160
|
|
|
1161
|
+
.release-notes-preview {
|
|
1162
|
+
padding: 8px 12px 24px 14px;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.release-notes-preview > :first-child {
|
|
1166
|
+
margin-top: 0;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.release-notes-preview > :last-child {
|
|
1170
|
+
margin-bottom: 0;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1161
1173
|
.file-viewer-editor-highlight-line-content .hl-comment {
|
|
1162
1174
|
color: var(--comment);
|
|
1163
1175
|
font-style: italic;
|
package/lib/public/css/theme.css
CHANGED
|
@@ -685,3 +685,17 @@ textarea:focus {
|
|
|
685
685
|
resize: vertical;
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
+
.watchdog-terminal-host {
|
|
689
|
+
position: relative;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.watchdog-terminal-host .xterm {
|
|
693
|
+
height: 100%;
|
|
694
|
+
letter-spacing: 0;
|
|
695
|
+
font-kerning: none;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.watchdog-terminal-host .xterm-viewport {
|
|
699
|
+
overflow-y: auto !important;
|
|
700
|
+
}
|
|
701
|
+
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import htm from "https://esm.sh/htm";
|
|
9
9
|
import { Tooltip } from "../tooltip.js";
|
|
10
10
|
import { ModalShell } from "../modal-shell.js";
|
|
11
|
-
import { CloseIcon
|
|
11
|
+
import { CloseIcon } from "../icons.js";
|
|
12
12
|
import {
|
|
13
13
|
formatCost,
|
|
14
14
|
formatCronScheduleLabel,
|
|
@@ -306,6 +306,9 @@ export const CronCalendar = ({
|
|
|
306
306
|
};
|
|
307
307
|
}, []);
|
|
308
308
|
const todayDayKey = toLocalDayKey(nowMs);
|
|
309
|
+
const nowDateValue = useMemo(() => new Date(nowMs), [nowMs]);
|
|
310
|
+
const currentHourOfDay = nowDateValue.getHours();
|
|
311
|
+
const currentMinuteProgress = nowDateValue.getMinutes() / 60;
|
|
309
312
|
const { repeatingJobs, scheduledJobs } = useMemo(
|
|
310
313
|
() => classifyRepeatingJobs(jobs),
|
|
311
314
|
[jobs],
|
|
@@ -569,17 +572,7 @@ export const CronCalendar = ({
|
|
|
569
572
|
: html`
|
|
570
573
|
<div class="cron-calendar-grid-wrap">
|
|
571
574
|
<div class="cron-calendar-grid-header">
|
|
572
|
-
<div class="cron-calendar-hour-cell cron-calendar-grid-corner">
|
|
573
|
-
<button
|
|
574
|
-
type="button"
|
|
575
|
-
class="cron-calendar-expand-btn"
|
|
576
|
-
title="Expand calendar"
|
|
577
|
-
aria-label="Expand calendar"
|
|
578
|
-
onClick=${() => setCalendarLightboxOpen(true)}
|
|
579
|
-
>
|
|
580
|
-
<${FullscreenLineIcon} className="w-3.5 h-3.5" />
|
|
581
|
-
</button>
|
|
582
|
-
</div>
|
|
575
|
+
<div class="cron-calendar-hour-cell cron-calendar-grid-corner"></div>
|
|
583
576
|
${timeline.days.map(
|
|
584
577
|
(day) => html`
|
|
585
578
|
<div
|
|
@@ -611,6 +604,18 @@ export const CronCalendar = ({
|
|
|
611
604
|
key=${cellKey}
|
|
612
605
|
class=${`cron-calendar-grid-cell ${day.dayKey === todayDayKey ? "is-today" : ""}`}
|
|
613
606
|
>
|
|
607
|
+
${day.dayKey === todayDayKey &&
|
|
608
|
+
hourOfDay === currentHourOfDay
|
|
609
|
+
? html`
|
|
610
|
+
<div
|
|
611
|
+
class="cron-calendar-now-indicator"
|
|
612
|
+
style=${`top: ${Math.max(0, Math.min(100, currentMinuteProgress * 100))}%;`}
|
|
613
|
+
aria-hidden="true"
|
|
614
|
+
>
|
|
615
|
+
<span class="cron-calendar-now-indicator-dot"></span>
|
|
616
|
+
</div>
|
|
617
|
+
`
|
|
618
|
+
: null}
|
|
614
619
|
${visibleSlots.map((slot) => {
|
|
615
620
|
const status = statusBySlotKey[slot.key] || "";
|
|
616
621
|
const isPast = slot.scheduledAtMs <= nowMs;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import {
|
|
5
5
|
formatCronScheduleLabel,
|
|
@@ -202,7 +202,16 @@ export const CronJobList = ({
|
|
|
202
202
|
onSelectAllJobs = () => {},
|
|
203
203
|
onSelectJob = () => {},
|
|
204
204
|
}) => {
|
|
205
|
+
const searchInputRef = useRef(null);
|
|
205
206
|
const [searchQuery, setSearchQuery] = useState("");
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const frameId = window.requestAnimationFrame(() => {
|
|
209
|
+
searchInputRef.current?.focus();
|
|
210
|
+
});
|
|
211
|
+
return () => {
|
|
212
|
+
window.cancelAnimationFrame(frameId);
|
|
213
|
+
};
|
|
214
|
+
}, []);
|
|
206
215
|
const normalizedQuery = String(searchQuery || "").trim().toLowerCase();
|
|
207
216
|
const filteredJobs = useMemo(() => {
|
|
208
217
|
if (!normalizedQuery) return jobs;
|
|
@@ -236,6 +245,7 @@ export const CronJobList = ({
|
|
|
236
245
|
<div class="cron-list-panel-inner">
|
|
237
246
|
<div class="cron-list-sticky-search">
|
|
238
247
|
<input
|
|
248
|
+
ref=${searchInputRef}
|
|
239
249
|
type="text"
|
|
240
250
|
value=${searchQuery}
|
|
241
251
|
placeholder="Search cron jobs..."
|
|
@@ -16,7 +16,7 @@ import { ErrorWarningLineIcon } from "../icons.js";
|
|
|
16
16
|
const html = htm.bind(h);
|
|
17
17
|
const kRecentRunFetchLimit = 100;
|
|
18
18
|
const kRecentRunRowsLimit = 20;
|
|
19
|
-
const kRecentRunCollapseThreshold =
|
|
19
|
+
const kRecentRunCollapseThreshold = 2;
|
|
20
20
|
const kTrendRange24h = "24h";
|
|
21
21
|
const kTrendRange7d = "7d";
|
|
22
22
|
const kTrendRange30d = "30d";
|
|
@@ -107,6 +107,7 @@ const buildCollapsedRunRows = (recentRuns = []) => {
|
|
|
107
107
|
newestTs: Number(streak[0]?.ts || 0),
|
|
108
108
|
oldestTs: Number(streak[streak.length - 1]?.ts || 0),
|
|
109
109
|
statusCounts,
|
|
110
|
+
entries: streak,
|
|
110
111
|
});
|
|
111
112
|
index = streakEnd;
|
|
112
113
|
continue;
|
|
@@ -41,6 +41,52 @@ const formatRowTimestamp = (timestampMs, variant = "overview") =>
|
|
|
41
41
|
variant === "detail"
|
|
42
42
|
? formatDetailTimestamp(timestampMs)
|
|
43
43
|
: formatOverviewTimestamp(timestampMs);
|
|
44
|
+
const renderEntrySummaryRow = ({ runEntry = {}, variant = "overview" }) => {
|
|
45
|
+
const runStatus = String(runEntry?.status || "unknown");
|
|
46
|
+
const runTokens = getCronRunTotalTokens(runEntry);
|
|
47
|
+
const runEstimatedCost = getCronRunEstimatedCost(runEntry);
|
|
48
|
+
const runTitle = String(runEntry?.jobName || "").trim();
|
|
49
|
+
const hasRunTitle = runTitle.length > 0;
|
|
50
|
+
const isDetail = variant === "detail";
|
|
51
|
+
return html`
|
|
52
|
+
<div class="ac-history-summary-row">
|
|
53
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
54
|
+
${isDetail
|
|
55
|
+
? html`
|
|
56
|
+
<span class="truncate text-xs text-gray-300">
|
|
57
|
+
${formatRowTimestamp(runEntry.ts, variant)}
|
|
58
|
+
</span>
|
|
59
|
+
`
|
|
60
|
+
: hasRunTitle
|
|
61
|
+
? html`
|
|
62
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
63
|
+
<span class="truncate text-xs text-gray-300">${runTitle}</span>
|
|
64
|
+
<span class="text-xs text-gray-500 shrink-0">
|
|
65
|
+
${formatRowTimestamp(runEntry.ts, variant)}
|
|
66
|
+
</span>
|
|
67
|
+
</span>
|
|
68
|
+
`
|
|
69
|
+
: html`
|
|
70
|
+
<span class="truncate text-xs text-gray-300">
|
|
71
|
+
${runEntry.jobId} - ${formatRowTimestamp(runEntry.ts, variant)}
|
|
72
|
+
</span>
|
|
73
|
+
`}
|
|
74
|
+
</span>
|
|
75
|
+
<span class="inline-flex items-center gap-3 shrink-0 text-xs">
|
|
76
|
+
<span class=${runStatusClassName(runStatus)}>${runStatus}</span>
|
|
77
|
+
<span class="text-gray-400">${formatDurationCompactMs(runEntry.durationMs)}</span>
|
|
78
|
+
<span class="text-gray-400">${formatTokenCount(runTokens)} tk</span>
|
|
79
|
+
${isDetail
|
|
80
|
+
? html`<span class="text-gray-500">${runDeliveryLabel(runEntry)}</span>`
|
|
81
|
+
: html`
|
|
82
|
+
<span class="text-gray-500">
|
|
83
|
+
${runEstimatedCost == null ? "—" : `~${formatCost(runEstimatedCost)}`}
|
|
84
|
+
</span>
|
|
85
|
+
`}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
`;
|
|
89
|
+
};
|
|
44
90
|
const renderCollapsedGroupRow = ({ row, rowIndex, onSelectJob = () => {} }) => {
|
|
45
91
|
const statusSummary = Object.entries(row.statusCounts || {})
|
|
46
92
|
.map(([status, count]) => `${status}: ${count}`)
|
|
@@ -68,6 +114,20 @@ const renderCollapsedGroupRow = ({ row, rowIndex, onSelectJob = () => {} }) => {
|
|
|
68
114
|
(${timeRangeLabel})
|
|
69
115
|
</div>
|
|
70
116
|
<div class="text-gray-500">Statuses: ${statusSummary}</div>
|
|
117
|
+
${Array.isArray(row?.entries) && row.entries.length > 0
|
|
118
|
+
? html`
|
|
119
|
+
<div class="ac-surface-inset rounded-lg px-2.5 py-1">
|
|
120
|
+
${row.entries.map((runEntry, entryIndex) => html`
|
|
121
|
+
<div
|
|
122
|
+
key=${`collapsed-entry:${rowIndex}:${entryIndex}:${runEntry?.ts || 0}`}
|
|
123
|
+
class=${entryIndex > 0 ? "border-t border-border py-1.5" : "py-1.5"}
|
|
124
|
+
>
|
|
125
|
+
${renderEntrySummaryRow({ runEntry, variant: "overview" })}
|
|
126
|
+
</div>
|
|
127
|
+
`)}
|
|
128
|
+
</div>
|
|
129
|
+
`
|
|
130
|
+
: null}
|
|
71
131
|
${row?.jobId
|
|
72
132
|
? html`
|
|
73
133
|
<div>
|
|
@@ -94,7 +154,6 @@ const renderEntryRow = ({
|
|
|
94
154
|
}) => {
|
|
95
155
|
const runEntry = row?.entry || row || {};
|
|
96
156
|
const runUsage = runEntry?.usage || {};
|
|
97
|
-
const runStatus = String(runEntry?.status || "unknown");
|
|
98
157
|
const runInputTokens = Number(
|
|
99
158
|
runUsage?.input_tokens ?? runUsage?.inputTokens ?? 0,
|
|
100
159
|
);
|
|
@@ -103,60 +162,17 @@ const renderEntryRow = ({
|
|
|
103
162
|
);
|
|
104
163
|
const runTokens = getCronRunTotalTokens(runEntry);
|
|
105
164
|
const runEstimatedCost = getCronRunEstimatedCost(runEntry);
|
|
106
|
-
const runTitle = String(runEntry?.jobName || "").trim();
|
|
107
|
-
const hasRunTitle = runTitle.length > 0;
|
|
108
|
-
const isDetail = variant === "detail";
|
|
109
165
|
return html`
|
|
110
166
|
<details
|
|
111
167
|
key=${`entry:${rowIndex}:${runEntry.ts}:${runEntry.jobId || ""}`}
|
|
112
168
|
class="ac-history-item"
|
|
113
169
|
>
|
|
114
170
|
<summary class="ac-history-summary">
|
|
115
|
-
<div class="
|
|
116
|
-
<span class="
|
|
117
|
-
|
|
118
|
-
${
|
|
119
|
-
|
|
120
|
-
<span class="truncate text-xs text-gray-300">
|
|
121
|
-
${formatRowTimestamp(runEntry.ts, variant)}
|
|
122
|
-
</span>
|
|
123
|
-
`
|
|
124
|
-
: hasRunTitle
|
|
125
|
-
? html`
|
|
126
|
-
<span class="inline-flex items-center gap-2 min-w-0">
|
|
127
|
-
<span class="truncate text-xs text-gray-300"
|
|
128
|
-
>${runTitle}</span
|
|
129
|
-
>
|
|
130
|
-
<span class="text-xs text-gray-500 shrink-0">
|
|
131
|
-
${formatRowTimestamp(runEntry.ts, variant)}
|
|
132
|
-
</span>
|
|
133
|
-
</span>
|
|
134
|
-
`
|
|
135
|
-
: html`
|
|
136
|
-
<span class="truncate text-xs text-gray-300">
|
|
137
|
-
${runEntry.jobId} -
|
|
138
|
-
${formatRowTimestamp(runEntry.ts, variant)}
|
|
139
|
-
</span>
|
|
140
|
-
`}
|
|
141
|
-
</span>
|
|
142
|
-
<span class="inline-flex items-center gap-3 shrink-0 text-xs">
|
|
143
|
-
<span class=${runStatusClassName(runStatus)}>${runStatus}</span>
|
|
144
|
-
<span class="text-gray-400"
|
|
145
|
-
>${formatDurationCompactMs(runEntry.durationMs)}</span
|
|
146
|
-
>
|
|
147
|
-
<span class="text-gray-400">${formatTokenCount(runTokens)} tk</span>
|
|
148
|
-
${isDetail
|
|
149
|
-
? html`<span class="text-gray-500"
|
|
150
|
-
>${runDeliveryLabel(runEntry)}</span
|
|
151
|
-
>`
|
|
152
|
-
: html`
|
|
153
|
-
<span class="text-gray-500"
|
|
154
|
-
>${runEstimatedCost == null
|
|
155
|
-
? "—"
|
|
156
|
-
: `~${formatCost(runEstimatedCost)}`}</span
|
|
157
|
-
>
|
|
158
|
-
`}
|
|
159
|
-
</span>
|
|
171
|
+
<div class="inline-flex items-center gap-2 min-w-0 w-full">
|
|
172
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
|
|
173
|
+
<div class="min-w-0 flex-1">
|
|
174
|
+
${renderEntrySummaryRow({ runEntry, variant })}
|
|
175
|
+
</div>
|
|
160
176
|
</div>
|
|
161
177
|
</summary>
|
|
162
178
|
<div class="ac-history-body space-y-2 text-xs">
|
|
@@ -2,6 +2,7 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { ActionButton } from "../action-button.js";
|
|
5
|
+
import { AlarmLineIcon } from "../icons.js";
|
|
5
6
|
import { PageHeader } from "../page-header.js";
|
|
6
7
|
import { CronJobList } from "./cron-job-list.js";
|
|
7
8
|
import { CronJobDetail } from "./cron-job-detail.js";
|
|
@@ -144,8 +145,21 @@ export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
|
|
|
144
145
|
<main class="cron-detail-panel">
|
|
145
146
|
${noJobs
|
|
146
147
|
? html`
|
|
147
|
-
<div
|
|
148
|
-
|
|
148
|
+
<div
|
|
149
|
+
class="bg-surface border border-border rounded-xl px-6 py-10 min-h-[26rem] flex flex-col items-center justify-center text-center"
|
|
150
|
+
>
|
|
151
|
+
<div class="max-w-md w-full flex flex-col items-center gap-4">
|
|
152
|
+
<${AlarmLineIcon} className="h-12 w-12 text-cyan-400" />
|
|
153
|
+
<div class="space-y-2">
|
|
154
|
+
<h2 class="font-semibold text-lg text-gray-100">
|
|
155
|
+
No cron jobs yet
|
|
156
|
+
</h2>
|
|
157
|
+
<p class="text-xs text-gray-400 leading-5">
|
|
158
|
+
Cron jobs are managed via the OpenClaw CLI. Once jobs are
|
|
159
|
+
configured, schedules and run history will appear here.
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
149
163
|
</div>
|
|
150
164
|
`
|
|
151
165
|
: isAllJobsSelected
|
|
@@ -429,6 +429,17 @@ export const ErrorWarningLineIcon = ({ className = "" }) => html`
|
|
|
429
429
|
</svg>
|
|
430
430
|
`;
|
|
431
431
|
|
|
432
|
+
export const AlarmLineIcon = ({ className = "" }) => html`
|
|
433
|
+
<svg
|
|
434
|
+
class=${className}
|
|
435
|
+
viewBox="0 0 24 24"
|
|
436
|
+
fill="currentColor"
|
|
437
|
+
aria-hidden="true"
|
|
438
|
+
>
|
|
439
|
+
<path d="M12.0001 22.0001C7.02956 22.0001 3.00012 17.9707 3.00012 13.0001C3.00012 8.02956 7.02956 4.00012 12.0001 4.00012C16.9707 4.00012 21.0001 8.02956 21.0001 13.0001C21.0001 17.9707 16.9707 22.0001 12.0001 22.0001ZM12.0001 20.0001C15.8661 20.0001 19.0001 16.8661 19.0001 13.0001C19.0001 9.13412 15.8661 6.00012 12.0001 6.00012C8.13412 6.00012 5.00012 9.13412 5.00012 13.0001C5.00012 16.8661 8.13412 20.0001 12.0001 20.0001ZM13.0001 13.0001H16.0001V15.0001H11.0001V8.00012H13.0001V13.0001ZM1.74707 6.2826L5.2826 2.74707L6.69682 4.16128L3.16128 7.69682L1.74707 6.2826ZM18.7176 2.74707L22.2532 6.2826L20.839 7.69682L17.3034 4.16128L18.7176 2.74707Z" />
|
|
440
|
+
</svg>
|
|
441
|
+
`;
|
|
442
|
+
|
|
432
443
|
export const FullscreenLineIcon = ({ className = "" }) => html`
|
|
433
444
|
<svg
|
|
434
445
|
class=${className}
|
|
@@ -6,6 +6,7 @@ import { FileTree } from "./file-tree.js";
|
|
|
6
6
|
import { OverflowMenu, OverflowMenuItem } from "./overflow-menu.js";
|
|
7
7
|
import { UpdateActionButton } from "./update-action-button.js";
|
|
8
8
|
import { SidebarGitPanel } from "./sidebar-git-panel.js";
|
|
9
|
+
import { UpdateModal } from "./update-modal.js";
|
|
9
10
|
import { readUiSettings, writeUiSettings } from "../lib/ui-settings.js";
|
|
10
11
|
|
|
11
12
|
const html = htm.bind(h);
|
|
@@ -62,6 +63,7 @@ export const AppSidebar = ({
|
|
|
62
63
|
readStoredBrowseBottomPanelHeight,
|
|
63
64
|
);
|
|
64
65
|
const [isResizingBrowsePanels, setIsResizingBrowsePanels] = useState(false);
|
|
66
|
+
const [updateModalOpen, setUpdateModalOpen] = useState(false);
|
|
65
67
|
|
|
66
68
|
useEffect(() => {
|
|
67
69
|
const settings = readUiSettings();
|
|
@@ -237,10 +239,10 @@ export const AppSidebar = ({
|
|
|
237
239
|
`,
|
|
238
240
|
)}
|
|
239
241
|
<div class="sidebar-footer">
|
|
240
|
-
${acHasUpdate && acLatest
|
|
242
|
+
${acHasUpdate && acLatest
|
|
241
243
|
? html`
|
|
242
244
|
<${UpdateActionButton}
|
|
243
|
-
onClick=${
|
|
245
|
+
onClick=${() => setUpdateModalOpen(true)}
|
|
244
246
|
loading=${acUpdating}
|
|
245
247
|
warning=${true}
|
|
246
248
|
idleLabel=${`Update to v${acLatest}`}
|
|
@@ -292,6 +294,16 @@ export const AppSidebar = ({
|
|
|
292
294
|
</div>
|
|
293
295
|
</div>
|
|
294
296
|
</div>
|
|
297
|
+
<${UpdateModal}
|
|
298
|
+
visible=${updateModalOpen}
|
|
299
|
+
onClose=${() => {
|
|
300
|
+
if (acUpdating) return;
|
|
301
|
+
setUpdateModalOpen(false);
|
|
302
|
+
}}
|
|
303
|
+
version=${acLatest}
|
|
304
|
+
onUpdate=${onAcUpdate}
|
|
305
|
+
updating=${acUpdating}
|
|
306
|
+
/>
|
|
295
307
|
</div>
|
|
296
308
|
`;
|
|
297
309
|
};
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { marked } from "https://esm.sh/marked";
|
|
5
|
+
import { fetchAlphaclawReleaseNotes } from "../lib/api.js";
|
|
6
|
+
import { ModalShell } from "./modal-shell.js";
|
|
7
|
+
import { ActionButton } from "./action-button.js";
|
|
8
|
+
import { LoadingSpinner } from "./loading-spinner.js";
|
|
9
|
+
import { CloseIcon } from "./icons.js";
|
|
10
|
+
|
|
11
|
+
const html = htm.bind(h);
|
|
12
|
+
|
|
13
|
+
const getReleaseTagFromVersion = (version) => {
|
|
14
|
+
const rawVersion = String(version || "").trim();
|
|
15
|
+
if (!rawVersion) return "";
|
|
16
|
+
return rawVersion.startsWith("v") ? rawVersion : `v${rawVersion}`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const formatPublishedAt = (value) => {
|
|
20
|
+
const dateMs = Date.parse(String(value || ""));
|
|
21
|
+
if (!Number.isFinite(dateMs)) return "";
|
|
22
|
+
try {
|
|
23
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
24
|
+
dateStyle: "medium",
|
|
25
|
+
timeStyle: "short",
|
|
26
|
+
}).format(new Date(dateMs));
|
|
27
|
+
} catch {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getReleaseUrl = (tag) =>
|
|
33
|
+
tag
|
|
34
|
+
? `https://github.com/chrysb/alphaclaw/releases/tag/${encodeURIComponent(tag)}`
|
|
35
|
+
: "https://github.com/chrysb/alphaclaw/releases";
|
|
36
|
+
|
|
37
|
+
export const UpdateModal = ({
|
|
38
|
+
visible = false,
|
|
39
|
+
onClose = () => {},
|
|
40
|
+
version = "",
|
|
41
|
+
onUpdate = () => {},
|
|
42
|
+
updating = false,
|
|
43
|
+
}) => {
|
|
44
|
+
const requestedTag = useMemo(() => getReleaseTagFromVersion(version), [version]);
|
|
45
|
+
const [loadingNotes, setLoadingNotes] = useState(false);
|
|
46
|
+
const [notesError, setNotesError] = useState("");
|
|
47
|
+
const [notesData, setNotesData] = useState(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!visible) return;
|
|
51
|
+
let isActive = true;
|
|
52
|
+
const loadNotes = async () => {
|
|
53
|
+
setLoadingNotes(true);
|
|
54
|
+
setNotesError("");
|
|
55
|
+
try {
|
|
56
|
+
const data = await fetchAlphaclawReleaseNotes(requestedTag);
|
|
57
|
+
if (!isActive) return;
|
|
58
|
+
if (!data?.ok) {
|
|
59
|
+
setNotesError(data?.error || "Could not load release notes");
|
|
60
|
+
setNotesData(null);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setNotesData(data);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (!isActive) return;
|
|
66
|
+
setNotesError(err?.message || "Could not load release notes");
|
|
67
|
+
setNotesData(null);
|
|
68
|
+
} finally {
|
|
69
|
+
if (!isActive) return;
|
|
70
|
+
setLoadingNotes(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
loadNotes();
|
|
74
|
+
return () => {
|
|
75
|
+
isActive = false;
|
|
76
|
+
};
|
|
77
|
+
}, [visible, requestedTag]);
|
|
78
|
+
|
|
79
|
+
const effectiveTag = String(notesData?.tag || requestedTag || "").trim();
|
|
80
|
+
const effectiveReleaseUrl =
|
|
81
|
+
String(notesData?.htmlUrl || "").trim() || getReleaseUrl(effectiveTag);
|
|
82
|
+
const updateLabel = effectiveTag ? `Update to ${effectiveTag}` : "Update now";
|
|
83
|
+
const publishedAtLabel = formatPublishedAt(notesData?.publishedAt);
|
|
84
|
+
const releaseBody = String(notesData?.body || "").trim();
|
|
85
|
+
const releasePreviewHtml = useMemo(
|
|
86
|
+
() =>
|
|
87
|
+
marked.parse(releaseBody, {
|
|
88
|
+
gfm: true,
|
|
89
|
+
breaks: true,
|
|
90
|
+
}),
|
|
91
|
+
[releaseBody],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return html`
|
|
95
|
+
<${ModalShell}
|
|
96
|
+
visible=${visible}
|
|
97
|
+
onClose=${onClose}
|
|
98
|
+
panelClassName="relative bg-modal border border-border rounded-xl p-5 w-full max-w-3xl max-h-[92vh] overflow-hidden flex flex-col gap-4"
|
|
99
|
+
>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onclick=${onClose}
|
|
103
|
+
class="absolute top-5 right-5 h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
|
|
104
|
+
aria-label="Close modal"
|
|
105
|
+
>
|
|
106
|
+
<${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
|
|
107
|
+
</button>
|
|
108
|
+
<div class="space-y-1 pr-10">
|
|
109
|
+
<h3 class="text-sm font-semibold">AlphaClaw release notes</h3>
|
|
110
|
+
${publishedAtLabel
|
|
111
|
+
? html`<p class="text-xs text-gray-500">Published ${publishedAtLabel}</p>`
|
|
112
|
+
: null}
|
|
113
|
+
</div>
|
|
114
|
+
<div class="ac-surface-inset border border-border rounded-lg p-2 overflow-auto min-h-[220px] max-h-[66vh]">
|
|
115
|
+
${loadingNotes
|
|
116
|
+
? html`
|
|
117
|
+
<div class="min-h-[200px] flex items-center justify-center text-gray-400">
|
|
118
|
+
<span class="inline-flex items-center gap-2 text-sm">
|
|
119
|
+
<${LoadingSpinner} className="h-4 w-4" />
|
|
120
|
+
Loading release notes...
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
`
|
|
124
|
+
: notesError
|
|
125
|
+
? html`
|
|
126
|
+
<div class="space-y-2">
|
|
127
|
+
<p class="text-sm text-red-300">${notesError}</p>
|
|
128
|
+
<a
|
|
129
|
+
class="ac-tip-link text-xs"
|
|
130
|
+
href=${effectiveReleaseUrl}
|
|
131
|
+
target="_blank"
|
|
132
|
+
rel="noreferrer"
|
|
133
|
+
>View release on GitHub</a
|
|
134
|
+
>
|
|
135
|
+
</div>
|
|
136
|
+
`
|
|
137
|
+
: releaseBody
|
|
138
|
+
? html`<div
|
|
139
|
+
class="file-viewer-preview release-notes-preview"
|
|
140
|
+
dangerouslySetInnerHTML=${{ __html: releasePreviewHtml }}
|
|
141
|
+
></div>`
|
|
142
|
+
: html`
|
|
143
|
+
<div class="space-y-2">
|
|
144
|
+
<p class="text-sm text-gray-300">No release notes were published for this tag.</p>
|
|
145
|
+
<a
|
|
146
|
+
class="ac-tip-link text-xs"
|
|
147
|
+
href=${effectiveReleaseUrl}
|
|
148
|
+
target="_blank"
|
|
149
|
+
rel="noreferrer"
|
|
150
|
+
>Open release on GitHub</a
|
|
151
|
+
>
|
|
152
|
+
</div>
|
|
153
|
+
`}
|
|
154
|
+
</div>
|
|
155
|
+
<div class="flex items-center justify-end gap-2 pt-1">
|
|
156
|
+
<${ActionButton}
|
|
157
|
+
onClick=${onClose}
|
|
158
|
+
tone="ghost"
|
|
159
|
+
idleLabel="Later"
|
|
160
|
+
disabled=${updating}
|
|
161
|
+
/>
|
|
162
|
+
<${ActionButton}
|
|
163
|
+
onClick=${onUpdate}
|
|
164
|
+
tone="warning"
|
|
165
|
+
idleLabel=${updateLabel}
|
|
166
|
+
loadingLabel="Updating..."
|
|
167
|
+
loading=${updating}
|
|
168
|
+
disabled=${loadingNotes}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</${ModalShell}>
|
|
172
|
+
`;
|
|
173
|
+
};
|