@linxin666/dsh-client-ui-task-board 0.3.23 → 0.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1636,6 +1636,7 @@ window.__ModuleLoader__.load({
1636
1636
  "new.prompt": "执行 Prompt",
1637
1637
  "new.promptPlaceholder": "发给 agent 的完整指令(留空则使用标题)",
1638
1638
  "new.submit": "创建",
1639
+ "new.createAndRun": "创建并执行",
1639
1640
  "new.cancel": "取消",
1640
1641
  "new.required": "标题不能为空",
1641
1642
  "new.freeze": "冻结快照(可选)",
@@ -1824,6 +1825,7 @@ window.__ModuleLoader__.load({
1824
1825
  "new.prompt": "Run Prompt",
1825
1826
  "new.promptPlaceholder": "The full instruction sent to the agent (title is used when blank)",
1826
1827
  "new.submit": "Create",
1828
+ "new.createAndRun": "Create and run",
1827
1829
  "new.cancel": "Cancel",
1828
1830
  "new.required": "Title is required",
1829
1831
  "new.freeze": "Frozen snapshot (optional)",
@@ -2096,7 +2098,7 @@ window.__ModuleLoader__.load({
2096
2098
  /** DOM id shared by the tag-name inputs and their datalist (one board at a time). */
2097
2099
  const TAG_NAME_LIST_ID = "dsh-task-board-tag-names";
2098
2100
  /** Modal overlay: closes on backdrop press, submits through the form. */
2099
- function ModalShell({ ariaLabel, title, error, pending, submitLabel, onSubmit, onClose, children }) {
2101
+ function ModalShell({ ariaLabel, title, error, pending, submitLabel, onSubmit, onClose, secondaryAction, children }) {
2100
2102
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2101
2103
  className: board_module_css_default.modalBackdrop,
2102
2104
  onMouseDown: (event) => {
@@ -2122,17 +2124,27 @@ window.__ModuleLoader__.load({
2122
2124
  }),
2123
2125
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
2124
2126
  className: board_module_css_default.modalFooter,
2125
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2126
- type: "button",
2127
- className: board_module_css_default.ghostButton,
2128
- onClick: onClose,
2129
- children: t("new.cancel")
2130
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2131
- type: "submit",
2132
- className: board_module_css_default.primaryButton,
2133
- disabled: pending,
2134
- children: submitLabel
2135
- })]
2127
+ children: [
2128
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2129
+ type: "button",
2130
+ className: board_module_css_default.ghostButton,
2131
+ onClick: onClose,
2132
+ children: t("new.cancel")
2133
+ }),
2134
+ secondaryAction !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2135
+ type: "button",
2136
+ className: board_module_css_default.ghostButton,
2137
+ disabled: pending,
2138
+ onClick: secondaryAction.onSubmit,
2139
+ children: secondaryAction.label
2140
+ }),
2141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2142
+ type: "submit",
2143
+ className: board_module_css_default.primaryButton,
2144
+ disabled: pending,
2145
+ children: submitLabel
2146
+ })
2147
+ ]
2136
2148
  })
2137
2149
  ]
2138
2150
  })
