@syncended/dsh-automations 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ A DeepSeek Harness plugin for durable, configurable cron jobs. Each occurrence s
8
8
 
9
9
  - Standard five-field cron expressions with `UTC` or IANA timezones.
10
10
  - Per-job project directory, provider/model, reasoning effort, agent preset, permission preset, and timeout.
11
- - Enable/disable, Run now, edit, delete, cancel, and recent run history from the main **Automations** sidebar action or **Settings → Automations**.
11
+ - Enable/disable, Run now, edit, delete, cancel, and recent run history from a full center workspace opened by the main **Automations** sidebar action, or from **Settings → Automations**.
12
12
  - Overlap policies:
13
13
  - `skip` — record and skip an occurrence while the job has queued/running work.
14
14
  - `queue` — serialize occurrences for the same job.
@@ -42,7 +42,7 @@ pnpm build
42
42
  dsh plugin --profile web add .
43
43
  ```
44
44
 
45
- The package declares a DSH bundle, so `dsh plugin` appends it to the Web profile automatically. Restart the running Web Harness after the initial install, then refresh the page. Open **Automations** from the main sidebar, or use **Settings → Automations**.
45
+ The package declares a DSH bundle, so `dsh plugin` appends it to the Web profile automatically. Restart the running Web Harness after the initial install, then refresh the page. Open the full **Automations** workspace from the main sidebar, or use **Settings → Automations**.
46
46
 
47
47
  To remove it:
48
48
 
@@ -15,7 +15,7 @@ Non-goals for the first release:
15
15
  ## Components
16
16
 
17
17
  ```text
18
- Sidebar / Settings UI / HTTP API
18
+ Sidebar / Center workspace / Settings UI / HTTP API
19
19
 
20
20
 
21
21
  AutomationService ─── ProjectPolicy
@@ -30,7 +30,7 @@ Sidebar / Settings UI / HTTP API
30
30
 
31
31
  ### Browser surfaces
32
32
 
33
- The same automation page is available through two additive DSH extension points: `settings.section` keeps the configuration page inside Settings, while `sidebar.footer.action` opens a frame-wide `shell.overlay`. A small client-only disclosure store coordinates the sidebar trigger and overlay; neither surface owns scheduler state, and both read the same package HTTP API.
33
+ The same automation page is available through Settings and a dedicated center workspace. `settings.section` keeps configuration available inside Settings; `sidebar.footer.action` activates a temporary, higher-priority `conversation` entry that replaces the current center occupant. This provides a split-screen-like full-center experience while using the single-slot election directly rather than Split Screen's internal view contributions. Closing Automations disposes its entry immediately, revealing whichever center surface was previously active. A small client-only disclosure store coordinates the sidebar trigger and dynamic registration; neither surface owns scheduler state, and both read the same package HTTP API.
34
34
 
35
35
  Interactive chrome uses the ambient `@deepseek-ai/dsh-client-ui-primitives` `Button`, `Menu`, and icon components. Package CSS is limited to the automation-specific layout and composes only public DSH semantic tokens, so theme, menu, focus, and button behavior stay aligned with the host UI.
36
36
 
