@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/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: "skip", hint: "Skip the run if a previous run is still active." },
137
- { value: "queue", label: "queue", hint: "Defer the run until the previous run finishes." },
138
- { value: "allow", label: "allow", hint: "Run concurrently with any active run." },
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: "skip", hint: "Drop occurrences that were missed while the scheduler was down." },
142
- { value: "run-once", label: "run-once", hint: "Run once after downtime for the latest missed occurrence." },
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
- "div",
1430
- { className: "dsh-auto-formgrid" },
1922
+ "header",
1923
+ { className: "dsh-auto-form-header" },
1431
1924
  h(
1432
- Field,
1433
- { label: "Name", htmlFor: fieldId("name"), required: true },
1434
- h("input", {
1435
- id: fieldId("name"),
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
- Field,
1475
- { label: "Cron", htmlFor: fieldId("cron"), required: true, hint: "Five-field cron (minute hour day-of-month month day-of-week)." },
1931
+ "label",
1932
+ { className: "dsh-auto-switch dsh-auto-form-enabled" },
1476
1933
  h("input", {
1477
- id: fieldId("cron"),
1478
- className: "dsh-auto-input",
1479
- type: "text",
1480
- value: draft.cron,
1481
- spellCheck: false,
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
- Field,
1501
- { label: "Enabled", htmlFor: fieldId("enabled") },
1952
+ "div",
1953
+ { className: "dsh-auto-formgrid" },
1502
1954
  h(
1503
- "label",
1504
- { className: "dsh-auto-check" },
1955
+ Field,
1956
+ { label: "Name", htmlFor: fieldId("name"), required: true, full: true },
1505
1957
  h("input", {
1506
- id: fieldId("enabled"),
1507
- type: "checkbox",
1508
- checked: draft.enabled,
1509
- onChange: (event) => onChange("enabled", event.target.checked),
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
- "datalist",
1562
- { id: fieldId("model-list") },
1563
- models.map((model) => h("option", {
1564
- key: model.id,
1565
- value: model.id,
1566
- label: model.name === model.id ? undefined : model.name,
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
- Field,
1572
- { label: "Reasoning effort", htmlFor: fieldId("effort"), hint: "Default follows the selected model. Custom adapter-owned IDs remain supported." },
1573
- h(ReasoningEffortPicker, {
1574
- id: fieldId("effort"),
1575
- value: draft.reasoningEffort,
1576
- provider: draft.provider,
1577
- model: draft.model,
1578
- defaultModel: meta?.defaultModel,
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
- Field,
1609
- { label: "Overlap policy", htmlFor: fieldId("overlap"), full: true, hint: "What happens when a scheduled run fires while another run for the same job is still active." },
2029
+ "div",
2030
+ { className: "dsh-auto-formgrid" },
1610
2031
  h(
1611
- "select",
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
- id: fieldId("overlap"),
1614
- className: "dsh-auto-select",
1615
- value: draft.overlap,
1616
- onChange: (event) => onChange("overlap", event.target.value),
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
- OVERLAP_OPTIONS.map((option) => h("option", { key: option.value, value: option.value }, option.label)),
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
- "select",
2096
+ Field,
1626
2097
  {
1627
- id: fieldId("misfire"),
1628
- className: "dsh-auto-select",
1629
- value: draft.misfire,
1630
- onChange: (event) => onChange("misfire", event.target.value),
2098
+ label: "Permission preset",
2099
+ htmlFor: fieldId("permission"),
2100
+ required: true,
2101
+ full: true,
2102
+ hint: permissionPresetPresentation(draft.permissionPreset).detail,
1631
2103
  },
1632
- MISFIRE_OPTIONS.map((option) => h("option", { key: option.value, value: option.value }, option.label)),
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
- "div",
1665
- { className: "dsh-auto-formactions" },
2114
+ "section",
2115
+ { className: "dsh-auto-advanced", "data-open": advancedOpen ? "true" : undefined },
1666
2116
  h(
1667
- Button,
2117
+ "button",
1668
2118
  {
1669
- type: "submit",
1670
- variant: "primary",
1671
- icon: saving ? null : h(IconCheckOutline16),
1672
- disabled: saving,
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
- saving ? "Saving\u2026" : creating ? "Create automation" : "Save changes",
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
- Button,
1678
- { type: "button", variant: "outline", onClick: onCancel, disabled: saving },
1679
- "Cancel",
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 AutomationsSection({ centerMode = false } = {}) {
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
- setDraft(emptyDraft(meta));
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 setFormError("Give the automation a name.");
2745
+ if (name === "") return fail("name", "Give the automation a name.");
2149
2746
  const cron = draft.cron.trim();
2150
- if (cron === "") return setFormError("A cron expression is required, e.g. 0 9 * * 1-5.");
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 setFormError("A timezone is required.");
2752
+ if (timezone === "") return fail("timezone", "A timezone is required.");
2153
2753
  const cwd = draft.cwd.trim();
2154
- if (cwd === "") return setFormError("The working directory is required.");
2155
- const looksAbsolute = cwd.charAt(0) === "/" || /^[A-Za-z]:[\\/]/.test(cwd) || cwd.startsWith("\\\\");
2156
- if (!looksAbsolute) {
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 setFormError("A prompt is required.");
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 setFormError(
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 setFormError("Pick a model for the chosen provider (or leave both blank for the Harness default).");
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 setFormError("A model needs a provider (or leave both blank for the Harness default).");
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 setFormError("Job id must match [a-z0-9][a-z0-9-]{0,62} (lowercase letters, digits, hyphens).");
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
- setFormError(errMessage(error));
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:1px solid var(--dsh-auto-border);border-radius:14px;padding:16px;background:var(--dsh-auto-surface);display:flex;flex-direction:column;gap:14px;}",
2564
- ".dsh-auto-form h3{margin:0;font-size:15px;font-weight:600;}",
2565
- ".dsh-auto-formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;}",
2566
- ".dsh-auto-field{display:flex;flex-direction:column;gap:4px;min-width:0;}",
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,