@syncended/dsh-automations 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/lib/client.js +533 -33
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -56,7 +56,7 @@ dsh plugin --profile web remove @syncended/dsh-automations
56
56
  |---|---|
57
57
  | Name | Human-readable job name. |
58
58
  | Cron | Five fields: `minute hour day-of-month month day-of-week`. Example: `0 9 * * 1-5`. |
59
- | Timezone | `UTC` or an IANA name such as `Europe/Berlin`. DST is handled by `cron-parser`. |
59
+ | Time zone | Search by city or region in the picker, or enter `UTC` / an IANA name such as `Europe/Berlin`. Current UTC offsets are shown; DST is handled by `cron-parser`. |
60
60
  | Project | Existing absolute directory. Its canonical filesystem identity becomes the Session cwd and `workspace-write` root. |
61
61
  | Prompt | The user message sent to a fresh Harness Agent. |
62
62
  | Provider / model | Leave both blank to resolve the current Harness default at dispatch time. Set both to pin a route. |
package/lib/client.js CHANGED
@@ -11,6 +11,7 @@ window.__ModuleLoader__.load({
11
11
  // the same-origin HTTP API at /api/automations; every mutation carries the
12
12
  // x-dsh-automation-client fence header plus an application/json body.
13
13
  const React = require("react");
14
+ const { createPortal } = require("react-dom");
14
15
  const h = React.createElement;
15
16
  const { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } = React;
16
17
  const {
@@ -23,9 +24,11 @@ window.__ModuleLoader__.load({
23
24
  IconChevronDownOutline14,
24
25
  IconChevronLeftOutline14,
25
26
  IconEditOutline16,
27
+ IconGlobeOutline14,
26
28
  IconPlayOutline16,
27
29
  IconPlusOutline16,
28
30
  IconRefreshOutline16,
31
+ IconSearchOutline16,
29
32
  IconSettingsOutline16,
30
33
  IconTrashOutline16,
31
34
  IconWarningOutline16,
@@ -40,6 +43,7 @@ window.__ModuleLoader__.load({
40
43
  const MIN_TIMEOUT_MS = 1000;
41
44
  const MAX_TIMEOUT_MS = 86400000;
42
45
  const JOB_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
46
+ let formInstanceSerial = 0;
43
47
 
44
48
  const BROWSER_TIMEZONE = (() => {
45
49
  try {
@@ -49,24 +53,75 @@ window.__ModuleLoader__.load({
49
53
  }
50
54
  })();
51
55
 
52
- const TIMEZONES = Array.from(
53
- new Set([
54
- BROWSER_TIMEZONE,
55
- "UTC",
56
- "America/New_York",
57
- "America/Chicago",
58
- "America/Denver",
59
- "America/Los_Angeles",
60
- "Europe/London",
61
- "Europe/Paris",
62
- "Europe/Berlin",
63
- "Asia/Shanghai",
64
- "Asia/Tokyo",
65
- "Asia/Singapore",
66
- "Asia/Kolkata",
67
- "Australia/Sydney",
68
- ]),
56
+ const POPULAR_TIMEZONE_CANDIDATES = [
57
+ BROWSER_TIMEZONE,
58
+ "UTC",
59
+ "America/New_York",
60
+ "America/Los_Angeles",
61
+ "America/Sao_Paulo",
62
+ "Europe/London",
63
+ "Europe/Berlin",
64
+ "Europe/Moscow",
65
+ "Asia/Dubai",
66
+ "Asia/Kolkata",
67
+ "Asia/Shanghai",
68
+ "Asia/Tokyo",
69
+ "Australia/Sydney",
70
+ ];
71
+ const PREFERRED_TIMEZONE_ALIASES = [
72
+ "Africa/Asmara",
73
+ "America/Argentina/Buenos_Aires",
74
+ "America/Atikokan",
75
+ "America/Indiana/Indianapolis",
76
+ "America/Kentucky/Louisville",
77
+ "America/Nuuk",
78
+ "Asia/Ho_Chi_Minh",
79
+ "Asia/Kathmandu",
80
+ "Asia/Kolkata",
81
+ "Asia/Yangon",
82
+ "Atlantic/Faroe",
83
+ "Europe/Kyiv",
84
+ "Pacific/Chuuk",
85
+ "Pacific/Kanton",
86
+ "Pacific/Pohnpei",
87
+ ];
88
+ const TIMEZONE_ALIASES_BY_ZONE = new Map();
89
+ for (const alias of PREFERRED_TIMEZONE_ALIASES) {
90
+ const zone = canonicalTimezone(alias);
91
+ if (!zone || zone === alias) continue;
92
+ const aliases = TIMEZONE_ALIASES_BY_ZONE.get(zone) || [];
93
+ aliases.push(alias);
94
+ TIMEZONE_ALIASES_BY_ZONE.set(zone, aliases);
95
+ }
96
+ const POPULAR_TIMEZONES = Array.from(
97
+ new Set(POPULAR_TIMEZONE_CANDIDATES.map(canonicalTimezone).filter(Boolean)),
69
98
  );
99
+ const SUPPORTED_TIMEZONES = (() => {
100
+ let discovered = [];
101
+ try {
102
+ discovered = typeof Intl.supportedValuesOf === "function"
103
+ ? Intl.supportedValuesOf("timeZone")
104
+ : [];
105
+ } catch {
106
+ discovered = [];
107
+ }
108
+ return Array.from(
109
+ new Set([
110
+ ...POPULAR_TIMEZONES,
111
+ ...TIMEZONE_ALIASES_BY_ZONE.keys(),
112
+ ...discovered.filter((zone) => typeof zone === "string" && zone !== ""),
113
+ ]),
114
+ ).sort((left, right) => left.localeCompare(right));
115
+ })();
116
+ const TIMEZONE_SEARCH_INDEX = SUPPORTED_TIMEZONES.map((zone) => {
117
+ const aliases = TIMEZONE_ALIASES_BY_ZONE.get(zone) || [];
118
+ const preferred = aliases[0] || zone;
119
+ return {
120
+ zone,
121
+ city: timezoneCityLabel(preferred),
122
+ search: timezoneSearchKey([zone, ...aliases].join(" ")),
123
+ };
124
+ });
70
125
 
71
126
  const REASONING_EFFORTS = ["low", "medium", "high"];
72
127
  const OVERLAP_OPTIONS = [
@@ -146,6 +201,110 @@ window.__ModuleLoader__.load({
146
201
  });
147
202
  }
148
203
 
204
+ function canonicalTimezone(value) {
205
+ const timezone = String(value || "").trim();
206
+ if (timezone === "") return null;
207
+ try {
208
+ return new Intl.DateTimeFormat("en-US", { timeZone: timezone }).resolvedOptions().timeZone;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function timezoneCityLabel(zone) {
215
+ if (zone === "UTC") return "UTC";
216
+ const segments = zone.split("/");
217
+ return (segments[segments.length - 1] || zone).replace(/_/g, " ");
218
+ }
219
+
220
+ function timezonePreferredValue(zone) {
221
+ return TIMEZONE_ALIASES_BY_ZONE.get(zone)?.[0] || zone;
222
+ }
223
+
224
+ function timezoneSearchKey(zone) {
225
+ const literal = zone.toLowerCase();
226
+ const expanded = literal
227
+ .normalize("NFKD")
228
+ .replace(/[\u0300-\u036f]/g, "")
229
+ .replace(/[\/_\-.]+/g, " ")
230
+ .replace(/\s+/g, " ")
231
+ .trim();
232
+ return expanded;
233
+ }
234
+
235
+ function timezoneOffsetLabel(zone, at = new Date()) {
236
+ try {
237
+ const formatter = new Intl.DateTimeFormat("en-US", {
238
+ timeZone: zone,
239
+ timeZoneName: "longOffset",
240
+ hour: "2-digit",
241
+ });
242
+ const value = formatter.formatToParts(at).find((part) => part.type === "timeZoneName")?.value;
243
+ if (!value) return "UTC offset unavailable";
244
+ if (value === "GMT") return "UTC+00:00";
245
+ return value.replace(/^GMT/, "UTC");
246
+ } catch {
247
+ return "UTC offset unavailable";
248
+ }
249
+ }
250
+
251
+ function timezoneSearchResults(query, currentValue, limit = 30) {
252
+ const rawQuery = String(query || "").trim().toLowerCase();
253
+ const current = canonicalTimezone(currentValue);
254
+ if (rawQuery === "") {
255
+ return Array.from(
256
+ new Set([current, BROWSER_TIMEZONE, ...POPULAR_TIMEZONES].filter(Boolean)),
257
+ ).slice(0, 12);
258
+ }
259
+ const normalizedQuery = timezoneSearchKey(rawQuery);
260
+ if (normalizedQuery === "") return [];
261
+ const terms = normalizedQuery.split(" ").filter(Boolean);
262
+ return TIMEZONE_SEARCH_INDEX
263
+ .filter((entry) => terms.every((term) => entry.search.includes(term)))
264
+ .map((entry) => {
265
+ const canonical = entry.zone.toLowerCase();
266
+ const preferred = timezonePreferredValue(entry.zone).toLowerCase();
267
+ const city = timezoneSearchKey(entry.city);
268
+ let score = 5;
269
+ if (canonical === rawQuery || preferred === rawQuery) score = 0;
270
+ else if (city === normalizedQuery) score = 1;
271
+ else if (city.startsWith(normalizedQuery)) score = 2;
272
+ else if (canonical.split("/").some((segment) => timezoneSearchKey(segment).startsWith(normalizedQuery))) score = 3;
273
+ else if (entry.search.startsWith(normalizedQuery)) score = 4;
274
+ return { ...entry, score };
275
+ })
276
+ .sort((left, right) => left.score - right.score || left.city.localeCompare(right.city) || left.zone.localeCompare(right.zone))
277
+ .slice(0, limit)
278
+ .map((entry) => entry.zone);
279
+ }
280
+
281
+ function timezoneNavigationIndex(key, current, resultCount, reopening) {
282
+ if (resultCount === 0) return -1;
283
+ if (reopening) return key === "ArrowDown" ? 0 : resultCount - 1;
284
+ if (key === "ArrowDown") return current < 0 || current >= resultCount - 1 ? 0 : current + 1;
285
+ return current <= 0 ? resultCount - 1 : current - 1;
286
+ }
287
+
288
+ function timezoneOptionPresentation(zone, offsetCache) {
289
+ let offset = offsetCache?.get(zone);
290
+ if (!offset) {
291
+ offset = timezoneOffsetLabel(zone);
292
+ offsetCache?.set(zone, offset);
293
+ }
294
+ const current = BROWSER_TIMEZONE === zone;
295
+ const preferred = timezonePreferredValue(zone);
296
+ if (zone === "UTC") {
297
+ return {
298
+ label: "UTC",
299
+ detail: offset + " now · Coordinated Universal Time" + (current ? " · Your browser time zone" : ""),
300
+ };
301
+ }
302
+ return {
303
+ label: timezoneCityLabel(preferred),
304
+ detail: preferred + " · " + offset + " now" + (current ? " · Your browser time zone" : ""),
305
+ };
306
+ }
307
+
149
308
  function browserTimezone() {
150
309
  return BROWSER_TIMEZONE;
151
310
  }
@@ -445,6 +604,310 @@ window.__ModuleLoader__.load({
445
604
  );
446
605
  }
447
606
 
607
+ function useTimeZonePopoverLayout(open, anchorRef, panelRef) {
608
+ const [layout, setLayout] = useState(null);
609
+ useEffect(() => {
610
+ if (!open) {
611
+ setLayout(null);
612
+ return undefined;
613
+ }
614
+ const place = () => {
615
+ const anchor = anchorRef.current;
616
+ if (!anchor) return;
617
+ const rect = anchor.getBoundingClientRect();
618
+ const panel = panelRef.current;
619
+ const margin = 12;
620
+ const gap = 4;
621
+ const width = Math.min(Math.max(rect.width, 280), window.innerWidth - margin * 2);
622
+ const left = Math.min(Math.max(rect.left, margin), window.innerWidth - width - margin);
623
+ const desiredHeight = Math.min(360, panel?.scrollHeight || 360);
624
+ const below = window.innerHeight - rect.bottom - gap - margin;
625
+ const above = rect.top - gap - margin;
626
+ const side = below >= Math.min(desiredHeight, 220) || below >= above ? "bottom" : "top";
627
+ const available = Math.max(80, side === "bottom" ? below : above);
628
+ const maxHeight = Math.min(360, available);
629
+ const renderedHeight = Math.min(panel?.offsetHeight || desiredHeight, maxHeight);
630
+ const top = side === "bottom"
631
+ ? rect.bottom + gap
632
+ : Math.max(margin, rect.top - gap - renderedHeight);
633
+ setLayout((current) => {
634
+ const next = { left, top, width, maxHeight, side };
635
+ return current
636
+ && current.left === next.left
637
+ && current.top === next.top
638
+ && current.width === next.width
639
+ && current.maxHeight === next.maxHeight
640
+ && current.side === next.side
641
+ ? current
642
+ : next;
643
+ });
644
+ };
645
+ const frame = requestAnimationFrame(place);
646
+ window.addEventListener("scroll", place, true);
647
+ window.addEventListener("resize", place);
648
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(place);
649
+ if (anchorRef.current) observer?.observe(anchorRef.current);
650
+ if (panelRef.current) observer?.observe(panelRef.current);
651
+ return () => {
652
+ cancelAnimationFrame(frame);
653
+ observer?.disconnect();
654
+ window.removeEventListener("scroll", place, true);
655
+ window.removeEventListener("resize", place);
656
+ };
657
+ }, [open, anchorRef, panelRef]);
658
+ return layout;
659
+ }
660
+
661
+ function TimeZonePicker({ id, value, onChange }) {
662
+ const [open, setOpen] = useState(false);
663
+ const [searching, setSearching] = useState(false);
664
+ const [activeIndex, setActiveIndex] = useState(-1);
665
+ const [announcement, setAnnouncement] = useState("");
666
+ const anchorRef = useRef(null);
667
+ const inputRef = useRef(null);
668
+ const panelRef = useRef(null);
669
+ const offsetCacheRef = useRef(new Map());
670
+ const listboxId = id + "-options";
671
+ const query = searching ? value : "";
672
+ const results = timezoneSearchResults(query, value);
673
+ const active = activeIndex >= 0 && activeIndex < results.length ? activeIndex : -1;
674
+ const selected = canonicalTimezone(value);
675
+ const resumeSearch = value.trim() !== "" && selected === null;
676
+ const popoverLayout = useTimeZonePopoverLayout(open, anchorRef, panelRef);
677
+ const close = () => {
678
+ setOpen(false);
679
+ setSearching(false);
680
+ setActiveIndex(-1);
681
+ setAnnouncement("");
682
+ };
683
+ const show = () => {
684
+ offsetCacheRef.current.clear();
685
+ setAnnouncement("");
686
+ setOpen(true);
687
+ };
688
+ const choose = (zone) => {
689
+ onChange(timezonePreferredValue(zone));
690
+ close();
691
+ requestAnimationFrame(() => inputRef.current?.focus());
692
+ };
693
+
694
+ useEffect(() => {
695
+ if (!open) return undefined;
696
+ const dismiss = (event) => {
697
+ const target = event.target;
698
+ if (!(target instanceof Node)) return;
699
+ if (anchorRef.current?.contains(target) || panelRef.current?.contains(target)) return;
700
+ close();
701
+ };
702
+ document.addEventListener("pointerdown", dismiss, true);
703
+ return () => document.removeEventListener("pointerdown", dismiss, true);
704
+ }, [open]);
705
+
706
+ useEffect(() => {
707
+ if (!open) return undefined;
708
+ const timer = window.setTimeout(() => {
709
+ setAnnouncement(
710
+ results.length === 0
711
+ ? "No matching time zones."
712
+ : results.length + (results.length === 1 ? " time zone suggestion available." : " time zone suggestions available."),
713
+ );
714
+ }, 250);
715
+ return () => window.clearTimeout(timer);
716
+ }, [open, query, results.length]);
717
+
718
+ useEffect(() => {
719
+ if (!open || active < 0) return;
720
+ document.getElementById(listboxId + "-option-" + active)?.scrollIntoView({ block: "nearest" });
721
+ }, [open, active, listboxId]);
722
+
723
+ const onInputKeyDown = (event) => {
724
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
725
+ event.preventDefault();
726
+ const navigationResults = !open && resumeSearch
727
+ ? timezoneSearchResults(value, value)
728
+ : results;
729
+ if (!open) {
730
+ setSearching(resumeSearch);
731
+ show();
732
+ }
733
+ if (navigationResults.length === 0) {
734
+ setActiveIndex(-1);
735
+ return;
736
+ }
737
+ setActiveIndex((current) =>
738
+ timezoneNavigationIndex(event.key, current, navigationResults.length, !open),
739
+ );
740
+ return;
741
+ }
742
+ if (event.key === "Enter" && open) {
743
+ event.preventDefault();
744
+ if (active >= 0) {
745
+ choose(results[active]);
746
+ return;
747
+ }
748
+ const canonical = canonicalTimezone(value);
749
+ if (canonical) choose(canonical);
750
+ else setAnnouncement("Enter a valid IANA time zone, such as Europe/Berlin.");
751
+ return;
752
+ }
753
+ if (event.key === "Escape" && open) {
754
+ event.preventDefault();
755
+ event.stopPropagation();
756
+ (event.nativeEvent || event).__dshAutomationsNested = true;
757
+ close();
758
+ return;
759
+ }
760
+ if (event.key === "Tab" && open) close();
761
+ };
762
+
763
+ const panel = open
764
+ ? h(
765
+ "div",
766
+ {
767
+ ref: panelRef,
768
+ className: "dsh-auto-timezone-popover",
769
+ style: popoverLayout
770
+ ? {
771
+ left: popoverLayout.left,
772
+ top: popoverLayout.top,
773
+ width: popoverLayout.width,
774
+ maxHeight: popoverLayout.maxHeight,
775
+ }
776
+ : { left: 12, top: 12, width: 320, visibility: "hidden" },
777
+ "data-side": popoverLayout?.side,
778
+ "data-dsh-auto-timezone-popover": "true",
779
+ },
780
+ h(
781
+ "div",
782
+ { className: "dsh-auto-timezone-popover-title" },
783
+ searching && query.trim() !== "" ? "Matching time zones" : "Suggested time zones",
784
+ ),
785
+ h(
786
+ "div",
787
+ {
788
+ id: listboxId,
789
+ className: "dsh-auto-timezone-list",
790
+ role: "listbox",
791
+ "aria-label": "Time zones",
792
+ },
793
+ results.length > 0
794
+ ? results.map((zone, index) => {
795
+ const copy = timezoneOptionPresentation(zone, offsetCacheRef.current);
796
+ const optionId = listboxId + "-option-" + index;
797
+ return h(
798
+ "div",
799
+ {
800
+ key: zone,
801
+ id: optionId,
802
+ className: "dsh-auto-timezone-option",
803
+ role: "option",
804
+ "aria-selected": selected === zone,
805
+ "data-active": active === index ? "true" : undefined,
806
+ onPointerMove: () => setActiveIndex(index),
807
+ onPointerDown: (event) => event.preventDefault(),
808
+ onClick: () => choose(zone),
809
+ },
810
+ h("span", { className: "dsh-auto-timezone-option-icon", "aria-hidden": "true" }, h(IconGlobeOutline14)),
811
+ h(
812
+ "span",
813
+ { className: "dsh-auto-picker-item-copy" },
814
+ h("span", { className: "dsh-auto-picker-item-label" }, copy.label),
815
+ h("span", { className: "dsh-auto-picker-item-detail" }, copy.detail),
816
+ ),
817
+ selected === zone
818
+ ? h("span", { className: "dsh-auto-timezone-option-check", "aria-hidden": "true" }, h(IconCheckOutline16))
819
+ : null,
820
+ );
821
+ })
822
+ : h(
823
+ "div",
824
+ {
825
+ className: "dsh-auto-timezone-empty",
826
+ role: "option",
827
+ "aria-disabled": "true",
828
+ "aria-selected": false,
829
+ },
830
+ "No matching time zones. You can still enter a recognized IANA time zone, such as Europe/Berlin.",
831
+ ),
832
+ ),
833
+ h("p", { className: "dsh-auto-timezone-popover-hint" }, "Type a city, region, or IANA time zone."),
834
+ )
835
+ : null;
836
+
837
+ return h(
838
+ React.Fragment,
839
+ null,
840
+ h(
841
+ "span",
842
+ {
843
+ ref: anchorRef,
844
+ className: "dsh-auto-timezone-control",
845
+ "data-open": open ? "true" : undefined,
846
+ onPointerDown: (event) => {
847
+ if (event.target === inputRef.current) return;
848
+ event.preventDefault();
849
+ if (open) close();
850
+ else {
851
+ setSearching(resumeSearch);
852
+ show();
853
+ requestAnimationFrame(() => {
854
+ inputRef.current?.focus();
855
+ inputRef.current?.select();
856
+ });
857
+ }
858
+ },
859
+ },
860
+ h("span", { className: "dsh-auto-timezone-control-icon", "aria-hidden": "true" }, h(IconGlobeOutline14)),
861
+ h("input", {
862
+ ref: inputRef,
863
+ id,
864
+ className: "dsh-auto-timezone-input",
865
+ type: "text",
866
+ role: "combobox",
867
+ value,
868
+ placeholder: "Search city or time zone",
869
+ autoComplete: "off",
870
+ spellCheck: false,
871
+ "aria-autocomplete": "list",
872
+ "aria-haspopup": "listbox",
873
+ "aria-expanded": open,
874
+ "aria-controls": open ? listboxId : undefined,
875
+ "aria-activedescendant": open && active >= 0 ? listboxId + "-option-" + active : undefined,
876
+ "aria-invalid": !open && value.trim() !== "" && selected === null ? "true" : undefined,
877
+ onFocus: (event) => {
878
+ setSearching(resumeSearch);
879
+ setActiveIndex(-1);
880
+ show();
881
+ event.currentTarget.select();
882
+ },
883
+ onClick: () => {
884
+ if (open) return;
885
+ setSearching(resumeSearch);
886
+ setActiveIndex(-1);
887
+ show();
888
+ },
889
+ onChange: (event) => {
890
+ setSearching(true);
891
+ setActiveIndex(0);
892
+ show();
893
+ onChange(event.target.value);
894
+ },
895
+ onKeyDown: onInputKeyDown,
896
+ }),
897
+ h(
898
+ "span",
899
+ {
900
+ className: "dsh-auto-timezone-chevron" + (open ? " dsh-auto-timezone-chevron-open" : ""),
901
+ "aria-hidden": "true",
902
+ },
903
+ h(IconChevronDownOutline14),
904
+ ),
905
+ ),
906
+ open && typeof document !== "undefined" ? createPortal(panel, document.body) : null,
907
+ h("span", { className: "dsh-auto-sr-only", role: "status", "aria-live": "polite" }, announcement),
908
+ );
909
+ }
910
+
448
911
  function AgentPresetPicker({ id, value, presets, onChange }) {
449
912
  const [open, setOpen] = useState(false);
450
913
  const ownerId = useId();
@@ -600,6 +1063,11 @@ window.__ModuleLoader__.load({
600
1063
  return h("span", { className: "dsh-auto-status dsh-auto-status-" + props.status }, props.status);
601
1064
  }
602
1065
 
1066
+ function nextFormInstancePrefix(reactFormId) {
1067
+ formInstanceSerial += 1;
1068
+ return "dsh-auto-form-" + formInstanceSerial + "-" + reactFormId;
1069
+ }
1070
+
603
1071
  function JobForm(props) {
604
1072
  const { meta, draft, editing, saving, error, onChange, onIdTouched, onSubmit, onCancel } = props;
605
1073
  const providers = meta && Array.isArray(meta.providers) ? meta.providers : [];
@@ -611,8 +1079,12 @@ window.__ModuleLoader__.load({
611
1079
  : ["workspace-write"];
612
1080
  const agentPresets = meta && Array.isArray(meta.agentPresets) ? meta.agentPresets : [];
613
1081
  const creating = editing === null;
614
- const formPrefix = "dsh-auto-form-" + String(useId()).replace(/[^A-Za-z0-9_-]/g, "");
615
- const fieldId = (name) => formPrefix + "-" + name;
1082
+ const reactFormId = String(useId()).replace(/[^A-Za-z0-9_-]/g, "");
1083
+ const formPrefixRef = useRef(null);
1084
+ if (formPrefixRef.current === null) {
1085
+ formPrefixRef.current = nextFormInstancePrefix(reactFormId);
1086
+ }
1087
+ const fieldId = (name) => formPrefixRef.current + "-" + name;
616
1088
 
617
1089
  return h(
618
1090
  "form",
@@ -682,21 +1154,17 @@ window.__ModuleLoader__.load({
682
1154
  ),
683
1155
  h(
684
1156
  Field,
685
- { label: "Timezone", htmlFor: fieldId("timezone"), required: true },
686
- h("input", {
1157
+ {
1158
+ label: "Time zone",
1159
+ htmlFor: fieldId("timezone"),
1160
+ required: true,
1161
+ hint: "Enter an IANA time zone. Current UTC offsets may change with daylight saving time.",
1162
+ },
1163
+ h(TimeZonePicker, {
687
1164
  id: fieldId("timezone"),
688
- className: "dsh-auto-input",
689
- type: "text",
690
- list: fieldId("timezone-list"),
691
1165
  value: draft.timezone,
692
- spellCheck: false,
693
- onChange: (event) => onChange("timezone", event.target.value),
1166
+ onChange: (value) => onChange("timezone", value),
694
1167
  }),
695
- h(
696
- "datalist",
697
- { id: fieldId("timezone-list") },
698
- TIMEZONES.map((zone) => h("option", { key: zone, value: zone })),
699
- ),
700
1168
  ),
701
1169
  h(
702
1170
  Field,
@@ -1647,7 +2115,7 @@ window.__ModuleLoader__.load({
1647
2115
  const onKeyDown = (event) => {
1648
2116
  if (event.key !== "Escape" || event.defaultPrevented || event.__dshAutomationsNested) return;
1649
2117
  if (event.target?.closest?.('[role="dialog"],[role="menu"]')) return;
1650
- if (document.querySelector('.dsh-auto-picker-trigger[aria-expanded="true"]')) return;
2118
+ if (document.querySelector('.dsh-auto-picker-trigger[aria-expanded="true"],.dsh-auto-timezone-input[aria-expanded="true"]')) return;
1651
2119
  event.preventDefault();
1652
2120
  disclosure.close();
1653
2121
  };
@@ -1735,7 +2203,29 @@ window.__ModuleLoader__.load({
1735
2203
  ".dsh-auto-picker-chevron-open{transform:rotate(180deg);}",
1736
2204
  ".dsh-auto-picker-item-copy{min-width:0;display:flex;flex-direction:column;white-space:normal;}",
1737
2205
  ".dsh-auto-picker-item-label{color:inherit;font-size:14px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
1738
- ".dsh-auto-picker-item-detail{color:var(--dsh-auto-muted);font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
2206
+ ".dsh-auto-picker-item-detail{color:var(--dsh-auto-muted,var(--dsw-alias-label-tertiary,#81858c));font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
2207
+ ".dsh-auto-timezone-control{width:100%;height:34px;padding:0 8px;display:flex;align-items:center;gap:7px;border:1px solid var(--dsh-auto-border);border-radius:8px;background:var(--dsh-auto-input-bg);color:var(--dsh-auto-text);cursor:text;}",
2208
+ ".dsh-auto-timezone-control:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsh-auto-input-bg));}",
2209
+ ".dsh-auto-timezone-control:focus-within,.dsh-auto-timezone-control[data-open=\"true\"]{border-color:var(--dsh-auto-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsh-auto-accent) 18%,transparent);}",
2210
+ ".dsh-auto-timezone-control:has(.dsh-auto-timezone-input[aria-invalid=\"true\"]){border-color:var(--dsh-auto-danger);}",
2211
+ ".dsh-auto-timezone-control-icon,.dsh-auto-timezone-chevron{width:16px;height:16px;flex:none;display:inline-flex;align-items:center;justify-content:center;color:var(--dsh-auto-muted);pointer-events:none;}",
2212
+ ".dsh-auto-timezone-input{appearance:none;min-width:0;height:100%;flex:1;padding:0;border:0;outline:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:20px;}",
2213
+ ".dsh-auto-timezone-input::placeholder{color:var(--dsh-auto-caption);}",
2214
+ ".dsh-auto-timezone-chevron{transition:transform .12s ease;}",
2215
+ ".dsh-auto-timezone-chevron-open{transform:rotate(180deg);}",
2216
+ ".dsh-auto-timezone-popover{box-sizing:border-box;position:fixed;z-index:1100;max-height:min(360px,calc(100vh - 24px));padding:4px;display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-inverted,var(--dsw-alias-border-l2,rgba(15,17,21,.16)));border-radius:12px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2,#fff));box-shadow:var(--dsw-shadow-lv3,0 12px 32px rgba(15,17,21,.16));color:var(--dsw-alias-label-primary,#0f1115);font-family:inherit;}",
2217
+ ".dsh-auto-timezone-popover *,.dsh-auto-timezone-popover *::before,.dsh-auto-timezone-popover *::after{box-sizing:border-box;}",
2218
+ ".dsh-auto-timezone-popover-title{flex:none;padding:7px 10px 5px;color:var(--dsw-alias-label-tertiary,#81858c);font-size:11px;font-weight:600;line-height:16px;text-transform:uppercase;letter-spacing:.04em;}",
2219
+ ".dsh-auto-timezone-list{min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);}",
2220
+ ".dsh-auto-timezone-option{width:100%;min-height:48px;padding:6px 9px;display:flex;align-items:center;gap:8px;border-radius:9px;cursor:pointer;user-select:none;}",
2221
+ ".dsh-auto-timezone-option:hover,.dsh-auto-timezone-option[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));}",
2222
+ ".dsh-auto-timezone-option[aria-selected=\"true\"]{color:var(--dsw-alias-state-business-primary,#4176e6);}",
2223
+ ".dsh-auto-timezone-option-icon,.dsh-auto-timezone-option-check{width:16px;height:16px;flex:none;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,#81858c);}",
2224
+ ".dsh-auto-timezone-option>.dsh-auto-picker-item-copy{min-width:0;flex:1;}",
2225
+ ".dsh-auto-timezone-option .dsh-auto-picker-item-detail{color:var(--dsw-alias-label-tertiary,#81858c);}",
2226
+ ".dsh-auto-timezone-option-check{color:var(--dsw-alias-state-business-primary,#4176e6);}",
2227
+ ".dsh-auto-timezone-empty{margin:0;padding:16px 12px;color:var(--dsw-alias-label-secondary,#4f5661);font-size:12px;line-height:18px;text-align:center;}",
2228
+ ".dsh-auto-timezone-popover-hint{flex:none;margin:4px 0 0;padding:7px 10px 5px;border-top:1px solid var(--dsw-alias-border-l2,rgba(15,17,21,.12));color:var(--dsw-alias-label-tertiary,#81858c);font-size:11px;line-height:16px;}",
1739
2229
  ".dsh-auto-permission-glyph{flex:none;}",
1740
2230
  ".dsh-auto-permission-glyph-read{color:var(--dsh-auto-muted);}",
1741
2231
  ".dsh-auto-permission-glyph-write{color:var(--dsh-auto-accent);}",
@@ -1878,6 +2368,16 @@ window.__ModuleLoader__.load({
1878
2368
  exports.agentPresetDisplayLabel = agentPresetDisplayLabel;
1879
2369
  exports.permissionPresetPresentation = permissionPresetPresentation;
1880
2370
  exports.PermissionPresetPicker = PermissionPresetPicker;
2371
+ exports.TimeZonePicker = TimeZonePicker;
2372
+ exports.__testing = Object.freeze({
2373
+ nextFormInstancePrefix,
2374
+ canonicalTimezone,
2375
+ timezoneOffsetLabel,
2376
+ timezoneNavigationIndex,
2377
+ timezoneOptionPresentation,
2378
+ timezonePreferredValue,
2379
+ timezoneSearchResults,
2380
+ });
1881
2381
  exports.inject = inject;
1882
2382
  exports.apply = apply;
1883
2383
  return module.exports;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncended/dsh-automations",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Durable cron automations for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -91,7 +91,8 @@
91
91
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
92
92
  "@deepseek-ai/dsh-permission-presets": "^0.1.1-rc.2",
93
93
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
94
- "react": "^18.2.0"
94
+ "react": "^18.2.0",
95
+ "react-dom": "^18.2.0"
95
96
  },
96
97
  "devDependencies": {
97
98
  "@deepseek-ai/cordis": "4.0.1",
@@ -110,6 +111,7 @@
110
111
  "@deepseek-ai/dsh-session": "0.1.1-rc.2",
111
112
  "@types/node": "^22.0.0",
112
113
  "react": "^18.2.0",
114
+ "react-dom": "^18.2.0",
113
115
  "typescript": "^5.9.3"
114
116
  }
115
117
  }