@chrysb/alphaclaw 0.6.2-beta.2 → 0.6.2-beta.3

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.
@@ -210,6 +210,9 @@
210
210
  flex-direction: column;
211
211
  gap: 12px;
212
212
  min-height: 100%;
213
+ width: min(100%, 1024px);
214
+ margin-left: auto;
215
+ margin-right: auto;
213
216
  }
214
217
 
215
218
  .cron-prompt-editor-shell {
@@ -222,6 +225,11 @@
222
225
  background: rgba(255, 255, 255, 0.01);
223
226
  }
224
227
 
228
+ .cron-prompt-editor-shell .file-viewer-editor-line-num-col {
229
+ width: 44px;
230
+ padding: 16px 8px 112px 0;
231
+ }
232
+
225
233
  .cron-calendar-repeating-strip {
226
234
  border: 1px solid var(--border);
227
235
  border-radius: 10px;
@@ -284,7 +292,7 @@
284
292
  .cron-calendar-grid-header,
285
293
  .cron-calendar-grid-row {
286
294
  display: grid;
287
- grid-template-columns: 88px repeat(7, minmax(130px, 1fr));
295
+ grid-template-columns: 80px repeat(7, minmax(80px, 1fr));
288
296
  }
289
297
 
290
298
  .cron-calendar-day-header {
@@ -11,13 +11,13 @@ import {
11
11
  kLargeFileSimpleEditorLineThreshold,
12
12
  } from "../file-viewer/constants.js";
13
13
  import { highlightEditorLines } from "../../lib/syntax-highlighters/index.js";
14
- import { formatDurationCompactMs, formatLocaleDateTimeWithTodayTime } from "../../lib/format.js";
15
14
  import {
16
15
  formatCronScheduleLabel,
17
16
  formatNextRunRelativeMs,
18
17
  formatTokenCount,
19
18
  } from "./cron-helpers.js";
20
19
  import { CronJobUsage } from "./cron-job-usage.js";
20
+ import { CronRunHistoryPanel } from "./cron-run-history-panel.js";
21
21
  import { readUiSettings, writeUiSettings } from "../../lib/ui-settings.js";
22
22
 
23
23
  const html = htm.bind(h);
@@ -41,8 +41,7 @@ const PromptEditor = ({
41
41
  promptValue = "",
42
42
  savedPromptValue = "",
43
43
  onChangePrompt = () => {},
44
- onSavePrompt = () => {},
45
- savingPrompt = false,
44
+ onSaveChanges = () => {},
46
45
  }) => {
47
46
  const promptEditorShellRef = useRef(null);
48
47
  const editorTextareaRef = useRef(null);
@@ -84,7 +83,7 @@ const PromptEditor = ({
84
83
  const handleEditorKeyDown = (event) => {
85
84
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
86
85
  event.preventDefault();
87
- onSavePrompt();
86
+ onSaveChanges();
88
87
  }
89
88
  if (event.key === "Tab") {
90
89
  event.preventDefault();
@@ -133,17 +132,6 @@ const PromptEditor = ({
133
132
  Prompt
134
133
  ${isDirty ? html`<span class="file-viewer-dirty-dot"></span>` : null}
135
134
  </h3>
136
- <div class="flex items-center gap-2">
137
- <${ActionButton}
138
- onClick=${onSavePrompt}
139
- disabled=${!isDirty}
140
- loading=${savingPrompt}
141
- tone="primary"
142
- size="sm"
143
- idleLabel="Save"
144
- loadingLabel="Saving..."
145
- />
146
- </div>
147
135
  </div>
148
136
  <div
149
137
  class="cron-prompt-editor-shell"
@@ -173,15 +161,6 @@ const PromptEditor = ({
173
161
  `;
174
162
  };
175
163
 
176
- const runStatusClassName = (status) => {
177
- const normalized = String(status || "").trim().toLowerCase();
178
- if (normalized === "ok") return "text-green-300";
179
- if (normalized === "error") return "text-red-300";
180
- if (normalized === "skipped") return "text-yellow-300";
181
- return "text-gray-400";
182
- };
183
-
184
- const runDeliveryLabel = (run) => String(run?.deliveryStatus || "not-requested");
185
164
  const kMetaCardClassName = "ac-surface-inset rounded-lg p-2.5 space-y-1.5";
186
165
  const kRunStatusFilterOptions = [
187
166
  { label: "all", value: "all" },
@@ -189,11 +168,15 @@ const kRunStatusFilterOptions = [
189
168
  { label: "error", value: "error" },
190
169
  { label: "skipped", value: "skipped" },
191
170
  ];
192
- const kRunDeliveryFilterOptions = [
193
- { label: "all", value: "all" },
194
- { label: "delivered", value: "delivered" },
195
- { label: "not-delivered", value: "not-delivered" },
171
+ const kSessionTargetOptions = [
172
+ { label: "main", value: "main" },
173
+ { label: "isolated", value: "isolated" },
174
+ ];
175
+ const kWakeModeOptions = [
176
+ { label: "now", value: "now" },
177
+ { label: "next-heartbeat", value: "next-heartbeat" },
196
178
  ];
179
+ const kDeliveryNoneValue = "__none__";
197
180
  const isSameCalendarDay = (leftDate, rightDate) =>
198
181
  leftDate.getFullYear() === rightDate.getFullYear() &&
199
182
  leftDate.getMonth() === rightDate.getMonth() &&
@@ -231,9 +214,7 @@ export const CronJobDetail = ({
231
214
  runHasMore = false,
232
215
  loadingMoreRuns = false,
233
216
  runStatusFilter = "all",
234
- runDeliveryFilter = "all",
235
217
  onSetRunStatusFilter = () => {},
236
- onSetRunDeliveryFilter = () => {},
237
218
  onLoadMoreRuns = () => {},
238
219
  onRunNow = () => {},
239
220
  runningJob = false,
@@ -245,8 +226,15 @@ export const CronJobDetail = ({
245
226
  promptValue = "",
246
227
  savedPromptValue = "",
247
228
  onChangePrompt = () => {},
248
- onSavePrompt = () => {},
249
- savingPrompt = false,
229
+ onSaveChanges = () => {},
230
+ savingChanges = false,
231
+ routingDraft = null,
232
+ onChangeRoutingDraft = () => {},
233
+ deliverySessions = [],
234
+ loadingDeliverySessions = false,
235
+ deliverySessionsError = "",
236
+ destinationSessionKey = "",
237
+ onChangeDestinationSessionKey = () => {},
250
238
  }) => {
251
239
  if (!job) {
252
240
  return html`
@@ -256,6 +244,50 @@ export const CronJobDetail = ({
256
244
  `;
257
245
  }
258
246
 
247
+ const sessionTarget = String(
248
+ routingDraft?.sessionTarget || job?.sessionTarget || "main",
249
+ );
250
+ const wakeMode = String(routingDraft?.wakeMode || job?.wakeMode || "now");
251
+ const deliveryMode = String(
252
+ routingDraft?.deliveryMode || job?.delivery?.mode || "none",
253
+ );
254
+ const currentSessionTarget = String(job?.sessionTarget || "main");
255
+ const currentWakeMode = String(job?.wakeMode || "now");
256
+ const currentDeliveryMode = String(job?.delivery?.mode || "none");
257
+ const deliverySessionOptions = useMemo(() => {
258
+ const seenLabels = new Set();
259
+ const deduped = [];
260
+ const selectedKey = String(destinationSessionKey || "").trim();
261
+ let selectedPresent = false;
262
+ (Array.isArray(deliverySessions) ? deliverySessions : []).forEach((sessionRow) => {
263
+ const key = String(sessionRow?.key || "").trim();
264
+ if (!key) return;
265
+ if (key === selectedKey) selectedPresent = true;
266
+ const label = String(sessionRow?.label || sessionRow?.key || "Session").trim();
267
+ const dedupeKey = label.toLowerCase();
268
+ if (seenLabels.has(dedupeKey)) return;
269
+ seenLabels.add(dedupeKey);
270
+ deduped.push(sessionRow);
271
+ });
272
+ if (!selectedPresent && selectedKey) {
273
+ const selectedRow = (Array.isArray(deliverySessions) ? deliverySessions : []).find(
274
+ (sessionRow) => String(sessionRow?.key || "").trim() === selectedKey,
275
+ );
276
+ if (selectedRow) deduped.unshift(selectedRow);
277
+ }
278
+ return deduped;
279
+ }, [deliverySessions, destinationSessionKey]);
280
+ const deliverySelectValue =
281
+ deliveryMode === "announce" && String(destinationSessionKey || "").trim()
282
+ ? String(destinationSessionKey || "")
283
+ : kDeliveryNoneValue;
284
+ const isRoutingDirty =
285
+ sessionTarget !== currentSessionTarget ||
286
+ wakeMode !== currentWakeMode ||
287
+ deliveryMode !== currentDeliveryMode;
288
+ const isPromptDirty = promptValue !== savedPromptValue;
289
+ const hasUnsavedChanges = isRoutingDirty || isPromptDirty;
290
+
259
291
  return html`
260
292
  <div class="cron-detail-scroll">
261
293
  <div class="cron-detail-content">
@@ -265,22 +297,15 @@ export const CronJobDetail = ({
265
297
  <h2 class="font-semibold text-base text-gray-100">${job.name || job.id}</h2>
266
298
  <div class="text-xs text-gray-500 mt-1">ID: <code>${job.id}</code></div>
267
299
  </div>
268
- <div class="flex items-center gap-2">
269
- <${ToggleSwitch}
270
- checked=${job.enabled !== false}
271
- disabled=${togglingJobEnabled}
272
- onChange=${onToggleEnabled}
273
- label=${job.enabled === false ? "Disabled" : "Enabled"}
274
- />
275
- <${ActionButton}
276
- onClick=${onRunNow}
277
- loading=${runningJob}
278
- tone="secondary"
279
- size="sm"
280
- idleLabel="Run Now"
281
- loadingLabel="Running..."
282
- />
283
- </div>
300
+ <${ActionButton}
301
+ onClick=${onSaveChanges}
302
+ loading=${savingChanges}
303
+ disabled=${!hasUnsavedChanges}
304
+ tone="primary"
305
+ size="sm"
306
+ idleLabel="Save changes"
307
+ loadingLabel="Saving..."
308
+ />
284
309
  </div>
285
310
  <div class="grid grid-cols-2 gap-2 text-xs">
286
311
  <div class=${kMetaCardClassName}>
@@ -304,32 +329,100 @@ export const CronJobDetail = ({
304
329
  <div class="grid grid-cols-3 gap-2 text-xs">
305
330
  <div class=${kMetaCardClassName}>
306
331
  <div class="text-gray-500">Session target</div>
307
- <div class="text-gray-300 font-mono">${job.sessionTarget || "main"}</div>
332
+ <div class="pt-1">
333
+ <${SegmentedControl}
334
+ options=${kSessionTargetOptions}
335
+ value=${sessionTarget}
336
+ onChange=${(value) =>
337
+ onChangeRoutingDraft((currentValue = {}) => ({
338
+ ...currentValue,
339
+ sessionTarget: String(value || "main"),
340
+ }))}
341
+ />
342
+ </div>
308
343
  </div>
309
344
  <div class=${kMetaCardClassName}>
310
345
  <div class="text-gray-500">Wake mode</div>
311
- <div class="text-gray-300 font-mono">${job.wakeMode || "now"}</div>
346
+ <div class="pt-1">
347
+ <${SegmentedControl}
348
+ options=${kWakeModeOptions}
349
+ value=${wakeMode}
350
+ onChange=${(value) =>
351
+ onChangeRoutingDraft((currentValue = {}) => ({
352
+ ...currentValue,
353
+ wakeMode: String(value || "now"),
354
+ }))}
355
+ />
356
+ </div>
312
357
  </div>
313
358
  <div class=${kMetaCardClassName}>
314
359
  <div class="text-gray-500">Delivery</div>
315
- <div class="text-gray-300 font-mono">
316
- ${String(job?.delivery?.mode || "none")}
317
- ${job?.delivery?.channel
318
- ? html`- ${job.delivery.channel}${job?.delivery?.to
319
- ? `:${job.delivery.to}`
320
- : ""}`
321
- : ""}
360
+ <div class="pt-1">
361
+ <select
362
+ value=${deliverySelectValue}
363
+ onInput=${(event) => {
364
+ const nextValue = String(event.currentTarget?.value || "");
365
+ if (!nextValue || nextValue === kDeliveryNoneValue) {
366
+ onChangeRoutingDraft((currentValue = {}) => ({
367
+ ...currentValue,
368
+ deliveryMode: "none",
369
+ deliveryChannel: "",
370
+ deliveryTo: "",
371
+ }));
372
+ onChangeDestinationSessionKey("");
373
+ return;
374
+ }
375
+ onChangeDestinationSessionKey(nextValue);
376
+ onChangeRoutingDraft((currentValue = {}) => ({
377
+ ...currentValue,
378
+ deliveryMode: "announce",
379
+ }));
380
+ }}
381
+ disabled=${savingChanges}
382
+ class="w-full bg-black/30 border border-border rounded-lg px-2 py-1.5 text-[11px] text-gray-200 focus:border-gray-500"
383
+ >
384
+ <option value=${kDeliveryNoneValue}>None</option>
385
+ ${deliverySessionOptions.map(
386
+ (sessionRow) => html`
387
+ <option value=${String(sessionRow?.key || "")}>
388
+ ${String(sessionRow?.label || sessionRow?.key || "Session")}
389
+ </option>
390
+ `,
391
+ )}
392
+ </select>
322
393
  </div>
394
+ ${loadingDeliverySessions
395
+ ? html`<div class="text-[11px] text-gray-500 pt-1">Loading delivery sessions...</div>`
396
+ : null}
397
+ ${deliverySessionsError
398
+ ? html`<div class="text-[11px] text-red-400 pt-1">${deliverySessionsError}</div>`
399
+ : null}
323
400
  </div>
324
401
  </div>
402
+ <div class="flex items-center justify-between gap-3">
403
+ <${ToggleSwitch}
404
+ checked=${job.enabled !== false}
405
+ disabled=${togglingJobEnabled || savingChanges}
406
+ onChange=${onToggleEnabled}
407
+ label=${job.enabled === false ? "Disabled" : "Enabled"}
408
+ />
409
+ <${ActionButton}
410
+ onClick=${onRunNow}
411
+ loading=${runningJob}
412
+ disabled=${hasUnsavedChanges || savingChanges}
413
+ tone="secondary"
414
+ size="sm"
415
+ idleLabel="Run now"
416
+ loadingLabel="Running..."
417
+ />
418
+ </div>
325
419
  </section>
326
420
 
327
421
  <${PromptEditor}
328
422
  promptValue=${promptValue}
329
423
  savedPromptValue=${savedPromptValue}
330
424
  onChangePrompt=${onChangePrompt}
331
- onSavePrompt=${onSavePrompt}
332
- savingPrompt=${savingPrompt}
425
+ onSaveChanges=${onSaveChanges}
333
426
  />
334
427
 
335
428
  <${CronJobUsage}
@@ -338,73 +431,14 @@ export const CronJobDetail = ({
338
431
  onSetUsageDays=${onSetUsageDays}
339
432
  />
340
433
 
341
- <section class="bg-surface border border-border rounded-xl p-4 space-y-3">
342
- <div class="flex items-center justify-between gap-2">
343
- <h3 class="card-label card-label-bright">Run history</h3>
344
- <div class="text-xs text-gray-500">${formatTokenCount(runTotal)} entries</div>
345
- </div>
346
- <div class="flex items-center gap-2">
347
- <${SegmentedControl}
348
- options=${kRunStatusFilterOptions}
349
- value=${runStatusFilter}
350
- onChange=${onSetRunStatusFilter}
351
- />
352
- <${SegmentedControl}
353
- options=${kRunDeliveryFilterOptions}
354
- value=${runDeliveryFilter}
355
- onChange=${onSetRunDeliveryFilter}
356
- />
357
- </div>
358
-
359
- ${runEntries.length === 0
360
- ? html`<div class="text-sm text-gray-500">No runs found.</div>`
361
- : html`
362
- <div class="ac-history-list">
363
- ${runEntries.map(
364
- (entry) => html`
365
- <details key=${`${entry.ts}:${entry.sessionKey || ""}`} class="ac-history-item">
366
- <summary class="ac-history-summary">
367
- <div class="ac-history-summary-row">
368
- <span class="inline-flex items-center gap-2 min-w-0">
369
- <span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
370
- <span class="truncate text-xs text-gray-300">
371
- ${formatLocaleDateTimeWithTodayTime(entry.ts, {
372
- fallback: "—",
373
- valueIsEpochMs: true,
374
- })}
375
- </span>
376
- </span>
377
- <span class="inline-flex items-center gap-3 shrink-0 text-xs">
378
- <span class=${runStatusClassName(entry.status)}>${entry.status || "unknown"}</span>
379
- <span class="text-gray-400">${formatDurationCompactMs(entry.durationMs)}</span>
380
- <span class="text-gray-400">
381
- ${formatTokenCount(entry?.usage?.total_tokens || 0)} tk
382
- </span>
383
- <span class="text-gray-500">${runDeliveryLabel(entry)}</span>
384
- </span>
385
- </div>
386
- </summary>
387
- <div class="ac-history-body space-y-2 text-xs">
388
- ${entry.summary
389
- ? html`<div><span class="text-gray-500">Summary:</span> ${entry.summary}</div>`
390
- : null}
391
- ${entry.error
392
- ? html`<div class="text-red-300"><span class="text-gray-500">Error:</span> ${entry.error}</div>`
393
- : null}
394
- <div class="text-gray-500">
395
- Model: <span class="text-gray-300 font-mono">${entry.model || "—"}</span>
396
- ${entry.sessionKey
397
- ? html` | Session:
398
- <span class="text-gray-300 font-mono">${entry.sessionKey}</span>`
399
- : null}
400
- </div>
401
- </div>
402
- </details>
403
- `,
404
- )}
405
- </div>
406
- `}
407
- ${runHasMore
434
+ <${CronRunHistoryPanel}
435
+ entryCountLabel=${`${formatTokenCount(runTotal)} entries`}
436
+ primaryFilterOptions=${kRunStatusFilterOptions}
437
+ primaryFilterValue=${runStatusFilter}
438
+ onChangePrimaryFilter=${onSetRunStatusFilter}
439
+ rows=${runEntries}
440
+ variant="detail"
441
+ footer=${runHasMore
408
442
  ? html`
409
443
  <div class="pt-2">
410
444
  <${ActionButton}
@@ -418,7 +452,7 @@ export const CronJobDetail = ({
418
452
  </div>
419
453
  `
420
454
  : null}
421
- </section>
455
+ />
422
456
  </div>
423
457
  </div>
424
458
  `;
@@ -1,8 +1,13 @@
1
1
  import { h } from "https://esm.sh/preact";
2
2
  import htm from "https://esm.sh/htm";
3
3
  import { formatCost, formatTokenCount } from "./cron-helpers.js";
4
+ import { SegmentedControl } from "../segmented-control.js";
4
5
 
5
6
  const html = htm.bind(h);
7
+ const kUsageRangeOptions = [
8
+ { label: "7d", value: 7 },
9
+ { label: "30d", value: 30 },
10
+ ];
6
11
 
7
12
  const resolveDominantModel = (usage = null) => {
8
13
  const list = Array.isArray(usage?.modelBreakdown) ? usage.modelBreakdown : [];
@@ -25,23 +30,11 @@ export const CronJobUsage = ({ usage = null, usageDays = 30, onSetUsageDays = ()
25
30
  <section class="bg-surface border border-border rounded-xl p-4 space-y-3">
26
31
  <div class="flex items-center justify-between gap-2">
27
32
  <h3 class="card-label">Usage</h3>
28
- <div class="flex items-center gap-1">
29
- ${[7, 30].map(
30
- (days) => html`
31
- <button
32
- type="button"
33
- class=${`text-xs px-2 py-1 rounded border ${
34
- usageDays === days
35
- ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
36
- : "border-border text-gray-400 hover:text-gray-200"
37
- }`}
38
- onclick=${() => onSetUsageDays(days)}
39
- >
40
- ${days}d
41
- </button>
42
- `,
43
- )}
44
- </div>
33
+ <${SegmentedControl}
34
+ options=${kUsageRangeOptions}
35
+ value=${usageDays}
36
+ onChange=${onSetUsageDays}
37
+ />
45
38
  </div>
46
39
  <div class="grid grid-cols-2 gap-2 text-xs">
47
40
  <div class="ac-surface-inset rounded-lg p-2">
@@ -62,7 +55,7 @@ export const CronJobUsage = ({ usage = null, usageDays = 30, onSetUsageDays = ()
62
55
  </div>
63
56
  </div>
64
57
  <div class="text-xs text-gray-500">
65
- Dominant model:
58
+ Dominant model:
66
59
  <span class="text-gray-300 font-mono">${resolveDominantModel(usage)}</span>
67
60
  </div>
68
61
  </section>