@chrysb/alphaclaw 0.7.0-beta.0 → 0.7.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.
@@ -344,7 +344,6 @@
344
344
  .cron-calendar-legend-pill {
345
345
  font-size: 10px;
346
346
  line-height: 1;
347
- border: 1px solid transparent;
348
347
  border-radius: 999px;
349
348
  padding: 4px 7px;
350
349
  }
@@ -353,13 +352,13 @@
353
352
  border: 1px solid var(--border);
354
353
  border-radius: 10px;
355
354
  overflow: auto;
356
- background: rgba(255, 255, 255, 0.01);
355
+ background: var(--bg-surface);
357
356
  }
358
357
 
359
358
  .cron-calendar-grid-header,
360
359
  .cron-calendar-grid-row {
361
360
  display: grid;
362
- grid-template-columns: 80px repeat(7, minmax(80px, 1fr));
361
+ grid-template-columns: 72px repeat(7, minmax(80px, 1fr));
363
362
  }
364
363
 
365
364
  .cron-calendar-day-header {
@@ -395,10 +394,67 @@
395
394
  z-index: 3;
396
395
  }
397
396
 
397
+ .cron-calendar-grid-corner {
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: center;
401
+ }
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
+
398
420
  .cron-calendar-grid-wrap {
399
421
  position: relative;
400
422
  }
401
423
 
424
+ .cron-calendar-lightbox-panel {
425
+ width: min(96vw, 1200px);
426
+ max-height: 88vh;
427
+ display: flex;
428
+ flex-direction: column;
429
+ gap: 10px;
430
+ padding: 16px;
431
+ border: 1px solid var(--border);
432
+ border-radius: 12px;
433
+ background: var(--bg-sidebar);
434
+ }
435
+
436
+ .cron-calendar-lightbox-close {
437
+ width: 26px;
438
+ height: 26px;
439
+ border-radius: 8px;
440
+ border: 1px solid var(--border);
441
+ background: rgba(255, 255, 255, 0.03);
442
+ color: var(--text-dim);
443
+ display: inline-flex;
444
+ align-items: center;
445
+ justify-content: center;
446
+ }
447
+
448
+ .cron-calendar-lightbox-close:hover {
449
+ color: var(--text);
450
+ border-color: rgba(148, 163, 184, 0.5);
451
+ }
452
+
453
+ .cron-calendar-lightbox-body {
454
+ min-height: 0;
455
+ overflow: auto;
456
+ }
457
+
402
458
  .cron-calendar-grid-row .cron-calendar-hour-cell {
403
459
  box-shadow: 1px 0 0 var(--border);
404
460
  }
@@ -493,31 +549,50 @@
493
549
  box-shadow: inset 0 0 0 1px rgba(250, 204, 21, 0.45);
494
550
  }
495
551
 