@@ -2271,6 +2283,37 @@ window.__ModuleLoader__.load({
2271
2283
  return normalizeTags(tags) ?? [];
2272
2284
  }
2273
2285
  //#endregion
2286
+ //#region src/client/board/parse-model-pref.ts
2287
+ /**
2288
+ * The model the user last picked for AI parsing, remembered per browser so the
2289
+ * next new-task modal starts from it instead of the roster's first entry
2290
+ * (issue #1621). An empty value is a real choice — "let the Host use its
2291
+ * default model" — and is remembered as such.
2292
+ */
2293
+ const PARSE_MODEL_KEY = "dsh-task-board.parse-model";
2294
+ /** The nearest browser storage, or undefined when the global itself throws. */
2295
+ function defaultStorage() {
2296
+ try {
2297
+ return globalThis.localStorage;
2298
+ } catch {
2299
+ return;
2300
+ }
2301
+ }
2302
+ /** The remembered parse model; '' means the Host default (or no preference). */
2303
+ function readParseModelPreference(storage = defaultStorage()) {
2304
+ try {
2305
+ return storage?.getItem(PARSE_MODEL_KEY) ?? "";
2306
+ } catch {
2307
+ return "";
2308
+ }
2309
+ }
2310
+ /** Remember the picked parse model; '' records the Host-default choice. */
2311
+ function writeParseModelPreference(model, storage = defaultStorage()) {
2312
+ try {
2313
+ storage?.setItem(PARSE_MODEL_KEY, model);
2314
+ } catch {}
2315
+ }
2316
+ //#endregion
2274
2317
  //#region src/client/board/NewTaskModal.tsx
2275
2318
  /**
2276
2319
  * New-task modal: title + description + the prompt that execution will send.
@@ -2300,14 +2343,17 @@ window.__ModuleLoader__.load({
2300
2343
  const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
2301
2344
  const [canParse] = (0, react.useState)(controller.getSnapshot().canParseTask === true);
2302
2345
  const [parseText, setParseText] = (0, react.useState)("");
2303
- const [parseModel, setParseModel] = (0, react.useState)("");
2346
+ const [parseModel, setParseModel] = (0, react.useState)(() => readParseModelPreference());
2304
2347
  const [parsePending, setParsePending] = (0, react.useState)(false);
2305
2348
  const [parseError, setParseError] = (0, react.useState)(void 0);
2306
2349
  const parseAbort = (0, react.useRef)(void 0);
2307
2350
  const parseModels = options.models ?? [];
2308
2351
  (0, react.useEffect)(() => controller.subscribe(() => setOptions(controller.getSnapshot().executionOptions)), [controller]);
2309
2352
  (0, react.useEffect)(() => {
2310
- if (parseModel === "" && parseModels.length > 0) setParseModel(parseModels[0].id);
2353
+ if (parseModel === "" || parseModels.length === 0) return;
2354
+ if (parseModels.some((option) => option.id === parseModel)) return;
2355
+ setParseModel("");
2356
+ writeParseModelPreference("");
2311
2357
  }, [parseModel, options.models]);
2312
2358
  const runParse = async () => {
2313
2359
  const text = parseText.trim();
@@ -2334,7 +2380,13 @@ window.__ModuleLoader__.load({
2334
2380
  setParsePending(false);
2335
2381
  }
2336
2382
  };
2337
- const submit = async () => {
2383
+ /**
2384
+ * Create the task through the Host, then optionally start it.
2385
+ * @param runAfterCreate - true for the "create and run" action: the task is
2386
+ * committed either way, and a refused start opens the task instead of
2387
+ * reporting the creation as failed.
2388
+ */
2389
+ const submit = async (runAfterCreate) => {
2338
2390
  if (scheduleEnabled) {
2339
2391
  const cron = scheduleCron.trim();
2340
2392
  if (cron === "" || !isValidCron(cron)) {
@@ -2363,7 +2415,7 @@ window.__ModuleLoader__.load({
2363
2415
  };
2364
2416
  const tagList = cleanTags(tags);
2365
2417
  setPending(true);
2366
- if (await controller.createTaskConfirmed({
2418
+ const task = await controller.createTaskConfirmed({
2367
2419
  title,
2368
2420
  description,
2369
2421
  prompt,
@@ -2379,13 +2431,17 @@ window.__ModuleLoader__.load({
2379
2431
  enabled: true,
2380
2432
  cron: scheduleCron.trim()
2381
2433
  } : void 0
2382
- }) === void 0) {
2434
+ });
2435
+ if (task === void 0) {
2383
2436
  setPending(false);
2384
2437
  setError(controller.getSnapshot().transportError ?? t("new.required"));
2385
2438
  return;
2386
2439
  }
2387
2440
  if (isDuplicate && archiveOriginal && initialTask !== void 0) if (onDuplicateSuccess !== void 0) await onDuplicateSuccess(initialTask.id);
2388
2441
  else await controller.archiveTask(initialTask.id);
2442
+ if (runAfterCreate) {
2443
+ if (!await controller.runTask(task.id)) controller.openTask(task.id);
2444
+ }
2389
2445
  onClose();
2390
2446
  };
2391
2447
  /** Next-run preview for a valid armed cron (creation-time only). */
@@ -2398,9 +2454,15 @@ window.__ModuleLoader__.load({
2398
2454
  pending,
2399
2455
  submitLabel: t("new.submit"),
2400
2456
  onSubmit: () => {
2401
- submit();
2457
+ submit(false);
2402
2458
  },
2403
2459
  onClose,
2460
+ secondaryAction: {
2461
+ label: t("new.createAndRun"),
2462
+ onSubmit: () => {
2463
+ submit(true);
2464
+ }
2465
+ },
2404
2466
  children: [
2405
2467
  canParse && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2406
2468
  className: board_module_css_default.aiParse,
@@ -2427,17 +2489,21 @@ window.__ModuleLoader__.load({
2427
2489
  }),
2428
2490
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2429
2491
  className: board_module_css_default.aiParseRow,
2430
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2492
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2431
2493
  className: board_module_css_default.select,
2432
2494
  value: parseModel,
2433
2495
  "aria-label": t("new.aiParseModel"),
2434
2496
  onChange: (event) => {
2435
2497
  setParseModel(event.target.value);
2498
+ writeParseModelPreference(event.target.value);
2436
2499
  },
2437
- children: parseModels.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2500
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2501
+ value: "",
2502
+ children: t("exec.model.default")
2503
+ }), parseModels.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2438
2504
  value: option.id,
2439
2505
  children: option.name ?? option.id
2440
- }, option.id))
2506
+ }, option.id))]
2441
2507
  }), parsePending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2442