package/lib/client.js CHANGED
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({
21
21
  IconBrowseOutline16,
22
22
  IconCheckOutline16,
23
23
  IconChevronDownOutline14,
24
- IconCloseOutline16,
24
+ IconChevronLeftOutline14,
25
25
  IconEditOutline16,
26
26
  IconPlayOutline16,
27
27
  IconPlusOutline16,
@@ -40,6 +40,7 @@ window.__ModuleLoader__.load({
40
40
  const MIN_TIMEOUT_MS = 1000;
41
41
  const MAX_TIMEOUT_MS = 86400000;
42
42
  const JOB_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
43
+ let formInstanceSerial = 0;
43
44
 
44
45
  const BROWSER_TIMEZONE = (() => {
45
46
  try {
@@ -327,12 +328,12 @@ window.__ModuleLoader__.load({
327
328
 
328
329
  function focusAfterPickerTab(trigger, backwards) {
329
330
  requestAnimationFrame(() => {
330
- const panel = trigger?.closest('[role="dialog"]');
331
- if (!panel) {
331
+ const focusBoundary = trigger?.closest(".dsh-auto-form") || trigger?.closest('[role="dialog"]');
332
+ if (!focusBoundary) {
332
333
  trigger?.focus();
333
334
  return;
334
335
  }
335
- const focusables = modalFocusables(panel);
336
+ const focusables = visibleFocusables(focusBoundary);
336
337
  const index = focusables.indexOf(trigger);
337
338
  const target = index < 0 ? trigger : focusables[index + (backwards ? -1 : 1)];
338
339
  (target || trigger)?.focus();
@@ -406,10 +407,14 @@ window.__ModuleLoader__.load({
406
407
  });
407
408
  });
408
409
  const onKeyDown = (event) => {
409
- if (event.key !== "Tab") return;
410
410
  const dialog = riskDialog();
411
411
  if (!dialog) return;
412
- const focusables = modalFocusables(dialog);
412
+ if (event.key === "Escape" && dialog.contains(event.target)) {
413
+ event.__dshAutomationsNested = true;
414
+ return;
415
+ }
416
+ if (event.key !== "Tab") return;
417
+ const focusables = visibleFocusables(dialog);
413
418
  if (focusables.length === 0) return;
414
419
  const index = focusables.indexOf(event.target);
415
420
  const next = event.shiftKey
@@ -596,6 +601,11 @@ window.__ModuleLoader__.load({
596
601
  return h("span", { className: "dsh-auto-status dsh-auto-status-" + props.status }, props.status);
597
602
  }
598
603
 
604
+ function nextFormInstancePrefix(reactFormId) {
605
+ formInstanceSerial += 1;
606
+ return "dsh-auto-form-" + formInstanceSerial + "-" + reactFormId;
607
+ }
608
+
599
609
  function JobForm(props) {
600
610
  const { meta, draft, editing, saving, error, onChange, onIdTouched, onSubmit, onCancel } = props;
601
611
  const providers = meta && Array.isArray(meta.providers) ? meta.providers : [];
@@ -607,6 +617,12 @@ window.__ModuleLoader__.load({
607
617
  : ["workspace-write"];
608
618
  const agentPresets = meta && Array.isArray(meta.agentPresets) ? meta.agentPresets : [];
609
619
  const creating = editing === null;
620
+ const reactFormId = String(useId()).replace(/[^A-Za-z0-9_-]/g, "");
621
+ const formPrefixRef = useRef(null);
622
+ if (formPrefixRef.current === null) {
623
+ formPrefixRef.current = nextFormInstancePrefix(reactFormId);
624
+ }
625
+ const fieldId = (name) => formPrefixRef.current + "-" + name;
610
626
 
611
627
  return h(
612
628
  "form",
@@ -622,9 +638,9 @@ window.__ModuleLoader__.load({
622
638
  { className: "dsh-auto-formgrid" },
623
639
  h(
624
640
  Field,
625
- { label: "Name", htmlFor: "dsh-auto-f-name", required: true },
641
+ { label: "Name", htmlFor: fieldId("name"), required: true },
626
642
  h("input", {
627
- id: "dsh-auto-f-name",
643
+ id: fieldId("name"),
628
644
  className: "dsh-auto-input",
629
645
  type: "text",
630
646
  value: draft.name,
@@ -636,9 +652,9 @@ window.__ModuleLoader__.load({
636
652
  creating
637
653
  ? h(
638
654
  Field,
639
- { label: "Job id", htmlFor: "dsh-auto-f-id", hint: "Lowercase letters, digits and hyphens; auto-derived from the name. Leave blank to let the server generate one." },
655
+ { 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." },
640
656
  h("input", {
641
- id: "dsh-auto-f-id",
657
+ id: fieldId("id"),
642
658
  className: "dsh-auto-input",
643
659
  type: "text",
644
660
  value: draft.id,
@@ -652,9 +668,9 @@ window.__ModuleLoader__.load({
652
668
  )
653
669
  : h(
654
670
  Field,
655
- { label: "Job id", htmlFor: "dsh-auto-f-id", hint: "Fixed after creation." },
671
+ { label: "Job id", htmlFor: fieldId("id"), hint: "Fixed after creation." },
656
672
  h("input", {
657
- id: "dsh-auto-f-id",
673
+ id: fieldId("id"),
658
674
  className: "dsh-auto-input",
659
675
  type: "text",
660
676
  value: draft.id,
@@ -664,9 +680,9 @@ window.__ModuleLoader__.load({
664
680
  ),
665
681
  h(
666
682
  Field,
667
- { label: "Cron", htmlFor: "dsh-auto-f-cron", required: true, hint: "Five-field cron (minute hour day-of-month month day-of-week)." },
683
+ { label: "Cron", htmlFor: fieldId("cron"), required: true, hint: "Five-field cron (minute hour day-of-month month day-of-week)." },
668
684
  h("input", {
669
- id: "dsh-auto-f-cron",
685
+ id: fieldId("cron"),
670
686
  className: "dsh-auto-input",
671
687
  type: "text",
672
688
  value: draft.cron,
@@ -676,30 +692,30 @@ window.__ModuleLoader__.load({
676
692
  ),
677
693
  h(
678
694
  Field,
679
- { label: "Timezone", htmlFor: "dsh-auto-f-timezone", required: true },
695
+ { label: "Timezone", htmlFor: fieldId("timezone"), required: true },
680
696
  h("input", {
681
- id: "dsh-auto-f-timezone",
697
+ id: fieldId("timezone"),
682
698
  className: "dsh-auto-input",
683
699
  type: "text",
684
- list: "dsh-auto-tz-list",
700
+ list: fieldId("timezone-list"),
685
701
  value: draft.timezone,
686
702
  spellCheck: false,
687
703
  onChange: (event) => onChange("timezone", event.target.value),
688
704
  }),
689
705
  h(
690
706
  "datalist",
691
- { id: "dsh-auto-tz-list" },
707
+ { id: fieldId("timezone-list") },
692
708
  TIMEZONES.map((zone) => h("option", { key: zone, value: zone })),
693
709
  ),
694
710
  ),
695
711
  h(
696
712
  Field,
697
- { label: "Enabled", htmlFor: "dsh-auto-f-enabled" },
713
+ { label: "Enabled", htmlFor: fieldId("enabled") },
698
714
  h(
699
715
  "label",
700
716
  { className: "dsh-auto-check" },
701
717
  h("input", {
702
- id: "dsh-auto-f-enabled",
718
+ id: fieldId("enabled"),
703
719
  type: "checkbox",
704
720
  checked: draft.enabled,
705
721
  onChange: (event) => onChange("enabled", event.target.checked),
@@ -709,9 +725,9 @@ window.__ModuleLoader__.load({
709
725
  ),
710
726
  h(
711
727
  Field,
712
- { label: "Timeout (ms)", htmlFor: "dsh-auto-f-timeout", required: true, hint: "Wall-clock limit for each run (1s to 24h)." },
728
+ { label: "Timeout (ms)", htmlFor: fieldId("timeout"), required: true, hint: "Wall-clock limit for each run (1s to 24h)." },
713
729
  h("input", {
714
- id: "dsh-auto-f-timeout",
730
+ id: fieldId("timeout"),
715
731
  className: "dsh-auto-input",
716
732
  type: "number",
717
733
  min: MIN_TIMEOUT_MS,
@@ -723,12 +739,12 @@ window.__ModuleLoader__.load({
723
739
  ),
724
740
  h(
725
741
  Field,
726
- { label: "Provider", htmlFor: "dsh-auto-f-provider", hint: "Blank uses the current Harness default. You may type an unlisted provider route." },
742
+ { label: "Provider", htmlFor: fieldId("provider"), hint: "Blank uses the current Harness default. You may type an unlisted provider route." },
727
743
  h("input", {
728
- id: "dsh-auto-f-provider",
744
+ id: fieldId("provider"),
729
745
  className: "dsh-auto-input",
730
746
  type: "text",
731
- list: "dsh-auto-provider-list",
747
+ list: fieldId("provider-list"),
732
748
  value: draft.provider,
733
749
  spellCheck: false,
734
750
  placeholder: "Harness default",
@@ -736,7 +752,7 @@ window.__ModuleLoader__.load({
736
752
  }),
737
753
  h(
738
754
  "datalist",
739
- { id: "dsh-auto-provider-list" },
755
+ { id: fieldId("provider-list") },
740
756
  providers.map((provider) => h("option", { key: provider.id, value: provider.id })),
741
757
  ),
742
758
  ),
@@ -744,17 +760,17 @@ window.__ModuleLoader__.load({
744
760
  Field,
745
761
  {
746
762
  label: "Model",
747
- htmlFor: "dsh-auto-f-model",
763
+ htmlFor: fieldId("model"),
748
764
  required: draft.provider !== "",
749
765
  hint: draft.provider === ""
750
766
  ? "Set a provider first, or leave both blank for the Harness default."
751
767
  : "Choose a discovered model or type an adapter-supported model id.",
752
768
  },
753
769
  h("input", {
754
- id: "dsh-auto-f-model",
770
+ id: fieldId("model"),
755
771
  className: "dsh-auto-input",
756
772
  type: "text",
757
- list: "dsh-auto-model-list",
773
+ list: fieldId("model-list"),
758
774
  value: draft.model,
759
775
  spellCheck: false,
760
776
  placeholder: draft.provider === "" ? "Harness default" : "Model id",
@@ -763,18 +779,18 @@ window.__ModuleLoader__.load({
763
779
  }),
764
780
  h(
765
781
  "datalist",
766
- { id: "dsh-auto-model-list" },
782
+ { id: fieldId("model-list") },
767
783
  models.map((model) => h("option", { key: model, value: model })),
768
784
  ),
769
785
  ),
770
786
  h(
771
787
  Field,
772
- { label: "Reasoning effort", htmlFor: "dsh-auto-f-effort", hint: "Blank uses the provider default." },
788
+ { label: "Reasoning effort", htmlFor: fieldId("effort"), hint: "Blank uses the provider default." },
773
789
  h("input", {
774
- id: "dsh-auto-f-effort",
790
+ id: fieldId("effort"),
775
791
  className: "dsh-auto-input",
776
792
  type: "text",
777
- list: "dsh-auto-effort-list",
793
+ list: fieldId("effort-list"),
778
794
  value: draft.reasoningEffort,
779
795
  spellCheck: false,
780
796
  placeholder: "e.g. medium",
@@ -782,15 +798,15 @@ window.__ModuleLoader__.load({
782
798
  }),
783
799
  h(
784
800
  "datalist",
785
- { id: "dsh-auto-effort-list" },
801
+ { id: fieldId("effort-list") },
786
802
  REASONING_EFFORTS.map((effort) => h("option", { key: effort, value: effort })),
787
803
  ),
788
804
  ),
789
805
  h(
790
806
  Field,
791
- { label: "Agent preset", htmlFor: "dsh-auto-f-preset", hint: "Uses the current Harness default when not explicitly selected." },
807
+ { label: "Agent preset", htmlFor: fieldId("preset"), hint: "Uses the current Harness default when not explicitly selected." },
792
808
  h(AgentPresetPicker, {
793
- id: "dsh-auto-f-preset",
809
+ id: fieldId("preset"),
794
810
  value: draft.agentPreset,
795
811
  presets: agentPresets,
796
812
  onChange: (value) => onChange("agentPreset", value),
@@ -800,12 +816,12 @@ window.__ModuleLoader__.load({
800
816
  Field,
801
817
  {
802
818
  label: "Permission preset",
803
- htmlFor: "dsh-auto-f-permission",
819
+ htmlFor: fieldId("permission"),
804
820
  required: true,
805
821
  hint: permissionPresetPresentation(draft.permissionPreset).detail,
806
822
  },
807
823
  h(PermissionPresetPicker, {
808
- id: "dsh-auto-f-permission",
824
+ id: fieldId("permission"),
809
825
  value: draft.permissionPreset,
810
826
  presets: permissionPresets,
811
827
  onChange: (value) => onChange("permissionPreset", value),
@@ -813,11 +829,11 @@ window.__ModuleLoader__.load({
813
829
  ),
814
830
  h(
815
831
  Field,
816
- { label: "Overlap policy", htmlFor: "dsh-auto-f-overlap", full: true, hint: "What happens when a scheduled run fires while another run for the same job is still active." },
832
+ { 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." },
817
833
  h(
818
834
  "select",
819
835
  {
820
- id: "dsh-auto-f-overlap",
836
+ id: fieldId("overlap"),
821
837
  className: "dsh-auto-select",
822
838
  value: draft.overlap,
823
839
  onChange: (event) => onChange("overlap", event.target.value),
@@ -827,11 +843,11 @@ window.__ModuleLoader__.load({
827
843
  ),
828
844
  h(
829
845
  Field,
830
- { label: "Misfire policy", htmlFor: "dsh-auto-f-misfire", hint: "How a missed occurrence is handled after the scheduler is back." },
846
+ { label: "Misfire policy", htmlFor: fieldId("misfire"), hint: "How a missed occurrence is handled after the scheduler is back." },
831
847
  h(
832
848
  "select",
833
849
  {
834
- id: "dsh-auto-f-misfire",
850
+ id: fieldId("misfire"),
835
851
  className: "dsh-auto-select",
836
852
  value: draft.misfire,
837
853
  onChange: (event) => onChange("misfire", event.target.value),
@@ -841,9 +857,9 @@ window.__ModuleLoader__.load({
841
857
  ),
842
858
  h(
843
859
  Field,
844
- { label: "Working directory", htmlFor: "dsh-auto-f-cwd", required: true, full: true, hint: "Absolute project directory the agent runs in." },
860
+ { label: "Working directory", htmlFor: fieldId("cwd"), required: true, full: true, hint: "Absolute project directory the agent runs in." },
845
861
  h("input", {
846
- id: "dsh-auto-f-cwd",
862
+ id: fieldId("cwd"),
847
863
  className: "dsh-auto-input",
848
864
  type: "text",
849
865
  value: draft.cwd,
@@ -854,9 +870,9 @@ window.__ModuleLoader__.load({
854
870
  ),
855
871
  h(
856
872
  Field,
857
- { label: "Prompt", htmlFor: "dsh-auto-f-prompt", required: true, full: true, hint: "Instructions for the agent run. Prompts are never shown in run history." },
873
+ { label: "Prompt", htmlFor: fieldId("prompt"), required: true, full: true, hint: "Instructions for the agent run. Prompts are never shown in run history." },
858
874
  h("textarea", {
859
- id: "dsh-auto-f-prompt",
875
+ id: fieldId("prompt"),
860
876
  className: "dsh-auto-textarea",
861
877
  value: draft.prompt,
862
878
  placeholder: "Summarize yesterday's progress and list today's priorities\u2026",
@@ -1141,7 +1157,7 @@ window.__ModuleLoader__.load({
1141
1157
  );
1142
1158
  }
1143
1159
 
1144
- function AutomationsSection() {
1160
+ function AutomationsSection({ centerMode = false } = {}) {
1145
1161
  const [meta, setMeta] = useState(null);
1146
1162
  const [snapshot, setSnapshot] = useState(null);
1147
1163
  const [loading, setLoading] = useState(true);
@@ -1418,14 +1434,17 @@ window.__ModuleLoader__.load({
1418
1434
 
1419
1435
  return h(
1420
1436
  "section",
1421
- { className: "dsh-auto-root", "aria-busy": loading ? "true" : null },
1437
+ {
1438
+ className: "dsh-auto-root" + (centerMode ? " dsh-auto-root-workspace" : ""),
1439
+ "aria-busy": loading ? "true" : null,
1440
+ },
1422
1441
  h(
1423
1442
  "div",
1424
1443
  { className: "dsh-auto-head" },
1425
1444
  h(
1426
1445
  "div",
1427
1446
  null,
1428
- h("h2", null, "Automations"),
1447
+ h("h2", { className: centerMode ? "dsh-auto-sr-only" : undefined }, "Automations"),
1429
1448
  h(
1430
1449
  "p",
1431
1450
  { className: "dsh-auto-sub" },
@@ -1547,7 +1566,7 @@ window.__ModuleLoader__.load({
1547
1566
  };
1548
1567
  }
1549
1568
 
1550
- const MODAL_FOCUS_SELECTOR = [
1569
+ const FOCUSABLE_SELECTOR = [
1551
1570
  "a[href]",
1552
1571
  "button:not([disabled])",
1553
1572
  "input:not([disabled])",
@@ -1557,45 +1576,15 @@ window.__ModuleLoader__.load({
1557
1576
  "[tabindex]:not([tabindex=\"-1\"])",
1558
1577
  ].join(",");
1559
1578
 
1560
- function modalFocusables(panel) {
1579
+ function visibleFocusables(panel) {
1561
1580
  if (!panel) return [];
1562
- return Array.from(panel.querySelectorAll(MODAL_FOCUS_SELECTOR)).filter((element) =>
1581
+ return Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) =>
1563
1582
  !element.hasAttribute("hidden") &&
1564
1583
  element.getAttribute("aria-hidden") !== "true" &&
1565
1584
  element.getClientRects().length > 0,
1566
1585
  );
1567
1586
  }
1568
1587
 
1569
- function trapModalTab(event, panel) {
1570
- if (
1571
- event.key !== "Tab" ||
1572
- event.defaultPrevented ||
1573
- event.altKey ||
1574
- event.ctrlKey ||
1575
- event.metaKey
1576
- ) return;
1577
- const focusables = modalFocusables(panel);
1578
- if (focusables.length === 0) {
1579
- event.preventDefault();
1580
- panel?.focus();
1581
- return;
1582
- }
1583
- const first = focusables[0];
1584
- const last = focusables[focusables.length - 1];
1585
- const active = panel.ownerDocument.activeElement;
1586
- if (event.shiftKey) {
1587
- if (active === first || !panel.contains(active)) {
1588
- event.preventDefault();
1589
- last.focus();
1590
- }
1591
- return;
1592
- }
1593
- if (active === last || !panel.contains(active)) {
1594
- event.preventDefault();
1595
- first.focus();
1596
- }
1597
- }
1598
-
1599
1588
  function AutomationGlyph({ size = 18 }) {
1600
1589
  return h(
1601
1590
  "svg",
@@ -1644,10 +1633,9 @@ window.__ModuleLoader__.load({
1644
1633
  ref: triggerRef,
1645
1634
  type: "button",
1646
1635
  className: "dsh-auto-sidebar-trigger",
1647
- title: wide ? undefined : "Automations",
1648
- "aria-label": "Automations",
1649
- "aria-haspopup": "dialog",
1650
- "aria-expanded": open,
1636
+ title: wide ? undefined : open ? "Exit Automations" : "Automations",
1637
+ "aria-label": open ? "Exit Automations" : "Open Automations",
1638
+ "aria-pressed": open,
1651
1639
  "data-active": open ? "true" : undefined,
1652
1640
  "data-dsh-automations-trigger": "true",
1653
1641
  onClick: disclosure.toggle,
@@ -1658,67 +1646,55 @@ window.__ModuleLoader__.load({
1658
1646
  );
1659
1647
  }
1660
1648
 
1661
- function AutomationsOverlay({ disclosure }) {
1662
- const open = useSyncExternalStore(
1663
- disclosure.subscribe,
1664
- disclosure.getSnapshot,
1665
- disclosure.getSnapshot,
1666
- );
1667
- const closeRef = useRef(null);
1668
- const panelRef = useRef(null);
1649
+ function AutomationsWorkspace({ disclosure }) {
1650
+ const workspaceRef = useRef(null);
1669
1651
  const titleId = useId();
1670
1652
 
1671
1653
  useEffect(() => {
1672
- if (!open) return undefined;
1673
- const frame = window.requestAnimationFrame(() => closeRef.current?.focus());
1674
- return () => window.cancelAnimationFrame(frame);
1675
- }, [open]);
1654
+ const frame = window.requestAnimationFrame(() =>
1655
+ workspaceRef.current?.querySelector('[data-dsh-automations-exit="true"]')?.focus(),
1656
+ );
1657
+ const onKeyDown = (event) => {
1658
+ if (event.key !== "Escape" || event.defaultPrevented || event.__dshAutomationsNested) return;
1659
+ if (event.target?.closest?.('[role="dialog"],[role="menu"]')) return;
1660
+ if (document.querySelector('.dsh-auto-picker-trigger[aria-expanded="true"]')) return;
1661
+ event.preventDefault();
1662
+ disclosure.close();
1663
+ };
1664
+ window.addEventListener("keydown", onKeyDown);
1665
+ return () => {
1666
+ window.cancelAnimationFrame(frame);
1667
+ window.removeEventListener("keydown", onKeyDown);
1668
+ };
1669
+ }, [disclosure]);
1676
1670
 
1677
- if (!open) return null;
1678
1671
  return h(
1679
- "div",
1680
- { className: "dsh-auto-overlay", "data-dsh-automations-overlay": "true" },
1681
- h("div", { className: "dsh-auto-overlay-mask", "aria-hidden": "true", onClick: disclosure.close }),
1672
+ "section",
1673
+ {
1674
+ ref: workspaceRef,
1675
+ className: "dsh-auto-workspace",
1676
+ "data-dsh-automations-workspace": "true",
1677
+ "aria-labelledby": titleId,
1678
+ },
1682
1679
  h(
1683
- "section",
1684
- {
1685
- ref: panelRef,
1686
- className: "dsh-auto-overlay-panel",
1687
- role: "dialog",
1688
- tabIndex: -1,
1689
- "aria-modal": "true",
1690
- "aria-labelledby": titleId,
1691
- onKeyDownCapture: (event) => {
1692
- const panel = panelRef.current;
1693
- if (panel && event.target && !panel.contains(event.target)) return;
1694
- if (event.key === "Escape") {
1695
- if (panelRef.current?.querySelector('.dsh-auto-picker-trigger[aria-expanded="true"]')) return;
1696
- event.preventDefault();
1697
- disclosure.close();
1698
- return;
1699
- }
1700
- trapModalTab(event, panelRef.current);
1701
- },
1702
- },
1680
+ "header",
1681
+ { className: "dsh-auto-workspace-toolbar" },
1682
+ h("span", { className: "dsh-auto-workspace-icon", "aria-hidden": "true" }, h(AutomationGlyph, { size: 16 })),
1683
+ h("strong", { id: titleId, className: "dsh-auto-workspace-title" }, "Automations"),
1703
1684
  h(
1704
- "header",
1705
- { className: "dsh-auto-overlay-header" },
1706
- h("span", { id: titleId, className: "dsh-auto-sr-only" }, "Automations"),
1707
- h(
1708
- "button",
1709
- {
1710
- ref: closeRef,
1711
- type: "button",
1712
- className: "dsh-auto-overlay-close",
1713
- "aria-label": "Close Automations",
1714
- title: "Close",
1715
- onClick: disclosure.close,
1716
- },
1717
- h(IconCloseOutline16),
1718
- ),
1685
+ Button,
1686
+ {
1687
+ type: "button",
1688
+ variant: "toolbar",
1689
+ size: "sm",
1690
+ icon: h(IconChevronLeftOutline14),
1691
+ onClick: disclosure.close,
1692
+ "data-dsh-automations-exit": "true",
1693
+ },
1694
+ "Exit Automations",
1719
1695
  ),
1720
- h("div", { className: "dsh-auto-overlay-body" }, h(AutomationsSection)),
1721
1696
  ),
1697
+ h("div", { className: "dsh-auto-workspace-scroll" }, h(AutomationsSection, { centerMode: true })),
1722
1698
  );
1723
1699
  }
1724
1700
 
@@ -1731,17 +1707,15 @@ window.__ModuleLoader__.load({
1731
1707
  ".dsh-auto-sidebar-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
1732
1708
  ".dsh-auto-sidebar-action-rail{width:36px;height:36px;margin:0;}",
1733
1709
  ".dsh-auto-sidebar-action-rail .dsh-auto-sidebar-trigger{width:36px;height:36px;margin:0;padding:0;justify-content:center;gap:0;border-radius:50%;}",
1734
- ".dsh-auto-overlay{z-index:1000;pointer-events:auto;position:fixed;inset:0;display:flex;align-items:center;justify-content:center;}",
1735
- ".dsh-auto-overlay-mask{position:absolute;inset:0;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.46));backdrop-filter:var(--dsw-mask-blur,blur(2px));}",
1736
- ".dsh-auto-overlay-panel{box-sizing:border-box;z-index:1;position:relative;width:900px;max-width:calc(100vw - 48px);height:min(800px,calc(100vh - 48px));border:1px solid var(--dsw-alias-border-inverted,var(--dsw-alias-border-l2));border-radius:24px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:var(--dsw-shadow-lv3,0 18px 60px rgba(0,0,0,.3));color:var(--dsw-alias-label-primary,#0f1115);display:flex;flex-direction:column;overflow:hidden;}",
1737
- ".dsh-auto-overlay-header{box-sizing:border-box;height:52px;flex:none;display:flex;align-items:center;justify-content:flex-end;padding:10px 16px;}",
1738
- ".dsh-auto-overlay-close{appearance:none;width:28px;height:28px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#4f5661);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;}",
1739
- ".dsh-auto-overlay-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsw-alias-label-primary,#0f1115);}",
1740
- ".dsh-auto-overlay-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:1px;}",
1741
- ".dsh-auto-overlay-body{box-sizing:border-box;min-height:0;flex:1;padding:0 28px 28px;overflow-y:auto;overscroll-behavior:contain;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);}",
1742
- ".dsh-auto-overlay-body>.dsh-auto-root{width:100%;margin:0 auto;padding-top:0;}",
1710
+ ".dsh-auto-workspace{box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden;color:var(--dsw-alias-label-primary,#0f1115);background:var(--dsw-alias-bg-base,#fff);font-family:inherit;}",
1711
+ ".dsh-auto-workspace-toolbar{box-sizing:border-box;width:100%;height:44px;flex:none;display:flex;align-items:center;gap:8px;padding:6px 10px 6px 12px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(15,17,21,.14));background:var(--dsw-alias-bg-base,#fff);}",
1712
+ ".dsh-auto-workspace-icon{width:28px;height:28px;flex:none;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-state-business-primary,#4176e6);background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4176e6) 10%,transparent);}",
1713
+ ".dsh-auto-workspace-title{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600;line-height:20px;}",
1714
+ ".dsh-auto-workspace-scroll{container:dsh-auto-workspace / inline-size;box-sizing:border-box;min-height:0;flex:1;padding:24px clamp(20px,4vw,48px) 40px;overflow-y:auto;overscroll-behavior:contain;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);}",
1715
+ ".dsh-auto-workspace-scroll>.dsh-auto-root{width:100%;margin:0 auto;padding-top:0;}",
1743
1716
  ".dsh-auto-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important;}",
1744
1717
  ".dsh-auto-root{--dsh-auto-border:var(--dsw-alias-border-l2,rgba(15,17,21,.14));--dsh-auto-surface:var(--dsw-alias-bg-layer-3,#fff);--dsh-auto-surface-active:var(--dsw-alias-bg-layer-2,#fff);--dsh-auto-input-bg:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-1,#fff));--dsh-auto-text:var(--dsw-alias-label-primary,#0f1115);--dsh-auto-text-secondary:var(--dsw-alias-label-secondary,#4f5661);--dsh-auto-muted:var(--dsw-alias-label-tertiary,#81858c);--dsh-auto-caption:var(--dsw-alias-label-caption,#adb2b8);--dsh-auto-accent:var(--dsw-alias-state-business-primary,#4176e6);--dsh-auto-danger:var(--dsw-alias-state-error-primary,#ec1313);--dsh-auto-success:var(--dsw-alias-state-success-primary,#22c55e);--dsh-auto-warning:var(--dsw-alias-state-warn-primary,#f59e0b);box-sizing:border-box;max-width:820px;padding-top:12px;display:flex;flex-direction:column;gap:16px;font-family:inherit;color:var(--dsh-auto-text);}",
1718
+ ".dsh-auto-root-workspace{max-width:960px;}",
1745
1719
  "body:not([data-ds-dark-theme]) .dsh-auto-root{color-scheme:light;}",
1746
1720
  "body[data-ds-dark-theme] .dsh-auto-root{color-scheme:dark;}",
1747
1721
  ".dsh-auto-root *,.dsh-auto-root *::before,.dsh-auto-root *::after{box-sizing:border-box;}",
@@ -1837,13 +1811,34 @@ window.__ModuleLoader__.load({
1837
1811
  "@keyframes dsh-auto-pulse{0%,100%{opacity:1}50%{opacity:.35}}",
1838
1812
  ".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);}",
1839
1813
  ".dsh-auto-error{color:var(--dsh-auto-danger);display:flex;flex-direction:column;gap:10px;align-items:center;}",
1840
- "@media (max-width:640px){.dsh-auto-overlay-panel{max-width:calc(100vw - 16px);height:calc(100vh - 16px);border-radius:16px;}.dsh-auto-overlay-header{height:46px;padding:8px 10px;}.dsh-auto-overlay-body{padding:0 16px 16px;}.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;}}",
1814
+ "@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;}}",
1815
+ "@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;}}",
1841
1816
  ].join("\n");
1842
1817
 
1843
1818
  function apply(ctx) {
1844
1819
  const disclosure = createDisclosureStore();
1845
- // All package-owned surfaces share one style element and one disclosure
1846
- // store; both are released with the client-plugin fiber.
1820
+ let centerDeclared = false;
1821
+ let disposeCenter = null;
1822
+ const unmountCenter = () => {
1823
+ if (!disposeCenter) return;
1824
+ const dispose = disposeCenter;
1825
+ disposeCenter = null;
1826
+ dispose();
1827
+ };
1828
+ const mountCenter = () => {
1829
+ if (!centerDeclared || !disclosure.getSnapshot() || disposeCenter) return;
1830
+ try {
1831
+ disposeCenter = ctx.slots.register({
1832
+ name: "conversation",
1833
+ priority: -200,
1834
+ inject: () => ({ disclosure }),
1835
+ }, AutomationsWorkspace);
1836
+ } catch (error) {
1837
+ console.error("dsh automations: could not mount center workspace", error);
1838
+ disclosure.close();
1839
+ }
1840
+ };
1841
+
1847
1842
  ctx.effect(() => {
1848
1843
  const tag = document.createElement("style");
1849
1844
  tag.setAttribute("data-plugin", "@syncended/dsh-automations");
@@ -1853,7 +1848,28 @@ window.__ModuleLoader__.load({
1853
1848
  tag.remove();
1854
1849
  };
1855
1850
  }, "@syncended/dsh-automations: client styles");
1856
- ctx.effect(() => () => disclosure.dispose(), "@syncended/dsh-automations: disclosure store");
1851
+ ctx.effect(
1852
+ () => ctx.slots.inject("conversation", () => {
1853
+ centerDeclared = true;
1854
+ mountCenter();
1855
+ return () => {
1856
+ centerDeclared = false;
1857
+ unmountCenter();
1858
+ };
1859
+ }),
1860
+ "@syncended/dsh-automations: center workspace",
1861
+ );
1862
+ ctx.effect(() => {
1863
+ const unsubscribe = disclosure.subscribe(() => {
1864
+ if (disclosure.getSnapshot()) mountCenter();
1865
+ else unmountCenter();
1866
+ });
1867
+ return () => {
1868
+ unsubscribe();
1869
+ unmountCenter();
1870
+ disclosure.dispose();
1871
+ };
1872
+ }, "@syncended/dsh-automations: workspace state");
1857
1873
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1858
1874
  name: "settings.section",
1859
1875
  id: "automations",
@@ -1867,18 +1883,12 @@ window.__ModuleLoader__.load({
1867
1883
  label: "Automations",
1868
1884
  inject: () => ({ disclosure }),
1869
1885
  }, AutomationsSidebarAction));
1870
- ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1871
- name: "shell.overlay",
1872
- id: "automations",
1873
- order: 50,
1874
- label: "Automations",
1875
- inject: () => ({ disclosure }),
1876
- }, AutomationsOverlay));
1877
1886
  }
1878
1887
 
1879
1888
  exports.agentPresetDisplayLabel = agentPresetDisplayLabel;
1880
1889
  exports.permissionPresetPresentation = permissionPresetPresentation;
1881
1890
  exports.PermissionPresetPicker = PermissionPresetPicker;
1891
+ exports.__testing = Object.freeze({ nextFormInstancePrefix });
1882
1892
  exports.inject = inject;
1883
1893
  exports.apply = apply;
1884
1894
  return module.exports;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncended/dsh-automations",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Durable cron automations for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,6 +30,7 @@
30
30
  "inject": [
31
31
  "@deepseek-ai/dsh-client-runtime",
32
32
  "@deepseek-ai/dsh-client-ui-layout",
33
+ "@deepseek-ai/dsh-client-ui-conversation",
33
34
  "@deepseek-ai/dsh-client-ui-sidebar",
34
35
  "@deepseek-ai/dsh-client-ui-settings"
35
36
  ],
@@ -82,6 +83,7 @@
82
83
  "@deepseek-ai/dsh-agent-default-model": "^0.1.1-rc.2",
83
84
  "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2",
84
85
  "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
86
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
85
87
  "@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.2",
86
88
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
87
89
  "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.1-rc.2",
@@ -97,6 +99,7 @@
97
99
  "@deepseek-ai/dsh-agent-default-model": "0.1.1-rc.2",
98
100
  "@deepseek-ai/dsh-agent-presets": "0.1.1-rc.2",
99
101
  "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
102
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.1-rc.2",
100
103
  "@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
101
104
  "@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
102
105
  "@deepseek-ai/dsh-client-ui-settings": "0.1.1-rc.2",