496
- .cron-calendar-compact-strip {
552
+ .cron-calendar-compact-list {
497
553
  display: flex;
498
- flex-wrap: wrap;
554
+ flex-direction: column;
499
555
  gap: 6px;
500
- align-items: center;
501
556
  }
502
557
 
503
- .cron-calendar-compact-chip {
558
+ .cron-calendar-compact-row {
559
+ width: 100%;
560
+ display: flex;
561
+ align-items: center;
562
+ justify-content: space-between;
563
+ gap: 10px;
564
+ border: 1px solid rgba(148, 163, 184, 0.32);
565
+ border-radius: 8px;
566
+ padding: 7px 9px;
567
+ text-align: left;
504
568
  font-size: 11px;
505
569
  line-height: 1.2;
506
- border: 1px solid rgba(148, 163, 184, 0.32);
507
- border-radius: 7px;
508
- padding: 4px 8px;
570
+ }
571
+
572
+ .cron-calendar-compact-main {
573
+ min-width: 0;
509
574
  display: inline-flex;
510
575
  align-items: center;
511
- gap: 6px;
512
- max-width: 260px;
513
- cursor: pointer;
576
+ gap: 8px;
514
577
  }
515
578
 
516
579
  .cron-calendar-compact-time {
517
580
  font-size: 10px;
518
581
  font-family: var(--font-mono, monospace);
519
- opacity: 0.7;
582
+ opacity: 0.72;
583
+ white-space: nowrap;
584
+ }
585
+
586
+ .cron-calendar-compact-name {
587
+ color: var(--text);
588
+ }
589
+
590
+ .cron-calendar-compact-estimate {
591
+ font-size: 10px;
592
+ color: var(--text);
593
+ font-weight: 500;
520
594
  white-space: nowrap;
595
+ text-align: right;
521
596
  }
522
597
 
523
598
  .cron-runs-trend-bars {
@@ -6,8 +6,9 @@ import {
6
6
  useState,
7
7
  } from "https://esm.sh/preact/hooks";
8
8
  import htm from "https://esm.sh/htm";
9
- import { SegmentedControl } from "../segmented-control.js";
10
9
  import { Tooltip } from "../tooltip.js";
10
+ import { ModalShell } from "../modal-shell.js";
11
+ import { CloseIcon, FullscreenLineIcon } from "../icons.js";
11
12
  import {
12
13
  formatCost,
13
14
  formatCronScheduleLabel,
@@ -15,7 +16,6 @@ import {
15
16
  getCronRunEstimatedCost,
16
17
  getCronRunTotalTokens,
17
18
  } from "./cron-helpers.js";
18
- import { readUiSettings, updateUiSettings } from "../../lib/ui-settings.js";
19
19
  import {
20
20
  classifyRepeatingJobs,
21
21
  expandJobsToRollingSlots,
@@ -70,9 +70,6 @@ const slotStateClassName = ({
70
70
  const renderLegend = () => html`
71
71
  <div class="cron-calendar-legend">
72
72
  <span class="cron-calendar-legend-label">Token intensity</span>
73
- <span class="cron-calendar-legend-pill cron-calendar-slot-tier-unknown"
74
- >No usage</span
75
- >
76
73
  <span class="cron-calendar-legend-pill cron-calendar-slot-tier-low"
77
74
  >Low</span
78
75
  >
@@ -89,14 +86,6 @@ const renderLegend = () => html`
89
86
  `;
90
87
 
91
88
  const kNowRefreshMs = 60 * 1000;
92
- const kCalendarExpandedUiSettingKey = "cronCalendarExpanded";
93
- const kCalendarViewUiSettingKey = "cronCalendarView";
94
- const kCalendarViewUpcoming = "upcoming";
95
- const kCalendarViewCalendar = "calendar";
96
- const kCalendarViewOptions = [
97
- { label: "Up next", value: kCalendarViewUpcoming },
98
- { label: "Calendar", value: kCalendarViewCalendar },
99
- ];
100
89
  const kRunWindow7dMs = 7 * 24 * 60 * 60 * 1000;
101
90
  const kSlotRunToleranceMs = 45 * 60 * 1000;
102
91
  const kUnknownTier = "unknown";
@@ -304,30 +293,8 @@ export const CronCalendar = ({
304
293
  runsByJobId = {},
305
294
  onSelectJob = () => {},
306
295
  }) => {
307
- const [calendarView, setCalendarView] = useState(() => {
308
- const settings = readUiSettings();
309
- const savedView = String(
310
- settings?.[kCalendarViewUiSettingKey] || "",
311
- ).trim();
312
- if (savedView === kCalendarViewCalendar) return kCalendarViewCalendar;
313
- if (savedView === kCalendarViewUpcoming) return kCalendarViewUpcoming;
314
- return settings[kCalendarExpandedUiSettingKey] === true
315
- ? kCalendarViewCalendar
316
- : kCalendarViewUpcoming;
317
- });
318
- const isCalendarView = calendarView === kCalendarViewCalendar;
319
- const onChangeCalendarView = useCallback((nextValue) => {
320
- const nextView =
321
- nextValue === kCalendarViewCalendar
322
- ? kCalendarViewCalendar
323
- : kCalendarViewUpcoming;
324
- setCalendarView(nextView);
325
- updateUiSettings((settings) => ({
326
- ...settings,
327
- [kCalendarViewUiSettingKey]: nextView,
328
- [kCalendarExpandedUiSettingKey]: nextView === kCalendarViewCalendar,
329
- }));
330
- }, []);
296
+ const [calendarLightboxOpen, setCalendarLightboxOpen] = useState(false);
297
+ const [showNoisyUpcoming, setShowNoisyUpcoming] = useState(false);
331
298
 
332
299
  const [nowMs, setNowMs] = useState(() => Date.now());
333
300
  useEffect(() => {
@@ -463,10 +430,34 @@ export const CronCalendar = ({
463
430
  [jobById, runSummary7dByJobId, slotTierThresholds],
464
431
  );
465
432
 
466
- const upcomingSlots = useMemo(
433
+ const upcomingSlotsPreview = useMemo(
467
434
  () => getUpcomingSlots({ slots: timeline.slots, nowMs, limit: 3 }),
468
435
  [timeline.slots, nowMs],
469
436
  );
437
+ const noisyUpcomingItems = useMemo(() => {
438
+ const windowEndMs = nowMs + 24 * 60 * 60 * 1000;
439
+ return repeatingJobs
440
+ .map((job) => {
441
+ const jobId = String(job?.id || "");
442
+ const nextRunAtMs = Number(job?.state?.nextRunAtMs || 0);
443
+ if (!jobId || !Number.isFinite(nextRunAtMs) || nextRunAtMs <= nowMs) return null;
444
+ if (nextRunAtMs > windowEndMs) return null;
445
+ return {
446
+ key: `noisy:${jobId}:${nextRunAtMs}`,
447
+ jobId,
448
+ jobName: String(job?.name || jobId),
449
+ scheduledAtMs: nextRunAtMs,
450
+ };
451
+ })
452
+ .filter(Boolean)
453
+ .sort((left, right) => left.scheduledAtMs - right.scheduledAtMs);
454
+ }, [repeatingJobs, nowMs]);
455
+ const displayedUpcomingItems = useMemo(() => {
456
+ if (!showNoisyUpcoming) return upcomingSlotsPreview;
457
+ return [...upcomingSlotsPreview, ...noisyUpcomingItems].sort(
458
+ (left, right) => Number(left?.scheduledAtMs || 0) - Number(right?.scheduledAtMs || 0),
459
+ );
460
+ }, [noisyUpcomingItems, showNoisyUpcoming, upcomingSlotsPreview]);
470
461
 
471
462
  const hourRows = useMemo(() => {
472
463
  const uniqueHours = new Set(timeline.slots.map((slot) => slot.hourOfDay));
@@ -485,77 +476,92 @@ export const CronCalendar = ({
485
476
  [timeline.slots],
486
477
  );
487
478
 
488
- const totalUpcoming = useMemo(
489
- () => timeline.slots.filter((slot) => slot.scheduledAtMs > nowMs).length,
490
- [timeline.slots, nowMs],
491
- );
492
-
493
479
  const renderCompactStrip = () => {
494
480
  return html`
495
- ${upcomingSlots.length === 0
496
- ? html`<div class="text-xs text-gray-500 py-1">
497
- No upcoming jobs in the next 24 hours.
498
- </div>`
499
- : html`
500
- <div class="cron-calendar-compact-strip">
501
- ${upcomingSlots.map((slot) => {
502
- const tooltipText = buildJobTooltipText({
503
- jobName: slot.jobName,
504
- job: jobById[slot.jobId] || null,
505
- runSummary7d: runSummary7dByJobId[slot.jobId] || {},
506
- slotRun: runBySlotKey[slot.key] || null,
507
- latestRun: latestRunByJobId[slot.jobId],
508
- scheduledAtMs: slot.scheduledAtMs,
509
- nowMs,
510
- });
511
- return html`
512
- <${Tooltip}
513
- text=${tooltipText}
514
- widthClass="w-72"
515
- tooltipClassName="whitespace-pre-line"
516
- triggerClassName="inline-flex max-w-full"
517
- >
518
- <div
519
- key=${slot.key}
520
- class="cron-calendar-compact-chip cron-calendar-slot-tier-unknown cron-calendar-slot-upcoming"
521
- role="button"
522
- tabindex="0"
523
- onClick=${() => onSelectJob(slot.jobId)}
524
- onKeyDown=${(event) => {
525
- if (event.key !== "Enter" && event.key !== " ") return;
526
- event.preventDefault();
527
- onSelectJob(slot.jobId);
528
- }}
529
- >
530
- <span class="cron-calendar-compact-time">${formatUpcomingTime(slot.scheduledAtMs)}</span>
531
- <span class="truncate">${slot.jobName}</span>
532
- </div>
533
- </${Tooltip}>
534
- `;
535
- })}
536
- ${Math.max(0, totalUpcoming - upcomingSlots.length) > 0
537
- ? html`
538
- <button
539
- class="text-[11px] text-gray-500 hover:text-gray-300 self-center transition-colors"
540
- onClick=${() =>
541
- onChangeCalendarView(kCalendarViewCalendar)}
481
+ <div class="space-y-2">
482
+ ${displayedUpcomingItems.length === 0
483
+ ? html`<div class="text-xs text-gray-500 py-1">
484
+ No upcoming jobs in the next 24 hours.
485
+ </div>`
486
+ : html`
487
+ <div class="cron-calendar-compact-list">
488
+ ${displayedUpcomingItems.map((slot) => {
489
+ const summary7d = runSummary7dByJobId[slot.jobId] || {};
490
+ const avgTokensPerRun = Number(summary7d?.avgTokensPerRun || 0);
491
+ const avgCostPerRun = Number(summary7d?.avgCostPerRun || 0);
492
+ const estimateLabel =
493
+ avgTokensPerRun > 0 || avgCostPerRun > 0
494
+ ? `Est. ${avgTokensPerRun > 0 ? `${formatTokenCount(avgTokensPerRun)} tk` : "— tk"} · ${avgCostPerRun > 0 ? formatCost(avgCostPerRun) : "—"}`
495
+ : "Est. —";
496
+ const tooltipText = buildJobTooltipText({
497
+ jobName: slot.jobName,
498
+ job: jobById[slot.jobId] || null,
499
+ runSummary7d: runSummary7dByJobId[slot.jobId] || {},
500
+ slotRun: runBySlotKey[slot.key] || null,
501
+ latestRun: latestRunByJobId[slot.jobId],
502
+ scheduledAtMs: slot.scheduledAtMs,
503
+ nowMs,
504
+ });
505
+ return html`
506
+ <${Tooltip}
507
+ text=${tooltipText}
508
+ widthClass="w-72"
509
+ tooltipClassName="whitespace-pre-line"
510
+ triggerClassName="block w-full"
542
511
  >
543
- +${Math.max(0, totalUpcoming - upcomingSlots.length)} more
544
- this week
545
- </button>
546
- `
547
- : null}
548
- </div>
549
- `}
512
+ <button
513
+ key=${slot.key}
514
+ type="button"
515
+ class=${`cron-calendar-compact-row ${slotStateClassName({
516
+ isPast: false,
517
+ mappedStatus: "",
518
+ tokenTier: getJobProjectedTier(slot.jobId),
519
+ })}`}
520
+ onClick=${() => onSelectJob(slot.jobId)}
521
+ >
522
+ <span class="cron-calendar-compact-main">
523
+ <span class="cron-calendar-compact-time"
524
+ >${formatUpcomingTime(slot.scheduledAtMs)}</span
525
+ >
526
+ <span class="cron-calendar-compact-name truncate"
527
+ >${slot.jobName}</span
528
+ >
529
+ </span>
530
+ <span class="cron-calendar-compact-estimate"
531
+ >${estimateLabel}</span
532
+ >
533
+ </button>
534
+ </${Tooltip}>
535
+ `;
536
+ })}
537
+ </div>
538
+ `}
539
+ <div class="flex items-center justify-between mt-2">
540
+ ${
541
+ noisyUpcomingItems.length > 0
542
+ ? html`
543
+ <button
544
+ type="button"
545
+ class="ac-btn-ghost text-xs px-2.5 py-1 rounded-lg"
546
+ onClick=${() => setShowNoisyUpcoming((value) => !value)}
547
+ >
548
+ ${
549
+ showNoisyUpcoming
550
+ ? "Show fewer"
551
+ : `+${noisyUpcomingItems.length} noisy runs`
552
+ }
553
+ </button>
554
+ `
555
+ : html`<span></span>`
556
+ }
557
+ <${renderLegend} />
558
+ </div>
559
+ </div>
550
560
  `;
551
561
  };
552
562
 
553
563
  const renderFullGrid = () => html`
554
564
  <div class="space-y-3">
555
- <div class="flex justify-end">
556
- <${renderLegend} />
557
- </div>
558
-
559
565
  ${hourRows.length === 0
560
566
  ? html`<div class="text-sm text-gray-500">
561
567
  No scheduled jobs in this rolling window.
@@ -563,7 +569,17 @@ export const CronCalendar = ({
563
569
  : html`
564
570
  <div class="cron-calendar-grid-wrap">
565
571
  <div class="cron-calendar-grid-header">
566
- <div class="cron-calendar-hour-cell"></div>
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>
567
583
  ${timeline.days.map(
568
584
  (day) => html`
569
585
  <div
@@ -725,15 +741,41 @@ export const CronCalendar = ({
725
741
 
726
742
  return html`
727
743
  <section class="bg-surface border border-border rounded-xl p-4 space-y-3">
728
- <div class="flex items-center gap-2">
729
- <${SegmentedControl}
730
- options=${kCalendarViewOptions}
731
- value=${calendarView}
732
- onChange=${onChangeCalendarView}
733
- />
744
+ <div class="flex items-center justify-between gap-2">
745
+ <h3 class="card-label card-label-bright">Up next</h3>
746
+ <button
747
+ type="button"
748
+ class="ac-btn-secondary text-xs px-3 py-1.5 rounded-lg"
749
+ onClick=${() => setCalendarLightboxOpen(true)}
750
+ >
751
+ Open calendar
752
+ </button>
734
753
  </div>
735
754
 
736
- ${isCalendarView ? renderFullGrid() : renderCompactStrip()}
755
+ ${renderCompactStrip()}
737
756
  </section>
757
+ <${ModalShell}
758
+ visible=${calendarLightboxOpen}
759
+ onClose=${() => setCalendarLightboxOpen(false)}
760
+ panelClassName="cron-calendar-lightbox-panel"
761
+ >
762
+ <div class="flex items-center justify-between gap-2">
763
+ <h3 class="card-label cron-calendar-title">Calendar</h3>
764
+ <button
765
+ type="button"
766
+ class="cron-calendar-lightbox-close"
767
+ onClick=${() => setCalendarLightboxOpen(false)}
768
+ aria-label="Close expanded calendar"
769
+ >
770
+ <${CloseIcon} className="w-4 h-4" />
771
+ </button>
772
+ </div>
773
+ <div class="flex items-center justify-center">
774
+ <${renderLegend} />
775
+ </div>
776
+ <div class="cron-calendar-lightbox-body">
777
+ ${renderFullGrid()}
778
+ </div>
779
+ </${ModalShell}>
738
780
  `;
739
781
  };
@@ -428,3 +428,14 @@ export const ErrorWarningLineIcon = ({ className = "" }) => html`
428
428
  />
429
429
  </svg>
430
430
  `;
431
+
432
+ export const FullscreenLineIcon = ({ className = "" }) => html`
433
+ <svg
434
+ class=${className}
435
+ viewBox="0 0 24 24"
436
+ fill="currentColor"
437
+ aria-hidden="true"
438
+ >
439
+ <path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z" />
440
+ </svg>
441
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.7.0-beta.0",
3
+ "version": "0.7.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },