@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.
@@ -1,8 +1,7 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useEffect, 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 { SegmentedControl } from "../segmented-control.js";
5
- import { Tooltip } from "../tooltip.js";
6
5
  import { formatCost } from "./cron-helpers.js";
7
6
 
8
7
  const html = htm.bind(h);
@@ -146,6 +145,8 @@ export const CronRunsTrendCard = ({
146
145
  selectedBucketFilter = null,
147
146
  onBucketFilterChange = () => {},
148
147
  }) => {
148
+ const chartCanvasRef = useRef(null);
149
+ const chartInstanceRef = useRef(null);
149
150
  const [range, setRange] = useState(
150
151
  initialRange === kRange30d ? kRange30d : kRange7d,
151
152
  );
@@ -166,6 +167,156 @@ export const CronRunsTrendCard = ({
166
167
  );
167
168
  return matchingPoint?.key || "";
168
169
  }, [range, selectedBucketFilter, trend.points]);
170
+ const selectedPointIndex = useMemo(
171
+ () => trend.points.findIndex((point) => point.key === selectedBucketKey),
172
+ [selectedBucketKey, trend.points],
173
+ );
174
+
175
+ const chartData = useMemo(() => {
176
+ const dimAlpha = "0.22";
177
+ const fullAlpha = "0.86";
178
+ const isDimmed = (index) => selectedPointIndex >= 0 && selectedPointIndex !== index;
179
+ const labels = trend.points.map((point) => (point.showLabel ? point.label : ""));
180
+ return {
181
+ labels,
182
+ datasets: [
183
+ {
184
+ label: "ok",
185
+ data: trend.points.map((point) => Number(point.ok || 0)),
186
+ stack: "outcomes",
187
+ backgroundColor: trend.points.map((_, index) =>
188
+ `rgba(34,255,170,${isDimmed(index) ? dimAlpha : fullAlpha})`),
189
+ borderColor: trend.points.map((_, index) =>
190
+ `rgba(34,255,170,${isDimmed(index) ? "0.35" : "1"})`),
191
+ borderWidth: 1,
192
+ borderRadius: 0,
193
+ borderSkipped: false,
194
+ },
195
+ {
196
+ label: "error",
197
+ data: trend.points.map((point) => Number(point.error || 0)),
198
+ stack: "outcomes",
199
+ backgroundColor: trend.points.map((_, index) =>
200
+ `rgba(255,74,138,${isDimmed(index) ? dimAlpha : fullAlpha})`),
201
+ borderColor: trend.points.map((_, index) =>
202
+ `rgba(255,74,138,${isDimmed(index) ? "0.35" : "1"})`),
203
+ borderWidth: 1,
204
+ borderRadius: 0,
205
+ borderSkipped: false,
206
+ },
207
+ {
208
+ label: "skipped",
209
+ data: trend.points.map((point) => Number(point.skipped || 0)),
210
+ stack: "outcomes",
211
+ backgroundColor: trend.points.map((_, index) =>
212
+ `rgba(255,214,64,${isDimmed(index) ? dimAlpha : fullAlpha})`),
213
+ borderColor: trend.points.map((_, index) =>
214
+ `rgba(255,214,64,${isDimmed(index) ? "0.35" : "1"})`),
215
+ borderWidth: 1,
216
+ borderRadius: 0,
217
+ borderSkipped: false,
218
+ },
219
+ ],
220
+ };
221
+ }, [selectedPointIndex, trend.points]);
222
+
223
+ useEffect(() => {
224
+ const canvas = chartCanvasRef.current;
225
+ const Chart = window.Chart;
226
+ if (!canvas || !Chart) return;
227
+ if (chartInstanceRef.current) {
228
+ chartInstanceRef.current.destroy();
229
+ chartInstanceRef.current = null;
230
+ }
231
+ const getBucketFilter = (index) => {
232
+ const selectedPoint = trend.points[index];
233
+ if (!selectedPoint) return null;
234
+ return {
235
+ key: selectedPoint.key,
236
+ label: selectedPoint.label,
237
+ range,
238
+ startMs: Number(selectedPoint.startMs || 0),
239
+ endMs: Number(selectedPoint.endMs || 0),
240
+ };
241
+ };
242
+ chartInstanceRef.current = new Chart(canvas, {
243
+ type: "bar",
244
+ data: chartData,
245
+ options: {
246
+ responsive: true,
247
+ maintainAspectRatio: false,
248
+ interaction: { mode: "index", intersect: false },
249
+ animation: false,
250
+ onHover: (event, elements) => {
251
+ const target = event?.native?.target;
252
+ if (!target || !target.style) return;
253
+ target.style.cursor = Array.isArray(elements) && elements.length > 0
254
+ ? "pointer"
255
+ : "default";
256
+ },
257
+ onClick: (_event, elements) => {
258
+ const index = Number(elements?.[0]?.index);
259
+ if (!Number.isFinite(index)) return;
260
+ const nextFilter = getBucketFilter(index);
261
+ if (!nextFilter) return;
262
+ if (nextFilter.key === selectedBucketKey) {
263
+ onBucketFilterChange(null);
264
+ return;
265
+ }
266
+ onBucketFilterChange(nextFilter);
267
+ },
268
+ scales: {
269
+ x: {
270
+ stacked: true,
271
+ grid: { color: "rgba(148,163,184,0.08)" },
272
+ ticks: {
273
+ color: "rgba(156,163,175,1)",
274
+ maxRotation: 0,
275
+ autoSkip: false,
276
+ },
277
+ },
278
+ y: {
279
+ stacked: true,
280
+ beginAtZero: true,
281
+ grid: { color: "rgba(148,163,184,0.12)" },
282
+ ticks: {
283
+ precision: 0,
284
+ color: "rgba(156,163,175,1)",
285
+ },
286
+ },
287
+ },
288
+ plugins: {
289
+ legend: {
290
+ labels: {
291
+ color: "rgba(209,213,219,1)",
292
+ boxWidth: 10,
293
+ boxHeight: 10,
294
+ },
295
+ },
296
+ tooltip: {
297
+ callbacks: {
298
+ title: (items) => String(items?.[0]?.label || ""),
299
+ label: (context) => `${context.dataset.label}: ${Number(context.parsed.y || 0)}`,
300
+ footer: (items) => {
301
+ const index = Number(items?.[0]?.dataIndex);
302
+ const point = trend.points[index];
303
+ if (!point) return "";
304
+ const costLabel =
305
+ point.costCount > 0 ? `~${formatCost(point.totalCost)}` : "—";
306
+ return `total: ${point.total}\ncost: ${costLabel}`;
307
+ },
308
+ },
309
+ },
310
+ },
311
+ },
312
+ });
313
+ return () => {
314
+ if (chartInstanceRef.current) {
315
+ chartInstanceRef.current.destroy();
316
+ chartInstanceRef.current = null;
317
+ }
318
+ };
319
+ }, [chartData, onBucketFilterChange, range, selectedBucketKey, trend.points]);
169
320
 
