@syncended/dsh-automations 0.5.1 → 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.
- package/README.md +7 -7
- package/dist/agent-executor.js +2 -2
- package/dist/agent-executor.js.map +1 -1
- package/dist/project-policy.js +4 -4
- package/dist/project-policy.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/architecture.md +4 -4
- package/docs/security.md +2 -2
- package/lib/client.js +906 -251
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -17,6 +17,7 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
const {
|
|
18
18
|
Button,
|
|
19
19
|
Menu,
|
|
20
|
+
Pill,
|
|
20
21
|
RiskConfirmation,
|
|
21
22
|
IconAgentPresetOutline16,
|
|
22
23
|
IconBrowseOutline16,
|
|
@@ -24,6 +25,7 @@ window.__ModuleLoader__.load({
|
|
|
24
25
|
IconChevronDownOutline14,
|
|
25
26
|
IconChevronLeftOutline14,
|
|
26
27
|
IconEditOutline16,
|
|
28
|
+
IconFolderClose16,
|
|
27
29
|
IconGlobeOutline14,
|
|
28
30
|
IconPlayOutline16,
|
|
29
31
|
IconPlusOutline16,
|
|
@@ -33,7 +35,7 @@ window.__ModuleLoader__.load({
|
|
|
33
35
|
IconTrashOutline16,
|
|
34
36
|
IconWarningOutline16,
|
|
35
37
|
} = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
36
|
-
const inject = ["slots"];
|
|
38
|
+
const inject = ["slots", "workspaces"];
|
|
37
39
|
|
|
38
40
|
const API_PREFIX = "/api/automations";
|
|
39
41
|
const POLL_MS = 5000;
|
|
@@ -43,6 +45,32 @@ window.__ModuleLoader__.load({
|
|
|
43
45
|
const MIN_TIMEOUT_MS = 1000;
|
|
44
46
|
const MAX_TIMEOUT_MS = 86400000;
|
|
45
47
|
const JOB_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
48
|
+
const SCHEDULE_MODES = [
|
|
49
|
+
{ id: "minutes", label: "Minutes" },
|
|
50
|
+
{ id: "hourly", label: "Hourly" },
|
|
51
|
+
{ id: "daily", label: "Daily" },
|
|
52
|
+
{ id: "weekdays", label: "Weekdays" },
|
|
53
|
+
{ id: "weekly", label: "Weekly" },
|
|
54
|
+
{ id: "custom", label: "Custom" },
|
|
55
|
+
];
|
|
56
|
+
const MINUTE_INTERVALS = [5, 10, 15, 20, 30];
|
|
57
|
+
const HOURLY_MINUTES = Array.from({ length: 60 }, (_, index) => index);
|
|
58
|
+
const WEEKDAYS = [
|
|
59
|
+
{ value: "1", label: "Monday" },
|
|
60
|
+
{ value: "2", label: "Tuesday" },
|
|
61
|
+
{ value: "3", label: "Wednesday" },
|
|
62
|
+
{ value: "4", label: "Thursday" },
|
|
63
|
+
{ value: "5", label: "Friday" },
|
|
64
|
+
{ value: "6", label: "Saturday" },
|
|
65
|
+
{ value: "0", label: "Sunday" },
|
|
66
|
+
];
|
|
67
|
+
const CRON_FIELD_GUIDE = [
|
|
68
|
+
{ label: "Minute", range: "0–59" },
|
|
69
|
+
{ label: "Hour", range: "0–23" },
|
|
70
|
+
{ label: "Day", range: "1–31" },
|
|
71
|
+
{ label: "Month", range: "1–12" },
|
|
72
|
+
{ label: "Weekday", range: "0–7" },
|
|
73
|
+
];
|
|
46
74
|
let formInstanceSerial = 0;
|
|
47
75
|
|
|
48
76
|
const BROWSER_TIMEZONE = (() => {
|
|
@@ -132,14 +160,20 @@ window.__ModuleLoader__.load({
|
|
|
132
160
|
{ id: "high", name: "High", description: "Use a deeper reasoning level." },
|
|
133
161
|
{ id: "max", name: "Max", description: "Use the strongest available reasoning level." },
|
|
134
162
|
];
|
|
163
|
+
const EMPTY_WORKSPACE_SNAPSHOT = Object.freeze({
|
|
164
|
+
items: Object.freeze([]),
|
|
165
|
+
state: "idle",
|
|
166
|
+
phase: "pending",
|
|
167
|
+
error: null,
|
|
168
|
+
});
|
|
135
169
|
const OVERLAP_OPTIONS = [
|
|
136
|
-
{ value: "skip", label: "
|
|
137
|
-
{ value: "queue", label: "
|
|
138
|
-
{ value: "allow", label: "
|
|
170
|
+
{ value: "skip", label: "Skip the new run", hint: "Skip the run if a previous run is still active." },
|
|
171
|
+
{ value: "queue", label: "Queue the next run", hint: "Defer the run until the previous run finishes." },
|
|
172
|
+
{ value: "allow", label: "Allow concurrent runs", hint: "Run concurrently with any active run." },
|
|
139
173
|
];
|
|
140
174
|
const MISFIRE_OPTIONS = [
|
|
141
|
-
{ value: "skip", label: "
|
|
142
|
-
{ value: "run-once", label: "
|
|
175
|
+
{ value: "skip", label: "Skip missed runs", hint: "Drop occurrences that were missed while the scheduler was down." },
|
|
176
|
+
{ value: "run-once", label: "Run once after downtime", hint: "Run once after downtime for the latest missed occurrence." },
|
|
143
177
|
];
|
|
144
178
|
|
|
145
179
|
function errMessage(error) {
|
|
@@ -388,6 +422,31 @@ window.__ModuleLoader__.load({
|
|
|
388
422
|
return Math.round(ms / 1000) + "s";
|
|
389
423
|
}
|
|
390
424
|
|
|
425
|
+
function overlapPolicySummary(value) {
|
|
426
|
+
if (value === "queue") return "Queue overlaps";
|
|
427
|
+
if (value === "allow") return "Allow concurrent runs";
|
|
428
|
+
return "Skip overlaps";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function inferFormErrorField(message) {
|
|
432
|
+
const text = String(message || "").toLowerCase();
|
|
433
|
+
if (text.includes("job id")) return "id";
|
|
434
|
+
if (text.includes("timeout")) return "timeout";
|
|
435
|
+
if (text.includes("overlap")) return "overlap";
|
|
436
|
+
if (text.includes("misfire")) return "misfire";
|
|
437
|
+
if (text.includes("cron")) return "cron";
|
|
438
|
+
if (text.includes("timezone") || text.includes("time zone")) return "timezone";
|
|
439
|
+
if (text.includes("workspace")) return "cwd";
|
|
440
|
+
if (text.includes("prompt")) return "prompt";
|
|
441
|
+
if (text.includes("reasoning effort")) return "effort";
|
|
442
|
+
if (text.includes("agent preset")) return "preset";
|
|
443
|
+
if (text.includes("permission")) return "permission";
|
|
444
|
+
if (text.includes("model")) return "model";
|
|
445
|
+
if (text.includes("provider")) return "provider";
|
|
446
|
+
if (text.includes("name")) return "name";
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
391
450
|
async function apiFetch(path, options = {}) {
|
|
392
451
|
const headers = Object.assign(
|
|
393
452
|
{ "content-type": "application/json", "x-dsh-automation-client": "1" },
|
|
@@ -493,6 +552,304 @@ window.__ModuleLoader__.load({
|
|
|
493
552
|
};
|
|
494
553
|
}
|
|
495
554
|
|
|
555
|
+
function cronFields(value) {
|
|
556
|
+
return String(value || "").trim().split(/\s+/).filter(Boolean);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function twoDigits(value) {
|
|
560
|
+
return String(value).padStart(2, "0");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function simpleSchedule(cronValue) {
|
|
564
|
+
const cron = cronFields(cronValue).join(" ");
|
|
565
|
+
let match = cron.match(/^\*\/(5|10|15|20|30) \* \* \* \*$/);
|
|
566
|
+
if (match) return { mode: "minutes", interval: Number(match[1]), minute: 0, hour: 9, weekday: "1" };
|
|
567
|
+
match = cron.match(/^(\d{1,2}) \* \* \* \*$/);
|
|
568
|
+
if (match && Number(match[1]) <= 59) {
|
|
569
|
+
return { mode: "hourly", minute: Number(match[1]), hour: 9, weekday: "1", interval: 15 };
|
|
570
|
+
}
|
|
571
|
+
match = cron.match(/^(\d{1,2}) (\d{1,2}) \* \* 1-5$/);
|
|
572
|
+
if (match && Number(match[1]) <= 59 && Number(match[2]) <= 23) {
|
|
573
|
+
return { mode: "weekdays", minute: Number(match[1]), hour: Number(match[2]), weekday: "1", interval: 15 };
|
|
574
|
+
}
|
|
575
|
+
match = cron.match(/^(\d{1,2}) (\d{1,2}) \* \* ([0-7])$/);
|
|
576
|
+
if (match && Number(match[1]) <= 59 && Number(match[2]) <= 23) {
|
|
577
|
+
return {
|
|
578
|
+
mode: "weekly",
|
|
579
|
+
minute: Number(match[1]),
|
|
580
|
+
hour: Number(match[2]),
|
|
581
|
+
weekday: match[3] === "7" ? "0" : match[3],
|
|
582
|
+
interval: 15,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
match = cron.match(/^(\d{1,2}) (\d{1,2}) \* \* \*$/);
|
|
586
|
+
if (match && Number(match[1]) <= 59 && Number(match[2]) <= 23) {
|
|
587
|
+
return { mode: "daily", minute: Number(match[1]), hour: Number(match[2]), weekday: "1", interval: 15 };
|
|
588
|
+
}
|
|
589
|
+
return { mode: "custom", minute: 0, hour: 9, weekday: "1", interval: 15 };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function scheduleControlValues(cronValue, fallback = {}) {
|
|
593
|
+
const fields = cronFields(cronValue);
|
|
594
|
+
const minute = /^\d{1,2}$/.test(fields[0] || "") && Number(fields[0]) <= 59
|
|
595
|
+
? Number(fields[0])
|
|
596
|
+
: Number.isInteger(fallback.minute) ? fallback.minute : 0;
|
|
597
|
+
const hour = /^\d{1,2}$/.test(fields[1] || "") && Number(fields[1]) <= 23
|
|
598
|
+
? Number(fields[1])
|
|
599
|
+
: Number.isInteger(fallback.hour) ? fallback.hour : 9;
|
|
600
|
+
const normalizedWeekday = fields[4] === "7" ? "0" : fields[4];
|
|
601
|
+
const weekday = WEEKDAYS.some((day) => day.value === normalizedWeekday)
|
|
602
|
+
? normalizedWeekday
|
|
603
|
+
: WEEKDAYS.some((day) => day.value === String(fallback.weekday)) ? String(fallback.weekday) : "1";
|
|
604
|
+
const intervalMatch = /^\*\/(\d+)$/.exec(fields[0] || "");
|
|
605
|
+
const parsedInterval = intervalMatch ? Number(intervalMatch[1]) : NaN;
|
|
606
|
+
const interval = MINUTE_INTERVALS.includes(parsedInterval)
|
|
607
|
+
? parsedInterval
|
|
608
|
+
: MINUTE_INTERVALS.includes(fallback.interval) ? fallback.interval : 15;
|
|
609
|
+
return { minute, hour, weekday, interval };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function cronForSimpleSchedule(mode, values = {}) {
|
|
613
|
+
const minute = Number.isInteger(values.minute) && values.minute >= 0 && values.minute <= 59 ? values.minute : 0;
|
|
614
|
+
const hour = Number.isInteger(values.hour) && values.hour >= 0 && values.hour <= 23 ? values.hour : 9;
|
|
615
|
+
const interval = MINUTE_INTERVALS.includes(values.interval) ? values.interval : 15;
|
|
616
|
+
const weekday = WEEKDAYS.some((day) => day.value === String(values.weekday)) ? String(values.weekday) : "1";
|
|
617
|
+
if (mode === "minutes") return "*/" + interval + " * * * *";
|
|
618
|
+
if (mode === "hourly") return minute + " * * * *";
|
|
619
|
+
if (mode === "daily") return minute + " " + hour + " * * *";
|
|
620
|
+
if (mode === "weekdays") return minute + " " + hour + " * * 1-5";
|
|
621
|
+
if (mode === "weekly") return minute + " " + hour + " * * " + weekday;
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function scheduleModeTransition(mode, cron, values, customCron) {
|
|
626
|
+
if (mode === "custom") {
|
|
627
|
+
const restored = customCron === null ? cron : customCron;
|
|
628
|
+
return { cron: restored, customCron: restored };
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
cron: cronForSimpleSchedule(mode, values) || cron,
|
|
632
|
+
customCron,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function describeSimpleSchedule(schedule, timezone) {
|
|
637
|
+
const time = twoDigits(schedule.hour) + ":" + twoDigits(schedule.minute);
|
|
638
|
+
const zone = timezone.trim() || "the selected time zone";
|
|
639
|
+
if (schedule.mode === "minutes") return "Every " + schedule.interval + " minutes · " + zone;
|
|
640
|
+
if (schedule.mode === "hourly") return "Every hour at :" + twoDigits(schedule.minute) + " · " + zone;
|
|
641
|
+
if (schedule.mode === "daily") return "Every day at " + time + " · " + zone;
|
|
642
|
+
if (schedule.mode === "weekdays") return "Monday–Friday at " + time + " · " + zone;
|
|
643
|
+
if (schedule.mode === "weekly") {
|
|
644
|
+
const day = WEEKDAYS.find((entry) => entry.value === schedule.weekday)?.label || "Monday";
|
|
645
|
+
return "Every " + day + " at " + time + " · " + zone;
|
|
646
|
+
}
|
|
647
|
+
return cronFields(schedule.cron).length === 5
|
|
648
|
+
? "Custom five-field schedule · " + zone
|
|
649
|
+
: "Cron needs exactly five fields · " + zone;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function FormSection({ title, description, Icon, children, className = "" }) {
|
|
653
|
+
return h(
|
|
654
|
+
"section",
|
|
655
|
+
{ className: "dsh-auto-form-section" + (className ? " " + className : "") },
|
|
656
|
+
h(
|
|
657
|
+
"header",
|
|
658
|
+
{ className: "dsh-auto-form-section-head" },
|
|
659
|
+
Icon ? h("span", { className: "dsh-auto-form-section-icon", "aria-hidden": "true" }, h(Icon)) : null,
|
|
660
|
+
h(
|
|
661
|
+
"span",
|
|
662
|
+
{ className: "dsh-auto-form-section-copy" },
|
|
663
|
+
h("h4", { className: "dsh-auto-form-section-title" }, title),
|
|
664
|
+
description ? h("p", { className: "dsh-auto-form-section-description" }, description) : null,
|
|
665
|
+
),
|
|
666
|
+
),
|
|
667
|
+
children,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function CronFieldGuide({ value }) {
|
|
672
|
+
const fields = cronFields(value);
|
|
673
|
+
return h(
|
|
674
|
+
React.Fragment,
|
|
675
|
+
null,
|
|
676
|
+
h(
|
|
677
|
+
"div",
|
|
678
|
+
{ className: "dsh-auto-cron-guide", "aria-label": "Cron field order" },
|
|
679
|
+
CRON_FIELD_GUIDE.map((field, index) => h(
|
|
680
|
+
"span",
|
|
681
|
+
{ key: field.label, className: "dsh-auto-cron-guide-field" },
|
|
682
|
+
h("code", { className: "dsh-auto-cron-guide-value" }, fields[index] || "—"),
|
|
683
|
+
h("span", { className: "dsh-auto-cron-guide-label" }, field.label),
|
|
684
|
+
h("span", { className: "dsh-auto-cron-guide-range" }, field.range),
|
|
685
|
+
)),
|
|
686
|
+
),
|
|
687
|
+
h("p", { className: "dsh-auto-cron-syntax" }, "* any value · , list · - range · / step"),
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function ScheduleEditor({ fieldId, cron, timezone, onCronChange, onTimezoneChange }) {
|
|
692
|
+
const detected = simpleSchedule(cron);
|
|
693
|
+
const [forceCustom, setForceCustom] = useState(false);
|
|
694
|
+
const [values, setValues] = useState(() => scheduleControlValues(cron));
|
|
695
|
+
const customCronRef = useRef(detected.mode === "custom" ? cron : null);
|
|
696
|
+
const customCronPinnedRef = useRef(detected.mode === "custom");
|
|
697
|
+
const activeMode = forceCustom || detected.mode === "custom" ? "custom" : detected.mode;
|
|
698
|
+
const applySimpleValues = (mode, nextValues) => {
|
|
699
|
+
setValues(nextValues);
|
|
700
|
+
const expression = cronForSimpleSchedule(mode, nextValues);
|
|
701
|
+
if (expression !== null) onCronChange(expression);
|
|
702
|
+
};
|
|
703
|
+
const setMode = (mode) => {
|
|
704
|
+
if (mode !== "custom" && !customCronPinnedRef.current) customCronRef.current = null;
|
|
705
|
+
const transition = scheduleModeTransition(mode, cron, values, customCronRef.current);
|
|
706
|
+
customCronRef.current = transition.customCron;
|
|
707
|
+
setForceCustom(mode === "custom");
|
|
708
|
+
if (transition.cron !== cron) onCronChange(transition.cron);
|
|
709
|
+
};
|
|
710
|
+
const setTime = (raw) => {
|
|
711
|
+
const match = /^(\d{2}):(\d{2})$/.exec(raw);
|
|
712
|
+
if (!match) return;
|
|
713
|
+
applySimpleValues(activeMode, Object.assign({}, values, {
|
|
714
|
+
hour: Number(match[1]),
|
|
715
|
+
minute: Number(match[2]),
|
|
716
|
+
}));
|
|
717
|
+
};
|
|
718
|
+
const summarySchedule = Object.assign({}, values, { mode: activeMode, cron });
|
|
719
|
+
const displayedCron = cronFields(cron).join(" ") || "—";
|
|
720
|
+
|
|
721
|
+
return h(
|
|
722
|
+
React.Fragment,
|
|
723
|
+
null,
|
|
724
|
+
h(
|
|
725
|
+
"div",
|
|
726
|
+
{ className: "dsh-auto-schedule-modes dsh-auto-field-full", role: "group", "aria-label": "Schedule frequency" },
|
|
727
|
+
SCHEDULE_MODES.map((mode) => h(
|
|
728
|
+
Pill,
|
|
729
|
+
{
|
|
730
|
+
key: mode.id,
|
|
731
|
+
type: "button",
|
|
732
|
+
active: activeMode === mode.id,
|
|
733
|
+
"aria-pressed": activeMode === mode.id,
|
|
734
|
+
onClick: () => setMode(mode.id),
|
|
735
|
+
},
|
|
736
|
+
mode.label,
|
|
737
|
+
)),
|
|
738
|
+
),
|
|
739
|
+
activeMode === "minutes"
|
|
740
|
+
? h(
|
|
741
|
+
Field,
|
|
742
|
+
{ label: "Interval", htmlFor: fieldId("interval") },
|
|
743
|
+
h(
|
|
744
|
+
"select",
|
|
745
|
+
{
|
|
746
|
+
id: fieldId("interval"),
|
|
747
|
+
className: "dsh-auto-select",
|
|
748
|
+
value: values.interval,
|
|
749
|
+
onChange: (event) => applySimpleValues("minutes", Object.assign({}, values, { interval: Number(event.target.value) })),
|
|
750
|
+
},
|
|
751
|
+
MINUTE_INTERVALS.map((minutes) => h("option", { key: minutes, value: minutes }, "Every " + minutes + " minutes")),
|
|
752
|
+
),
|
|
753
|
+
)
|
|
754
|
+
: null,
|
|
755
|
+
activeMode === "hourly"
|
|
756
|
+
? h(
|
|
757
|
+
Field,
|
|
758
|
+
{ label: "Minute past the hour", htmlFor: fieldId("hour-minute") },
|
|
759
|
+
h(
|
|
760
|
+
"select",
|
|
761
|
+
{
|
|
762
|
+
id: fieldId("hour-minute"),
|
|
763
|
+
className: "dsh-auto-select",
|
|
764
|
+
value: values.minute,
|
|
765
|
+
onChange: (event) => applySimpleValues("hourly", Object.assign({}, values, { minute: Number(event.target.value) })),
|
|
766
|
+
},
|
|
767
|
+
HOURLY_MINUTES.map((minute) => h("option", { key: minute, value: minute }, ":" + twoDigits(minute))),
|
|
768
|
+
),
|
|
769
|
+
)
|
|
770
|
+
: null,
|
|
771
|
+
["daily", "weekdays", "weekly"].includes(activeMode)
|
|
772
|
+
? h(
|
|
773
|
+
Field,
|
|
774
|
+
{ label: "Time", htmlFor: fieldId("schedule-time") },
|
|
775
|
+
h("input", {
|
|
776
|
+
id: fieldId("schedule-time"),
|
|
777
|
+
className: "dsh-auto-input",
|
|
778
|
+
type: "time",
|
|
779
|
+
value: twoDigits(values.hour) + ":" + twoDigits(values.minute),
|
|
780
|
+
onChange: (event) => setTime(event.target.value),
|
|
781
|
+
}),
|
|
782
|
+
)
|
|
783
|
+
: null,
|
|
784
|
+
activeMode === "weekly"
|
|
785
|
+
? h(
|
|
786
|
+
Field,
|
|
787
|
+
{ label: "Day", htmlFor: fieldId("schedule-day") },
|
|
788
|
+
h(
|
|
789
|
+
"select",
|
|
790
|
+
{
|
|
791
|
+
id: fieldId("schedule-day"),
|
|
792
|
+
className: "dsh-auto-select",
|
|
793
|
+
value: values.weekday,
|
|
794
|
+
onChange: (event) => applySimpleValues("weekly", Object.assign({}, values, { weekday: event.target.value })),
|
|
795
|
+
},
|
|
796
|
+
WEEKDAYS.map((day) => h("option", { key: day.value, value: day.value }, day.label)),
|
|
797
|
+
),
|
|
798
|
+
)
|
|
799
|
+
: null,
|
|
800
|
+
h(
|
|
801
|
+
Field,
|
|
802
|
+
{
|
|
803
|
+
label: "Time zone",
|
|
804
|
+
htmlFor: fieldId("timezone"),
|
|
805
|
+
required: true,
|
|
806
|
+
full: activeMode === "weekly" || activeMode === "custom",
|
|
807
|
+
hint: "Current UTC offsets and daylight-saving changes are handled by the selected IANA time zone.",
|
|
808
|
+
},
|
|
809
|
+
h(TimeZonePicker, {
|
|
810
|
+
id: fieldId("timezone"),
|
|
811
|
+
value: timezone,
|
|
812
|
+
onChange: onTimezoneChange,
|
|
813
|
+
}),
|
|
814
|
+
),
|
|
815
|
+
activeMode === "custom"
|
|
816
|
+
? h(
|
|
817
|
+
Field,
|
|
818
|
+
{
|
|
819
|
+
label: "Cron expression",
|
|
820
|
+
htmlFor: fieldId("cron"),
|
|
821
|
+
required: true,
|
|
822
|
+
full: true,
|
|
823
|
+
hint: "Five fields only. Seconds and hashed H expressions are not supported.",
|
|
824
|
+
},
|
|
825
|
+
h("input", {
|
|
826
|
+
id: fieldId("cron"),
|
|
827
|
+
className: "dsh-auto-input dsh-auto-cron-input",
|
|
828
|
+
type: "text",
|
|
829
|
+
value: cron,
|
|
830
|
+
spellCheck: false,
|
|
831
|
+
placeholder: "0 9 * * 1-5",
|
|
832
|
+
onChange: (event) => {
|
|
833
|
+
const nextCron = event.target.value;
|
|
834
|
+
customCronRef.current = nextCron;
|
|
835
|
+
customCronPinnedRef.current = true;
|
|
836
|
+
setValues(scheduleControlValues(nextCron, values));
|
|
837
|
+
setForceCustom(true);
|
|
838
|
+
onCronChange(nextCron);
|
|
839
|
+
},
|
|
840
|
+
}),
|
|
841
|
+
h(CronFieldGuide, { value: cron }),
|
|
842
|
+
)
|
|
843
|
+
: null,
|
|
844
|
+
h(
|
|
845
|
+
"div",
|
|
846
|
+
{ className: "dsh-auto-schedule-summary dsh-auto-field-full" },
|
|
847
|
+
h("span", { className: "dsh-auto-schedule-summary-copy" }, describeSimpleSchedule(summarySchedule, timezone)),
|
|
848
|
+
h("code", { className: "dsh-auto-schedule-expression", title: displayedCron }, displayedCron),
|
|
849
|
+
),
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
496
853
|
function Field(props) {
|
|
497
854
|
const { label, htmlFor, required, hint, full, children } = props;
|
|
498
855
|
return h(
|
|
@@ -699,6 +1056,7 @@ window.__ModuleLoader__.load({
|
|
|
699
1056
|
const {
|
|
700
1057
|
id,
|
|
701
1058
|
value,
|
|
1059
|
+
displayValue,
|
|
702
1060
|
onChange,
|
|
703
1061
|
optionsForQuery,
|
|
704
1062
|
selectedId,
|
|
@@ -939,7 +1297,7 @@ window.__ModuleLoader__.load({
|
|
|
939
1297
|
className: "dsh-auto-combobox-input",
|
|
940
1298
|
type: "text",
|
|
941
1299
|
role: "combobox",
|
|
942
|
-
value,
|
|
1300
|
+
value: !searching && displayValue !== undefined ? displayValue : value,
|
|
943
1301
|
placeholder,
|
|
944
1302
|
disabled,
|
|
945
1303
|
autoComplete: "off",
|
|
@@ -1026,6 +1384,105 @@ window.__ModuleLoader__.load({
|
|
|
1026
1384
|
});
|
|
1027
1385
|
}
|
|
1028
1386
|
|
|
1387
|
+
function looksLikeAbsolutePath(value) {
|
|
1388
|
+
const path = String(value || "").trim();
|
|
1389
|
+
return path.charAt(0) === "/" || /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\");
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function workspacePathName(path) {
|
|
1393
|
+
const withoutTrailingSeparators = String(path || "").replace(/[\\/]+$/, "");
|
|
1394
|
+
const segments = withoutTrailingSeparators.split(/[\\/]/).filter(Boolean);
|
|
1395
|
+
return segments[segments.length - 1] || String(path || "");
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function normalizedWorkspaces(workspaces) {
|
|
1399
|
+
const seenPaths = new Set();
|
|
1400
|
+
return (Array.isArray(workspaces) ? workspaces : [])
|
|
1401
|
+
.map((workspace, index) => {
|
|
1402
|
+
if (!workspace || typeof workspace.path !== "string" || workspace.path.trim() === "") return null;
|
|
1403
|
+
const path = workspace.path.trim();
|
|
1404
|
+
if (seenPaths.has(path)) return null;
|
|
1405
|
+
seenPaths.add(path);
|
|
1406
|
+
const rawTitle = typeof workspace.title === "string" ? workspace.title.trim() : "";
|
|
1407
|
+
return {
|
|
1408
|
+
id: String(workspace.workspaceId || workspace.id || "workspace-" + index),
|
|
1409
|
+
path,
|
|
1410
|
+
title: rawTitle || workspacePathName(path),
|
|
1411
|
+
sessionCount: Array.isArray(workspace.sessionIds) ? workspace.sessionIds.length : 0,
|
|
1412
|
+
};
|
|
1413
|
+
})
|
|
1414
|
+
.filter(Boolean);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function preferredWorkspacePath(workspaceSnapshot) {
|
|
1418
|
+
const catalog = normalizedWorkspaces(workspaceSnapshot?.items);
|
|
1419
|
+
const recentId = workspaceSnapshot?.recentWorkspaceId;
|
|
1420
|
+
const recent = recentId === undefined
|
|
1421
|
+
? null
|
|
1422
|
+
: catalog.find((workspace) => workspace.id === String(recentId));
|
|
1423
|
+
return (recent || catalog[0] || {}).path || "";
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function WorkspacePicker({ id, value, workspaceSnapshot, onChange }) {
|
|
1427
|
+
const catalog = normalizedWorkspaces(workspaceSnapshot?.items);
|
|
1428
|
+
const exactValue = value.trim();
|
|
1429
|
+
const selected = catalog.find((workspace) => workspace.path === exactValue) || null;
|
|
1430
|
+
const custom = exactValue !== "" && selected === null && looksLikeAbsolutePath(exactValue);
|
|
1431
|
+
const options = [
|
|
1432
|
+
...catalog.map((workspace) => ({
|
|
1433
|
+
id: "workspace:" + workspace.id,
|
|
1434
|
+
value: workspace.path,
|
|
1435
|
+
label: workspace.title,
|
|
1436
|
+
detail: workspace.path + " · " + workspace.sessionCount + (workspace.sessionCount === 1 ? " session" : " sessions"),
|
|
1437
|
+
aliases: [workspace.id, workspacePathName(workspace.path)],
|
|
1438
|
+
Icon: IconFolderClose16,
|
|
1439
|
+
})),
|
|
1440
|
+
...(custom
|
|
1441
|
+
? [{
|
|
1442
|
+
id: "custom-workspace:" + exactValue,
|
|
1443
|
+
value: exactValue,
|
|
1444
|
+
label: workspacePathName(exactValue) || exactValue,
|
|
1445
|
+
detail: exactValue + " · Custom absolute path",
|
|
1446
|
+
aliases: [exactValue],
|
|
1447
|
+
custom: true,
|
|
1448
|
+
Icon: IconFolderClose16,
|
|
1449
|
+
}]
|
|
1450
|
+
: []),
|
|
1451
|
+
];
|
|
1452
|
+
let panelNotice = null;
|
|
1453
|
+
if (workspaceSnapshot?.state === "error") {
|
|
1454
|
+
panelNotice = "Harness workspaces are unavailable. You can still enter an absolute directory path.";
|
|
1455
|
+
} else if (workspaceSnapshot?.phase !== "ready" && catalog.length === 0) {
|
|
1456
|
+
panelNotice = "Loading Harness workspaces. You can still enter an absolute directory path.";
|
|
1457
|
+
}
|
|
1458
|
+
return h(EditableCombobox, {
|
|
1459
|
+
id,
|
|
1460
|
+
value,
|
|
1461
|
+
displayValue: selected ? selected.title : undefined,
|
|
1462
|
+
onChange,
|
|
1463
|
+
optionsForQuery: (query) => comboboxSearchResults(options, query),
|
|
1464
|
+
selectedId: selected ? "workspace:" + selected.id : custom ? "custom-workspace:" + exactValue : null,
|
|
1465
|
+
commitExactValue: (raw) => {
|
|
1466
|
+
const path = raw.trim();
|
|
1467
|
+
return looksLikeAbsolutePath(path) ? path : null;
|
|
1468
|
+
},
|
|
1469
|
+
placeholder: "Choose a Harness workspace or enter a path",
|
|
1470
|
+
Icon: IconFolderClose16,
|
|
1471
|
+
listboxLabel: "Harness workspaces",
|
|
1472
|
+
initialTitle: "Harness workspaces",
|
|
1473
|
+
searchTitle: "Matching workspaces",
|
|
1474
|
+
emptyText: catalog.length === 0
|
|
1475
|
+
? "No Harness workspaces are registered. Enter an absolute directory path."
|
|
1476
|
+
: "No matching workspaces. Enter an absolute directory path to use it directly.",
|
|
1477
|
+
hintText: "Choose a registered workspace or enter an absolute path without registering it.",
|
|
1478
|
+
resultNoun: "workspace",
|
|
1479
|
+
invalidMessage: "Enter an absolute workspace path.",
|
|
1480
|
+
invalid: exactValue !== "" && !looksLikeAbsolutePath(exactValue),
|
|
1481
|
+
resumeSearch: custom,
|
|
1482
|
+
panelNotice,
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1029
1486
|
function providerFallbackName(id) {
|
|
1030
1487
|
return humanizePresetId(id)
|
|
1031
1488
|
.replace(/^Openai\b/, "OpenAI")
|
|
@@ -1399,7 +1856,7 @@ window.__ModuleLoader__.load({
|
|
|
1399
1856
|
}
|
|
1400
1857
|
|
|
1401
1858
|
function JobForm(props) {
|
|
1402
|
-
const { meta, draft, editing, saving, error, onChange, onIdTouched, onSubmit, onCancel } = props;
|
|
1859
|
+
const { meta, workspaceSnapshot, draft, editing, saving, error, errorField, onChange, onIdTouched, onSubmit, onCancel } = props;
|
|
1403
1860
|
const providers = normalizedProviders(meta && Array.isArray(meta.providers) ? meta.providers : []);
|
|
1404
1861
|
const providerModels = providers.find((provider) => provider.id === draft.provider);
|
|
1405
1862
|
const models = providerModels ? providerModels.models : [];
|
|
@@ -1415,6 +1872,43 @@ window.__ModuleLoader__.load({
|
|
|
1415
1872
|
formPrefixRef.current = nextFormInstancePrefix(reactFormId);
|
|
1416
1873
|
}
|
|
1417
1874
|
const fieldId = (name) => formPrefixRef.current + "-" + name;
|
|
1875
|
+
const errorId = fieldId("form-error");
|
|
1876
|
+
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
1877
|
+
const advancedError = ["id", "timeout", "overlap", "misfire"].includes(errorField);
|
|
1878
|
+
const errorPanelReady = !advancedError || advancedOpen;
|
|
1879
|
+
useEffect(() => {
|
|
1880
|
+
if (!error || !errorField) return undefined;
|
|
1881
|
+
if (!errorPanelReady) {
|
|
1882
|
+
setAdvancedOpen(true);
|
|
1883
|
+
return undefined;
|
|
1884
|
+
}
|
|
1885
|
+
let target = null;
|
|
1886
|
+
let previousInvalid = null;
|
|
1887
|
+
let previousDescribedBy = null;
|
|
1888
|
+
const frame = requestAnimationFrame(() => {
|
|
1889
|
+
target = document.getElementById(fieldId(errorField));
|
|
1890
|
+
if (!target) return;
|
|
1891
|
+
previousInvalid = target.getAttribute("aria-invalid");
|
|
1892
|
+
previousDescribedBy = target.getAttribute("aria-describedby");
|
|
1893
|
+
target.setAttribute("aria-invalid", "true");
|
|
1894
|
+
target.setAttribute("aria-describedby", errorId);
|
|
1895
|
+
target.focus();
|
|
1896
|
+
});
|
|
1897
|
+
return () => {
|
|
1898
|
+
cancelAnimationFrame(frame);
|
|
1899
|
+
if (!target) return;
|
|
1900
|
+
if (previousInvalid === null) target.removeAttribute("aria-invalid");
|
|
1901
|
+
else target.setAttribute("aria-invalid", previousInvalid);
|
|
1902
|
+
if (previousDescribedBy === null) target.removeAttribute("aria-describedby");
|
|
1903
|
+
else target.setAttribute("aria-describedby", previousDescribedBy);
|
|
1904
|
+
};
|
|
1905
|
+
}, [error, errorField, errorPanelReady]);
|
|
1906
|
+
const advancedSummary = [
|
|
1907
|
+
creating ? (draft.id.trim() === "" ? "Automatic job ID" : "ID " + draft.id.trim()) : "ID " + draft.id,
|
|
1908
|
+
(formatTimeout(Number(draft.timeoutMs)) || draft.timeoutMs + "ms") + " timeout",
|
|
1909
|
+
overlapPolicySummary(draft.overlap),
|
|
1910
|
+
draft.misfire === "skip" ? "Skip missed runs" : "Run once after downtime",
|
|
1911
|
+
].join(" · ");
|
|
1418
1912
|
|
|
1419
1913
|
return h(
|
|
1420
1914
|
"form",
|
|
@@ -1424,259 +1918,327 @@ window.__ModuleLoader__.load({
|
|
|
1424
1918
|
noValidate: true,
|
|
1425
1919
|
"aria-label": creating ? "New automation" : "Edit automation",
|
|
1426
1920
|
},
|
|
1427
|
-
h("h3", null, creating ? "New automation" : "Edit \u201c" + editing.name + "\u201d"),
|
|
1428
1921
|
h(
|
|
1429
|
-
"
|
|
1430
|
-
{ className: "dsh-auto-
|
|
1922
|
+
"header",
|
|
1923
|
+
{ className: "dsh-auto-form-header" },
|
|
1431
1924
|
h(
|
|
1432
|
-
|
|
1433
|
-
{
|
|
1434
|
-
h("
|
|
1435
|
-
|
|
1436
|
-
className: "dsh-auto-input",
|
|
1437
|
-
type: "text",
|
|
1438
|
-
value: draft.name,
|
|
1439
|
-
autoFocus: true,
|
|
1440
|
-
placeholder: "e.g. Morning standup notes",
|
|
1441
|
-
onChange: (event) => onChange("name", event.target.value),
|
|
1442
|
-
}),
|
|
1925
|
+
"span",
|
|
1926
|
+
{ className: "dsh-auto-form-heading" },
|
|
1927
|
+
h("h3", null, creating ? "New automation" : "Edit \u201c" + editing.name + "\u201d"),
|
|
1928
|
+
h("p", null, "Configure what runs, when it runs, and which Harness agent executes it."),
|
|
1443
1929
|
),
|
|
1444
|
-
creating
|
|
1445
|
-
? h(
|
|
1446
|
-
Field,
|
|
1447
|
-
{ label: "Job id", htmlFor: fieldId("id"), hint: "Lowercase letters, digits and hyphens; auto-derived from the name. Leave blank to let the server generate one." },
|
|
1448
|
-
h("input", {
|
|
1449
|
-
id: fieldId("id"),
|
|
1450
|
-
className: "dsh-auto-input",
|
|
1451
|
-
type: "text",
|
|
1452
|
-
value: draft.id,
|
|
1453
|
-
spellCheck: false,
|
|
1454
|
-
placeholder: "auto",
|
|
1455
|
-
onChange: (event) => {
|
|
1456
|
-
onIdTouched();
|
|
1457
|
-
onChange("id", event.target.value);
|
|
1458
|
-
},
|
|
1459
|
-
}),
|
|
1460
|
-
)
|
|
1461
|
-
: h(
|
|
1462
|
-
Field,
|
|
1463
|
-
{ label: "Job id", htmlFor: fieldId("id"), hint: "Fixed after creation." },
|
|
1464
|
-
h("input", {
|
|
1465
|
-
id: fieldId("id"),
|
|
1466
|
-
className: "dsh-auto-input",
|
|
1467
|
-
type: "text",
|
|
1468
|
-
value: draft.id,
|
|
1469
|
-
disabled: true,
|
|
1470
|
-
spellCheck: false,
|
|
1471
|
-
}),
|
|
1472
|
-
),
|
|
1473
1930
|
h(
|
|
1474
|
-
|
|
1475
|
-
{
|
|
1931
|
+
"label",
|
|
1932
|
+
{ className: "dsh-auto-switch dsh-auto-form-enabled" },
|
|
1476
1933
|
h("input", {
|
|
1477
|
-
id: fieldId("
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
onChange: (event) => onChange("cron", event.target.value),
|
|
1483
|
-
}),
|
|
1484
|
-
),
|
|
1485
|
-
h(
|
|
1486
|
-
Field,
|
|
1487
|
-
{
|
|
1488
|
-
label: "Time zone",
|
|
1489
|
-
htmlFor: fieldId("timezone"),
|
|
1490
|
-
required: true,
|
|
1491
|
-
hint: "Enter an IANA time zone. Current UTC offsets may change with daylight saving time.",
|
|
1492
|
-
},
|
|
1493
|
-
h(TimeZonePicker, {
|
|
1494
|
-
id: fieldId("timezone"),
|
|
1495
|
-
value: draft.timezone,
|
|
1496
|
-
onChange: (value) => onChange("timezone", value),
|
|
1934
|
+
id: fieldId("enabled"),
|
|
1935
|
+
type: "checkbox",
|
|
1936
|
+
role: "switch",
|
|
1937
|
+
checked: draft.enabled,
|
|
1938
|
+
onChange: (event) => onChange("enabled", event.target.checked),
|
|
1497
1939
|
}),
|
|
1940
|
+
h("span", { className: "dsh-auto-switch-track" }),
|
|
1941
|
+
h("span", { className: "dsh-auto-switch-text" }, draft.enabled ? "Active" : "Paused"),
|
|
1498
1942
|
),
|
|
1943
|
+
),
|
|
1944
|
+
h(
|
|
1945
|
+
FormSection,
|
|
1946
|
+
{
|
|
1947
|
+
title: "Task",
|
|
1948
|
+
description: "Describe the work and choose where the agent should run.",
|
|
1949
|
+
Icon: IconEditOutline16,
|
|
1950
|
+
},
|
|
1499
1951
|
h(
|
|
1500
|
-
|
|
1501
|
-
{
|
|
1952
|
+
"div",
|
|
1953
|
+
{ className: "dsh-auto-formgrid" },
|
|
1502
1954
|
h(
|
|
1503
|
-
|
|
1504
|
-
{
|
|
1955
|
+
Field,
|
|
1956
|
+
{ label: "Name", htmlFor: fieldId("name"), required: true, full: true },
|
|
1505
1957
|
h("input", {
|
|
1506
|
-
id: fieldId("
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1958
|
+
id: fieldId("name"),
|
|
1959
|
+
className: "dsh-auto-input",
|
|
1960
|
+
type: "text",
|
|
1961
|
+
value: draft.name,
|
|
1962
|
+
autoFocus: true,
|
|
1963
|
+
placeholder: "e.g. Morning standup notes",
|
|
1964
|
+
onChange: (event) => onChange("name", event.target.value),
|
|
1510
1965
|
}),
|
|
1511
|
-
h("span", null, "Schedule is active"),
|
|
1512
1966
|
),
|
|
1513
|
-
),
|
|
1514
|
-
h(
|
|
1515
|
-
Field,
|
|
1516
|
-
{ label: "Timeout (ms)", htmlFor: fieldId("timeout"), required: true, hint: "Wall-clock limit for each run (1s to 24h)." },
|
|
1517
|
-
h("input", {
|
|
1518
|
-
id: fieldId("timeout"),
|
|
1519
|
-
className: "dsh-auto-input",
|
|
1520
|
-
type: "number",
|
|
1521
|
-
min: MIN_TIMEOUT_MS,
|
|
1522
|
-
max: MAX_TIMEOUT_MS,
|
|
1523
|
-
step: 1000,
|
|
1524
|
-
value: draft.timeoutMs,
|
|
1525
|
-
onChange: (event) => onChange("timeoutMs", event.target.value),
|
|
1526
|
-
}),
|
|
1527
|
-
),
|
|
1528
|
-
h(
|
|
1529
|
-
Field,
|
|
1530
|
-
{ label: "Provider", htmlFor: fieldId("provider"), hint: "Blank uses the current Harness default. You may type an unlisted provider route." },
|
|
1531
|
-
h(ProviderPicker, {
|
|
1532
|
-
id: fieldId("provider"),
|
|
1533
|
-
value: draft.provider,
|
|
1534
|
-
providers,
|
|
1535
|
-
defaultModel: meta?.defaultModel,
|
|
1536
|
-
onChange: (value) => onChange("provider", value),
|
|
1537
|
-
}),
|
|
1538
|
-
),
|
|
1539
|
-
h(
|
|
1540
|
-
Field,
|
|
1541
|
-
{
|
|
1542
|
-
label: "Model",
|
|
1543
|
-
htmlFor: fieldId("model"),
|
|
1544
|
-
required: draft.provider !== "",
|
|
1545
|
-
hint: draft.provider === ""
|
|
1546
|
-
? "Set a provider first, or leave both blank for the Harness default."
|
|
1547
|
-
: "Choose a discovered model or type an adapter-supported model id.",
|
|
1548
|
-
},
|
|
1549
|
-
h("input", {
|
|
1550
|
-
id: fieldId("model"),
|
|
1551
|
-
className: "dsh-auto-input",
|
|
1552
|
-
type: "text",
|
|
1553
|
-
list: fieldId("model-list"),
|
|
1554
|
-
value: draft.model,
|
|
1555
|
-
spellCheck: false,
|
|
1556
|
-
placeholder: draft.provider === "" ? "Harness default" : "Model id",
|
|
1557
|
-
disabled: draft.provider === "",
|
|
1558
|
-
onChange: (event) => onChange("model", event.target.value),
|
|
1559
|
-
}),
|
|
1560
1967
|
h(
|
|
1561
|
-
|
|
1562
|
-
{
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1968
|
+
Field,
|
|
1969
|
+
{
|
|
1970
|
+
label: "Prompt",
|
|
1971
|
+
htmlFor: fieldId("prompt"),
|
|
1972
|
+
required: true,
|
|
1973
|
+
full: true,
|
|
1974
|
+
hint: "Instructions sent to a fresh agent. Prompts are not copied into run-history summaries.",
|
|
1975
|
+
},
|
|
1976
|
+
h("textarea", {
|
|
1977
|
+
id: fieldId("prompt"),
|
|
1978
|
+
className: "dsh-auto-textarea dsh-auto-prompt",
|
|
1979
|
+
value: draft.prompt,
|
|
1980
|
+
placeholder: "Summarize yesterday's progress and list today's priorities\u2026",
|
|
1981
|
+
onChange: (event) => onChange("prompt", event.target.value),
|
|
1982
|
+
}),
|
|
1983
|
+
),
|
|
1984
|
+
h(
|
|
1985
|
+
Field,
|
|
1986
|
+
{
|
|
1987
|
+
label: "Workspace",
|
|
1988
|
+
htmlFor: fieldId("cwd"),
|
|
1989
|
+
required: true,
|
|
1990
|
+
full: true,
|
|
1991
|
+
hint: "Choose a Harness workspace or enter the absolute directory where the agent runs.",
|
|
1992
|
+
},
|
|
1993
|
+
h(WorkspacePicker, {
|
|
1994
|
+
id: fieldId("cwd"),
|
|
1995
|
+
value: draft.cwd,
|
|
1996
|
+
workspaceSnapshot,
|
|
1997
|
+
onChange: (value) => onChange("cwd", value),
|
|
1998
|
+
}),
|
|
1568
1999
|
),
|
|
1569
2000
|
),
|
|
2001
|
+
),
|
|
2002
|
+
h(
|
|
2003
|
+
FormSection,
|
|
2004
|
+
{
|
|
2005
|
+
title: "Schedule",
|
|
2006
|
+
description: "Choose a common pattern, or switch to Custom for a five-field cron expression.",
|
|
2007
|
+
Icon: IconGlobeOutline14,
|
|
2008
|
+
},
|
|
1570
2009
|
h(
|
|
1571
|
-
|
|
1572
|
-
{
|
|
1573
|
-
h(
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
onChange: (value) => onChange("reasoningEffort", value),
|
|
1580
|
-
}),
|
|
1581
|
-
),
|
|
1582
|
-
h(
|
|
1583
|
-
Field,
|
|
1584
|
-
{ label: "Agent preset", htmlFor: fieldId("preset"), hint: "Uses the current Harness default when not explicitly selected." },
|
|
1585
|
-
h(AgentPresetPicker, {
|
|
1586
|
-
id: fieldId("preset"),
|
|
1587
|
-
value: draft.agentPreset,
|
|
1588
|
-
presets: agentPresets,
|
|
1589
|
-
onChange: (value) => onChange("agentPreset", value),
|
|
1590
|
-
}),
|
|
1591
|
-
),
|
|
1592
|
-
h(
|
|
1593
|
-
Field,
|
|
1594
|
-
{
|
|
1595
|
-
label: "Permission preset",
|
|
1596
|
-
htmlFor: fieldId("permission"),
|
|
1597
|
-
required: true,
|
|
1598
|
-
hint: permissionPresetPresentation(draft.permissionPreset).detail,
|
|
1599
|
-
},
|
|
1600
|
-
h(PermissionPresetPicker, {
|
|
1601
|
-
id: fieldId("permission"),
|
|
1602
|
-
value: draft.permissionPreset,
|
|
1603
|
-
presets: permissionPresets,
|
|
1604
|
-
onChange: (value) => onChange("permissionPreset", value),
|
|
2010
|
+
"div",
|
|
2011
|
+
{ className: "dsh-auto-formgrid dsh-auto-schedule-grid" },
|
|
2012
|
+
h(ScheduleEditor, {
|
|
2013
|
+
fieldId,
|
|
2014
|
+
cron: draft.cron,
|
|
2015
|
+
timezone: draft.timezone,
|
|
2016
|
+
onCronChange: (value) => onChange("cron", value),
|
|
2017
|
+
onTimezoneChange: (value) => onChange("timezone", value),
|
|
1605
2018
|
}),
|
|
1606
2019
|
),
|
|
2020
|
+
),
|
|
2021
|
+
h(
|
|
2022
|
+
FormSection,
|
|
2023
|
+
{
|
|
2024
|
+
title: "Agent & access",
|
|
2025
|
+
description: "Select the model, agent behavior, and filesystem permissions for each fresh run.",
|
|
2026
|
+
Icon: IconAgentPresetOutline16,
|
|
2027
|
+
},
|
|
1607
2028
|
h(
|
|
1608
|
-
|
|
1609
|
-
{
|
|
2029
|
+
"div",
|
|
2030
|
+
{ className: "dsh-auto-formgrid" },
|
|
1610
2031
|
h(
|
|
1611
|
-
|
|
2032
|
+
Field,
|
|
2033
|
+
{ label: "Provider", htmlFor: fieldId("provider"), hint: "Blank uses the current Harness default. Custom provider routes remain supported." },
|
|
2034
|
+
h(ProviderPicker, {
|
|
2035
|
+
id: fieldId("provider"),
|
|
2036
|
+
value: draft.provider,
|
|
2037
|
+
providers,
|
|
2038
|
+
defaultModel: meta?.defaultModel,
|
|
2039
|
+
onChange: (value) => onChange("provider", value),
|
|
2040
|
+
}),
|
|
2041
|
+
),
|
|
2042
|
+
h(
|
|
2043
|
+
Field,
|
|
1612
2044
|
{
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2045
|
+
label: "Model",
|
|
2046
|
+
htmlFor: fieldId("model"),
|
|
2047
|
+
required: draft.provider !== "",
|
|
2048
|
+
hint: draft.provider === ""
|
|
2049
|
+
? "Set a provider first, or leave both blank for the Harness default."
|
|
2050
|
+
: "Choose a discovered model or type an adapter-supported model id.",
|
|
1617
2051
|
},
|
|
1618
|
-
|
|
2052
|
+
h("input", {
|
|
2053
|
+
id: fieldId("model"),
|
|
2054
|
+
className: "dsh-auto-input",
|
|
2055
|
+
type: "text",
|
|
2056
|
+
list: fieldId("model-list"),
|
|
2057
|
+
value: draft.model,
|
|
2058
|
+
spellCheck: false,
|
|
2059
|
+
placeholder: draft.provider === "" ? "Harness default" : "Model id",
|
|
2060
|
+
disabled: draft.provider === "",
|
|
2061
|
+
onChange: (event) => onChange("model", event.target.value),
|
|
2062
|
+
}),
|
|
2063
|
+
h(
|
|
2064
|
+
"datalist",
|
|
2065
|
+
{ id: fieldId("model-list") },
|
|
2066
|
+
models.map((model) => h("option", {
|
|
2067
|
+
key: model.id,
|
|
2068
|
+
value: model.id,
|
|
2069
|
+
label: model.name === model.id ? undefined : model.name,
|
|
2070
|
+
})),
|
|
2071
|
+
),
|
|
2072
|
+
),
|
|
2073
|
+
h(
|
|
2074
|
+
Field,
|
|
2075
|
+
{ label: "Reasoning effort", htmlFor: fieldId("effort"), hint: "Default follows the selected model. Custom adapter-owned IDs remain supported." },
|
|
2076
|
+
h(ReasoningEffortPicker, {
|
|
2077
|
+
id: fieldId("effort"),
|
|
2078
|
+
value: draft.reasoningEffort,
|
|
2079
|
+
provider: draft.provider,
|
|
2080
|
+
model: draft.model,
|
|
2081
|
+
defaultModel: meta?.defaultModel,
|
|
2082
|
+
onChange: (value) => onChange("reasoningEffort", value),
|
|
2083
|
+
}),
|
|
2084
|
+
),
|
|
2085
|
+
h(
|
|
2086
|
+
Field,
|
|
2087
|
+
{ label: "Agent preset", htmlFor: fieldId("preset"), hint: "Uses the current Harness default when not explicitly selected." },
|
|
2088
|
+
h(AgentPresetPicker, {
|
|
2089
|
+
id: fieldId("preset"),
|
|
2090
|
+
value: draft.agentPreset,
|
|
2091
|
+
presets: agentPresets,
|
|
2092
|
+
onChange: (value) => onChange("agentPreset", value),
|
|
2093
|
+
}),
|
|
1619
2094
|
),
|
|
1620
|
-
),
|
|
1621
|
-
h(
|
|
1622
|
-
Field,
|
|
1623
|
-
{ label: "Misfire policy", htmlFor: fieldId("misfire"), hint: "How a missed occurrence is handled after the scheduler is back." },
|
|
1624
2095
|
h(
|
|
1625
|
-
|
|
2096
|
+
Field,
|
|
1626
2097
|
{
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
2098
|
+
label: "Permission preset",
|
|
2099
|
+
htmlFor: fieldId("permission"),
|
|
2100
|
+
required: true,
|
|
2101
|
+
full: true,
|
|
2102
|
+
hint: permissionPresetPresentation(draft.permissionPreset).detail,
|
|
1631
2103
|
},
|
|
1632
|
-
|
|
2104
|
+
h(PermissionPresetPicker, {
|
|
2105
|
+
id: fieldId("permission"),
|
|
2106
|
+
value: draft.permissionPreset,
|
|
2107
|
+
presets: permissionPresets,
|
|
2108
|
+
onChange: (value) => onChange("permissionPreset", value),
|
|
2109
|
+
}),
|
|
1633
2110
|
),
|
|
1634
2111
|
),
|
|
1635
|
-
h(
|
|
1636
|
-
Field,
|
|
1637
|
-
{ label: "Working directory", htmlFor: fieldId("cwd"), required: true, full: true, hint: "Absolute project directory the agent runs in." },
|
|
1638
|
-
h("input", {
|
|
1639
|
-
id: fieldId("cwd"),
|
|
1640
|
-
className: "dsh-auto-input",
|
|
1641
|
-
type: "text",
|
|
1642
|
-
value: draft.cwd,
|
|
1643
|
-
spellCheck: false,
|
|
1644
|
-
placeholder: "/home/you/workspace/project",
|
|
1645
|
-
onChange: (event) => onChange("cwd", event.target.value),
|
|
1646
|
-
}),
|
|
1647
|
-
),
|
|
1648
|
-
h(
|
|
1649
|
-
Field,
|
|
1650
|
-
{ label: "Prompt", htmlFor: fieldId("prompt"), required: true, full: true, hint: "Instructions for the agent run. Prompts are never shown in run history." },
|
|
1651
|
-
h("textarea", {
|
|
1652
|
-
id: fieldId("prompt"),
|
|
1653
|
-
className: "dsh-auto-textarea",
|
|
1654
|
-
value: draft.prompt,
|
|
1655
|
-
placeholder: "Summarize yesterday's progress and list today's priorities\u2026",
|
|
1656
|
-
onChange: (event) => onChange("prompt", event.target.value),
|
|
1657
|
-
}),
|
|
1658
|
-
),
|
|
1659
2112
|
),
|
|
1660
|
-
error
|
|
1661
|
-
? h("p", { className: "dsh-auto-formerror", role: "alert" }, error)
|
|
1662
|
-
: null,
|
|
1663
2113
|
h(
|
|
1664
|
-
"
|
|
1665
|
-
{ className: "dsh-auto-
|
|
2114
|
+
"section",
|
|
2115
|
+
{ className: "dsh-auto-advanced", "data-open": advancedOpen ? "true" : undefined },
|
|
1666
2116
|
h(
|
|
1667
|
-
|
|
2117
|
+
"button",
|
|
1668
2118
|
{
|
|
1669
|
-
type: "
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2119
|
+
type: "button",
|
|
2120
|
+
className: "dsh-auto-advanced-trigger",
|
|
2121
|
+
"aria-expanded": advancedOpen,
|
|
2122
|
+
"aria-controls": fieldId("advanced-content"),
|
|
2123
|
+
onClick: () => setAdvancedOpen((open) => !open),
|
|
1673
2124
|
},
|
|
1674
|
-
|
|
2125
|
+
h("span", { className: "dsh-auto-form-section-icon", "aria-hidden": "true" }, h(IconSettingsOutline16)),
|
|
2126
|
+
h(
|
|
2127
|
+
"span",
|
|
2128
|
+
{ className: "dsh-auto-advanced-copy" },
|
|
2129
|
+
h("span", { className: "dsh-auto-form-section-title" }, "Advanced"),
|
|
2130
|
+
h("span", { className: "dsh-auto-advanced-summary" }, advancedSummary),
|
|
2131
|
+
),
|
|
2132
|
+
h(
|
|
2133
|
+
"span",
|
|
2134
|
+
{ className: "dsh-auto-advanced-chevron" + (advancedOpen ? " dsh-auto-advanced-chevron-open" : ""), "aria-hidden": "true" },
|
|
2135
|
+
h(IconChevronDownOutline14),
|
|
2136
|
+
),
|
|
1675
2137
|
),
|
|
1676
2138
|
h(
|
|
1677
|
-
|
|
1678
|
-
{
|
|
1679
|
-
|
|
2139
|
+
"div",
|
|
2140
|
+
{
|
|
2141
|
+
id: fieldId("advanced-content"),
|
|
2142
|
+
className: "dsh-auto-formgrid dsh-auto-advanced-content",
|
|
2143
|
+
hidden: !advancedOpen,
|
|
2144
|
+
},
|
|
2145
|
+
creating
|
|
2146
|
+
? h(
|
|
2147
|
+
Field,
|
|
2148
|
+
{ label: "Job ID", htmlFor: fieldId("id"), hint: "Auto-derived from the name. Use lowercase letters, digits, and hyphens." },
|
|
2149
|
+
h("input", {
|
|
2150
|
+
id: fieldId("id"),
|
|
2151
|
+
className: "dsh-auto-input",
|
|
2152
|
+
type: "text",
|
|
2153
|
+
value: draft.id,
|
|
2154
|
+
spellCheck: false,
|
|
2155
|
+
placeholder: "auto",
|
|
2156
|
+
onChange: (event) => {
|
|
2157
|
+
onIdTouched();
|
|
2158
|
+
onChange("id", event.target.value);
|
|
2159
|
+
},
|
|
2160
|
+
}),
|
|
2161
|
+
)
|
|
2162
|
+
: h(
|
|
2163
|
+
Field,
|
|
2164
|
+
{ label: "Job ID", htmlFor: fieldId("id"), hint: "Fixed after creation." },
|
|
2165
|
+
h("input", {
|
|
2166
|
+
id: fieldId("id"),
|
|
2167
|
+
className: "dsh-auto-input",
|
|
2168
|
+
type: "text",
|
|
2169
|
+
value: draft.id,
|
|
2170
|
+
disabled: true,
|
|
2171
|
+
spellCheck: false,
|
|
2172
|
+
}),
|
|
2173
|
+
),
|
|
2174
|
+
h(
|
|
2175
|
+
Field,
|
|
2176
|
+
{ label: "Timeout (ms)", htmlFor: fieldId("timeout"), required: true, hint: "Wall-clock limit for each run (1s to 24h)." },
|
|
2177
|
+
h("input", {
|
|
2178
|
+
id: fieldId("timeout"),
|
|
2179
|
+
className: "dsh-auto-input",
|
|
2180
|
+
type: "number",
|
|
2181
|
+
min: MIN_TIMEOUT_MS,
|
|
2182
|
+
max: MAX_TIMEOUT_MS,
|
|
2183
|
+
step: 1000,
|
|
2184
|
+
value: draft.timeoutMs,
|
|
2185
|
+
onChange: (event) => onChange("timeoutMs", event.target.value),
|
|
2186
|
+
}),
|
|
2187
|
+
),
|
|
2188
|
+
h(
|
|
2189
|
+
Field,
|
|
2190
|
+
{ label: "Overlap policy", htmlFor: fieldId("overlap"), hint: OVERLAP_OPTIONS.find((option) => option.value === draft.overlap)?.hint },
|
|
2191
|
+
h(
|
|
2192
|
+
"select",
|
|
2193
|
+
{
|
|
2194
|
+
id: fieldId("overlap"),
|
|
2195
|
+
className: "dsh-auto-select",
|
|
2196
|
+
value: draft.overlap,
|
|
2197
|
+
onChange: (event) => onChange("overlap", event.target.value),
|
|
2198
|
+
},
|
|
2199
|
+
OVERLAP_OPTIONS.map((option) => h("option", { key: option.value, value: option.value }, option.label)),
|
|
2200
|
+
),
|
|
2201
|
+
),
|
|
2202
|
+
h(
|
|
2203
|
+
Field,
|
|
2204
|
+
{ label: "Misfire policy", htmlFor: fieldId("misfire"), hint: MISFIRE_OPTIONS.find((option) => option.value === draft.misfire)?.hint },
|
|
2205
|
+
h(
|
|
2206
|
+
"select",
|
|
2207
|
+
{
|
|
2208
|
+
id: fieldId("misfire"),
|
|
2209
|
+
className: "dsh-auto-select",
|
|
2210
|
+
value: draft.misfire,
|
|
2211
|
+
onChange: (event) => onChange("misfire", event.target.value),
|
|
2212
|
+
},
|
|
2213
|
+
MISFIRE_OPTIONS.map((option) => h("option", { key: option.value, value: option.value }, option.label)),
|
|
2214
|
+
),
|
|
2215
|
+
),
|
|
2216
|
+
),
|
|
2217
|
+
),
|
|
2218
|
+
h(
|
|
2219
|
+
"footer",
|
|
2220
|
+
{ className: "dsh-auto-form-footer" },
|
|
2221
|
+
error
|
|
2222
|
+
? h("p", { id: errorId, className: "dsh-auto-formerror", role: "alert" }, error)
|
|
2223
|
+
: null,
|
|
2224
|
+
h(
|
|
2225
|
+
"div",
|
|
2226
|
+
{ className: "dsh-auto-formactions" },
|
|
2227
|
+
h(
|
|
2228
|
+
Button,
|
|
2229
|
+
{
|
|
2230
|
+
type: "submit",
|
|
2231
|
+
variant: "primary",
|
|
2232
|
+
icon: saving ? null : h(IconCheckOutline16),
|
|
2233
|
+
disabled: saving,
|
|
2234
|
+
},
|
|
2235
|
+
saving ? "Saving\u2026" : creating ? "Create automation" : "Save changes",
|
|
2236
|
+
),
|
|
2237
|
+
h(
|
|
2238
|
+
Button,
|
|
2239
|
+
{ type: "button", variant: "outline", onClick: onCancel, disabled: saving },
|
|
2240
|
+
"Cancel",
|
|
2241
|
+
),
|
|
1680
2242
|
),
|
|
1681
2243
|
),
|
|
1682
2244
|
);
|
|
@@ -1934,7 +2496,21 @@ window.__ModuleLoader__.load({
|
|
|
1934
2496
|
);
|
|
1935
2497
|
}
|
|
1936
2498
|
|
|
1937
|
-
function
|
|
2499
|
+
function useHarnessWorkspaceSnapshot(workspaceRuntime) {
|
|
2500
|
+
const list = workspaceRuntime?.list;
|
|
2501
|
+
const subscribe = useCallback(
|
|
2502
|
+
(listener) => list ? list.subscribe(listener) : () => {},
|
|
2503
|
+
[list],
|
|
2504
|
+
);
|
|
2505
|
+
const getSnapshot = useCallback(
|
|
2506
|
+
() => list ? list.getSnapshot() : EMPTY_WORKSPACE_SNAPSHOT,
|
|
2507
|
+
[list],
|
|
2508
|
+
);
|
|
2509
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function AutomationsSection({ centerMode = false, workspaceRuntime = null } = {}) {
|
|
2513
|
+
const workspaceSnapshot = useHarnessWorkspaceSnapshot(workspaceRuntime);
|
|
1938
2514
|
const [meta, setMeta] = useState(null);
|
|
1939
2515
|
const [snapshot, setSnapshot] = useState(null);
|
|
1940
2516
|
const [loading, setLoading] = useState(true);
|
|
@@ -1946,6 +2522,7 @@ window.__ModuleLoader__.load({
|
|
|
1946
2522
|
const [editing, setEditing] = useState(null);
|
|
1947
2523
|
const [draft, setDraft] = useState(() => emptyDraft(null));
|
|
1948
2524
|
const [formError, setFormError] = useState(null);
|
|
2525
|
+
const [formErrorField, setFormErrorField] = useState(null);
|
|
1949
2526
|
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
|
|
1950
2527
|
const [confirmCancelId, setConfirmCancelId] = useState(null);
|
|
1951
2528
|
|
|
@@ -1953,6 +2530,7 @@ window.__ModuleLoader__.load({
|
|
|
1953
2530
|
const pollRef = useRef(false);
|
|
1954
2531
|
const hasDataRef = useRef(false);
|
|
1955
2532
|
const idTouchedRef = useRef(false);
|
|
2533
|
+
const workspaceTouchedRef = useRef(false);
|
|
1956
2534
|
const flashTimerRef = useRef(null);
|
|
1957
2535
|
|
|
1958
2536
|
const loadMeta = useCallback(async () => {
|
|
@@ -2098,16 +2676,20 @@ window.__ModuleLoader__.load({
|
|
|
2098
2676
|
const openCreate = useCallback(() => {
|
|
2099
2677
|
setEditing(null);
|
|
2100
2678
|
idTouchedRef.current = false;
|
|
2101
|
-
|
|
2679
|
+
workspaceTouchedRef.current = false;
|
|
2680
|
+
setDraft(Object.assign(emptyDraft(meta), { cwd: preferredWorkspacePath(workspaceSnapshot) }));
|
|
2102
2681
|
setFormError(null);
|
|
2682
|
+
setFormErrorField(null);
|
|
2103
2683
|
setFormOpen(true);
|
|
2104
|
-
}, [meta]);
|
|
2684
|
+
}, [meta, workspaceSnapshot]);
|
|
2105
2685
|
|
|
2106
2686
|
const openEdit = useCallback((job) => {
|
|
2107
2687
|
setEditing(job);
|
|
2108
2688
|
idTouchedRef.current = true;
|
|
2689
|
+
workspaceTouchedRef.current = true;
|
|
2109
2690
|
setDraft(draftFromJob(job));
|
|
2110
2691
|
setFormError(null);
|
|
2692
|
+
setFormErrorField(null);
|
|
2111
2693
|
setFormOpen(true);
|
|
2112
2694
|
}, []);
|
|
2113
2695
|
|
|
@@ -2115,10 +2697,21 @@ window.__ModuleLoader__.load({
|
|
|
2115
2697
|
setFormOpen(false);
|
|
2116
2698
|
setEditing(null);
|
|
2117
2699
|
setFormError(null);
|
|
2700
|
+
setFormErrorField(null);
|
|
2118
2701
|
}, []);
|
|
2119
2702
|
|
|
2703
|
+
useEffect(() => {
|
|
2704
|
+
if (!formOpen || editing !== null || workspaceTouchedRef.current || draft.cwd !== "") return;
|
|
2705
|
+
const preferred = preferredWorkspacePath(workspaceSnapshot);
|
|
2706
|
+
if (preferred === "") return;
|
|
2707
|
+
setDraft((previous) => previous.cwd === "" ? Object.assign({}, previous, { cwd: preferred }) : previous);
|
|
2708
|
+
}, [formOpen, editing, draft.cwd, workspaceSnapshot]);
|
|
2709
|
+
|
|
2120
2710
|
const handleDraftChange = useCallback(
|
|
2121
2711
|
(key, value) => {
|
|
2712
|
+
if (key === "cwd") workspaceTouchedRef.current = true;
|
|
2713
|
+
setFormError(null);
|
|
2714
|
+
setFormErrorField(null);
|
|
2122
2715
|
setDraft((previous) => {
|
|
2123
2716
|
let next = Object.assign({}, previous, { [key]: value });
|
|
2124
2717
|
if (key === "name" && editing === null && !idTouchedRef.current) {
|
|
@@ -2143,44 +2736,52 @@ window.__ModuleLoader__.load({
|
|
|
2143
2736
|
async (event) => {
|
|
2144
2737
|
event.preventDefault();
|
|
2145
2738
|
const creating = editing === null;
|
|
2739
|
+
const fail = (field, message) => {
|
|
2740
|
+
setFormErrorField(field);
|
|
2741
|
+
setFormError(message);
|
|
2742
|
+
};
|
|
2146
2743
|
|
|
2147
2744
|
const name = draft.name.trim();
|
|
2148
|
-
if (name === "") return
|
|
2745
|
+
if (name === "") return fail("name", "Give the automation a name.");
|
|
2149
2746
|
const cron = draft.cron.trim();
|
|
2150
|
-
if (cron === "") return
|
|
2747
|
+
if (cron === "") return fail("cron", "A cron expression is required, e.g. 0 9 * * 1-5.");
|
|
2748
|
+
if (cronFields(cron).length !== 5) {
|
|
2749
|
+
return fail("cron", "Cron needs exactly five fields: minute, hour, day, month, and weekday.");
|
|
2750
|
+
}
|
|
2151
2751
|
const timezone = draft.timezone.trim();
|
|
2152
|
-
if (timezone === "") return
|
|
2752
|
+
if (timezone === "") return fail("timezone", "A timezone is required.");
|
|
2153
2753
|
const cwd = draft.cwd.trim();
|
|
2154
|
-
if (cwd === "") return
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
return setFormError("The working directory must be an absolute filesystem path.");
|
|
2754
|
+
if (cwd === "") return fail("cwd", "A workspace is required.");
|
|
2755
|
+
if (!looksLikeAbsolutePath(cwd)) {
|
|
2756
|
+
return fail("cwd", "The workspace must be an absolute filesystem path.");
|
|
2158
2757
|
}
|
|
2159
2758
|
const prompt = draft.prompt.trim();
|
|
2160
|
-
if (prompt === "") return
|
|
2759
|
+
if (prompt === "") return fail("prompt", "A prompt is required.");
|
|
2161
2760
|
const timeoutMs = Number(draft.timeoutMs);
|
|
2162
2761
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < MIN_TIMEOUT_MS || timeoutMs > MAX_TIMEOUT_MS) {
|
|
2163
|
-
return
|
|
2762
|
+
return fail(
|
|
2763
|
+
"timeout",
|
|
2164
2764
|
"Timeout must be a whole number of milliseconds between " + MIN_TIMEOUT_MS + " and " + MAX_TIMEOUT_MS + ".",
|
|
2165
2765
|
);
|
|
2166
2766
|
}
|
|
2167
2767
|
const provider = draft.provider.trim();
|
|
2168
2768
|
const model = draft.model.trim();
|
|
2169
2769
|
if (provider !== "" && model === "") {
|
|
2170
|
-
return
|
|
2770
|
+
return fail("model", "Pick a model for the chosen provider (or leave both blank for the Harness default).");
|
|
2171
2771
|
}
|
|
2172
2772
|
if (provider === "" && model !== "") {
|
|
2173
|
-
return
|
|
2773
|
+
return fail("provider", "A model needs a provider (or leave both blank for the Harness default).");
|
|
2174
2774
|
}
|
|
2175
2775
|
let id = "";
|
|
2176
2776
|
if (creating) {
|
|
2177
2777
|
id = draft.id.trim().toLowerCase();
|
|
2178
2778
|
if (id !== "" && !JOB_ID_PATTERN.test(id)) {
|
|
2179
|
-
return
|
|
2779
|
+
return fail("id", "Job id must match [a-z0-9][a-z0-9-]{0,62} (lowercase letters, digits, hyphens).");
|
|
2180
2780
|
}
|
|
2181
2781
|
}
|
|
2182
2782
|
const spec = buildSpec(draft);
|
|
2183
2783
|
setFormError(null);
|
|
2784
|
+
setFormErrorField(null);
|
|
2184
2785
|
setBusyKey("save", true);
|
|
2185
2786
|
try {
|
|
2186
2787
|
if (creating) {
|
|
@@ -2198,7 +2799,9 @@ window.__ModuleLoader__.load({
|
|
|
2198
2799
|
flashMessage("success", creating ? "Automation created." : "Automation updated.");
|
|
2199
2800
|
closeForm();
|
|
2200
2801
|
} catch (error) {
|
|
2201
|
-
|
|
2802
|
+
const message = errMessage(error);
|
|
2803
|
+
setFormErrorField(inferFormErrorField(message));
|
|
2804
|
+
setFormError(message);
|
|
2202
2805
|
} finally {
|
|
2203
2806
|
setBusyKey("save", false);
|
|
2204
2807
|
}
|
|
@@ -2255,11 +2858,14 @@ window.__ModuleLoader__.load({
|
|
|
2255
2858
|
: null,
|
|
2256
2859
|
formOpen
|
|
2257
2860
|
? h(JobForm, {
|
|
2861
|
+
key: editing === null ? "create" : "edit:" + editing.id,
|
|
2258
2862
|
meta,
|
|
2863
|
+
workspaceSnapshot,
|
|
2259
2864
|
draft,
|
|
2260
2865
|
editing,
|
|
2261
2866
|
saving: busy.save === true,
|
|
2262
2867
|
error: formError,
|
|
2868
|
+
errorField: formErrorField,
|
|
2263
2869
|
onChange: handleDraftChange,
|
|
2264
2870
|
onIdTouched: handleIdTouched,
|
|
2265
2871
|
onSubmit: handleSubmit,
|
|
@@ -2425,7 +3031,7 @@ window.__ModuleLoader__.load({
|
|
|
2425
3031
|
);
|
|
2426
3032
|
}
|
|
2427
3033
|
|
|
2428
|
-
function AutomationsWorkspace({ disclosure }) {
|
|
3034
|
+
function AutomationsWorkspace({ disclosure, workspaceRuntime }) {
|
|
2429
3035
|
const workspaceRef = useRef(null);
|
|
2430
3036
|
const titleId = useId();
|
|
2431
3037
|
|
|
@@ -2473,7 +3079,7 @@ window.__ModuleLoader__.load({
|
|
|
2473
3079
|
"Exit Automations",
|
|
2474
3080
|
),
|
|
2475
3081
|
),
|
|
2476
|
-
h("div", { className: "dsh-auto-workspace-scroll" }, h(AutomationsSection, { centerMode: true })),
|
|
3082
|
+
h("div", { className: "dsh-auto-workspace-scroll" }, h(AutomationsSection, { centerMode: true, workspaceRuntime })),
|
|
2477
3083
|
);
|
|
2478
3084
|
}
|
|
2479
3085
|
|
|
@@ -2513,6 +3119,7 @@ window.__ModuleLoader__.load({
|
|
|
2513
3119
|
".dsh-auto-input::placeholder,.dsh-auto-textarea::placeholder{color:var(--dsh-auto-caption);}",
|
|
2514
3120
|
".dsh-auto-select option{background:var(--dsh-auto-input-bg);color:var(--dsh-auto-text);}",
|
|
2515
3121
|
".dsh-auto-input:focus-visible,.dsh-auto-select:focus-visible,.dsh-auto-textarea:focus-visible{outline:none;border-color:var(--dsh-auto-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsh-auto-accent) 18%,transparent);}",
|
|
3122
|
+
".dsh-auto-input[aria-invalid=\"true\"],.dsh-auto-select[aria-invalid=\"true\"],.dsh-auto-textarea[aria-invalid=\"true\"]{border-color:var(--dsh-auto-danger);}",
|
|
2516
3123
|
".dsh-auto-input:disabled,.dsh-auto-select:disabled,.dsh-auto-textarea:disabled{opacity:1;color:var(--dsh-auto-muted);background:var(--dsh-auto-surface-active);cursor:default;}",
|
|
2517
3124
|
".dsh-auto-picker-root{width:100%;display:flex;}",
|
|
2518
3125
|
".dsh-auto-picker-trigger{appearance:none;width:100%;height:34px;padding:0 9px;border:1px solid var(--dsh-auto-border);border-radius:8px;background:var(--dsh-auto-input-bg);color:var(--dsh-auto-text);display:flex;align-items:center;gap:8px;font-family:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer;}",
|
|
@@ -2558,16 +3165,50 @@ window.__ModuleLoader__.load({
|
|
|
2558
3165
|
".dsh-auto-permission-glyph-danger{color:var(--dsh-auto-danger);}",
|
|
2559
3166
|
".dsh-auto-permission-glyph-custom{color:var(--dsh-auto-muted);}",
|
|
2560
3167
|
".dsh-auto-textarea{resize:vertical;min-height:110px;}",
|
|
3168
|
+
".dsh-auto-prompt{min-height:142px;line-height:1.55;}",
|
|
2561
3169
|
".dsh-auto-check{display:inline-flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;min-height:34px;}",
|
|
2562
3170
|
".dsh-auto-check input{width:16px;height:16px;margin:0;accent-color:var(--dsh-auto-accent);cursor:pointer;}",
|
|
2563
|
-
".dsh-auto-form{border:
|
|
2564
|
-
".dsh-auto-form
|
|
2565
|
-
".dsh-auto-
|
|
2566
|
-
".dsh-auto-
|
|
3171
|
+
".dsh-auto-form{box-sizing:border-box;width:min(100%,760px);margin:0 auto;padding:0 0 4px;background:transparent;display:flex;flex-direction:column;gap:16px;}",
|
|
3172
|
+
".dsh-auto-form-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:2px 2px 0;}",
|
|
3173
|
+
".dsh-auto-form-heading{min-width:0;display:flex;flex-direction:column;gap:3px;}",
|
|
3174
|
+
".dsh-auto-form h3{margin:0;font-size:17px;font-weight:600;line-height:24px;}",
|
|
3175
|
+
".dsh-auto-form-heading p{margin:0;color:var(--dsh-auto-muted);font-size:12px;line-height:18px;}",
|
|
3176
|
+
".dsh-auto-form-enabled{flex:none;margin-top:2px;padding:5px 8px;border:1px solid var(--dsh-auto-border);border-radius:999px;background:var(--dsh-auto-surface);}",
|
|
3177
|
+
".dsh-auto-form-section,.dsh-auto-advanced{border:1px solid var(--dsh-auto-border);border-radius:14px;background:var(--dsh-auto-surface);box-shadow:0 1px 1px color-mix(in srgb,var(--dsh-auto-text) 4%,transparent);}",
|
|
3178
|
+
".dsh-auto-form-section{padding:16px;display:flex;flex-direction:column;gap:15px;}",
|
|
3179
|
+
".dsh-auto-form-section-head{display:flex;align-items:flex-start;gap:10px;}",
|
|
3180
|
+
".dsh-auto-form-section-icon{width:30px;height:30px;flex:none;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsh-auto-accent);background:color-mix(in srgb,var(--dsh-auto-accent) 10%,transparent);}",
|
|
3181
|
+
".dsh-auto-form-section-copy{min-width:0;display:flex;flex-direction:column;gap:1px;}",
|
|
3182
|
+
".dsh-auto-form-section-title{margin:0;color:var(--dsh-auto-text);font-size:14px;font-weight:600;line-height:20px;}",
|
|
3183
|
+
".dsh-auto-form-section-description{margin:0;color:var(--dsh-auto-muted);font-size:11px;line-height:17px;}",
|
|
3184
|
+
".dsh-auto-formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 12px;}",
|
|
3185
|
+
".dsh-auto-field{display:flex;flex-direction:column;gap:5px;min-width:0;}",
|
|
2567
3186
|
".dsh-auto-field-full{grid-column:1/-1;}",
|
|
2568
3187
|
".dsh-auto-label{font-size:12px;font-weight:500;color:var(--dsh-auto-text-secondary);}",
|
|
2569
3188
|
".dsh-auto-required{color:var(--dsh-auto-danger);}",
|
|
2570
3189
|
".dsh-auto-hint{margin:0;font-size:11px;line-height:1.45;color:var(--dsh-auto-muted);}",
|
|
3190
|
+
".dsh-auto-schedule-modes{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}",
|
|
3191
|
+
".dsh-auto-schedule-summary{min-height:42px;padding:9px 11px;border:1px solid color-mix(in srgb,var(--dsh-auto-accent) 20%,var(--dsh-auto-border));border-radius:10px;background:color-mix(in srgb,var(--dsh-auto-accent) 6%,var(--dsh-auto-input-bg));display:flex;align-items:center;justify-content:space-between;gap:10px;}",
|
|
3192
|
+
".dsh-auto-schedule-summary-copy{min-width:0;color:var(--dsh-auto-text-secondary);font-size:12px;font-weight:500;line-height:18px;}",
|
|
3193
|
+
".dsh-auto-schedule-expression{min-width:0;max-width:100%;flex:none;padding:2px 6px;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsh-auto-muted);font-size:11px;line-height:17px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
|
|
3194
|
+
".dsh-auto-cron-input{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums;letter-spacing:.02em;}",
|
|
3195
|
+
".dsh-auto-cron-guide{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:6px;margin-top:3px;}",
|
|
3196
|
+
".dsh-auto-cron-guide-field{min-width:0;padding:7px 6px;border:1px solid var(--dsh-auto-border);border-radius:8px;background:var(--dsh-auto-input-bg);text-align:center;display:flex;flex-direction:column;align-items:center;gap:1px;}",
|
|
3197
|
+
".dsh-auto-cron-guide-value{max-width:100%;overflow:hidden;text-overflow:ellipsis;color:var(--dsh-auto-text);font-size:12px;font-weight:600;line-height:17px;}",
|
|
3198
|
+
".dsh-auto-cron-guide-label{color:var(--dsh-auto-text-secondary);font-size:10px;font-weight:500;line-height:14px;}",
|
|
3199
|
+
".dsh-auto-cron-guide-range{color:var(--dsh-auto-caption);font-size:9px;line-height:12px;}",
|
|
3200
|
+
".dsh-auto-cron-syntax{margin:0;color:var(--dsh-auto-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;line-height:16px;}",
|
|
3201
|
+
".dsh-auto-advanced{overflow:hidden;}",
|
|
3202
|
+
".dsh-auto-advanced-trigger{box-sizing:border-box;width:100%;min-height:62px;padding:13px 15px;border:0;background:transparent;color:inherit;text-align:left;font:inherit;display:flex;align-items:center;gap:10px;cursor:pointer;}",
|
|
3203
|
+
".dsh-auto-advanced-trigger:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));}",
|
|
3204
|
+
".dsh-auto-advanced-trigger:focus-visible{outline:2px solid var(--dsh-auto-accent);outline-offset:-2px;border-radius:13px;}",
|
|
3205
|
+
".dsh-auto-advanced-copy{min-width:0;flex:1;display:flex;flex-direction:column;gap:1px;}",
|
|
3206
|
+
".dsh-auto-advanced-summary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsh-auto-muted);font-size:11px;line-height:17px;}",
|
|
3207
|
+
".dsh-auto-advanced-chevron{flex:none;color:var(--dsh-auto-muted);transition:transform .15s ease;}",
|
|
3208
|
+
".dsh-auto-advanced-chevron-open{transform:rotate(180deg);}",
|
|
3209
|
+
".dsh-auto-advanced-content{margin:0 15px;padding:15px 0;border-top:1px solid var(--dsh-auto-border);}",
|
|
3210
|
+
".dsh-auto-advanced-content[hidden]{display:none;}",
|
|
3211
|
+
".dsh-auto-form-footer{position:sticky;z-index:4;bottom:8px;align-self:flex-end;max-width:100%;padding:9px 10px;border:1px solid var(--dsh-auto-border);border-radius:12px;background:var(--dsh-auto-surface);box-shadow:var(--dsw-shadow-lv2,0 6px 18px rgba(15,17,21,.12));display:flex;flex-direction:column;gap:8px;}",
|
|
2571
3212
|
".dsh-auto-formerror{margin:0;padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--dsh-auto-danger) 10%,transparent);color:var(--dsh-auto-danger);font-size:12px;line-height:1.45;}",
|
|
2572
3213
|
".dsh-auto-formactions{display:flex;gap:8px;justify-content:flex-end;align-items:center;flex-wrap:wrap;}",
|
|
2573
3214
|
".dsh-auto-jobs{display:flex;flex-direction:column;gap:10px;}",
|
|
@@ -2617,8 +3258,8 @@ window.__ModuleLoader__.load({
|
|
|
2617
3258
|
"@keyframes dsh-auto-pulse{0%,100%{opacity:1}50%{opacity:.35}}",
|
|
2618
3259
|
".dsh-auto-empty,.dsh-auto-loading,.dsh-auto-error{padding:22px 16px;text-align:center;border:1px dashed var(--dsh-auto-border);border-radius:12px;font-size:13px;color:var(--dsh-auto-text-secondary);}",
|
|
2619
3260
|
".dsh-auto-error{color:var(--dsh-auto-danger);display:flex;flex-direction:column;gap:10px;align-items:center;}",
|
|
2620
|
-
"@container dsh-auto-workspace (max-width:680px){.dsh-auto-formgrid{grid-template-columns:1fr;}.dsh-auto-card-head{flex-direction:column;align-items:flex-start;}.dsh-auto-run-name{max-width:150px;}}",
|
|
2621
|
-
"@media (max-width:640px){.dsh-auto-workspace-toolbar{padding-inline:8px;}.dsh-auto-workspace-scroll{padding:16px 14px 28px;}.dsh-auto-formgrid{grid-template-columns:1fr;}.dsh-auto-card-head{flex-direction:column;align-items:flex-start;}.dsh-auto-run-name{max-width:150px;}}",
|
|
3261
|
+
"@container dsh-auto-workspace (max-width:680px){.dsh-auto-formgrid{grid-template-columns:1fr;}.dsh-auto-form-header{align-items:center;}.dsh-auto-schedule-summary{align-items:flex-start;flex-direction:column;}.dsh-auto-card-head{flex-direction:column;align-items:flex-start;}.dsh-auto-run-name{max-width:150px;}}",
|
|
3262
|
+
"@media (max-width:640px){.dsh-auto-workspace-toolbar{padding-inline:8px;}.dsh-auto-workspace-scroll{padding:16px 14px 28px;}.dsh-auto-formgrid{grid-template-columns:1fr;}.dsh-auto-form-header{align-items:center;}.dsh-auto-form-section{padding:14px;}.dsh-auto-advanced-trigger{padding:12px 13px;}.dsh-auto-advanced-content{margin-inline:13px;}.dsh-auto-schedule-summary{align-items:flex-start;flex-direction:column;}.dsh-auto-cron-guide{gap:4px;}.dsh-auto-cron-guide-field{padding-inline:3px;}.dsh-auto-card-head{flex-direction:column;align-items:flex-start;}.dsh-auto-run-name{max-width:150px;}}",
|
|
2622
3263
|
].join("\n");
|
|
2623
3264
|
|
|
2624
3265
|
function apply(ctx) {
|
|
@@ -2637,7 +3278,7 @@ window.__ModuleLoader__.load({
|
|
|
2637
3278
|
disposeCenter = ctx.slots.register({
|
|
2638
3279
|
name: "conversation",
|
|
2639
3280
|
priority: -200,
|
|
2640
|
-
inject: () => ({ disclosure }),
|
|
3281
|
+
inject: () => ({ disclosure, workspaceRuntime: ctx.workspaces }),
|
|
2641
3282
|
}, AutomationsWorkspace);
|
|
2642
3283
|
} catch (error) {
|
|
2643
3284
|
console.error("dsh automations: could not mount center workspace", error);
|
|
@@ -2681,6 +3322,7 @@ window.__ModuleLoader__.load({
|
|
|
2681
3322
|
id: "automations",
|
|
2682
3323
|
order: 25,
|
|
2683
3324
|
label: "Automations",
|
|
3325
|
+
inject: () => ({ workspaceRuntime: ctx.workspaces }),
|
|
2684
3326
|
}, AutomationsSection));
|
|
2685
3327
|
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
2686
3328
|
name: "sidebar.footer.action",
|
|
@@ -2695,13 +3337,26 @@ window.__ModuleLoader__.load({
|
|
|
2695
3337
|
exports.permissionPresetPresentation = permissionPresetPresentation;
|
|
2696
3338
|
exports.PermissionPresetPicker = PermissionPresetPicker;
|
|
2697
3339
|
exports.EditableCombobox = EditableCombobox;
|
|
3340
|
+
exports.ScheduleEditor = ScheduleEditor;
|
|
3341
|
+
exports.WorkspacePicker = WorkspacePicker;
|
|
2698
3342
|
exports.ProviderPicker = ProviderPicker;
|
|
2699
3343
|
exports.ReasoningEffortPicker = ReasoningEffortPicker;
|
|
2700
3344
|
exports.TimeZonePicker = TimeZonePicker;
|
|
2701
3345
|
exports.__testing = Object.freeze({
|
|
2702
3346
|
nextFormInstancePrefix,
|
|
2703
3347
|
canonicalTimezone,
|
|
3348
|
+
cronFields,
|
|
3349
|
+
simpleSchedule,
|
|
3350
|
+
scheduleControlValues,
|
|
3351
|
+
cronForSimpleSchedule,
|
|
3352
|
+
scheduleModeTransition,
|
|
3353
|
+
describeSimpleSchedule,
|
|
3354
|
+
inferFormErrorField,
|
|
3355
|
+
overlapPolicySummary,
|
|
2704
3356
|
comboboxSearchResults,
|
|
3357
|
+
looksLikeAbsolutePath,
|
|
3358
|
+
normalizedWorkspaces,
|
|
3359
|
+
preferredWorkspacePath,
|
|
2705
3360
|
normalizedProviders,
|
|
2706
3361
|
providerFallbackName,
|
|
2707
3362
|
reasoningEffortsForState,
|