@syncended/dsh-automations 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,9 @@ 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
+
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.
34
36
 
35
37
  ### AutomationService
36
38
 
package/lib/client.js CHANGED
@@ -13,6 +13,23 @@ window.__ModuleLoader__.load({
13
13
  const React = require("react");
14
14
  const h = React.createElement;
15
15
  const { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } = React;
16
+ const {
17
+ Button,
18
+ Menu,
19
+ RiskConfirmation,
20
+ IconAgentPresetOutline16,
21
+ IconBrowseOutline16,
22
+ IconCheckOutline16,
23
+ IconChevronDownOutline14,
24
+ IconChevronLeftOutline14,
25
+ IconEditOutline16,
26
+ IconPlayOutline16,
27
+ IconPlusOutline16,
28
+ IconRefreshOutline16,
29
+ IconSettingsOutline16,
30
+ IconTrashOutline16,
31
+ IconWarningOutline16,
32
+ } = require("@deepseek-ai/dsh-client-ui-primitives");
16
33
  const inject = ["slots"];
17
34
 
18
35
  const API_PREFIX = "/api/automations";
@@ -73,6 +90,26 @@ window.__ModuleLoader__.load({
73
90
  minimal: "Minimal mode",
74
91
  cordis: "Creator mode",
75
92
  };
93
+ const PERMISSION_PRESET_PRESENTATION = {
94
+ "read-only": {
95
+ label: "Read Only",
96
+ detail: "Read files without modifying the workspace.",
97
+ icon: IconBrowseOutline16,
98
+ tone: "read",
99
+ },
100
+ "workspace-write": {
101
+ label: "Workspace Write",
102
+ detail: "Write inside the workspace; wider retries require approval.",
103
+ icon: IconEditOutline16,
104
+ tone: "write",
105
+ },
106
+ "danger-full-access": {
107
+ label: "Full access",
108
+ detail: "Full file access without approval prompts.",
109
+ icon: IconWarningOutline16,
110
+ tone: "danger",
111
+ },
112
+ };
76
113
 
77
114
  function humanizePresetId(id) {
78
115
  return id
@@ -91,6 +128,24 @@ window.__ModuleLoader__.load({
91
128
  return readableId === id ? id : readableId + " (" + id + ")";
92
129
  }
93
130
 
131
+ function permissionPresetPresentation(value) {
132
+ const known = PERMISSION_PRESET_PRESENTATION[value];
133
+ if (known) return known;
134
+ return {
135
+ label: humanizePresetId(value) || value || "Unknown permission",
136
+ detail: "Host-provided permission preset.",
137
+ icon: IconSettingsOutline16,
138
+ tone: "custom",
139
+ };
140
+ }
141
+
142
+ function PermissionPresetIcon({ value, className }) {
143
+ const presentation = permissionPresetPresentation(value);
144
+ return h(presentation.icon, {
145
+ className: (className ? className + " " : "") + "dsh-auto-permission-glyph dsh-auto-permission-glyph-" + presentation.tone,
146
+ });
147
+ }
148
+
94
149
  function browserTimezone() {
95
150
  return BROWSER_TIMEZONE;
96
151
  }
@@ -258,6 +313,289 @@ window.__ModuleLoader__.load({
258
313
  );
259
314
  }
260
315
 
316
+ function pickerMenuFor(ownerId) {
317
+ return Array.from(document.querySelectorAll('[role="menu"]')).find((menu) =>
318
+ Array.from(menu.querySelectorAll("[data-dsh-auto-picker-owner]")).some((item) =>
319
+ item.getAttribute("data-dsh-auto-picker-owner") === ownerId,
320
+ ),
321
+ ) || null;
322
+ }
323
+
324
+ function focusPickerTrigger(triggerRef) {
325
+ requestAnimationFrame(() => triggerRef.current?.focus());
326
+ }
327
+
328
+ function focusAfterPickerTab(trigger, backwards) {
329
+ requestAnimationFrame(() => {
330
+ const focusBoundary = trigger?.closest(".dsh-auto-form") || trigger?.closest('[role="dialog"]');
331
+ if (!focusBoundary) {
332
+ trigger?.focus();
333
+ return;
334
+ }
335
+ const focusables = visibleFocusables(focusBoundary);
336
+ const index = focusables.indexOf(trigger);
337
+ const target = index < 0 ? trigger : focusables[index + (backwards ? -1 : 1)];
338
+ (target || trigger)?.focus();
339
+ });
340
+ }
341
+
342
+ function usePickerMenuNavigation({ open, setOpen, ownerId, selectedId, triggerRef }) {
343
+ useEffect(() => {
344
+ if (!open) return;
345
+ const focusSelected = () => {
346
+ const menu = pickerMenuFor(ownerId);
347
+ if (!menu) return;
348
+ const buttons = Array.from(menu.querySelectorAll('button[role="menuitem"]:not(:disabled)'));
349
+ const selected = buttons.find((button) =>
350
+ button.querySelector("[data-dsh-auto-picker-item]")?.getAttribute("data-dsh-auto-picker-item") === selectedId,
351
+ );
352
+ (selected || buttons[0])?.focus();
353
+ };
354
+ const frame = requestAnimationFrame(focusSelected);
355
+ const onKeyDown = (event) => {
356
+ const menu = pickerMenuFor(ownerId);
357
+ if (!menu || !(event.target instanceof Node) || !menu.contains(event.target)) return;
358
+ const buttons = Array.from(menu.querySelectorAll('button[role="menuitem"]:not(:disabled)'));
359
+ const index = buttons.indexOf(event.target.closest('button[role="menuitem"]'));
360
+ let target;
361
+ if (event.key === "ArrowDown") target = buttons[(Math.max(index, -1) + 1) % buttons.length];
362
+ else if (event.key === "ArrowUp") target = buttons[(index <= 0 ? buttons.length : index) - 1];
363
+ else if (event.key === "Home") target = buttons[0];
364
+ else if (event.key === "End") target = buttons[buttons.length - 1];
365
+ else if (event.key === "Escape") {
366
+ event.preventDefault();
367
+ event.stopImmediatePropagation();
368
+ setOpen(false);
369
+ focusPickerTrigger(triggerRef);
370
+ return;
371
+ } else if (event.key === "Tab") {
372
+ event.preventDefault();
373
+ event.stopImmediatePropagation();
374
+ setOpen(false);
375
+ focusAfterPickerTab(triggerRef.current, event.shiftKey);
376
+ return;
377
+ } else return;
378
+ if (!target) return;
379
+ event.preventDefault();
380
+ target.focus();
381
+ };
382
+ document.addEventListener("keydown", onKeyDown, true);
383
+ return () => {
384
+ cancelAnimationFrame(frame);
385
+ document.removeEventListener("keydown", onKeyDown, true);
386
+ };
387
+ }, [open, ownerId, selectedId, setOpen, triggerRef]);
388
+ }
389
+
390
+ function openPickerFromKeyboard(event, setOpen) {
391
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
392
+ event.preventDefault();
393
+ setOpen(true);
394
+ }
395
+
396
+ function useRiskConfirmationFocus(open) {
397
+ useEffect(() => {
398
+ if (!open) return;
399
+ const riskDialog = () => Array.from(document.querySelectorAll('[role="dialog"]')).find((dialog) =>
400
+ dialog.getAttribute("aria-label") === "Enable Full access?",
401
+ ) || null;
402
+ let innerFrame;
403
+ const frame = requestAnimationFrame(() => {
404
+ innerFrame = requestAnimationFrame(() => {
405
+ riskDialog()?.querySelector('input[type="checkbox"]')?.focus();
406
+ });
407
+ });
408
+ const onKeyDown = (event) => {
409
+ const dialog = riskDialog();
410
+ if (!dialog) return;
411
+ if (event.key === "Escape" && dialog.contains(event.target)) {
412
+ event.__dshAutomationsNested = true;
413
+ return;
414
+ }
415
+ if (event.key !== "Tab") return;
416
+ const focusables = visibleFocusables(dialog);
417
+ if (focusables.length === 0) return;
418
+ const index = focusables.indexOf(event.target);
419
+ const next = event.shiftKey
420
+ ? focusables[index <= 0 ? focusables.length - 1 : index - 1]
421
+ : focusables[index < 0 || index === focusables.length - 1 ? 0 : index + 1];
422
+ event.preventDefault();
423
+ event.stopImmediatePropagation();
424
+ next.focus();
425
+ };
426
+ document.addEventListener("keydown", onKeyDown, true);
427
+ return () => {
428
+ cancelAnimationFrame(frame);
429
+ if (innerFrame !== undefined) cancelAnimationFrame(innerFrame);
430
+ document.removeEventListener("keydown", onKeyDown, true);
431
+ };
432
+ }, [open]);
433
+ }
434
+
435
+ function PickerItemCopy({ label, detail, ownerId, itemId }) {
436
+ return h(
437
+ "span",
438
+ {
439
+ className: "dsh-auto-picker-item-copy",
440
+ "data-dsh-auto-picker-owner": ownerId,
441
+ "data-dsh-auto-picker-item": itemId,
442
+ },
443
+ h("span", { className: "dsh-auto-picker-item-label" }, label),
444
+ detail ? h("span", { className: "dsh-auto-picker-item-detail" }, detail) : null,
445
+ );
446
+ }
447
+
448
+ function AgentPresetPicker({ id, value, presets, onChange }) {
449
+ const [open, setOpen] = useState(false);
450
+ const ownerId = useId();
451
+ const triggerRef = useRef(null);
452
+ usePickerMenuNavigation({ open, setOpen, ownerId, selectedId: value, triggerRef });
453
+ const selected = presets.find((preset) => preset.id === value);
454
+ const label = value === ""
455
+ ? "Harness default"
456
+ : agentPresetDisplayLabel(selected || { id: value, name: value });
457
+ const items = [
458
+ {
459
+ id: "",
460
+ label: h(PickerItemCopy, {
461
+ label: "Harness default",
462
+ detail: "Use the current Harness agent preset.",
463
+ ownerId,
464
+ itemId: "",
465
+ }),
466
+ icon: h(IconAgentPresetOutline16),
467
+ },
468
+ ...presets.map((preset) => ({
469
+ id: preset.id,
470
+ label: h(PickerItemCopy, {
471
+ label: agentPresetDisplayLabel(preset),
472
+ detail: preset.broken ? "Failed to load: " + preset.broken : null,
473
+ ownerId,
474
+ itemId: preset.id,
475
+ }),
476
+ icon: h(IconAgentPresetOutline16),
477
+ disabled: Boolean(preset.broken),
478
+ })),
479
+ ];
480
+ return h(Menu, {
481
+ open,
482
+ onClose: () => setOpen(false),
483
+ items,
484
+ selectedId: value,
485
+ onSelect: (next) => {
486
+ setOpen(false);
487
+ onChange(next);
488
+ focusPickerTrigger(triggerRef);
489
+ },
490
+ side: "top",
491
+ portal: true,
492
+ className: "dsh-auto-picker-root",
493
+ anchor: h(
494
+ "button",
495
+ {
496
+ ref: triggerRef,
497
+ id,
498
+ type: "button",
499
+ className: "dsh-auto-picker-trigger",
500
+ "aria-haspopup": "menu",
501
+ "aria-expanded": open,
502
+ onKeyDown: (event) => openPickerFromKeyboard(event, setOpen),
503
+ onClick: () => setOpen((current) => !current),
504
+ },
505
+ h(IconAgentPresetOutline16, { className: "dsh-auto-picker-trigger-icon" }),
506
+ h("span", { className: "dsh-auto-picker-trigger-label" }, label),
507
+ h(IconChevronDownOutline14, { className: "dsh-auto-picker-chevron" + (open ? " dsh-auto-picker-chevron-open" : "") }),
508
+ ),
509
+ });
510
+ }
511
+
512
+ function PermissionPresetPicker({ id, value, presets, onChange }) {
513
+ const [open, setOpen] = useState(false);
514
+ const [confirmingFullAccess, setConfirmingFullAccess] = useState(false);
515
+ const [acknowledged, setAcknowledged] = useState(false);
516
+ const ownerId = useId();
517
+ const triggerRef = useRef(null);
518
+ usePickerMenuNavigation({ open, setOpen, ownerId, selectedId: value, triggerRef });
519
+ useRiskConfirmationFocus(confirmingFullAccess);
520
+ const selected = permissionPresetPresentation(value);
521
+ const items = presets.map((preset) => {
522
+ const presentation = permissionPresetPresentation(preset);
523
+ return {
524
+ id: preset,
525
+ label: h(PickerItemCopy, {
526
+ label: presentation.label,
527
+ detail: presentation.detail,
528
+ ownerId,
529
+ itemId: preset,
530
+ }),
531
+ icon: h(PermissionPresetIcon, { value: preset }),
532
+ danger: preset === "danger-full-access",
533
+ };
534
+ });
535
+ const closeConfirmation = () => {
536
+ setConfirmingFullAccess(false);
537
+ setAcknowledged(false);
538
+ focusPickerTrigger(triggerRef);
539
+ };
540
+ return h(
541
+ React.Fragment,
542
+ null,
543
+ h(Menu, {
544
+ open,
545
+ onClose: () => setOpen(false),
546
+ items,
547
+ selectedId: value,
548
+ onSelect: (next) => {
549
+ setOpen(false);
550
+ if (next === "danger-full-access" && value !== "danger-full-access") {
551
+ setAcknowledged(false);
552
+ setConfirmingFullAccess(true);
553
+ return;
554
+ }
555
+ onChange(next);
556
+ focusPickerTrigger(triggerRef);
557
+ },
558
+ side: "top",
559
+ portal: true,
560
+ className: "dsh-auto-picker-root",
561
+ anchor: h(
562
+ "button",
563
+ {
564
+ ref: triggerRef,
565
+ id,
566
+ type: "button",
567
+ className: "dsh-auto-picker-trigger",
568
+ "data-permission-preset": value,
569
+ "aria-haspopup": "menu",
570
+ "aria-expanded": open,
571
+ onKeyDown: (event) => openPickerFromKeyboard(event, setOpen),
572
+ onClick: () => setOpen((current) => !current),
573
+ },
574
+ h(PermissionPresetIcon, { value, className: "dsh-auto-picker-trigger-icon" }),
575
+ h("span", { className: "dsh-auto-picker-trigger-label" }, selected.label),
576
+ h(IconChevronDownOutline14, { className: "dsh-auto-picker-chevron" + (open ? " dsh-auto-picker-chevron-open" : "") }),
577
+ ),
578
+ }),
579
+ h(RiskConfirmation, {
580
+ open: confirmingFullAccess,
581
+ title: "Enable Full access?",
582
+ description: "Full access lets scheduled agents perform sensitive actions, file changes, and external commands without approval prompts. Only use it for automations you trust.",
583
+ acknowledgeLabel: "I understand the risks and want to continue",
584
+ cancelLabel: "Cancel",
585
+ confirmLabel: "Enable Full access",
586
+ acknowledged,
587
+ onAcknowledgedChange: setAcknowledged,
588
+ onCancel: closeConfirmation,
589
+ onConfirm: () => {
590
+ setConfirmingFullAccess(false);
591
+ setAcknowledged(false);
592
+ onChange("danger-full-access");
593
+ focusPickerTrigger(triggerRef);
594
+ },
595
+ }),
596
+ );
597
+ }
598
+
261
599
  function StatusPill(props) {
262
600
  return h("span", { className: "dsh-auto-status dsh-auto-status-" + props.status }, props.status);
263
601
  }
@@ -273,6 +611,8 @@ window.__ModuleLoader__.load({
273
611
  : ["workspace-write"];
274
612
  const agentPresets = meta && Array.isArray(meta.agentPresets) ? meta.agentPresets : [];
275
613
  const creating = editing === null;
614
+ const formPrefix = "dsh-auto-form-" + String(useId()).replace(/[^A-Za-z0-9_-]/g, "");
615
+ const fieldId = (name) => formPrefix + "-" + name;
276
616
 
277
617
  return h(
278
618
  "form",
@@ -288,9 +628,9 @@ window.__ModuleLoader__.load({
288
628
  { className: "dsh-auto-formgrid" },
289
629
  h(
290
630
  Field,
291
- { label: "Name", htmlFor: "dsh-auto-f-name", required: true },
631
+ { label: "Name", htmlFor: fieldId("name"), required: true },
292
632
  h("input", {
293
- id: "dsh-auto-f-name",
633
+ id: fieldId("name"),
294
634
  className: "dsh-auto-input",
295
635
  type: "text",
296
636
  value: draft.name,
@@ -302,9 +642,9 @@ window.__ModuleLoader__.load({
302
642
  creating
303
643
  ? h(
304
644
  Field,
305
- { 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." },
645
+ { 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." },
306
646
  h("input", {
307
- id: "dsh-auto-f-id",
647
+ id: fieldId("id"),
308
648
  className: "dsh-auto-input",
309
649
  type: "text",
310
650
  value: draft.id,
@@ -318,9 +658,9 @@ window.__ModuleLoader__.load({
318
658
  )
319
659
  : h(
320
660
  Field,
321
- { label: "Job id", htmlFor: "dsh-auto-f-id", hint: "Fixed after creation." },
661
+ { label: "Job id", htmlFor: fieldId("id"), hint: "Fixed after creation." },
322
662
  h("input", {
323
- id: "dsh-auto-f-id",
663
+ id: fieldId("id"),
324
664
  className: "dsh-auto-input",
325
665
  type: "text",
326
666
  value: draft.id,
@@ -330,9 +670,9 @@ window.__ModuleLoader__.load({
330
670
  ),
331
671
  h(
332
672
  Field,
333
- { label: "Cron", htmlFor: "dsh-auto-f-cron", required: true, hint: "Five-field cron (minute hour day-of-month month day-of-week)." },
673
+ { label: "Cron", htmlFor: fieldId("cron"), required: true, hint: "Five-field cron (minute hour day-of-month month day-of-week)." },
334
674
  h("input", {
335
- id: "dsh-auto-f-cron",
675
+ id: fieldId("cron"),
336
676
  className: "dsh-auto-input",
337
677
  type: "text",
338
678
  value: draft.cron,
@@ -342,30 +682,30 @@ window.__ModuleLoader__.load({
342
682
  ),
343
683
  h(
344
684
  Field,
345
- { label: "Timezone", htmlFor: "dsh-auto-f-timezone", required: true },
685
+ { label: "Timezone", htmlFor: fieldId("timezone"), required: true },
346
686
  h("input", {
347
- id: "dsh-auto-f-timezone",
687
+ id: fieldId("timezone"),
348
688
  className: "dsh-auto-input",
349
689
  type: "text",
350
- list: "dsh-auto-tz-list",
690
+ list: fieldId("timezone-list"),
351
691
  value: draft.timezone,
352
692
  spellCheck: false,
353
693
  onChange: (event) => onChange("timezone", event.target.value),
354
694
  }),
355
695
  h(
356
696
  "datalist",
357
- { id: "dsh-auto-tz-list" },
697
+ { id: fieldId("timezone-list") },
358
698
  TIMEZONES.map((zone) => h("option", { key: zone, value: zone })),
359
699
  ),
360
700
  ),
361
701
  h(
362
702
  Field,
363
- { label: "Enabled", htmlFor: "dsh-auto-f-enabled" },
703
+ { label: "Enabled", htmlFor: fieldId("enabled") },
364
704
  h(
365
705
  "label",
366
706
  { className: "dsh-auto-check" },
367
707
  h("input", {
368
- id: "dsh-auto-f-enabled",
708
+ id: fieldId("enabled"),
369
709
  type: "checkbox",
370
710
  checked: draft.enabled,
371
711
  onChange: (event) => onChange("enabled", event.target.checked),
@@ -375,9 +715,9 @@ window.__ModuleLoader__.load({
375
715
  ),
376
716
  h(
377
717
  Field,
378
- { label: "Timeout (ms)", htmlFor: "dsh-auto-f-timeout", required: true, hint: "Wall-clock limit for each run (1s to 24h)." },
718
+ { label: "Timeout (ms)", htmlFor: fieldId("timeout"), required: true, hint: "Wall-clock limit for each run (1s to 24h)." },
379
719
  h("input", {
380
- id: "dsh-auto-f-timeout",
720
+ id: fieldId("timeout"),
381
721
  className: "dsh-auto-input",
382
722
  type: "number",
383
723
  min: MIN_TIMEOUT_MS,
@@ -389,12 +729,12 @@ window.__ModuleLoader__.load({
389
729
  ),
390
730
  h(
391
731
  Field,
392
- { label: "Provider", htmlFor: "dsh-auto-f-provider", hint: "Blank uses the current Harness default. You may type an unlisted provider route." },
732
+ { label: "Provider", htmlFor: fieldId("provider"), hint: "Blank uses the current Harness default. You may type an unlisted provider route." },
393
733
  h("input", {
394
- id: "dsh-auto-f-provider",
734
+ id: fieldId("provider"),
395
735
  className: "dsh-auto-input",
396
736
  type: "text",
397
- list: "dsh-auto-provider-list",
737
+ list: fieldId("provider-list"),
398
738
  value: draft.provider,
399
739
  spellCheck: false,
400
740
  placeholder: "Harness default",
@@ -402,7 +742,7 @@ window.__ModuleLoader__.load({
402
742
  }),
403
743
  h(
404
744
  "datalist",
405
- { id: "dsh-auto-provider-list" },
745
+ { id: fieldId("provider-list") },
406
746
  providers.map((provider) => h("option", { key: provider.id, value: provider.id })),
407
747
  ),
408
748
  ),
@@ -410,17 +750,17 @@ window.__ModuleLoader__.load({
410
750
  Field,
411
751
  {
412
752
  label: "Model",
413
- htmlFor: "dsh-auto-f-model",
753
+ htmlFor: fieldId("model"),
414
754
  required: draft.provider !== "",
415
755
  hint: draft.provider === ""
416
756
  ? "Set a provider first, or leave both blank for the Harness default."
417
757
  : "Choose a discovered model or type an adapter-supported model id.",
418
758
  },
419
759
  h("input", {
420
- id: "dsh-auto-f-model",
760
+ id: fieldId("model"),
421
761
  className: "dsh-auto-input",
422
762
  type: "text",
423
- list: "dsh-auto-model-list",
763
+ list: fieldId("model-list"),
424
764
  value: draft.model,
425
765
  spellCheck: false,
426
766
  placeholder: draft.provider === "" ? "Harness default" : "Model id",
@@ -429,18 +769,18 @@ window.__ModuleLoader__.load({
429
769
  }),
430
770
  h(
431
771
  "datalist",
432
- { id: "dsh-auto-model-list" },
772
+ { id: fieldId("model-list") },
433
773
  models.map((model) => h("option", { key: model, value: model })),
434
774
  ),
435
775
  ),
436
776
  h(
437
777
  Field,
438
- { label: "Reasoning effort", htmlFor: "dsh-auto-f-effort", hint: "Blank uses the provider default." },
778
+ { label: "Reasoning effort", htmlFor: fieldId("effort"), hint: "Blank uses the provider default." },
439
779
  h("input", {
440
- id: "dsh-auto-f-effort",
780
+ id: fieldId("effort"),
441
781
  className: "dsh-auto-input",
442
782
  type: "text",
443
- list: "dsh-auto-effort-list",
783
+ list: fieldId("effort-list"),
444
784
  value: draft.reasoningEffort,
445
785
  spellCheck: false,
446
786
  placeholder: "e.g. medium",
@@ -448,52 +788,42 @@ window.__ModuleLoader__.load({
448
788
  }),
449
789
  h(
450
790
  "datalist",
451
- { id: "dsh-auto-effort-list" },
791
+ { id: fieldId("effort-list") },
452
792
  REASONING_EFFORTS.map((effort) => h("option", { key: effort, value: effort })),
453
793
  ),
454
794
  ),
455
795
  h(
456
796
  Field,
457
- { label: "Agent preset", htmlFor: "dsh-auto-f-preset", hint: "Blank uses the current Harness agent-preset default." },
458
- h(
459
- "select",
460
- {
461
- id: "dsh-auto-f-preset",
462
- className: "dsh-auto-select",
463
- value: draft.agentPreset,
464
- onChange: (event) => onChange("agentPreset", event.target.value),
465
- },
466
- h("option", { value: "" }, "Harness default"),
467
- agentPresets.map((preset) =>
468
- h(
469
- "option",
470
- { key: preset.id, value: preset.id },
471
- agentPresetDisplayLabel(preset) + (preset.broken ? " (broken: " + preset.broken + ")" : ""),
472
- ),
473
- ),
474
- ),
797
+ { label: "Agent preset", htmlFor: fieldId("preset"), hint: "Uses the current Harness default when not explicitly selected." },
798
+ h(AgentPresetPicker, {
799
+ id: fieldId("preset"),
800
+ value: draft.agentPreset,
801
+ presets: agentPresets,
802
+ onChange: (value) => onChange("agentPreset", value),
803
+ }),
475
804
  ),
476
805
  h(
477
806
  Field,
478
- { label: "Permission preset", htmlFor: "dsh-auto-f-permission", required: true },
479
- h(
480
- "select",
481
- {
482
- id: "dsh-auto-f-permission",
483
- className: "dsh-auto-select",
484
- value: draft.permissionPreset,
485
- onChange: (event) => onChange("permissionPreset", event.target.value),
486
- },
487
- permissionPresets.map((preset) => h("option", { key: preset, value: preset }, preset)),
488
- ),
807
+ {
808
+ label: "Permission preset",
809
+ htmlFor: fieldId("permission"),
810
+ required: true,
811
+ hint: permissionPresetPresentation(draft.permissionPreset).detail,
812
+ },
813
+ h(PermissionPresetPicker, {
814
+ id: fieldId("permission"),
815
+ value: draft.permissionPreset,
816
+ presets: permissionPresets,
817
+ onChange: (value) => onChange("permissionPreset", value),
818
+ }),
489
819
  ),
490
820
  h(
491
821
  Field,
492
- { 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." },
822
+ { 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." },
493
823
  h(
494
824
  "select",
495
825
  {
496
- id: "dsh-auto-f-overlap",
826
+ id: fieldId("overlap"),
497
827
  className: "dsh-auto-select",
498
828
  value: draft.overlap,
499
829
  onChange: (event) => onChange("overlap", event.target.value),
@@ -503,11 +833,11 @@ window.__ModuleLoader__.load({
503
833
  ),
504
834
  h(
505
835
  Field,
506
- { label: "Misfire policy", htmlFor: "dsh-auto-f-misfire", hint: "How a missed occurrence is handled after the scheduler is back." },
836
+ { label: "Misfire policy", htmlFor: fieldId("misfire"), hint: "How a missed occurrence is handled after the scheduler is back." },
507
837
  h(
508
838
  "select",
509
839
  {
510
- id: "dsh-auto-f-misfire",
840
+ id: fieldId("misfire"),
511
841
  className: "dsh-auto-select",
512
842
  value: draft.misfire,
513
843
  onChange: (event) => onChange("misfire", event.target.value),
@@ -517,9 +847,9 @@ window.__ModuleLoader__.load({
517
847
  ),
518
848
  h(
519
849
  Field,
520
- { label: "Working directory", htmlFor: "dsh-auto-f-cwd", required: true, full: true, hint: "Absolute project directory the agent runs in." },
850
+ { label: "Working directory", htmlFor: fieldId("cwd"), required: true, full: true, hint: "Absolute project directory the agent runs in." },
521
851
  h("input", {
522
- id: "dsh-auto-f-cwd",
852
+ id: fieldId("cwd"),
523
853
  className: "dsh-auto-input",
524
854
  type: "text",
525
855
  value: draft.cwd,
@@ -530,9 +860,9 @@ window.__ModuleLoader__.load({
530
860
  ),
531
861
  h(
532
862
  Field,
533
- { label: "Prompt", htmlFor: "dsh-auto-f-prompt", required: true, full: true, hint: "Instructions for the agent run. Prompts are never shown in run history." },
863
+ { label: "Prompt", htmlFor: fieldId("prompt"), required: true, full: true, hint: "Instructions for the agent run. Prompts are never shown in run history." },
534
864
  h("textarea", {
535
- id: "dsh-auto-f-prompt",
865
+ id: fieldId("prompt"),
536
866
  className: "dsh-auto-textarea",
537
867
  value: draft.prompt,
538
868
  placeholder: "Summarize yesterday's progress and list today's priorities\u2026",
@@ -547,13 +877,18 @@ window.__ModuleLoader__.load({
547
877
  "div",
548
878
  { className: "dsh-auto-formactions" },
549
879
  h(
550
- "button",
551
- { type: "submit", className: "dsh-auto-btn dsh-auto-btn-primary", disabled: saving },
880
+ Button,
881
+ {
882
+ type: "submit",
883
+ variant: "primary",
884
+ icon: saving ? null : h(IconCheckOutline16),
885
+ disabled: saving,
886
+ },
552
887
  saving ? "Saving\u2026" : creating ? "Create automation" : "Save changes",
553
888
  ),
554
889
  h(
555
- "button",
556
- { type: "button", className: "dsh-auto-btn", onClick: onCancel, disabled: saving },
890
+ Button,
891
+ { type: "button", variant: "outline", onClick: onCancel, disabled: saving },
557
892
  "Cancel",
558
893
  ),
559
894
  ),
@@ -589,6 +924,7 @@ window.__ModuleLoader__.load({
589
924
  name: job.execution.agentPreset,
590
925
  })
591
926
  : "";
927
+ const permission = permissionPresetPresentation(job.execution.permissionPreset);
592
928
 
593
929
  return h(
594
930
  "article",
@@ -634,7 +970,12 @@ window.__ModuleLoader__.load({
634
970
  presetLabel !== ""
635
971
  ? h("span", { title: "Agent preset" }, presetLabel)
636
972
  : null,
637
- h("span", { title: "Permission preset" }, job.execution.permissionPreset),
973
+ h(
974
+ "span",
975
+ { className: "dsh-auto-card-permission", title: "Permission preset: " + job.execution.permissionPreset },
976
+ h(PermissionPresetIcon, { value: job.execution.permissionPreset }),
977
+ permission.label,
978
+ ),
638
979
  h("span", { title: "Timeout" }, formatTimeout(job.execution.timeoutMs)),
639
980
  h("span", { className: "dsh-auto-muted", title: "Version" }, "v" + job.version),
640
981
  ),
@@ -642,15 +983,24 @@ window.__ModuleLoader__.load({
642
983
  "div",
643
984
  { className: "dsh-auto-card-actions" },
644
985
  h(
645
- "button",
646
- { type: "button", className: "dsh-auto-btn", onClick: onEdit, disabled: busyForJob || running },
986
+ Button,
987
+ {
988
+ type: "button",
989
+ variant: "ghost",
990
+ size: "sm",
991
+ icon: h(IconEditOutline16),
992
+ onClick: onEdit,
993
+ disabled: busyForJob || running,
994
+ },
647
995
  "Edit",
648
996
  ),
649
997
  h(
650
- "button",
998
+ Button,
651
999
  {
652
1000
  type: "button",
653
- className: "dsh-auto-btn dsh-auto-btn-primary",
1001
+ variant: "primary",
1002
+ size: "sm",
1003
+ icon: running ? null : h(IconPlayOutline16),
654
1004
  onClick: onRun,
655
1005
  disabled: busyForJob,
656
1006
  },
@@ -661,26 +1011,32 @@ window.__ModuleLoader__.load({
661
1011
  "span",
662
1012
  { className: "dsh-auto-confirm" },
663
1013
  h(
664
- "button",
1014
+ Button,
665
1015
  {
666
1016
  type: "button",
667
- className: "dsh-auto-btn dsh-auto-btn-danger",
1017
+ variant: "ghost",
1018
+ size: "sm",
1019
+ className: "dsh-auto-danger-button",
1020
+ icon: deleting ? null : h(IconTrashOutline16),
668
1021
  onClick: onDeleteConfirm,
669
1022
  disabled: deleting,
670
1023
  },
671
1024
  deleting ? "Deleting\u2026" : "Confirm delete",
672
1025
  ),
673
1026
  h(
674
- "button",
675
- { type: "button", className: "dsh-auto-btn", onClick: onDeleteCancel, disabled: deleting },
1027
+ Button,
1028
+ { type: "button", variant: "outline", size: "sm", onClick: onDeleteCancel, disabled: deleting },
676
1029
  "Keep",
677
1030
  ),
678
1031
  )
679
1032
  : h(
680
- "button",
1033
+ Button,
681
1034
  {
682
1035
  type: "button",
683
- className: "dsh-auto-btn dsh-auto-btn-danger-ghost",
1036
+ variant: "ghost",
1037
+ size: "sm",
1038
+ className: "dsh-auto-danger-button",
1039
+ icon: h(IconTrashOutline16),
684
1040
  onClick: onDelete,
685
1041
  disabled: busyForJob,
686
1042
  },
@@ -750,26 +1106,29 @@ window.__ModuleLoader__.load({
750
1106
  "span",
751
1107
  { className: "dsh-auto-confirm" },
752
1108
  h(
753
- "button",
1109
+ Button,
754
1110
  {
755
1111
  type: "button",
756
- className: "dsh-auto-btn dsh-auto-btn-danger",
1112
+ variant: "ghost",
1113
+ size: "sm",
1114
+ className: "dsh-auto-danger-button",
757
1115
  onClick: () => onCancelConfirm(run),
758
1116
  disabled: cancelling,
759
1117
  },
760
1118
  cancelling ? "Cancelling\u2026" : "Confirm",
761
1119
  ),
762
1120
  h(
763
- "button",
764
- { type: "button", className: "dsh-auto-btn", onClick: onCancelReset, disabled: cancelling },
1121
+ Button,
1122
+ { type: "button", variant: "outline", size: "sm", onClick: onCancelReset, disabled: cancelling },
765
1123
  "Back",
766
1124
  ),
767
1125
  )
768
1126
  : h(
769
- "button",
1127
+ Button,
770
1128
  {
771
1129
  type: "button",
772
- className: "dsh-auto-btn dsh-auto-btn-ghost",
1130
+ variant: "ghost",
1131
+ size: "sm",
773
1132
  onClick: () => onCancel(run),
774
1133
  },
775
1134
  "Cancel",
@@ -788,7 +1147,7 @@ window.__ModuleLoader__.load({
788
1147
  );
789
1148
  }
790
1149
 
791
- function AutomationsSection() {
1150
+ function AutomationsSection({ centerMode = false } = {}) {
792
1151
  const [meta, setMeta] = useState(null);
793
1152
  const [snapshot, setSnapshot] = useState(null);
794
1153
  const [loading, setLoading] = useState(true);
@@ -1065,14 +1424,17 @@ window.__ModuleLoader__.load({
1065
1424
 
1066
1425
  return h(
1067
1426
  "section",
1068
- { className: "dsh-auto-root", "aria-busy": loading ? "true" : null },
1427
+ {
1428
+ className: "dsh-auto-root" + (centerMode ? " dsh-auto-root-workspace" : ""),
1429
+ "aria-busy": loading ? "true" : null,
1430
+ },
1069
1431
  h(
1070
1432
  "div",
1071
1433
  { className: "dsh-auto-head" },
1072
1434
  h(
1073
1435
  "div",
1074
1436
  null,
1075
- h("h2", null, "Automations"),
1437
+ h("h2", { className: centerMode ? "dsh-auto-sr-only" : undefined }, "Automations"),
1076
1438
  h(
1077
1439
  "p",
1078
1440
  { className: "dsh-auto-sub" },
@@ -1083,8 +1445,8 @@ window.__ModuleLoader__.load({
1083
1445
  ),
1084
1446
  !formOpen
1085
1447
  ? h(
1086
- "button",
1087
- { type: "button", className: "dsh-auto-btn dsh-auto-btn-primary", onClick: openCreate },
1448
+ Button,
1449
+ { type: "button", variant: "primary", icon: h(IconPlusOutline16), onClick: openCreate },
1088
1450
  "New automation",
1089
1451
  )
1090
1452
  : null,
@@ -1121,8 +1483,8 @@ window.__ModuleLoader__.load({
1121
1483
  { className: "dsh-auto-error", role: "alert" },
1122
1484
  h("p", null, "Could not load automations: ", loadError),
1123
1485
  h(
1124
- "button",
1125
- { type: "button", className: "dsh-auto-btn dsh-auto-btn-primary", onClick: () => loadSnapshot(true) },
1486
+ Button,
1487
+ { type: "button", variant: "primary", size: "sm", icon: h(IconRefreshOutline16), onClick: () => loadSnapshot(true) },
1126
1488
  "Retry",
1127
1489
  ),
1128
1490
  )
@@ -1194,7 +1556,7 @@ window.__ModuleLoader__.load({
1194
1556
  };
1195
1557
  }
1196
1558
 
1197
- const MODAL_FOCUS_SELECTOR = [
1559
+ const FOCUSABLE_SELECTOR = [
1198
1560
  "a[href]",
1199
1561
  "button:not([disabled])",
1200
1562
  "input:not([disabled])",
@@ -1204,45 +1566,15 @@ window.__ModuleLoader__.load({
1204
1566
  "[tabindex]:not([tabindex=\"-1\"])",
1205
1567
  ].join(",");
1206
1568
 
1207
- function modalFocusables(panel) {
1569
+ function visibleFocusables(panel) {
1208
1570
  if (!panel) return [];
1209
- return Array.from(panel.querySelectorAll(MODAL_FOCUS_SELECTOR)).filter((element) =>
1571
+ return Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) =>
1210
1572
  !element.hasAttribute("hidden") &&
1211
1573
  element.getAttribute("aria-hidden") !== "true" &&
1212
1574
  element.getClientRects().length > 0,
1213
1575
  );
1214
1576
  }
1215
1577
 
1216
- function trapModalTab(event, panel) {
1217
- if (
1218
- event.key !== "Tab" ||
1219
- event.defaultPrevented ||
1220
- event.altKey ||
1221
- event.ctrlKey ||
1222
- event.metaKey
1223
- ) return;
1224
- const focusables = modalFocusables(panel);
1225
- if (focusables.length === 0) {
1226
- event.preventDefault();
1227
- panel?.focus();
1228
- return;
1229
- }
1230
- const first = focusables[0];
1231
- const last = focusables[focusables.length - 1];
1232
- const active = panel.ownerDocument.activeElement;
1233
- if (event.shiftKey) {
1234
- if (active === first || !panel.contains(active)) {
1235
- event.preventDefault();
1236
- last.focus();
1237
- }
1238
- return;
1239
- }
1240
- if (active === last || !panel.contains(active)) {
1241
- event.preventDefault();
1242
- first.focus();
1243
- }
1244
- }
1245
-
1246
1578
  function AutomationGlyph({ size = 18 }) {
1247
1579
  return h(
1248
1580
  "svg",
@@ -1265,24 +1597,6 @@ window.__ModuleLoader__.load({
1265
1597
  );
1266
1598
  }
1267
1599
 
1268
- function CloseGlyph() {
1269
- return h(
1270
- "svg",
1271
- {
1272
- width: "16",
1273
- height: "16",
1274
- viewBox: "0 0 16 16",
1275
- fill: "none",
1276
- stroke: "currentColor",
1277
- strokeWidth: "1.5",
1278
- strokeLinecap: "round",
1279
- "aria-hidden": "true",
1280
- focusable: "false",
1281
- },
1282
- h("path", { d: "M3.5 3.5l9 9M12.5 3.5l-9 9" }),
1283
- );
1284
- }
1285
-
1286
1600
  function AutomationsSidebarAction({ wide, disclosure }) {
1287
1601
  const open = useSyncExternalStore(
1288
1602
  disclosure.subscribe,
@@ -1309,10 +1623,9 @@ window.__ModuleLoader__.load({
1309
1623
  ref: triggerRef,
1310
1624
  type: "button",
1311
1625
  className: "dsh-auto-sidebar-trigger",
1312
- title: wide ? undefined : "Automations",
1313
- "aria-label": "Automations",
1314
- "aria-haspopup": "dialog",
1315
- "aria-expanded": open,
1626
+ title: wide ? undefined : open ? "Exit Automations" : "Automations",
1627
+ "aria-label": open ? "Exit Automations" : "Open Automations",
1628
+ "aria-pressed": open,
1316
1629
  "data-active": open ? "true" : undefined,
1317
1630
  "data-dsh-automations-trigger": "true",
1318
1631
  onClick: disclosure.toggle,
@@ -1323,87 +1636,76 @@ window.__ModuleLoader__.load({
1323
1636
  );
1324
1637
  }
1325
1638
 
1326
- function AutomationsOverlay({ disclosure }) {
1327
- const open = useSyncExternalStore(
1328
- disclosure.subscribe,
1329
- disclosure.getSnapshot,
1330
- disclosure.getSnapshot,
1331
- );
1332
- const closeRef = useRef(null);
1333
- const panelRef = useRef(null);
1639
+ function AutomationsWorkspace({ disclosure }) {
1640
+ const workspaceRef = useRef(null);
1334
1641
  const titleId = useId();
1335
1642
 
1336
1643
  useEffect(() => {
1337
- if (!open) return undefined;
1338
- const frame = window.requestAnimationFrame(() => closeRef.current?.focus());
1339
- return () => window.cancelAnimationFrame(frame);
1340
- }, [open]);
1644
+ const frame = window.requestAnimationFrame(() =>
1645
+ workspaceRef.current?.querySelector('[data-dsh-automations-exit="true"]')?.focus(),
1646
+ );
1647
+ const onKeyDown = (event) => {
1648
+ if (event.key !== "Escape" || event.defaultPrevented || event.__dshAutomationsNested) return;
1649
+ if (event.target?.closest?.('[role="dialog"],[role="menu"]')) return;
1650
+ if (document.querySelector('.dsh-auto-picker-trigger[aria-expanded="true"]')) return;
1651
+ event.preventDefault();
1652
+ disclosure.close();
1653
+ };
1654
+ window.addEventListener("keydown", onKeyDown);
1655
+ return () => {
1656
+ window.cancelAnimationFrame(frame);
1657
+ window.removeEventListener("keydown", onKeyDown);
1658
+ };
1659
+ }, [disclosure]);
1341
1660
 
1342
- if (!open) return null;
1343
1661
  return h(
1344
- "div",
1345
- { className: "dsh-auto-overlay", "data-dsh-automations-overlay": "true" },
1346
- h("div", { className: "dsh-auto-overlay-mask", "aria-hidden": "true", onClick: disclosure.close }),
1662
+ "section",
1663
+ {
1664
+ ref: workspaceRef,
1665
+ className: "dsh-auto-workspace",
1666
+ "data-dsh-automations-workspace": "true",
1667
+ "aria-labelledby": titleId,
1668
+ },
1347
1669
  h(
1348
- "section",
1349
- {
1350
- ref: panelRef,
1351
- className: "dsh-auto-overlay-panel",
1352
- role: "dialog",
1353
- tabIndex: -1,
1354
- "aria-modal": "true",
1355
- "aria-labelledby": titleId,
1356
- onKeyDownCapture: (event) => {
1357
- if (event.key === "Escape") {
1358
- event.preventDefault();
1359
- disclosure.close();
1360
- return;
1361
- }
1362
- trapModalTab(event, panelRef.current);
1363
- },
1364
- },
1670
+ "header",
1671
+ { className: "dsh-auto-workspace-toolbar" },
1672
+ h("span", { className: "dsh-auto-workspace-icon", "aria-hidden": "true" }, h(AutomationGlyph, { size: 16 })),
1673
+ h("strong", { id: titleId, className: "dsh-auto-workspace-title" }, "Automations"),
1365
1674
  h(
1366
- "header",
1367
- { className: "dsh-auto-overlay-header" },
1368
- h("span", { id: titleId, className: "dsh-auto-sr-only" }, "Automations"),
1369
- h(
1370
- "button",
1371
- {
1372
- ref: closeRef,
1373
- type: "button",
1374
- className: "dsh-auto-overlay-close",
1375
- "aria-label": "Close Automations",
1376
- title: "Close",
1377
- onClick: disclosure.close,
1378
- },
1379
- h(CloseGlyph),
1380
- ),
1675
+ Button,
1676
+ {
1677
+ type: "button",
1678
+ variant: "toolbar",
1679
+ size: "sm",
1680
+ icon: h(IconChevronLeftOutline14),
1681
+ onClick: disclosure.close,
1682
+ "data-dsh-automations-exit": "true",
1683
+ },
1684
+ "Exit Automations",
1381
1685
  ),
1382
- h("div", { className: "dsh-auto-overlay-body" }, h(AutomationsSection)),
1383
1686
  ),
1687
+ h("div", { className: "dsh-auto-workspace-scroll" }, h(AutomationsSection, { centerMode: true })),
1384
1688
  );
1385
1689
  }
1386
1690
 
1387
1691
  const STYLE_CSS = [
1388
1692
  ".dsh-auto-sidebar-action{box-sizing:border-box;width:100%;height:42px;flex:none;display:flex;align-items:center;margin:4px 0 0;}",
1389
- ".dsh-auto-sidebar-trigger{box-sizing:border-box;appearance:none;width:calc(100% + 4px);height:42px;margin:0 -2px;padding:0 10px 0 8px;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary,#0f1115);display:flex;align-items:center;gap:8px;overflow:hidden;font:14px/22px inherit;cursor:pointer;}",
1693
+ ".dsh-auto-sidebar-trigger{box-sizing:border-box;appearance:none;width:calc(100% + 4px);height:42px;margin:0 -2px;padding:0 10px 0 8px;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary,#0f1115);display:flex;align-items:center;gap:8px;overflow:hidden;font-family:inherit;font-size:14px;line-height:22px;cursor:pointer;}",
1390
1694
  ".dsh-auto-sidebar-trigger:hover,.dsh-auto-sidebar-trigger[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));}",
1391
1695
  ".dsh-auto-sidebar-trigger:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:-2px;}",
1392
1696
  ".dsh-auto-sidebar-trigger svg{flex:none;}",
1393
1697
  ".dsh-auto-sidebar-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
1394
1698
  ".dsh-auto-sidebar-action-rail{width:36px;height:36px;margin:0;}",
1395
1699
  ".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%;}",
1396
- ".dsh-auto-overlay{z-index:1000;pointer-events:auto;position:fixed;inset:0;display:flex;align-items:center;justify-content:center;}",
1397
- ".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));}",
1398
- ".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:0;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;}",
1399
- ".dsh-auto-overlay-header{box-sizing:border-box;height:52px;flex:none;display:flex;align-items:center;justify-content:flex-end;padding:10px 16px;}",
1400
- ".dsh-auto-overlay-close{appearance:none;width:30px;height:30px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,#4f5661);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;}",
1401
- ".dsh-auto-overlay-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsw-alias-label-primary,#0f1115);}",
1402
- ".dsh-auto-overlay-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:1px;}",
1403
- ".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);}",
1404
- ".dsh-auto-overlay-body>.dsh-auto-root{width:100%;margin:0 auto;padding-top:0;}",
1700
+ ".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;}",
1701
+ ".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);}",
1702
+ ".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);}",
1703
+ ".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;}",
1704
+ ".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);}",
1705
+ ".dsh-auto-workspace-scroll>.dsh-auto-root{width:100%;margin:0 auto;padding-top:0;}",
1405
1706
  ".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;}",
1406
- ".dsh-auto-root{--dsh-auto-border:var(--dsw-alias-border-l2,rgba(15,17,21,.14));--dsh-auto-border-strong:var(--dsw-alias-border-l3,rgba(15,17,21,.2));--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-primary-fill:var(--dsw-alias-button-primary-fill,#0f1115);--dsh-auto-primary-hover:var(--dsw-alias-button-primary-hover,#34415b);--dsh-auto-on-primary:var(--dsw-alias-label-primary-foreground,#fff);--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);}",
1707
+ ".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);}",
1708
+ ".dsh-auto-root-workspace{max-width:960px;}",
1407
1709
  "body:not([data-ds-dark-theme]) .dsh-auto-root{color-scheme:light;}",
1408
1710
  "body[data-ds-dark-theme] .dsh-auto-root{color-scheme:dark;}",
1409
1711
  ".dsh-auto-root *,.dsh-auto-root *::before,.dsh-auto-root *::after{box-sizing:border-box;}",
@@ -1415,21 +1717,30 @@ window.__ModuleLoader__.load({
1415
1717
  ".dsh-auto-flash-success{background:color-mix(in srgb,var(--dsh-auto-success) 12%,transparent);color:var(--dsh-auto-success);}",
1416
1718
  ".dsh-auto-flash-warn{background:color-mix(in srgb,var(--dsh-auto-warning) 12%,transparent);color:var(--dsh-auto-warning);}",
1417
1719
  ".dsh-auto-linkbtn{appearance:none;font:inherit;font-size:inherit;color:inherit;text-decoration:underline;cursor:pointer;background:none;border:none;padding:0;}",
1418
- ".dsh-auto-btn{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;padding:5px 12px;border:1px solid var(--dsh-auto-border);border-radius:16px;background:transparent;color:var(--dsh-auto-text);font-family:inherit;font-size:13px;line-height:1.2;cursor:pointer;transition:background .15s ease,color .15s ease,opacity .15s ease;white-space:nowrap;}",
1419
- ".dsh-auto-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.08)));}",
1420
- ".dsh-auto-btn:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsh-auto-border-strong);}",
1421
- ".dsh-auto-btn:disabled{opacity:.4;cursor:default;}",
1422
- ".dsh-auto-btn-primary{background:var(--dsh-auto-primary-fill);border-color:transparent;color:var(--dsh-auto-on-primary);}",
1423
- ".dsh-auto-btn-primary:hover:not(:disabled){background:var(--dsh-auto-primary-hover);}",
1424
- ".dsh-auto-btn-danger,.dsh-auto-btn-danger-ghost{background:transparent;border-color:transparent;color:var(--dsh-auto-danger);}",
1425
- ".dsh-auto-btn-danger:hover:not(:disabled),.dsh-auto-btn-danger-ghost:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger,color-mix(in srgb,var(--dsh-auto-danger) 10%,transparent));}",
1426
- ".dsh-auto-btn-ghost{border-color:transparent;color:var(--dsh-auto-text-secondary);}",
1720
+ ".dsh-auto-danger-button{color:var(--dsh-auto-danger);}",
1721
+ ".dsh-auto-danger-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger,color-mix(in srgb,var(--dsh-auto-danger) 10%,transparent));}",
1427
1722
  ".dsh-auto-input,.dsh-auto-select,.dsh-auto-textarea{width:100%;min-height:34px;padding:6px 10px;border:1px solid var(--dsh-auto-border);border-radius:8px;background:var(--dsh-auto-input-bg);color:var(--dsh-auto-text);font-family:inherit;font-size:13px;line-height:1.5;}",
1428
1723
  ".dsh-auto-input,.dsh-auto-textarea{appearance:none;}",
1429
1724
  ".dsh-auto-input::placeholder,.dsh-auto-textarea::placeholder{color:var(--dsh-auto-caption);}",
1430
1725
  ".dsh-auto-select option{background:var(--dsh-auto-input-bg);color:var(--dsh-auto-text);}",
1431
1726
  ".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);}",
1432
1727
  ".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;}",
1728
+ ".dsh-auto-picker-root{width:100%;display:flex;}",
1729
+ ".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;}",
1730
+ ".dsh-auto-picker-trigger:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsh-auto-input-bg));}",
1731
+ ".dsh-auto-picker-trigger: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);}",
1732
+ ".dsh-auto-picker-trigger-icon{flex:none;color:var(--dsh-auto-muted);}",
1733
+ ".dsh-auto-picker-trigger-label{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
1734
+ ".dsh-auto-picker-chevron{flex:none;color:var(--dsh-auto-muted);transition:transform .12s ease;}",
1735
+ ".dsh-auto-picker-chevron-open{transform:rotate(180deg);}",
1736
+ ".dsh-auto-picker-item-copy{min-width:0;display:flex;flex-direction:column;white-space:normal;}",
1737
+ ".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;}",
1739
+ ".dsh-auto-permission-glyph{flex:none;}",
1740
+ ".dsh-auto-permission-glyph-read{color:var(--dsh-auto-muted);}",
1741
+ ".dsh-auto-permission-glyph-write{color:var(--dsh-auto-accent);}",
1742
+ ".dsh-auto-permission-glyph-danger{color:var(--dsh-auto-danger);}",
1743
+ ".dsh-auto-permission-glyph-custom{color:var(--dsh-auto-muted);}",
1433
1744
  ".dsh-auto-textarea{resize:vertical;min-height:110px;}",
1434
1745
  ".dsh-auto-check{display:inline-flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;min-height:34px;}",
1435
1746
  ".dsh-auto-check input{width:16px;height:16px;margin:0;accent-color:var(--dsh-auto-accent);cursor:pointer;}",
@@ -1452,6 +1763,8 @@ window.__ModuleLoader__.load({
1452
1763
  ".dsh-auto-card-id{font-size:11px;color:var(--dsh-auto-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}",
1453
1764
  ".dsh-auto-card-meta{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:12px;color:var(--dsh-auto-text-secondary);line-height:1.5;}",
1454
1765
  ".dsh-auto-card-meta span{white-space:nowrap;}",
1766
+ ".dsh-auto-card-permission{display:inline-flex;align-items:center;gap:4px;}",
1767
+ ".dsh-auto-card-permission svg{width:14px;height:14px;}",
1455
1768
  ".dsh-auto-next{font-variant-numeric:tabular-nums;}",
1456
1769
  ".dsh-auto-muted{color:var(--dsh-auto-muted);}",
1457
1770
  ".dsh-auto-card-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}",
@@ -1488,13 +1801,34 @@ window.__ModuleLoader__.load({
1488
1801
  "@keyframes dsh-auto-pulse{0%,100%{opacity:1}50%{opacity:.35}}",
1489
1802
  ".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);}",
1490
1803
  ".dsh-auto-error{color:var(--dsh-auto-danger);display:flex;flex-direction:column;gap:10px;align-items:center;}",
1491
- "@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;}}",
1804
+ "@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;}}",
1805
+ "@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;}}",
1492
1806
  ].join("\n");
1493
1807
 
1494
1808
  function apply(ctx) {
1495
1809
  const disclosure = createDisclosureStore();
1496
- // All package-owned surfaces share one style element and one disclosure
1497
- // store; both are released with the client-plugin fiber.
1810
+ let centerDeclared = false;
1811
+ let disposeCenter = null;
1812
+ const unmountCenter = () => {
1813
+ if (!disposeCenter) return;
1814
+ const dispose = disposeCenter;
1815
+ disposeCenter = null;
1816
+ dispose();
1817
+ };
1818
+ const mountCenter = () => {
1819
+ if (!centerDeclared || !disclosure.getSnapshot() || disposeCenter) return;
1820
+ try {
1821
+ disposeCenter = ctx.slots.register({
1822
+ name: "conversation",
1823
+ priority: -200,
1824
+ inject: () => ({ disclosure }),
1825
+ }, AutomationsWorkspace);
1826
+ } catch (error) {
1827
+ console.error("dsh automations: could not mount center workspace", error);
1828
+ disclosure.close();
1829
+ }
1830
+ };
1831
+
1498
1832
  ctx.effect(() => {
1499
1833
  const tag = document.createElement("style");
1500
1834
  tag.setAttribute("data-plugin", "@syncended/dsh-automations");
@@ -1504,7 +1838,28 @@ window.__ModuleLoader__.load({
1504
1838
  tag.remove();
1505
1839
  };
1506
1840
  }, "@syncended/dsh-automations: client styles");
1507
- ctx.effect(() => () => disclosure.dispose(), "@syncended/dsh-automations: disclosure store");
1841
+ ctx.effect(
1842
+ () => ctx.slots.inject("conversation", () => {
1843
+ centerDeclared = true;
1844
+ mountCenter();
1845
+ return () => {
1846
+ centerDeclared = false;
1847
+ unmountCenter();
1848
+ };
1849
+ }),
1850
+ "@syncended/dsh-automations: center workspace",
1851
+ );
1852
+ ctx.effect(() => {
1853
+ const unsubscribe = disclosure.subscribe(() => {
1854
+ if (disclosure.getSnapshot()) mountCenter();
1855
+ else unmountCenter();
1856
+ });
1857
+ return () => {
1858
+ unsubscribe();
1859
+ unmountCenter();
1860
+ disclosure.dispose();
1861
+ };
1862
+ }, "@syncended/dsh-automations: workspace state");
1508
1863
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1509
1864
  name: "settings.section",
1510
1865
  id: "automations",
@@ -1518,16 +1873,11 @@ window.__ModuleLoader__.load({
1518
1873
  label: "Automations",
1519
1874
  inject: () => ({ disclosure }),
1520
1875
  }, AutomationsSidebarAction));
1521
- ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1522
- name: "shell.overlay",
1523
- id: "automations",
1524
- order: 50,
1525
- label: "Automations",
1526
- inject: () => ({ disclosure }),
1527
- }, AutomationsOverlay));
1528
1876
  }
1529
1877
 
1530
1878
  exports.agentPresetDisplayLabel = agentPresetDisplayLabel;
1879
+ exports.permissionPresetPresentation = permissionPresetPresentation;
1880
+ exports.PermissionPresetPicker = PermissionPresetPicker;
1531
1881
  exports.inject = inject;
1532
1882
  exports.apply = apply;
1533
1883
  return module.exports;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncended/dsh-automations",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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,7 +99,9 @@
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",
104
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
101
105
  "@deepseek-ai/dsh-client-ui-settings": "0.1.1-rc.2",
102
106
  "@deepseek-ai/dsh-client-ui-sidebar": "0.1.1-rc.2",
103
107
  "@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",