170
321
  return html`
171
322
  <section class="bg-surface border border-border rounded-xl p-4 space-y-3">
@@ -177,100 +328,8 @@ export const CronRunsTrendCard = ({
177
328
  onChange=${setRange}
178
329
  />
179
330
  </div>
180
- <div
181
- class="cron-runs-trend-bars"
182
- style=${{ "--cron-runs-trend-columns": String(trend.points.length || 1) }}
183
- >
184
- ${trend.points.map((point) => {
185
- const isSelected = selectedBucketKey === point.key;
186
- const isDimmed = !!selectedBucketKey && !isSelected;
187
- const totalHeightPercent = (point.total / trend.maxTotal) * 100;
188
- const okHeightPercent = point.total > 0 ? (point.ok / point.total) * 100 : 0;
189
- const errorHeightPercent = point.total > 0 ? (point.error / point.total) * 100 : 0;
190
- const skippedHeightPercent = point.total > 0 ? (point.skipped / point.total) * 100 : 0;
191
- const tooltipText = [
192
- `${point.label}`,
193
- `ok: ${point.ok}`,
194
- `error: ${point.error}`,
195
- `skipped: ${point.skipped}`,
196
- `total: ${point.total}`,
197
- `cost: ${point.costCount > 0 ? `~${formatCost(point.totalCost)}` : "—"}`,
198
- ].join("\n");
199
- return html`
200
- <${Tooltip}
201
- text=${tooltipText}
202
- widthClass="w-40"
203
- tooltipClassName="whitespace-pre-line"
204
- triggerClassName="inline-flex justify-center w-full"
205
- >
206
- <div
207
- class=${`cron-runs-trend-col ${isSelected ? "is-selected" : ""} ${isDimmed ? "is-dimmed" : ""}`}
208
- role="button"
209
- tabindex="0"
210
- onClick=${() => {
211
- if (isSelected) {
212
- onBucketFilterChange(null);
213
- return;
214
- }
215
- onBucketFilterChange({
216
- key: point.key,
217
- label: point.label,
218
- range,
219
- startMs: Number(point.startMs || 0),
220
- endMs: Number(point.endMs || 0),
221
- });
222
- }}
223
- onKeyDown=${(event) => {
224
- if (event.key !== "Enter" && event.key !== " ") return;
225
- event.preventDefault();
226
- if (isSelected) {
227
- onBucketFilterChange(null);
228
- return;
229
- }
230
- onBucketFilterChange({
231
- key: point.key,
232
- label: point.label,
233
- range,
234
- startMs: Number(point.startMs || 0),
235
- endMs: Number(point.endMs || 0),
236
- });
237
- }}
238
- >
239
- <div class="cron-runs-trend-track">
240
- <div class="cron-runs-trend-bar" style=${{ height: `${totalHeightPercent}%` }}>
241
- <div
242
- class="cron-runs-trend-segment-skipped"
243
- style=${{ height: `${skippedHeightPercent}%` }}
244
- ></div>
245
- <div
246
- class="cron-runs-trend-segment-error"
247
- style=${{ height: `${errorHeightPercent}%` }}
248
- ></div>
249
- <div
250
- class="cron-runs-trend-segment-ok"
251
- style=${{ height: `${okHeightPercent}%` }}
252
- ></div>
253
- </div>
254
- </div>
255
- <span class="cron-runs-trend-label">${point.showLabel ? point.label : ""}</span>
256
- </div>
257
- </${Tooltip}>
258
- `;
259
- })}
260
- </div>
261
- <div class="cron-runs-trend-legend">
262
- <span class="cron-runs-trend-legend-item">
263
- <span class="cron-runs-trend-legend-dot is-ok"></span>
264
- ok
265
- </span>
266
- <span class="cron-runs-trend-legend-item">
267
- <span class="cron-runs-trend-legend-dot is-error"></span>
268
- error
269
- </span>
270
- <span class="cron-runs-trend-legend-item">
271
- <span class="cron-runs-trend-legend-dot is-skipped"></span>
272
- skipped
273
- </span>
331
+ <div class="h-40">
332
+ <canvas ref=${chartCanvasRef}></canvas>
274
333
  </div>
275
334
  </section>
276
335
  `;
