@syncended/dsh-automations 0.1.2 → 0.2.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 in **Settings → Automations**.
11
+ - Enable/disable, Run now, edit, delete, cancel, and recent run history from the main **Automations** sidebar action or **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 **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 **Automations** 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
- Settings UI / HTTP API
18
+ Sidebar / Settings UI / HTTP API
19
19
 
20
20
 
21
21
  AutomationService ─── ProjectPolicy
@@ -28,6 +28,10 @@ Settings UI / HTTP API
28
28
  └── HarnessAgentExecutor (MVP)
29
29
  ```
30
30
 
31
+ ### Browser surfaces
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.
34
+
31
35
  ### AutomationService
32
36
 
33
37
  Cordis service `ctx.automations`. It owns input validation, canonical project authorization, metadata discovery, the Web route, and the public executor-registration seam.
package/lib/client.js CHANGED
@@ -12,7 +12,7 @@ window.__ModuleLoader__.load({
12
12
  // x-dsh-automation-client fence header plus an application/json body.
13
13
  const React = require("react");
14
14
  const h = React.createElement;
15
- const { useCallback, useEffect, useRef, useState } = React;
15
+ const { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } = React;
16
16
  const inject = ["slots"];
17
17
 
18
18
  const API_PREFIX = "/api/automations";
@@ -1139,7 +1139,242 @@ window.__ModuleLoader__.load({
1139
1139
  );
1140
1140
  }
1141
1141
 
1142
+ function createDisclosureStore() {
1143
+ let open = false;
1144
+ let disposed = false;
1145
+ const listeners = new Set();
1146
+ const publish = (next) => {
1147
+ if (disposed || open === next) return;
1148
+ open = next;
1149
+ for (const listener of Array.from(listeners)) listener();
1150
+ };
1151
+ return {
1152
+ getSnapshot: () => open,
1153
+ subscribe: (listener) => {
1154
+ if (disposed) return () => {};
1155
+ listeners.add(listener);
1156
+ return () => listeners.delete(listener);
1157
+ },
1158
+ open: () => publish(true),
1159
+ close: () => publish(false),
1160
+ toggle: () => publish(!open),
1161
+ dispose: () => {
1162
+ disposed = true;
1163
+ open = false;
1164
+ listeners.clear();
1165
+ },
1166
+ };
1167
+ }
1168
+
1169
+ const MODAL_FOCUS_SELECTOR = [
1170
+ "a[href]",
1171
+ "button:not([disabled])",
1172
+ "input:not([disabled])",
1173
+ "select:not([disabled])",
1174
+ "textarea:not([disabled])",
1175
+ "[contenteditable=\"true\"]",
1176
+ "[tabindex]:not([tabindex=\"-1\"])",
1177
+ ].join(",");
1178
+
1179
+ function modalFocusables(panel) {
1180
+ if (!panel) return [];
1181
+ return Array.from(panel.querySelectorAll(MODAL_FOCUS_SELECTOR)).filter((element) =>
1182
+ !element.hasAttribute("hidden") &&
1183
+ element.getAttribute("aria-hidden") !== "true" &&
1184
+ element.getClientRects().length > 0,
1185
+ );
1186
+ }
1187
+
1188
+ function trapModalTab(event, panel) {
1189
+ if (
1190
+ event.key !== "Tab" ||
1191
+ event.defaultPrevented ||
1192
+ event.altKey ||
1193
+ event.ctrlKey ||
1194
+ event.metaKey
1195
+ ) return;
1196
+ const focusables = modalFocusables(panel);
1197
+ if (focusables.length === 0) {
1198
+ event.preventDefault();
1199
+ panel?.focus();
1200
+ return;
1201
+ }
1202
+ const first = focusables[0];
1203
+ const last = focusables[focusables.length - 1];
1204
+ const active = panel.ownerDocument.activeElement;
1205
+ if (event.shiftKey) {
1206
+ if (active === first || !panel.contains(active)) {
1207
+ event.preventDefault();
1208
+ last.focus();
1209
+ }
1210
+ return;
1211
+ }
1212
+ if (active === last || !panel.contains(active)) {
1213
+ event.preventDefault();
1214
+ first.focus();
1215
+ }
1216
+ }
1217
+
1218
+ function AutomationGlyph({ size = 18 }) {
1219
+ return h(
1220
+ "svg",
1221
+ {
1222
+ width: size,
1223
+ height: size,
1224
+ viewBox: "0 0 24 24",
1225
+ fill: "none",
1226
+ stroke: "currentColor",
1227
+ strokeWidth: "1.7",
1228
+ strokeLinecap: "round",
1229
+ strokeLinejoin: "round",
1230
+ "aria-hidden": "true",
1231
+ focusable: "false",
1232
+ },
1233
+ h("rect", { x: "3.5", y: "5.5", width: "17", height: "15", rx: "3" }),
1234
+ h("path", { d: "M8 3.5v4M16 3.5v4M3.5 10h17" }),
1235
+ h("circle", { cx: "12", cy: "15.25", r: "2.75" }),
1236
+ h("path", { d: "M12 13.8v1.65l1.15.7" }),
1237
+ );
1238
+ }
1239
+
1240
+ function CloseGlyph() {
1241
+ return h(
1242
+ "svg",
1243
+ {
1244
+ width: "16",
1245
+ height: "16",
1246
+ viewBox: "0 0 16 16",
1247
+ fill: "none",
1248
+ stroke: "currentColor",
1249
+ strokeWidth: "1.5",
1250
+ strokeLinecap: "round",
1251
+ "aria-hidden": "true",
1252
+ focusable: "false",
1253
+ },
1254
+ h("path", { d: "M3.5 3.5l9 9M12.5 3.5l-9 9" }),
1255
+ );
1256
+ }
1257
+
1258
+ function AutomationsSidebarAction({ wide, disclosure }) {
1259
+ const open = useSyncExternalStore(
1260
+ disclosure.subscribe,
1261
+ disclosure.getSnapshot,
1262
+ disclosure.getSnapshot,
1263
+ );
1264
+ const triggerRef = useRef(null);
1265
+ const previousOpenRef = useRef(open);
1266
+
1267
+ useEffect(() => {
1268
+ const previous = previousOpenRef.current;
1269
+ previousOpenRef.current = open;
1270
+ if (!previous || open) return undefined;
1271
+ const frame = window.requestAnimationFrame(() => triggerRef.current?.focus());
1272
+ return () => window.cancelAnimationFrame(frame);
1273
+ }, [open]);
1274
+
1275
+ return h(
1276
+ "div",
1277
+ { className: "dsh-auto-sidebar-action" + (wide ? "" : " dsh-auto-sidebar-action-rail") },
1278
+ h(
1279
+ "button",
1280
+ {
1281
+ ref: triggerRef,
1282
+ type: "button",
1283
+ className: "dsh-auto-sidebar-trigger",
1284
+ title: wide ? undefined : "Automations",
1285
+ "aria-label": "Automations",
1286
+ "aria-haspopup": "dialog",
1287
+ "aria-expanded": open,
1288
+ "data-active": open ? "true" : undefined,
1289
+ "data-dsh-automations-trigger": "true",
1290
+ onClick: disclosure.toggle,
1291
+ },
1292
+ h(AutomationGlyph, { size: wide ? 16 : 18 }),
1293
+ wide ? h("span", { className: "dsh-auto-sidebar-label" }, "Automations") : null,
1294
+ ),
1295
+ );
1296
+ }
1297
+
1298
+ function AutomationsOverlay({ disclosure }) {
1299
+ const open = useSyncExternalStore(
1300
+ disclosure.subscribe,
1301
+ disclosure.getSnapshot,
1302
+ disclosure.getSnapshot,
1303
+ );
1304
+ const closeRef = useRef(null);
1305
+ const panelRef = useRef(null);
1306
+ const titleId = useId();
1307
+
1308
+ useEffect(() => {
1309
+ if (!open) return undefined;
1310
+ const frame = window.requestAnimationFrame(() => closeRef.current?.focus());
1311
+ return () => window.cancelAnimationFrame(frame);
1312
+ }, [open]);
1313
+
1314
+ if (!open) return null;
1315
+ return h(
1316
+ "div",
1317
+ { className: "dsh-auto-overlay", "data-dsh-automations-overlay": "true" },
1318
+ h("div", { className: "dsh-auto-overlay-mask", "aria-hidden": "true", onClick: disclosure.close }),
1319
+ h(
1320
+ "section",
1321
+ {
1322
+ ref: panelRef,
1323
+ className: "dsh-auto-overlay-panel",
1324
+ role: "dialog",
1325
+ tabIndex: -1,
1326
+ "aria-modal": "true",
1327
+ "aria-labelledby": titleId,
1328
+ onKeyDownCapture: (event) => {
1329
+ if (event.key === "Escape") {
1330
+ event.preventDefault();
1331
+ disclosure.close();
1332
+ return;
1333
+ }
1334
+ trapModalTab(event, panelRef.current);
1335
+ },
1336
+ },
1337
+ h(
1338
+ "header",
1339
+ { className: "dsh-auto-overlay-header" },
1340
+ h("span", { id: titleId, className: "dsh-auto-sr-only" }, "Automations"),
1341
+ h(
1342
+ "button",
1343
+ {
1344
+ ref: closeRef,
1345
+ type: "button",
1346
+ className: "dsh-auto-overlay-close",
1347
+ "aria-label": "Close Automations",
1348
+ title: "Close",
1349
+ onClick: disclosure.close,
1350
+ },
1351
+ h(CloseGlyph),
1352
+ ),
1353
+ ),
1354
+ h("div", { className: "dsh-auto-overlay-body" }, h(AutomationsSection)),
1355
+ ),
1356
+ );
1357
+ }
1358
+
1142
1359
  const STYLE_CSS = [
1360
+ ".dsh-auto-sidebar-action{box-sizing:border-box;width:100%;height:42px;flex:none;display:flex;align-items:center;margin:4px 0 0;}",
1361
+ ".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;}",
1362
+ ".dsh-auto-sidebar-trigger:hover,.dsh-auto-sidebar-trigger[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));}",
1363
+ ".dsh-auto-sidebar-trigger:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:-2px;}",
1364
+ ".dsh-auto-sidebar-trigger svg{flex:none;}",
1365
+ ".dsh-auto-sidebar-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
1366
+ ".dsh-auto-sidebar-action-rail{width:36px;height:36px;margin:0;}",
1367
+ ".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%;}",
1368
+ ".dsh-auto-overlay{z-index:1000;pointer-events:auto;position:fixed;inset:0;display:flex;align-items:center;justify-content:center;}",
1369
+ ".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));}",
1370
+ ".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;}",
1371
+ ".dsh-auto-overlay-header{box-sizing:border-box;height:52px;flex:none;display:flex;align-items:center;justify-content:flex-end;padding:10px 16px;}",
1372
+ ".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;}",
1373
+ ".dsh-auto-overlay-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsw-alias-label-primary,#0f1115);}",
1374
+ ".dsh-auto-overlay-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:1px;}",
1375
+ ".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);}",
1376
+ ".dsh-auto-overlay-body>.dsh-auto-root{width:100%;margin:0 auto;padding-top:0;}",
1377
+ ".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;}",
1143
1378
  ".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);}",
1144
1379
  "body:not([data-ds-dark-theme]) .dsh-auto-root{color-scheme:light;}",
1145
1380
  "body[data-ds-dark-theme] .dsh-auto-root{color-scheme:dark;}",
@@ -1225,12 +1460,13 @@ window.__ModuleLoader__.load({
1225
1460
  "@keyframes dsh-auto-pulse{0%,100%{opacity:1}50%{opacity:.35}}",
1226
1461
  ".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);}",
1227
1462
  ".dsh-auto-error{color:var(--dsh-auto-danger);display:flex;flex-direction:column;gap:10px;align-items:center;}",
1228
- "@media (max-width:640px){.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;}}",
1463
+ "@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;}}",
1229
1464
  ].join("\n");
1230
1465
 
1231
1466
  function apply(ctx) {
1232
- // The page styles ride a style element owned by this plugin's fiber:
1233
- // created now and removed when the plugin unloads.
1467
+ const disclosure = createDisclosureStore();
1468
+ // All package-owned surfaces share one style element and one disclosure
1469
+ // store; both are released with the client-plugin fiber.
1234
1470
  ctx.effect(() => {
1235
1471
  const tag = document.createElement("style");
1236
1472
  tag.setAttribute("data-plugin", "@syncended/dsh-automations");
@@ -1239,13 +1475,28 @@ window.__ModuleLoader__.load({
1239
1475
  return () => {
1240
1476
  tag.remove();
1241
1477
  };
1242
- }, "@syncended/dsh-automations: settings page styles");
1478
+ }, "@syncended/dsh-automations: client styles");
1479
+ ctx.effect(() => () => disclosure.dispose(), "@syncended/dsh-automations: disclosure store");
1243
1480
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1244
1481
  name: "settings.section",
1245
1482
  id: "automations",
1246
1483
  order: 25,
1247
1484
  label: "Automations",
1248
1485
  }, AutomationsSection));
1486
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
1487
+ name: "sidebar.footer.action",
1488
+ id: "automations",
1489
+ order: 50,
1490
+ label: "Automations",
1491
+ inject: () => ({ disclosure }),
1492
+ }, AutomationsSidebarAction));
1493
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1494
+ name: "shell.overlay",
1495
+ id: "automations",
1496
+ order: 50,
1497
+ label: "Automations",
1498
+ inject: () => ({ disclosure }),
1499
+ }, AutomationsOverlay));
1249
1500
  }
1250
1501
 
1251
1502
  exports.inject = inject;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncended/dsh-automations",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Durable cron automations for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,6 +29,8 @@
29
29
  "client": {
30
30
  "inject": [
31
31
  "@deepseek-ai/dsh-client-runtime",
32
+ "@deepseek-ai/dsh-client-ui-layout",
33
+ "@deepseek-ai/dsh-client-ui-sidebar",
32
34
  "@deepseek-ai/dsh-client-ui-settings"
33
35
  ],
34
36
  "platform": "web"
@@ -80,7 +82,9 @@
80
82
  "@deepseek-ai/dsh-agent-default-model": "^0.1.1-rc.2",
81
83
  "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.2",
82
84
  "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
85
+ "@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.2",
83
86
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
87
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.1-rc.2",
84
88
  "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
85
89
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
86
90
  "@deepseek-ai/dsh-permission-presets": "^0.1.1-rc.2",
@@ -93,7 +97,9 @@
93
97
  "@deepseek-ai/dsh-agent-default-model": "0.1.1-rc.2",
94
98
  "@deepseek-ai/dsh-agent-presets": "0.1.1-rc.2",
95
99
  "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
100
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
96
101
  "@deepseek-ai/dsh-client-ui-settings": "0.1.1-rc.2",
102
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.1.1-rc.2",
97
103
  "@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",
98
104
  "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
99
105
  "@deepseek-ai/dsh-permission-presets": "0.1.1-rc.2",