@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 +275 -48
- package/lib/client.js.map +1 -1
- package/lib/index.js +65 -18
- package/lib/types/client/board/TaskForm.d.ts +6 -1
- package/lib/types/client/board/parse-model-pref.d.ts +4 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/client/plugin-card-seat.d.ts +96 -0
- package/lib/types/client/sidebar-entry-core.d.ts +32 -0
- package/lib/types/core/session-reuse.d.ts +4 -2
- package/lib/types/host-ledger.d.ts +15 -0
- package/package.json +2 -1
- package/src/client/board/NewTaskModal.tsx +33 -7
- package/src/client/board/TaskForm.tsx +13 -0
- package/src/client/board/parse-model-pref.ts +35 -0
- package/src/client/index.ts +11 -17
- package/src/client/locales.ts +2 -0
- package/src/client/plugin-card-seat.ts +185 -0
- package/src/client/sidebar-entry-core.ts +96 -12
- package/src/core/session-reuse.ts +14 -5
- package/src/host-ledger.ts +59 -11
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: [
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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 === ""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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.
|
|
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:
|
|
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 (
|
|
4336
|
+
/** Build the entry row (detached; inserted once the shell is up). */
|
|
4271
4337
|
function createEntry(options) {
|
|
4272
|
-
const
|
|
4273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4288
|
-
if (options.tooltip !== void 0)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
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;
|