@chrysb/alphaclaw 0.6.2-beta.1 → 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 {
@@ -7,7 +7,6 @@ import { formatCost, formatTokenCount } from "./cron-helpers.js";
7
7
  import { formatCronScheduleLabel } from "./cron-helpers.js";
8
8
  import { readUiSettings, updateUiSettings } from "../../lib/ui-settings.js";
9
9
  import {
10
- buildTokenTierByJobId,
11
10
  classifyRepeatingJobs,
12
11
  expandJobsToRollingSlots,
13
12
  getUpcomingSlots,
@@ -68,47 +67,195 @@ const renderLegend = () => html`
68
67
 
69
68
  const kNowRefreshMs = 60 * 1000;
70
69
  const kCalendarExpandedUiSettingKey = "cronCalendarExpanded";
70
+ const kRunWindow7dMs = 7 * 24 * 60 * 60 * 1000;
71
+ const kSlotRunToleranceMs = 45 * 60 * 1000;
72
+ const kUnknownTier = "unknown";
71
73
 
72
74
  const formatUpcomingTime = (timestampMs) => {
73
75
  const dateValue = new Date(timestampMs);
74
76
  return dateValue.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
75
77
  };
76
78
 
79
+ const getRunTotalTokens = (entry = {}) => {
80
+ const usage = entry?.usage || {};
81
+ const candidates = [
82
+ usage?.total_tokens,
83
+ usage?.totalTokens,
84
+ entry?.total_tokens,
85
+ entry?.totalTokens,
86
+ ];
87
+ for (const candidate of candidates) {
88
+ const numericValue = Number(candidate);
89
+ if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
90
+ }
91
+ return 0;
92
+ };
93
+
94
+ const getRunEstimatedCost = (entry = {}) => {
95
+ const usage = entry?.usage || {};
96
+ const candidates = [
97
+ entry?.estimatedCost,
98
+ entry?.estimated_cost,
99
+ usage?.estimatedCost,
100
+ usage?.estimated_cost,
101
+ usage?.totalCost,
102
+ usage?.total_cost,
103
+ usage?.costUsd,
104
+ usage?.cost,
105
+ ];
106
+ for (const candidate of candidates) {
107
+ const numericValue = Number(candidate);
108
+ if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
109
+ }
110
+ return null;
111
+ };
112
+
113
+ const buildRunSummaryByJobId = ({ runsByJobId = {}, nowMs = Date.now() } = {}) => {
114
+ const cutoffMs = Number(nowMs || Date.now()) - kRunWindow7dMs;
115
+ return Object.entries(runsByJobId || {}).reduce((accumulator, [jobId, runResult]) => {
116
+ const entries = Array.isArray(runResult?.entries) ? runResult.entries : [];
117
+ const recentEntries = entries.filter((entry) => {
118
+ const timestampMs = Number(entry?.ts || 0);
119
+ return Number.isFinite(timestampMs) && timestampMs >= cutoffMs && timestampMs <= nowMs;
120
+ });
121
+ const runCount = recentEntries.length;
122
+ const totalTokens = recentEntries.reduce(
123
+ (sum, entry) => sum + Number(getRunTotalTokens(entry) || 0),
124
+ 0,
125
+ );
126
+ const totalCost = recentEntries.reduce((sum, entry) => {
127
+ const cost = getRunEstimatedCost(entry);
128
+ return sum + Number(cost == null ? 0 : cost);
129
+ }, 0);
130
+ accumulator[String(jobId || "")] = {
131
+ runCount,
132
+ totalTokens,
133
+ totalCost,
134
+ avgTokensPerRun: runCount > 0 ? Math.round(totalTokens / runCount) : 0,
135
+ avgCostPerRun: runCount > 0 ? totalCost / runCount : 0,
136
+ };
137
+ return accumulator;
138
+ }, {});
139
+ };
140
+
141
+ const mapRunsToSlots = ({ slots = [], runsByJobId = {}, nowMs = Date.now() } = {}) => {
142
+ const runsBySlotKey = {};
143
+ const consumedRunTimestampsByJobId = {};
144
+ const runEntriesByJobId = Object.entries(runsByJobId || {}).reduce(
145
+ (accumulator, [jobId, runResult]) => {
146
+ const entries = Array.isArray(runResult?.entries) ? runResult.entries : [];
147
+ const normalizedEntries = entries
148
+ .map((entry) => ({ ...entry, ts: Number(entry?.ts || 0) }))
149
+ .filter((entry) => Number.isFinite(entry.ts) && entry.ts > 0)
150
+ .sort((left, right) => left.ts - right.ts);
151
+ accumulator[String(jobId || "")] = normalizedEntries;
152
+ return accumulator;
153
+ },
154
+ {},
155
+ );
156
+ slots.forEach((slot) => {
157
+ if (Number(slot?.scheduledAtMs || 0) > nowMs) return;
158
+ const jobId = String(slot?.jobId || "");
159
+ const runEntries = runEntriesByJobId[jobId] || [];
160
+ if (runEntries.length === 0) return;
161
+ const consumedSet = consumedRunTimestampsByJobId[jobId] || new Set();
162
+ consumedRunTimestampsByJobId[jobId] = consumedSet;
163
+ let nearestEntry = null;
164
+ let nearestDeltaMs = Number.MAX_SAFE_INTEGER;
165
+ runEntries.forEach((entry) => {
166
+ if (consumedSet.has(entry.ts)) return;
167
+ const deltaMs = Math.abs(entry.ts - Number(slot?.scheduledAtMs || 0));
168
+ if (deltaMs > kSlotRunToleranceMs) return;
169
+ if (deltaMs < nearestDeltaMs) {
170
+ nearestDeltaMs = deltaMs;
171
+ nearestEntry = entry;
172
+ }
173
+ });
174
+ if (!nearestEntry) return;
175
+ consumedSet.add(nearestEntry.ts);
176
+ runsBySlotKey[String(slot?.key || "")] = nearestEntry;
177
+ });
178
+ return runsBySlotKey;
179
+ };
180
+
181
+ const buildTierThresholds = (values = []) => {
182
+ const sortedValues = values
183
+ .map((value) => Number(value || 0))
184
+ .filter((value) => Number.isFinite(value) && value > 0)
185
+ .sort((left, right) => left - right);
186
+ if (sortedValues.length === 0) return null;
187
+ const percentileAt = (indexRatio = 0) => {
188
+ const index = Math.min(
189
+ sortedValues.length - 1,
190
+ Math.floor((sortedValues.length - 1) * indexRatio),
191
+ );
192
+ return sortedValues[Math.max(0, index)];
193
+ };
194
+ return {
195
+ q1: percentileAt(0.25),
196
+ q2: percentileAt(0.5),
197
+ p90: percentileAt(0.9),
198
+ };
199
+ };
200
+
201
+ const classifyTokenTier = ({
202
+ enabled = true,
203
+ tokenValue = 0,
204
+ thresholds = null,
205
+ } = {}) => {
206
+ if (!enabled) return "disabled";
207
+ const safeValue = Number(tokenValue || 0);
208
+ if (!Number.isFinite(safeValue) || safeValue <= 0 || !thresholds) return kUnknownTier;
209
+ if (safeValue <= thresholds.q1) return "low";
210
+ if (safeValue <= thresholds.q2) return "medium";
211
+ if (safeValue <= thresholds.p90) return "high";
212
+ return "very-high";
213
+ };
214
+
77
215
 
78
216
  const buildJobTooltipText = ({
79
217
  jobName = "",
80
218
  job = null,
81
- usage = {},
219
+ runSummary7d = {},
220
+ slotRun = null,
82
221
  latestRun = null,
83
222
  scheduledAtMs = 0,
84
223
  scheduledStatus = "",
224
+ nowMs = Date.now(),
85
225
  } = {}) => {
86
- const runCount = Number(usage?.runCount || 0);
87
- const totalTokens = Number(usage?.totalTokens || 0);
88
- const totalCost = Number(usage?.totalCost || 0);
89
- const avgTokensPerRun = runCount > 0
90
- ? Number(usage?.avgTokensPerRun || Math.round(totalTokens / runCount))
91
- : 0;
92
- const avgCostPerRun = runCount > 0 ? totalCost / runCount : 0;
226
+ const isPastSlot = Number(scheduledAtMs || 0) > 0 && Number(scheduledAtMs || 0) <= nowMs;
227
+ const runCount7d = Number(runSummary7d?.runCount || 0);
228
+ const avgTokensPerRun7d = Number(runSummary7d?.avgTokensPerRun || 0);
229
+ const avgCostPerRun7d = Number(runSummary7d?.avgCostPerRun || 0);
230
+ const slotRunTokens = getRunTotalTokens(slotRun || {});
231
+ const slotRunCost = getRunEstimatedCost(slotRun || {});
232
+ const slotRunStatus = String(slotRun?.status || "").trim().toLowerCase();
93
233
 
94
- const lines = [
95
- String(jobName || "Job"),
96
- `Avg tokens/run: ${runCount > 0 ? formatTokenCount(avgTokensPerRun) : "—"}`,
97
- `Avg cost/run: ${runCount > 0 ? formatCost(avgCostPerRun) : "—"}`,
98
- `Total cost: ${formatCost(totalCost)}`,
99
- ];
100
-
101
- if (runCount <= 0) {
102
- lines.push("Runs: none yet");
234
+ const lines = [String(jobName || "Job")];
235
+ if (isPastSlot) {
236
+ lines.push(`Run tokens: ${slotRun ? formatTokenCount(slotRunTokens) : "—"}`);
237
+ lines.push(`Run cost: ${slotRunCost == null ? "—" : formatCost(slotRunCost)}`);
238
+ lines.push(`Run status: ${slotRunStatus || scheduledStatus || "unknown"}`);
239
+ if (slotRun?.ts) {
240
+ lines.push(
241
+ `Run time: ${new Date(Number(slotRun.ts || 0)).toLocaleString()}`,
242
+ );
243
+ }
103
244
  } else {
104
- lines.push(`Runs: ${formatTokenCount(runCount)}`);
245
+ lines.push(
246
+ `Avg tokens/run (last 7d): ${runCount7d > 0 ? formatTokenCount(avgTokensPerRun7d) : "—"}`,
247
+ );
248
+ lines.push(
249
+ `Avg cost/run (last 7d): ${runCount7d > 0 ? formatCost(avgCostPerRun7d) : "—"}`,
250
+ );
251
+ lines.push(`Runs (last 7d): ${runCount7d > 0 ? formatTokenCount(runCount7d) : "none"}`);
105
252
  }
106
253
 
107
- if (latestRun?.status) {
254
+ if (!isPastSlot && latestRun?.status) {
108
255
  lines.push(
109
256
  `Latest run: ${latestRun.status} (${new Date(Number(latestRun.ts || 0)).toLocaleString()})`,
110
257
  );
111
- } else {
258
+ } else if (!isPastSlot) {
112
259
  lines.push("Latest run: none");
113
260
  }
114
261
  if (Number(job?.state?.runningAtMs || 0) > 0) {
@@ -125,7 +272,6 @@ const buildJobTooltipText = ({
125
272
 
126
273
  export const CronCalendar = ({
127
274
  jobs = [],
128
- usageByJobId = {},
129
275
  runsByJobId = {},
130
276
  onSelectJob = () => {},
131
277
  }) => {
@@ -163,10 +309,6 @@ export const CronCalendar = ({
163
309
  () => mapRunStatusesToSlots({ slots: timeline.slots, bulkRunsByJobId: runsByJobId, nowMs }),
164
310
  [timeline.slots, runsByJobId, nowMs],
165
311
  );
166
- const tokenTierByJobId = useMemo(
167
- () => buildTokenTierByJobId({ jobs, usageByJobId }),
168
- [jobs, usageByJobId],
169
- );
170
312
  const jobById = useMemo(
171
313
  () =>
172
314
  jobs.reduce((accumulator, job) => {
@@ -188,6 +330,63 @@ export const CronCalendar = ({
188
330
  }, {}),
189
331
  [runsByJobId],
190
332
  );
333
+ const runSummary7dByJobId = useMemo(
334
+ () => buildRunSummaryByJobId({ runsByJobId, nowMs }),
335
+ [runsByJobId, nowMs],
336
+ );
337
+ const runBySlotKey = useMemo(
338
+ () => mapRunsToSlots({ slots: timeline.slots, runsByJobId, nowMs }),
339
+ [timeline.slots, runsByJobId, nowMs],
340
+ );
341
+ const slotTierThresholds = useMemo(() => {
342
+ const values = [];
343
+ timeline.slots.forEach((slot) => {
344
+ const job = jobById[slot.jobId] || null;
345
+ if (!job || job.enabled === false) return;
346
+ const isPastSlot = Number(slot?.scheduledAtMs || 0) <= nowMs;
347
+ if (isPastSlot) {
348
+ const slotRunTokens = getRunTotalTokens(runBySlotKey[slot.key] || {});
349
+ if (slotRunTokens > 0) values.push(slotRunTokens);
350
+ return;
351
+ }
352
+ const projectedAvgTokens = Number(runSummary7dByJobId[slot.jobId]?.avgTokensPerRun || 0);
353
+ if (projectedAvgTokens > 0) values.push(projectedAvgTokens);
354
+ });
355
+ repeatingJobs.forEach((job) => {
356
+ const jobId = String(job?.id || "");
357
+ const projectedAvgTokens = Number(runSummary7dByJobId[jobId]?.avgTokensPerRun || 0);
358
+ if (projectedAvgTokens > 0) values.push(projectedAvgTokens);
359
+ });
360
+ return buildTierThresholds(values);
361
+ }, [jobById, nowMs, repeatingJobs, runBySlotKey, runSummary7dByJobId, timeline.slots]);
362
+ const getSlotTokenTier = useCallback((slot = null) => {
363
+ const jobId = String(slot?.jobId || "");
364
+ const job = jobById[jobId] || null;
365
+ const enabled = job?.enabled !== false;
366
+ const isPastSlot = Number(slot?.scheduledAtMs || 0) <= nowMs;
367
+ if (isPastSlot) {
368
+ const slotRunTokens = getRunTotalTokens(runBySlotKey[String(slot?.key || "")] || {});
369
+ return classifyTokenTier({
370
+ enabled,
371
+ tokenValue: slotRunTokens,
372
+ thresholds: slotTierThresholds,
373
+ });
374
+ }
375
+ const projectedAvgTokens = Number(runSummary7dByJobId[jobId]?.avgTokensPerRun || 0);
376
+ return classifyTokenTier({
377
+ enabled,
378
+ tokenValue: projectedAvgTokens,
379
+ thresholds: slotTierThresholds,
380
+ });
381
+ }, [jobById, nowMs, runBySlotKey, runSummary7dByJobId, slotTierThresholds]);
382
+ const getJobProjectedTier = useCallback((jobId = "") => {
383
+ const job = jobById[jobId] || null;
384
+ return classifyTokenTier({
385
+ enabled: job?.enabled !== false,
386
+ tokenValue: Number(runSummary7dByJobId[jobId]?.avgTokensPerRun || 0),
387
+ thresholds: slotTierThresholds,
388
+ });
389
+ }, [jobById, runSummary7dByJobId, slotTierThresholds]);
191
390
 
192
391
  const upcomingSlots = useMemo(
193
392
  () => getUpcomingSlots({ slots: timeline.slots, nowMs }),
@@ -223,13 +422,14 @@ export const CronCalendar = ({
223
422
  : html`
224
423
  <div class="cron-calendar-compact-strip">
225
424
  ${upcomingSlots.map((slot) => {
226
- const usage = usageByJobId[slot.jobId] || {};
227
425
  const tooltipText = buildJobTooltipText({
228
426
  jobName: slot.jobName,
229
427
  job: jobById[slot.jobId] || null,
230
- usage,
428
+ runSummary7d: runSummary7dByJobId[slot.jobId] || {},
429
+ slotRun: runBySlotKey[slot.key] || null,
231
430
  latestRun: latestRunByJobId[slot.jobId],
232
431
  scheduledAtMs: slot.scheduledAtMs,
432
+ nowMs,
233
433
  });
234
434
  return html`
235
435
  <${Tooltip}
@@ -309,15 +509,16 @@ export const CronCalendar = ({
309
509
  ${visibleSlots.map((slot) => {
310
510
  const status = statusBySlotKey[slot.key] || "";
311
511
  const isPast = slot.scheduledAtMs <= nowMs;
312
- const tokenTier = tokenTierByJobId[slot.jobId] || "unknown";
313
- const usage = usageByJobId[slot.jobId] || {};
512
+ const tokenTier = getSlotTokenTier(slot);
314
513
  const tooltipText = buildJobTooltipText({
315
514
  jobName: slot.jobName,
316
515
  job: jobById[slot.jobId] || null,
317
- usage,
516
+ runSummary7d: runSummary7dByJobId[slot.jobId] || {},
517
+ slotRun: runBySlotKey[slot.key] || null,
318
518
  latestRun: latestRunByJobId[slot.jobId],
319
519
  scheduledAtMs: slot.scheduledAtMs,
320
520
  scheduledStatus: status,
521
+ nowMs,
321
522
  });
322
523
  return html`
323
524
  <${Tooltip}
@@ -366,13 +567,16 @@ export const CronCalendar = ({
366
567
  <div class="cron-calendar-repeating-list">
367
568
  ${repeatingJobs.map((job) => {
368
569
  const jobId = String(job?.id || "");
369
- const usage = usageByJobId[jobId] || {};
370
- const avgTokensPerRun = Number(usage?.avgTokensPerRun || 0);
570
+ const avgTokensPerRun = Number(
571
+ runSummary7dByJobId[jobId]?.avgTokensPerRun || 0,
572
+ );
371
573
  const tooltipText = buildJobTooltipText({
372
574
  jobName: job.name || job.id,
373
575
  job,
374
- usage,
576
+ runSummary7d: runSummary7dByJobId[jobId] || {},
577
+ slotRun: null,
375
578
  latestRun: latestRunByJobId[jobId],
579
+ nowMs,
376
580
  });
377
581
  return html`
378
582
  <${Tooltip}
@@ -385,7 +589,7 @@ export const CronCalendar = ({
385
589
  class=${`cron-calendar-repeating-pill ${slotStateClassName({
386
590
  isPast: false,
387
591
  mappedStatus: "",
388
- tokenTier: tokenTierByJobId[jobId] || "unknown",
592
+ tokenTier: getJobProjectedTier(jobId),
389
593
  })}`}
390
594
  role="button"
391
595
  tabindex="0"