@@ -75,9 +75,7 @@ export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
75
75
  runHasMore=${state.runHasMore}
76
76
  loadingMoreRuns=${state.loadingMoreRuns}
77
77
  runStatusFilter=${state.runStatusFilter}
78
- runDeliveryFilter=${state.runDeliveryFilter}
79
78
  onSetRunStatusFilter=${actions.setRunStatusFilter}
80
- onSetRunDeliveryFilter=${actions.setRunDeliveryFilter}
81
79
  onLoadMoreRuns=${actions.loadMoreRuns}
82
80
  onRunNow=${actions.runSelectedJobNow}
83
81
  runningJob=${state.runningJob}
@@ -89,8 +87,15 @@ export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
89
87
  promptValue=${state.promptValue}
90
88
  savedPromptValue=${state.savedPromptValue}
91
89
  onChangePrompt=${actions.setPromptValue}
92
- onSavePrompt=${actions.savePrompt}
93
- savingPrompt=${state.savingPrompt}
90
+ onSaveChanges=${actions.saveChanges}
91
+ savingChanges=${state.savingChanges}
92
+ routingDraft=${state.routingDraft}
93
+ onChangeRoutingDraft=${actions.setRoutingDraft}
94
+ deliverySessions=${state.deliverySessions}
95
+ loadingDeliverySessions=${state.loadingDeliverySessions}
96
+ deliverySessionsError=${state.deliverySessionsError}
97
+ destinationSessionKey=${state.destinationSessionKey}
98
+ onChangeDestinationSessionKey=${actions.setDestinationSessionKey}
94
99
  />