2508
  type: "button",
2443
2509
  className: board_module_css_default.ghostButton,
@@ -4267,10 +4333,12 @@ window.__ModuleLoader__.load({
4267
4333
  if (nested !== null) return nested;
4268
4334
  for (const child of root.children) if (child.tagName === "BUTTON") return child;
4269
4335
  }
4270
- /** Build the entry row (a detached button; insert once the shell is up). */
4336
+ /** Build the entry row (detached; inserted once the shell is up). */
4271
4337
  function createEntry(options) {
4272
- const entry = document.createElement("button");
4273
- entry.type = "button";
4338
+ const actions = options.actions ?? [];
4339
+ const composite = actions.length > 0;
4340
+ const entry = document.createElement(composite ? "div" : "button");
4341
+ if (!composite) entry.type = "button";
4274
4342
  entry.setAttribute(options.rowAttribute, "");
4275
4343
  if (options.plugin !== void 0) {
4276
4344
  entry.setAttribute("data-dsh-plugin", options.plugin);
@@ -4282,17 +4350,57 @@ window.__ModuleLoader__.load({
4282
4350
  const iconSpan = document.createElement("span");
4283
4351
  iconSpan.className = options.css["entryIcon"] ?? "";
4284
4352
  iconSpan.innerHTML = options.icon;
4285
- entry.append(iconSpan, labelSpan);
4353
+ const main = composite ? document.createElement("button") : entry;
4354
+ if (composite) {
4355
+ main.type = "button";
4356
+ main.className = options.css["entryMain"] ?? "";
4357
+ main.append(iconSpan, labelSpan);
4358
+ entry.append(main);
4359
+ } else entry.append(iconSpan, labelSpan);
4360
+ const actionButtons = [];
4361
+ for (const action of actions) {
4362
+ const button = document.createElement("button");
4363
+ button.type = "button";
4364
+ button.className = options.css["entryAction"] ?? "";
4365
+ button.setAttribute("data-dsh-entry-action", action.id);
4366
+ button.innerHTML = action.icon;
4367
+ button.addEventListener("click", () => {
4368
+ action.onClick(button);
4369
+ });
4370
+ entry.append(button);
4371
+ actionButtons.push({
4372
+ action,
4373
+ button
4374
+ });
4375
+ }
4376
+ let open = false;
4377
+ const applyActions = () => {
4378
+ for (const { action, button } of actionButtons) {
4379
+ button.innerHTML = open || action.inactiveIcon === void 0 ? action.icon : action.inactiveIcon;
4380
+ const text = action.label(open);
4381
+ button.setAttribute("aria-label", text);
4382
+ button.setAttribute("title", text);
4383
+ if (action.inactiveIcon !== void 0) button.setAttribute("aria-expanded", String(open));
4384
+ }
4385
+ };
4286
4386
  const applyLabel = () => {
4287
- entry.setAttribute("aria-label", options.label());
4288
- if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
4387
+ main.setAttribute("aria-label", options.label());
4388
+ if (options.tooltip !== void 0) main.setAttribute("title", options.tooltip());
4289
4389
  labelSpan.textContent = options.label();
4390
+ applyActions();
4290
4391
  };
4291
4392
  applyLabel();
4292
- entry.addEventListener("click", options.onToggle);
4393
+ main.addEventListener("click", options.onToggle);
4394
+ if (composite) entry.addEventListener("click", (event) => {
4395
+ if (event.target === entry) options.onToggle();
4396
+ });
4293
4397
  return {
4294
4398
  entry,
4295
- applyLabel
4399
+ applyLabel,
4400
+ setOpen: (next) => {
4401
+ open = next;
4402
+ applyActions();
4403
+ }
4296
4404
  };
4297
4405
  }
4298
4406
  /** Re-insert the entry after the New Session row (before the browser region). */
@@ -4316,7 +4424,7 @@ window.__ModuleLoader__.load({
4316
4424
  */
4317
4425
  function mountSidebarEntry$1(options) {
4318
4426
  if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
4319
- const { entry, applyLabel } = createEntry(options);
4427
+ const { entry, applyLabel, setOpen } = createEntry(options);
4320
4428
  let root;
4321
4429
  let placed = false;
4322
4430
  let unsubscribeRefresh;
@@ -4356,8 +4464,10 @@ window.__ModuleLoader__.load({
4356
4464
  });
4357
4465
  const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
4358
4466
  const syncActive = () => {
4359
- if (options.active.isOpen()) entry.dataset.active = "true";
4467
+ const open = options.active.isOpen();
4468
+ if (open) entry.dataset.active = "true";
4360
4469
  else delete entry.dataset.active;
4470
+ setOpen(open);
4361
4471
  };
4362
4472
  const unsubscribe = options.active.subscribe(syncActive);
4363
4473
  syncActive();
@@ -5434,7 +5544,7 @@ window.__ModuleLoader__.load({
5434
5544
  /** The building package's version, when the bundle carries it. */
5435
5545
  function bakedVersion() {
5436
5546
  try {
5437
- return "0.3.23";
5547
+ return "0.3.24";
5438
5548
  } catch {
5439
5549
  return;
5440
5550
  }
@@ -5497,6 +5607,129 @@ window.__ModuleLoader__.load({
5497
5607
  } catch {}
5498
5608
  }
5499
5609
  //#endregion
5610
+ //#region src/client/plugin-card-seat.ts
5611
+ /**
5612
+ * Family plugin-card seat.
5613
+ *
5614
+ * A family plugin contributes its settings card to whichever plugin-card seat
5615
+ * the running host actually renders:
5616
+ *
5617
+ * - `web-ui.plugin.item` — the list seat declared by the dsh-web-settings
5618
+ * group section (this family's own first-level "Web UI plugins" section);
5619
+ * - `settings.plugin.item` — the official keyed seat of the harness's
5620
+ * `ui-settings-plugins` tab, keyed by the settings namespace the card edits.
5621
+ *
5622
+ * SEAT SELECTION IS NOT A DECLARATION PROBE. The official `ui-settings-plugins`
5623
+ * row belongs to the harness bundle and its `configurable` tab always declares
5624
+ * `settings.plugin.item` before any external plugin's `apply()` runs, so
5625
+ * "is the official seat declared?" answers yes even in the deployment whose
5626
+ * whole point is the family group. Choosing on that probe sends every family
5627
+ * card to the official Plugins tab and leaves the group's own section
5628
+ * permanently empty — the family of reports where the section renders its
5629
+ * heading and zero cards.
5630
+ *
5631
+ * The signal that actually distinguishes the two deployments is whether
5632
+ * dsh-web-settings is loaded: it is the package that owns the group section and
5633
+ * it publishes the `webUiSettings` service during `apply()`, which every
5634
+ * family plugin already reads for its settings scope. Group loaded -> the family
5635
+ * seat; group absent -> the official seat.
5636
+ *
5637
+ * The decision is re-evaluated on every `slots/changed` because the group may
5638
+ * apply after this plugin (the family aggregate orders it first, a profile that
5639
+ * installs the group separately need not): the initial contribution goes to the
5640
+ * official seat, then moves to the family seat the moment the group's section
5641
+ * registers. The entry is disposed before the replacement is registered, so a
5642
+ * card is never in two seats at once.
5643
+ *
5644
+ * The shared tree has no client-SDK dependency, so this module reads its
5645
+ * context through the structural shape below; callers pass the plugin's own
5646
+ * `ctx`.
5647
+ */
5648
+ /** The family list seat key. */
5649
+ const FAMILY_PLUGIN_CARD_SEAT = "web-ui.plugin.item";
5650
+ /** The official keyed plugin-card seat key. */
5651
+ const OFFICIAL_PLUGIN_CARD_SEAT = "settings.plugin.item";
5652
+ /** The service dsh-web-settings publishes while it is loaded. */
5653
+ const FAMILY_GROUP_SERVICE = "webUiSettings";
5654
+ /**
5655
+ * Whether the family group (dsh-web-settings) is loaded in this page. The
5656
+ * service is the group package's own contract, so the probe cannot be fooled
5657
+ * by a harness release that starts declaring the official seat differently.
5658
+ */
5659
+ function familyGroupLoaded(ctx) {
5660
+ const get = ctx.get;
5661
+ if (typeof get !== "function") return false;
5662
+ try {
5663
+ return get.call(ctx, FAMILY_GROUP_SERVICE) !== void 0;
5664
+ } catch {
5665
+ return false;
5666
+ }
5667
+ }
5668
+ /** Report a refused registration instead of leaving the user with no card. */
5669
+ function warnRefusedSeat(seat, error) {
5670
+ try {
5671
+ console.warn(`[dsh-web] plugin card registration into "${seat}" was refused; the card will not render`, error);
5672
+ } catch {}
5673
+ }
5674
+ /**
5675
+ * Contribute one family plugin card to the seat this host renders, following
5676
+ * the group if it loads later. The entry is disposed and re-registered on a
5677
+ * seat change, never duplicated.
5678
+ * @param ctx - client context (its slot registry decides the seat).
5679
+ * @param seat - the card contribution.
5680
+ */
5681
+ function installPluginCard(ctx, seat) {
5682
+ const slots = ctx.slots;
5683
+ const component = seat.component;
5684
+ const inject = seat.inject;
5685
+ let dispose;
5686
+ let current;
5687
+ /**
5688
+ * Re-entrancy latch. The registry emits a change event synchronously from
5689
+ * inside both `register` and the previous entry's disposer, so an unguarded
5690
+ * reconcile would re-enter itself mid-move and register the card twice into
5691
+ * the seat it is leaving ("already has an entry for key ...").
5692
+ */
5693
+ let reconciling = false;
5694
+ /** Reconcile the contribution with the currently live seat (no-op when unchanged). */
5695
+ const reconcile = () => {
5696
+ if (reconciling) return;
5697
+ const target = familyGroupLoaded(ctx) ? FAMILY_PLUGIN_CARD_SEAT : OFFICIAL_PLUGIN_CARD_SEAT;
5698
+ if (current === target) return;
5699
+ reconciling = true;
5700
+ const previous = dispose;
5701
+ dispose = void 0;
5702
+ current = void 0;
5703
+ previous?.();
5704
+ try {
5705
+ dispose = slots.register(target === "web-ui.plugin.item" ? {
5706
+ name: FAMILY_PLUGIN_CARD_SEAT,
5707
+ id: seat.id,
5708
+ ...seat.order === void 0 ? {} : { order: seat.order },
5709
+ ...seat.label === void 0 ? {} : { label: seat.label },
5710
+ locale: seat.locale,
5711
+ ...seat.inject === void 0 ? {} : { inject }
5712
+ } : {
5713
+ name: OFFICIAL_PLUGIN_CARD_SEAT,
5714
+ key: seat.namespace,
5715
+ locale: seat.locale,
5716
+ ...seat.inject === void 0 ? {} : { inject }
5717
+ }, component);
5718
+ current = target;
5719
+ } catch (error) {
5720
+ warnRefusedSeat(target, error);
5721
+ } finally {
5722
+ reconciling = false;
5723
+ }
5724
+ };
5725
+ if (typeof ctx.on === "function") try {
5726
+ ctx.on("slots/changed", () => {
5727
+ reconcile();
5728
+ });
5729
+ } catch {}
5730
+ reconcile();
5731
+ }
5732
+ //#endregion
5500
5733
  //#region src/client/index.ts
5501
5734
  /** Locale namespace this plugin owns. */
5502
5735
  const NS = "task-board";
@@ -5543,23 +5776,17 @@ window.__ModuleLoader__.load({
5543
5776
  } catch {}
5544
5777
  const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: TASK_BOARD_NS });
5545
5778
  const settingsCard = new TaskBoardSettingsCardController(settingsScope);
5546
- ctx.slots.inject("web-ui.plugin.item", () => {
5547
- try {
5548
- const unregister = ctx.slots.register({
5549
- name: "web-ui.plugin.item",
5550
- id: "task-board",
5551
- order: 110,
5552
- locale: NS,
5553
- inject: () => settingsCard.inject()
5554
- }, TaskBoardSettingsCard);
5555
- return () => {
5556
- settingsCard.dispose();
5557
- unregister();
5558
- };
5559
- } catch {
5560
- return () => {};
5561
- }
5779
+ installPluginCard(ctx, {
5780
+ namespace: TASK_BOARD_NS,
5781
+ id: "task-board",
5782
+ order: 110,
5783
+ locale: NS,
5784
+ inject: () => settingsCard.inject(),
5785
+ component: TaskBoardSettingsCard
5562
5786
  });
5787
+ ctx.effect(() => () => {
5788
+ settingsCard.dispose();
5789
+ }, "task-board: settings card");
5563
5790
  let uiDisposer;
5564
5791
  const mountUi = () => {
5565
5792
  if (uiDisposer !== void 0) return;