95
100
  `}
96
101
  </main>
@@ -6,6 +6,7 @@ import {
6
6
  useState,
7
7
  } from "https://esm.sh/preact/hooks";
8
8
  import { usePolling } from "../../hooks/usePolling.js";
9
+ import { useDestinationSessionSelection } from "../../hooks/use-destination-session-selection.js";
9
10
  import {
10
11
  fetchCronBulkRuns,
11
12
  fetchCronBulkUsage,
@@ -16,6 +17,7 @@ import {
16
17
  setCronJobEnabled,
17
18
  triggerCronJobRun,
18
19
  updateCronJobPrompt,
20
+ updateCronJobRouting,
19
21
  } from "../../lib/api.js";
20
22
  import { readUiSettings, writeUiSettings } from "../../lib/ui-settings.js";
21
23
  import { showToast } from "../toast.js";
@@ -28,6 +30,20 @@ const kListPanelWidthUiSettingKey = "cronListPanelWidthPx";
28
30
  const kRunsPageSize = 25;
29
31
  const kCalendarUsageDays = 30;
30
32
  const kCalendarPastDays = 30;
33
+ const kRoutingDefaults = {
34
+ sessionTarget: "main",
35
+ wakeMode: "now",
36
+ deliveryMode: "none",
37
+ deliveryChannel: "",
38
+ deliveryTo: "",
39
+ };
40
+ const readRoutingDraftFromJob = (job = null) => ({
41
+ sessionTarget: String(job?.sessionTarget || kRoutingDefaults.sessionTarget),
42
+ wakeMode: String(job?.wakeMode || kRoutingDefaults.wakeMode),
43
+ deliveryMode: String(job?.delivery?.mode || kRoutingDefaults.deliveryMode),
44
+ deliveryChannel: String(job?.delivery?.channel || ""),
45
+ deliveryTo: String(job?.delivery?.to || ""),
46
+ });
31
47
 
32
48
  const clampListPanelWidth = (value) =>
33
49
  Math.max(kListPanelMinWidthPx, Math.min(kListPanelMaxWidthPx, value));
@@ -38,6 +54,9 @@ const normalizeRouteJobId = (jobId = "") => {
38
54
  };
39
55
 
40
56
  export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
57
+ const selectedRouteKey = normalizeRouteJobId(jobId);
58
+ const selectedJobId =
59
+ selectedRouteKey === kAllCronJobsRouteKey ? "" : selectedRouteKey;
41
60
  const listPanelRef = useRef(null);
42
61
  const [listPanelWidthPx, setListPanelWidthPx] = useState(() => {
43
62
  const settings = readUiSettings();
@@ -48,7 +67,6 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
48
67
  });
49
68
  const [isResizingListPanel, setIsResizingListPanel] = useState(false);
50
69
  const [runStatusFilter, setRunStatusFilter] = useState("all");
51
- const [runDeliveryFilter, setRunDeliveryFilter] = useState("all");
52
70
  const [runEntries, setRunEntries] = useState([]);
53
71
  const [runHasMore, setRunHasMore] = useState(false);
54
72
  const [runNextOffset, setRunNextOffset] = useState(0);
@@ -56,14 +74,22 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
56
74
  const [loadingMoreRuns, setLoadingMoreRuns] = useState(false);
57
75
  const [promptValue, setPromptValue] = useState("");
58
76
  const [savedPromptValue, setSavedPromptValue] = useState("");
59
- const [savingPrompt, setSavingPrompt] = useState(false);
77
+ const [savingChanges, setSavingChanges] = useState(false);
60
78
  const [runningJob, setRunningJob] = useState(false);
61
79
  const [togglingJobEnabled, setTogglingJobEnabled] = useState(false);
80
+ const [routingDraft, setRoutingDraft] = useState(kRoutingDefaults);
62
81
  const [usageDays, setUsageDays] = useState(30);
63
-
64
- const selectedRouteKey = normalizeRouteJobId(jobId);
65
- const selectedJobId =
66
- selectedRouteKey === kAllCronJobsRouteKey ? "" : selectedRouteKey;
82
+ const {
83
+ sessions: deliverySessions,
84
+ loading: loadingDeliverySessions,
85
+ error: deliverySessionsError,
86
+ destinationSessionKey,
87
+ setDestinationSessionKey,
88
+ selectedDestination,
89
+ } = useDestinationSessionSelection({
90
+ enabled: !!selectedJobId,
91
+ resetKey: String(selectedJobId || ""),
92
+ });
67
93
 
68
94
  const jobsPoll = usePolling(
69
95
  () => fetchCronJobs({ sortBy: "nextRunAtMs", sortDir: "asc" }),
@@ -82,7 +108,6 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
82
108
  limit: kRunsPageSize,
83
109
  offset: 0,
84
110
  status: runStatusFilter,
85
- deliveryStatus: runDeliveryFilter,
86
111
  sortDir: "desc",
87
112
  });
88
113
  },
@@ -144,13 +169,25 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
144
169
  if (!selectedJobId) {
145
170
  setPromptValue("");
146
171
  setSavedPromptValue("");
172
+ setRoutingDraft(kRoutingDefaults);
147
173
  return;
148
174
  }
149
175
  const prompt = String(selectedJob?.payload?.message || "");
150
176
  setPromptValue(prompt);
151
177
  setSavedPromptValue(prompt);
178
+ setRoutingDraft(readRoutingDraftFromJob(selectedJob));
152
179
  }, [selectedJobId, selectedJob?.payload?.message]);
153
180
 
181
+ useEffect(() => {
182
+ if (!selectedJobId) return;
183
+ setRoutingDraft(readRoutingDraftFromJob(selectedJob));
184
+ }, [
185
+ selectedJobId,
186
+ selectedJob?.sessionTarget,
187
+ selectedJob?.wakeMode,
188
+ selectedJob?.delivery?.mode,
189
+ ]);
190
+
154
191
  useEffect(() => {
155
192
  setRunEntries([]);
156
193
  setRunHasMore(false);
@@ -158,7 +195,7 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
158
195
  setRunTotal(0);
159
196
  if (!selectedJobId) return;
160
197
  runsPoll.refresh();
161
- }, [selectedJobId, runStatusFilter, runDeliveryFilter]);
198
+ }, [selectedJobId, runStatusFilter]);
162
199
 
163
200
  useEffect(() => {
164
201
  if (!selectedJobId) return;
@@ -273,7 +310,6 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
273
310
  limit: kRunsPageSize,
274
311
  offset: runNextOffset,
275
312
  status: runStatusFilter,
276
- deliveryStatus: runDeliveryFilter,
277
313
  sortDir: "desc",
278
314
  });
279
315
  const nextEntries = Array.isArray(data?.runs?.entries)
@@ -290,28 +326,81 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
290
326
  }
291
327
  }, [
292
328
  loadingMoreRuns,
293
- runDeliveryFilter,
294
329
  runHasMore,
295
330
  runNextOffset,
296
331
  runStatusFilter,
297
332
  selectedJobId,
298
333
  ]);
299
334
 
300
- const savePrompt = useCallback(async () => {
301
- if (!selectedJobId || savingPrompt) return;
302
- if (promptValue === savedPromptValue) return;
303
- setSavingPrompt(true);
335
+ const saveChanges = useCallback(async () => {
336
+ if (!selectedJobId || !selectedJob || savingChanges) return;
337
+ const currentRouting = readRoutingDraftFromJob(selectedJob);
338
+ const nextRouting = {
339
+ sessionTarget: String(routingDraft?.sessionTarget || kRoutingDefaults.sessionTarget),
340
+ wakeMode: String(routingDraft?.wakeMode || kRoutingDefaults.wakeMode),
341
+ deliveryMode: String(routingDraft?.deliveryMode || kRoutingDefaults.deliveryMode),
342
+ deliveryChannel: String(routingDraft?.deliveryChannel || ""),
343
+ deliveryTo: String(routingDraft?.deliveryTo || ""),
344
+ };
345
+ const routingUnchanged =
346
+ nextRouting.sessionTarget === currentRouting.sessionTarget &&
347
+ nextRouting.wakeMode === currentRouting.wakeMode &&
348
+ nextRouting.deliveryMode === currentRouting.deliveryMode &&
349
+ nextRouting.deliveryChannel === currentRouting.deliveryChannel &&
350
+ nextRouting.deliveryTo === currentRouting.deliveryTo;
351
+ const promptUnchanged = promptValue === savedPromptValue;
352
+ if (routingUnchanged && promptUnchanged) return;
353
+ setSavingChanges(true);
304
354
  try {
305
- await updateCronJobPrompt(selectedJobId, promptValue);
306
- setSavedPromptValue(promptValue);
307
- showToast("Cron prompt updated", "success");
355
+ if (!routingUnchanged) {
356
+ await updateCronJobRouting(selectedJobId, nextRouting);
357
+ }
358
+ if (!promptUnchanged) {
359
+ await updateCronJobPrompt(selectedJobId, promptValue);
360
+ setSavedPromptValue(promptValue);
361
+ }
362
+ showToast("Changes saved", "success");
308
363
  refreshAll();
309
364
  } catch (error) {
310
- showToast(error.message || "Could not update prompt", "error");
365
+ showToast(error.message || "Could not save changes", "error");
311
366
  } finally {
312
- setSavingPrompt(false);
367
+ setSavingChanges(false);
313
368
  }
314
- }, [promptValue, refreshAll, savedPromptValue, savingPrompt, selectedJobId]);
369
+ }, [
370
+ promptValue,
371
+ refreshAll,
372
+ routingDraft,
373
+ savedPromptValue,
374
+ savingChanges,
375
+ selectedJob,
376
+ selectedJobId,
377
+ ]);
378
+
379
+ useEffect(() => {
380
+ if (!selectedJobId) return;
381
+ if (String(routingDraft?.deliveryMode || "none") !== "announce") return;
382
+ if (!selectedDestination?.channel && !selectedDestination?.to) return;
383
+ setRoutingDraft((currentValue = kRoutingDefaults) => {
384
+ const nextChannel = String(selectedDestination?.channel || currentValue.deliveryChannel || "");
385
+ const nextTo = String(selectedDestination?.to || currentValue.deliveryTo || "");
386
+ if (
387
+ nextChannel === String(currentValue.deliveryChannel || "") &&
388
+ nextTo === String(currentValue.deliveryTo || "")
389
+ ) {
390
+ return currentValue;
391
+ }
392
+ return {
393
+ ...currentValue,
394
+ deliveryChannel: nextChannel,
395
+ deliveryTo: nextTo,
396
+ };
397
+ });
398
+ }, [
399
+ routingDraft?.deliveryMode,
400
+ selectedDestination?.channel,
401
+ selectedDestination?.to,
402
+ selectedJobId,
403
+ ]);
315
404
 
316
405
  return {
317
406
  refs: {
@@ -332,7 +421,6 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
332
421
  runNextOffset,
333
422
  runTotal,
334
423
  runStatusFilter,
335
- runDeliveryFilter,
336
424
  runsError: runsPoll.error,
337
425
  loadingMoreRuns,
338
426
  usage: usagePoll.data?.usage || null,
@@ -344,16 +432,20 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
344
432
  bulkRunsError: bulkRunsPoll.error,
345
433
  promptValue,
346
434
  savedPromptValue,
347
- savingPrompt,
435
+ savingChanges,
348
436
  runningJob,
349
437
  togglingJobEnabled,
438
+ routingDraft,
439
+ deliverySessions,
440
+ loadingDeliverySessions,
441
+ deliverySessionsError,
442
+ destinationSessionKey,
350
443
  },
351
444
  actions: {
352
445
  setRunStatusFilter,
353
- setRunDeliveryFilter,
354
446
  setUsageDays,
355
447
  setPromptValue,
356
- savePrompt,
448
+ saveChanges,
357
449
  refreshAll,
358
450
  loadMoreRuns,
359
451
  runSelectedJobNow,
@@ -361,6 +453,8 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
361
453
  selectAllJobs,
362
454
  selectJob,
363
455
  onListResizerPointerDown,
456
+ setRoutingDraft,
457
+ setDestinationSessionKey,
364
458
  },
365
459
  };
366
460
  };
@@ -544,6 +544,31 @@ export async function updateCronJobPrompt(id, message) {
544
544
  return parseJsonOrThrow(res, "Could not update cron prompt");
545
545
  }
546
546
 
547
+ export async function updateCronJobRouting(
548
+ id,
549
+ {
550
+ sessionTarget = "",
551
+ wakeMode = "",
552
+ deliveryMode = "",
553
+ deliveryChannel = "",
554
+ deliveryTo = "",
555
+ } = {},
556
+ ) {
557
+ const safeId = encodeURIComponent(String(id || ""));
558
+ const res = await authFetch(`/api/cron/jobs/${safeId}/routing`, {
559
+ method: "PUT",
560
+ headers: { "Content-Type": "application/json" },
561
+ body: JSON.stringify({
562
+ sessionTarget: String(sessionTarget || ""),
563
+ wakeMode: String(wakeMode || ""),
564
+ deliveryMode: String(deliveryMode || ""),
565
+ deliveryChannel: String(deliveryChannel || ""),
566
+ deliveryTo: String(deliveryTo || ""),
567
+ }),
568
+ });
569
+ return parseJsonOrThrow(res, "Could not update cron routing");
570
+ }
571
+
547
572
  export async function fetchDevicePairings() {
548
573
  const res = await authFetch("/api/devices");
549
574
  return res.json();