dsh-group-status 0.2.0 → 0.2.2

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
@@ -5,10 +5,10 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
- let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
9
8
  let react = require("react");
10
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
11
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
+ let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
12
12
  //#region src/client/contract/slots.ts
13
13
  /**
14
14
  * Bind the row's render occurrence into the entries' `useMenuOpenState` hook:
@@ -223,19 +223,47 @@ window.__ModuleLoader__.load({
223
223
  * 引用——每次都构造新对象会让选择器的 `Object.is` 比较永不成立,触发
224
224
  * 「Maximum update depth exceeded」死循环(React #185),折叠行徽标会整块崩掉。
225
225
  *
226
- * 设置面缺席或未就绪时,回落到**默认显示已完成**,与 `Config.showCompletedInBadge` 的
227
- * schema 默认值同源:读不到配置不是「关掉」。
226
+ * 设置面缺席或未就绪时,回落到**默认全部显示**,与 `Config` 各字段的 schema 默认值同源:
227
+ * 读不到配置不是「关掉」。
228
228
  */
229
229
  /** Loader 条目 id —— 设置页的寻址键,也是宿主 `describe()` 的 ns。 */
230
230
  const ENTRY_ID = "dsh-group-status";
231
- /** 默认值:与 `Config.showCompletedInBadge` 的 schema 默认同源(true)。 */
232
- const DEFAULT_BADGE_SETTINGS = Object.freeze({ showCompletedInBadge: true });
233
- /** 从原始快照派生:只有**显式 false** 才隐藏,缺失/非法一律显示。 */
231
+ /** 默认值:与 `Config` 各字段的 schema 默认同源(全部显示,数字阈值 2 即 count=1 时省略)。 */
232
+ const DEFAULT_BADGE_SETTINGS = Object.freeze({
233
+ showCompletedInBadge: true,
234
+ showRunningInBadge: true,
235
+ showPendingInBadge: true,
236
+ showScheduledInBadge: true,
237
+ showWaitingReviewInBadge: true,
238
+ showHoverDetails: true,
239
+ countThreshold: 2
240
+ });
241
+ /** 两个设置对象是否逐字段相等(用于发布前比较,避免无谓通知)。 */
242
+ function settingsEqual(a, b) {
243
+ return a.showCompletedInBadge === b.showCompletedInBadge && a.showRunningInBadge === b.showRunningInBadge && a.showPendingInBadge === b.showPendingInBadge && a.showScheduledInBadge === b.showScheduledInBadge && a.showWaitingReviewInBadge === b.showWaitingReviewInBadge && a.showHoverDetails === b.showHoverDetails && a.countThreshold === b.countThreshold;
244
+ }
245
+ /** 从原始快照派生:只有**显式 false** 才隐藏,缺失/非法一律显示(countThreshold 默认 2)。 */
234
246
  function resolveFromSnapshot(snap) {
235
247
  if (snap === void 0 || snap.status !== "ready") return DEFAULT_BADGE_SETTINGS;
236
248
  const value = snap.value;
237
249
  if (value === void 0 || value === null || typeof value !== "object") return DEFAULT_BADGE_SETTINGS;
238
- return { showCompletedInBadge: value["showCompletedInBadge"] !== false };
250
+ const getBool = (field, def) => {
251
+ const v = value[field];
252
+ return typeof v === "boolean" ? v : def;
253
+ };
254
+ const getNumber = (field, def) => {
255
+ const v = value[field];
256
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : def;
257
+ };
258
+ return {
259
+ showCompletedInBadge: getBool("showCompletedInBadge", true),
260
+ showRunningInBadge: getBool("showRunningInBadge", true),
261
+ showPendingInBadge: getBool("showPendingInBadge", true),
262
+ showScheduledInBadge: getBool("showScheduledInBadge", true),
263
+ showWaitingReviewInBadge: getBool("showWaitingReviewInBadge", true),
264
+ showHoverDetails: getBool("showHoverDetails", true),
265
+ countThreshold: getNumber("countThreshold", 2)
266
+ };
239
267
  }
240
268
  /**
241
269
  * 建一个恒在、取值稳定的徽标设置源。
@@ -263,13 +291,324 @@ window.__ModuleLoader__.load({
263
291
  seen = snapshot;
264
292
  seenSet = true;
265
293
  const next = resolveFromSnapshot(snapshot);
266
- if (next.showCompletedInBadge === resolved.showCompletedInBadge) return;
294
+ if (settingsEqual(next, resolved)) return;
267
295
  resolved = next;
268
296
  for (const listener of [...listeners]) listener();
269
297
  }
270
298
  };
271
299
  }
272
300
  //#endregion
301
+ //#region src/client/badge-field.ts
302
+ /** 卡片字段名(与 `src/index.ts` 的 `Config` 同名同义)。 */
303
+ const BADGE_FIELDS = {
304
+ showCompletedInBadge: "showCompletedInBadge",
305
+ showRunningInBadge: "showRunningInBadge",
306
+ showPendingInBadge: "showPendingInBadge",
307
+ showScheduledInBadge: "showScheduledInBadge",
308
+ showWaitingReviewInBadge: "showWaitingReviewInBadge",
309
+ showHoverDetails: "showHoverDetails",
310
+ countThreshold: "countThreshold"
311
+ };
312
+ /**
313
+ * 草稿文本解析成开关状态:与 {@link isFieldEnabled} 不同,这里比较的是模型给的
314
+ * **文本**,别把字符串喂给上面那个函数(`'false' !== false` 恒真)。
315
+ */
316
+ function isDraftEnabled(text) {
317
+ return text !== "false";
318
+ }
319
+ /**
320
+ * 构造一个布尔字段的字段转换规格。
321
+ *
322
+ * `parse` 只接受 `'true'` / `'false'`:空串、任意其它文本都返回 `undefined`,
323
+ * 让模型把字段判为 invalid 并拒绝保存(官方模型的既定行为)。
324
+ *
325
+ * @param field - 字段名(与 Config 同名)。
326
+ * @param defaultValue - 缺失值时的默认(true = 默认显示,false = 默认隐藏)。
327
+ */
328
+ function makeBooleanField(field, defaultValue) {
329
+ return {
330
+ field,
331
+ format: (value) => {
332
+ return (value === void 0 || value === null || typeof value !== "boolean" ? defaultValue : value) ? "true" : "false";
333
+ },
334
+ parse: (text) => text === "true" ? {
335
+ kind: "set",
336
+ value: true
337
+ } : text === "false" ? {
338
+ kind: "set",
339
+ value: false
340
+ } : void 0
341
+ };
342
+ }
343
+ /** 所有字段规格(按卡片渲染顺序)。 */
344
+ const ALL_BADGE_FIELDS = [
345
+ makeBooleanField(BADGE_FIELDS.showCompletedInBadge, true),
346
+ makeBooleanField(BADGE_FIELDS.showRunningInBadge, true),
347
+ makeBooleanField(BADGE_FIELDS.showPendingInBadge, true),
348
+ makeBooleanField(BADGE_FIELDS.showScheduledInBadge, true),
349
+ makeBooleanField(BADGE_FIELDS.showWaitingReviewInBadge, true),
350
+ makeBooleanField(BADGE_FIELDS.showHoverDetails, true),
351
+ {
352
+ field: BADGE_FIELDS.countThreshold,
353
+ format: (value) => {
354
+ const n = value === void 0 || value === null || typeof value !== "number" || !Number.isFinite(value) ? 2 : Math.floor(value);
355
+ return String(n < 0 ? 0 : n);
356
+ },
357
+ parse: (text) => {
358
+ const n = Number(text);
359
+ if (!Number.isFinite(n) || n < 0 || n !== Math.floor(n)) return void 0;
360
+ return {
361
+ kind: "set",
362
+ value: n
363
+ };
364
+ }
365
+ }
366
+ ];
367
+ //#endregion
368
+ //#region \0dsh-css:/home/wujue/workspace/dsh-plugins/dsh-group-status/src/client/BadgeSettingsCard.module.css.mjs
369
+ const css$4 = ".EgC0Ja_section{min-width:0;padding:16px 0}.EgC0Ja_section:last-child{padding-bottom:0}.EgC0Ja_heading{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:600;line-height:1.5}.EgC0Ja_badgeField{gap:6px;padding:12px 0;display:grid}.EgC0Ja_badgeToggleRow{color:var(--dsw-alias-label-primary);justify-content:space-between;align-items:flex-start;gap:16px;font-size:13px;line-height:1.5;display:flex}.EgC0Ja_badgeToggleLabel{flex:1;min-width:0}.EgC0Ja_badgeNumberControl{align-items:center;gap:4px;display:flex}.EgC0Ja_badgeNumberButton{width:28px;height:28px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-elevated,#2a2a2a);border:1px solid var(--dsw-alias-border-primary,#555);cursor:pointer;user-select:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:16px;font-weight:600;line-height:1;display:flex}.EgC0Ja_badgeNumberButton:hover:not(:disabled){border-color:var(--dsw-alias-border-focus,#0078d4);background:var(--dsw-alias-bg-hover,#3a3a3a)}.EgC0Ja_badgeNumberButton:active:not(:disabled){background:var(--dsw-alias-bg-active,#4a4a4a)}.EgC0Ja_badgeNumberButton:disabled{opacity:.3;cursor:not-allowed}.EgC0Ja_badgeNumberValue{text-align:center;min-width:24px;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.EgC0Ja_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}";
370
+ const tagId$4 = "dsh-group-status/BadgeSettingsCard.module.css";
371
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
372
+ const tag = document.createElement("style");
373
+ tag.dataset.plugin = "dsh-group-status";
374
+ tag.dataset.pluginCss = tagId$4;
375
+ tag.textContent = css$4;
376
+ document.head.appendChild(tag);
377
+ }
378
+ var BadgeSettingsCard_module_css_default = {
379
+ "badgeField": "EgC0Ja_badgeField",
380
+ "badgeNumberButton": "EgC0Ja_badgeNumberButton",
381
+ "badgeNumberControl": "EgC0Ja_badgeNumberControl",
382
+ "badgeNumberValue": "EgC0Ja_badgeNumberValue",
383
+ "badgeToggleLabel": "EgC0Ja_badgeToggleLabel",
384
+ "badgeToggleRow": "EgC0Ja_badgeToggleRow",
385
+ "heading": "EgC0Ja_heading",
386
+ "hint": "EgC0Ja_hint",
387
+ "section": "EgC0Ja_section"
388
+ };
389
+ //#endregion
390
+ //#region src/client/BadgeSettingsCard.tsx
391
+ /**
392
+ * 折叠徽标的设置卡片:插件 → dsh-group-status(bundle 详情页的配置表单)。
393
+ *
394
+ * 为什么需要这张卡片:0.2.0 起宿主把插件 `Config`(`src/index.ts`,根节点
395
+ * `.volatile()`)投影到客户端 `configForms` 服务上,但**不会**自动把表单画进
396
+ * 「插件」页——插件页只在客户端往 `plugins.bundle.config` 注册了卡片时才渲染
397
+ * (见 dsh-later / dsh-gitea-dispatch 的同款做法)。没有它,README「设置项」一节
398
+ * 描述的配置表单在 GUI 里根本不存在。
399
+ *
400
+ * 视觉与交互**不自造**,全部交给官方组件(`@deepseek-ai/dsh-client-ui-primitives`):
401
+ * - `SettingsForm`:表单框架——只读提示、不可用提示、保存按钮、失败行、间距;
402
+ * - `SettingsFormModel`:分步草稿与保存(revision 围栏、宿主拒绝时不丢草稿);
403
+ * - `Switch`:开关本体。
404
+ * 官方设置页(设置 → 内置插件 → 子智能体 / 网页搜索)用的就是这三件,本卡片的
405
+ * 类名与取值也逐条对齐它们的 CSS(见 `BadgeSettingsCard.module.css` 头部)。
406
+ *
407
+ * 字段定义在 `badge-field.ts`(纯逻辑,可单测);这里只管把它们接到模型与槽位上。
408
+ */
409
+ /**
410
+ * 渲染折叠徽标的设置表单。
411
+ *
412
+ * 模型与投影都按 `scope` 记忆:宿主换一份表单(条目重新服务)时整体重建,旧模型
413
+ * 在 effect 清理里 `dispose()`,不会留下悬挂订阅。
414
+ * @param props - 宿主设置表单与文案取值器。
415
+ * @returns 官方表单框架包着本插件的开关字段。
416
+ */
417
+ function BadgeSettingsCard({ scope, t }) {
418
+ const model = (0, react.useMemo)(() => new _deepseek_ai_dsh_client_ui_primitives.SettingsFormModel(scope, ALL_BADGE_FIELDS), [scope]);
419
+ (0, react.useEffect)(() => () => {
420
+ model.dispose();
421
+ }, [model]);
422
+ const store = (0, react.useMemo)(() => model.bind(() => ({
423
+ ...model.shell(),
424
+ fields: ALL_BADGE_FIELDS.map((spec) => ({
425
+ field: spec.field,
426
+ text: model.field(spec.field).text
427
+ }))
428
+ })), [model]);
429
+ const state = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
430
+ const actions = (0, react.useMemo)(() => model.actions(), [model]);
431
+ const badgeSectionId = (0, react.useId)();
432
+ const hoverSectionId = (0, react.useId)();
433
+ const countSectionId = (0, react.useId)();
434
+ const labels = {
435
+ unavailable: t("settings.unavailable"),
436
+ readOnly: t("settings.readOnly"),
437
+ saveFailed: t("settings.saveFailed"),
438
+ save: t("settings.save"),
439
+ saving: t("settings.saving")
440
+ };
441
+ const fieldText = (field) => {
442
+ return state.fields.find((f) => f.field === field)?.text ?? "true";
443
+ };
444
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.SettingsForm, {
445
+ labels,
446
+ state,
447
+ onSave: () => {
448
+ actions.save();
449
+ },
450
+ onDiscard: () => {
451
+ actions.discard();
452
+ },
453
+ children: [
454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
455
+ className: BadgeSettingsCard_module_css_default.section,
456
+ "aria-labelledby": badgeSectionId,
457
+ children: [
458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
459
+ className: BadgeSettingsCard_module_css_default.heading,
460
+ id: badgeSectionId,
461
+ children: t("settings.badge.section")
462
+ }),
463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
464
+ field: BADGE_FIELDS.showRunningInBadge,
465
+ text: fieldText(BADGE_FIELDS.showRunningInBadge),
466
+ label: t("settings.badge.showRunning"),
467
+ hint: t("settings.badge.showRunningHint"),
468
+ state,
469
+ actions
470
+ }),
471
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
472
+ field: BADGE_FIELDS.showPendingInBadge,
473
+ text: fieldText(BADGE_FIELDS.showPendingInBadge),
474
+ label: t("settings.badge.showPending"),
475
+ hint: t("settings.badge.showPendingHint"),
476
+ state,
477
+ actions
478
+ }),
479
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
480
+ field: BADGE_FIELDS.showCompletedInBadge,
481
+ text: fieldText(BADGE_FIELDS.showCompletedInBadge),
482
+ label: t("settings.badge.showCompleted"),
483
+ hint: t("settings.badge.showCompletedHint"),
484
+ state,
485
+ actions
486
+ }),
487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
488
+ field: BADGE_FIELDS.showScheduledInBadge,
489
+ text: fieldText(BADGE_FIELDS.showScheduledInBadge),
490
+ label: t("settings.badge.showScheduled"),
491
+ hint: t("settings.badge.showScheduledHint"),
492
+ state,
493
+ actions
494
+ }),
495
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
496
+ field: BADGE_FIELDS.showWaitingReviewInBadge,
497
+ text: fieldText(BADGE_FIELDS.showWaitingReviewInBadge),
498
+ label: t("settings.badge.showWaitingReview"),
499
+ hint: t("settings.badge.showWaitingReviewHint"),
500
+ state,
501
+ actions
502
+ })
503
+ ]
504
+ }),
505
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
506
+ className: BadgeSettingsCard_module_css_default.section,
507
+ "aria-labelledby": hoverSectionId,
508
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
509
+ className: BadgeSettingsCard_module_css_default.heading,
510
+ id: hoverSectionId,
511
+ children: t("settings.hover.section")
512
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
513
+ field: BADGE_FIELDS.showHoverDetails,
514
+ text: fieldText(BADGE_FIELDS.showHoverDetails),
515
+ label: t("settings.hover.showDetails"),
516
+ hint: t("settings.hover.showDetailsHint"),
517
+ state,
518
+ actions
519
+ })]
520
+ }),
521
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
522
+ className: BadgeSettingsCard_module_css_default.section,
523
+ "aria-labelledby": countSectionId,
524
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
525
+ className: BadgeSettingsCard_module_css_default.heading,
526
+ id: countSectionId,
527
+ children: t("settings.count.section")
528
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberRow, {
529
+ field: BADGE_FIELDS.countThreshold,
530
+ text: fieldText(BADGE_FIELDS.countThreshold),
531
+ label: t("settings.count.threshold"),
532
+ hint: t("settings.count.thresholdHint"),
533
+ state,
534
+ actions
535
+ })]
536
+ })
537
+ ]
538
+ });
539
+ }
540
+ /** 单行开关:label + Switch + hint。 */
541
+ function ToggleRow({ field, text, label, hint, state, actions }) {
542
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
543
+ className: BadgeSettingsCard_module_css_default.badgeField,
544
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
545
+ className: BadgeSettingsCard_module_css_default.badgeToggleRow,
546
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
547
+ className: BadgeSettingsCard_module_css_default.badgeToggleLabel,
548
+ children: label
549
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
550
+ checked: isDraftEnabled(text),
551
+ label,
552
+ disabled: !state.writable || state.saving,
553
+ onChange: (next) => {
554
+ actions.edit(field, next ? "true" : "false");
555
+ }
556
+ })]
557
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
558
+ className: BadgeSettingsCard_module_css_default.hint,
559
+ children: hint
560
+ })]
561
+ });
562
+ }
563
+ /** 单行数字输入:label + 带加减按钮的数字输入 + hint。 */
564
+ function NumberRow({ field, text, label, hint, state, actions }) {
565
+ const disabled = !state.writable || state.saving;
566
+ const currentValue = Number(text) || 0;
567
+ const decrement = () => {
568
+ if (!disabled && currentValue > 0) actions.edit(field, String(currentValue - 1));
569
+ };
570
+ const increment = () => {
571
+ if (!disabled) actions.edit(field, String(currentValue + 1));
572
+ };
573
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
574
+ className: BadgeSettingsCard_module_css_default.badgeField,
575
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
576
+ className: BadgeSettingsCard_module_css_default.badgeToggleRow,
577
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
578
+ className: BadgeSettingsCard_module_css_default.badgeToggleLabel,
579
+ children: label
580
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
581
+ className: BadgeSettingsCard_module_css_default.badgeNumberControl,
582
+ children: [
583
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
584
+ type: "button",
585
+ className: BadgeSettingsCard_module_css_default.badgeNumberButton,
586
+ disabled: disabled || currentValue <= 0,
587
+ onClick: decrement,
588
+ "aria-label": "减少",
589
+ children: "−"
590
+ }),
591
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
592
+ className: BadgeSettingsCard_module_css_default.badgeNumberValue,
593
+ children: text
594
+ }),
595
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
596
+ type: "button",
597
+ className: BadgeSettingsCard_module_css_default.badgeNumberButton,
598
+ disabled,
599
+ onClick: increment,
600
+ "aria-label": "增加",
601
+ children: "+"
602
+ })
603
+ ]
604
+ })]
605
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
606
+ className: BadgeSettingsCard_module_css_default.hint,
607
+ children: hint
608
+ })]
609
+ });
610
+ }
611
+ //#endregion
273
612
  //#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
274
613
  /** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
275
614
  /**
@@ -318,118 +657,6 @@ window.__ModuleLoader__.load({
318
657
  const separator = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
319
658
  return trimmed.slice(separator + 1);
320
659
  }
321
- //#endregion
322
- //#region src/client/group-status.ts
323
- /**
324
- * Zero-valued sentinel for {@link GroupLiveStatus}.
325
- *
326
- * Used when a group is expanded (badge is hidden, so aggregation is wasted
327
- * work) or when no cross-plugin data is available. Kept as a frozen constant
328
- * so the reference is stable across renders — `Object.is` comparisons in
329
- * React selectors never trigger spurious re-renders on it.
330
- */
331
- const EMPTY_LIVE_STATUS = Object.freeze({
332
- running: 0,
333
- approval: 0,
334
- planReview: 0,
335
- question: 0,
336
- done: 0,
337
- subagents: 0,
338
- scheduledSessions: 0,
339
- scheduledCount: 0,
340
- scheduleState: void 0,
341
- waitingReviewCount: 0,
342
- waitingReviewState: void 0
343
- });
344
- /**
345
- * Strip the `session-` / `webhook-` prefix from a Session id.
346
- *
347
- * dsh-gitea-dispatch publishes its waiting map keyed by the host SessionId, but
348
- * its HTTP endpoints key by this normalized form. Accepting both here keeps the
349
- * badge working across that publisher's conventions without knowing which is in
350
- * play.
351
- */
352
- function stripSessionPrefix(id) {
353
- return id.replace(/^(?:session|webhook)-/, "");
354
- }
355
- /**
356
- * Fold every member row of a group into counts so a collapsed folder can hint at
357
- * hidden live sessions. Mirrors the session-row status priority (pending
358
- * outranks running outranks completed) at the counting level; labels resolve with
359
- * locale at render time.
360
- * @param nodes - member rows of the group.
361
- * @param sessionIds - the same members' Session ids (schedule and waiting facts are keyed by id).
362
- * @param waitingReviewSummary - dsh-gitea-dispatch waiting map, when that plugin is loaded.
363
- * @param scheduleSummary - dsh-later schedule map, when that plugin is loaded.
364
- */
365
- function summarizeGroupStatus(nodes, sessionIds, waitingReviewSummary, scheduleSummary) {
366
- let running = 0;
367
- let approval = 0;
368
- let planReview = 0;
369
- let question = 0;
370
- let done = 0;
371
- let subagents = 0;
372
- for (const n of nodes) {
373
- if (n.blank || n.archived) continue;
374
- if (n.pendingInteraction === "approval") {
375
- approval += 1;
376
- continue;
377
- }
378
- if (n.pendingInteraction === "plan-review") {
379
- planReview += 1;
380
- continue;
381
- }
382
- if (n.pendingInteraction === "question") {
383
- question += 1;
384
- continue;
385
- }
386
- if (n.running) running += 1;
387
- else if (n.completed) done += 1;
388
- subagents += n.runningSubagentCount;
389
- }
390
- let scheduledSessions = 0;
391
- let scheduledCount = 0;
392
- let scheduleState = void 0;
393
- if (scheduleSummary && sessionIds.length > 0) {
394
- let totalSessions = 0;
395
- let totalCount = 0;
396
- let highestPriority = 0;
397
- for (const id of sessionIds) {
398
- const entry = scheduleSummary.get(id);
399
- if (entry) {
400
- totalSessions += 1;
401
- totalCount += entry.count;
402
- const priority = entry.state === "overdue" ? 3 : entry.state === "urgent" ? 2 : 1;
403
- if (priority > highestPriority) highestPriority = priority;
404
- }
405
- }
406
- scheduledSessions = totalSessions;
407
- scheduledCount = totalCount;
408
- scheduleState = highestPriority === 3 ? "overdue" : highestPriority === 2 ? "urgent" : highestPriority === 1 ? "scheduled" : void 0;
409
- }
410
- let waitingReviewCount = 0;
411
- let waitingReviewState = void 0;
412
- if (waitingReviewSummary && sessionIds.length > 0) for (const id of sessionIds) {
413
- const entry = waitingReviewSummary.get(id) ?? waitingReviewSummary.get(stripSessionPrefix(id));
414
- if (entry === void 0) continue;
415
- waitingReviewCount += 1;
416
- if (entry.state === "overdue") waitingReviewState = "overdue";
417
- else if (waitingReviewState === void 0) waitingReviewState = "waiting";
418
- }
419
- return {
420
- running,
421
- approval,
422
- planReview,
423
- question,
424
- done,
425
- subagents,
426
- scheduledSessions,
427
- scheduledCount,
428
- scheduleState,
429
- waitingReviewCount,
430
- waitingReviewState
431
- };
432
- }
433
660
  /**
434
661
  * Resolve the Workspace browser group that owns one Session.
435
662
  * @param workspaces - authoritative Workspace membership.
@@ -666,7 +893,7 @@ window.__ModuleLoader__.load({
666
893
  * @param scheduleSummary - dsh-later schedule map (session id → schedule entry).
667
894
  * @returns group sections in render order.
668
895
  */
669
- function deriveGroups(list, workspaces, rowState, statuses, view, waitingReviewSummary, scheduleSummary) {
896
+ function deriveGroups(list, workspaces, rowState, statuses, view) {
670
897
  const archived = new Set(rowState.archivedSessionIds);
671
898
  const pinned = new Set(rowState.pinnedSessionIds);
672
899
  const expandedGroups = new Set(view.expandedGroups);
@@ -676,7 +903,6 @@ window.__ModuleLoader__.load({
676
903
  for (const g of groupByWorkspace(list, workspaces, archived, rowState.archivedFilter, view.ungroupedOrder)) {
677
904
  const expanded = expandedGroups.has(g.key);
678
905
  const members = sectionMembers(g.sessions, pinned, archived).map((session) => sessionNode(session, list, statuses, pinned, archived));
679
- const memberIds = members.map((m) => m.id);
680
906
  groups.push({
681
907
  key: g.key,
682
908
  workspaceId: g.workspaceId,
@@ -686,8 +912,7 @@ window.__ModuleLoader__.load({
686
912
  sessionCount: g.sessions.length,
687
913
  expanded,
688
914
  containsCurrent: g.key === currentGroup,
689
- liveStatus: expanded ? EMPTY_LIVE_STATUS : summarizeGroupStatus(members, memberIds, waitingReviewSummary, scheduleSummary),
690
- sessions: expanded ? members : []
915
+ sessions: members
691
916
  });
692
917
  }
693
918
  return groups;
@@ -1245,6 +1470,209 @@ window.__ModuleLoader__.load({
1245
1470
  return title === "default-workspace" ? localizedDefault : title;
1246
1471
  }
1247
1472
  //#endregion
1473
+ //#region src/client/group-status.ts
1474
+ /**
1475
+ * Zero-valued sentinel for {@link GroupLiveStatus}.
1476
+ *
1477
+ * Used when a group is expanded (badge is hidden, so aggregation is wasted
1478
+ * work) or when no cross-plugin data is available. Kept as a frozen constant
1479
+ * so the reference is stable across renders — `Object.is` comparisons in
1480
+ * React selectors never trigger spurious re-renders on it.
1481
+ */
1482
+ const EMPTY_LIVE_STATUS = Object.freeze({
1483
+ running: 0,
1484
+ approval: 0,
1485
+ planReview: 0,
1486
+ question: 0,
1487
+ done: 0,
1488
+ subagents: 0,
1489
+ scheduledSessions: 0,
1490
+ scheduledCount: 0,
1491
+ scheduleState: void 0,
1492
+ waitingReviewCount: 0,
1493
+ waitingReviewState: void 0
1494
+ });
1495
+ /**
1496
+ * Merge two {@link GroupLiveStatus} objects by summing counters and taking the
1497
+ * highest-priority state (overdue > urgent > scheduled for schedule; overdue > waiting
1498
+ * for waiting-review).
1499
+ *
1500
+ * Used for multi-level aggregation: when a parent group is collapsed, its badge
1501
+ * must reflect all descendant sessions, not just direct members.
1502
+ */
1503
+ function mergeLiveStatus(a, b) {
1504
+ const priorityOf = (s) => s === "overdue" ? 3 : s === "urgent" ? 2 : s === "scheduled" ? 1 : 0;
1505
+ const scheduleState = priorityOf(a.scheduleState) >= priorityOf(b.scheduleState) ? a.scheduleState : b.scheduleState;
1506
+ const waitingReviewState = (a.waitingReviewState === "overdue" ? 2 : a.waitingReviewState === "waiting" ? 1 : 0) >= (b.waitingReviewState === "overdue" ? 2 : b.waitingReviewState === "waiting" ? 1 : 0) ? a.waitingReviewState : b.waitingReviewState;
1507
+ return {
1508
+ running: a.running + b.running,
1509
+ approval: a.approval + b.approval,
1510
+ planReview: a.planReview + b.planReview,
1511
+ question: a.question + b.question,
1512
+ done: a.done + b.done,
1513
+ subagents: a.subagents + b.subagents,
1514
+ scheduledSessions: a.scheduledSessions + b.scheduledSessions,
1515
+ scheduledCount: a.scheduledCount + b.scheduledCount,
1516
+ scheduleState,
1517
+ waitingReviewCount: a.waitingReviewCount + b.waitingReviewCount,
1518
+ waitingReviewState
1519
+ };
1520
+ }
1521
+ /**
1522
+ * Strip the `session-` / `webhook-` prefix from a Session id.
1523
+ *
1524
+ * dsh-gitea-dispatch publishes its waiting map keyed by the host SessionId, but
1525
+ * its HTTP endpoints key by this normalized form. Accepting both here keeps the
1526
+ * badge working across that publisher's conventions without knowing which is in
1527
+ * play.
1528
+ */
1529
+ function stripSessionPrefix(id) {
1530
+ return id.replace(/^(?:session|webhook)-/, "");
1531
+ }
1532
+ /**
1533
+ * Fold every member row of a group into counts so a collapsed folder can hint at
1534
+ * hidden live sessions. Mirrors the session-row status priority (pending
1535
+ * outranks running outranks completed) at the counting level; labels resolve with
1536
+ * locale at render time.
1537
+ * @param nodes - member rows of the group.
1538
+ * @param sessionIds - the same members' Session ids (schedule and waiting facts are keyed by id).
1539
+ * @param waitingReviewSummary - dsh-gitea-dispatch waiting map, when that plugin is loaded.
1540
+ * @param scheduleSummary - dsh-later schedule map, when that plugin is loaded.
1541
+ */
1542
+ function summarizeGroupStatus(nodes, sessionIds, waitingReviewSummary, scheduleSummary) {
1543
+ let running = 0;
1544
+ let approval = 0;
1545
+ let planReview = 0;
1546
+ let question = 0;
1547
+ let done = 0;
1548
+ let subagents = 0;
1549
+ for (const n of nodes) {
1550
+ if (n.blank || n.archived) continue;
1551
+ if (n.pendingInteraction === "approval") {
1552
+ approval += 1;
1553
+ continue;
1554
+ }
1555
+ if (n.pendingInteraction === "plan-review") {
1556
+ planReview += 1;
1557
+ continue;
1558
+ }
1559
+ if (n.pendingInteraction === "question") {
1560
+ question += 1;
1561
+ continue;
1562
+ }
1563
+ if (n.running) running += 1;
1564
+ else if (n.completed) done += 1;
1565
+ subagents += n.runningSubagentCount;
1566
+ }
1567
+ let scheduledSessions = 0;
1568
+ let scheduledCount = 0;
1569
+ let scheduleState = void 0;
1570
+ if (scheduleSummary && sessionIds.length > 0) {
1571
+ let totalSessions = 0;
1572
+ let totalCount = 0;
1573
+ let highestPriority = 0;
1574
+ for (const id of sessionIds) {
1575
+ const entry = scheduleSummary.get(id);
1576
+ if (entry) {
1577
+ totalSessions += 1;
1578
+ totalCount += entry.count;
1579
+ const priority = entry.state === "overdue" ? 3 : entry.state === "urgent" ? 2 : 1;
1580
+ if (priority > highestPriority) highestPriority = priority;
1581
+ }
1582
+ }
1583
+ scheduledSessions = totalSessions;
1584
+ scheduledCount = totalCount;
1585
+ scheduleState = highestPriority === 3 ? "overdue" : highestPriority === 2 ? "urgent" : highestPriority === 1 ? "scheduled" : void 0;
1586
+ }
1587
+ let waitingReviewCount = 0;
1588
+ let waitingReviewState = void 0;
1589
+ if (waitingReviewSummary && sessionIds.length > 0) for (const id of sessionIds) {
1590
+ const entry = waitingReviewSummary.get(id) ?? waitingReviewSummary.get(stripSessionPrefix(id));
1591
+ if (entry === void 0) continue;
1592
+ waitingReviewCount += 1;
1593
+ if (entry.state === "overdue") waitingReviewState = "overdue";
1594
+ else if (waitingReviewState === void 0) waitingReviewState = "waiting";
1595
+ }
1596
+ return {
1597
+ running,
1598
+ approval,
1599
+ planReview,
1600
+ question,
1601
+ done,
1602
+ subagents,
1603
+ scheduledSessions,
1604
+ scheduledCount,
1605
+ scheduleState,
1606
+ waitingReviewCount,
1607
+ waitingReviewState
1608
+ };
1609
+ }
1610
+ /**
1611
+ * Measure each group's own members, as the input to {@link aggregateGroupStatuses}.
1612
+ *
1613
+ * Every group is measured, expanded ones included. A folder row shows no badge while
1614
+ * it is expanded, but its sessions still need attention and a collapsed ancestor must
1615
+ * keep counting them — so expansion is a rendering fact, never an input to the
1616
+ * aggregation. Short-circuiting expanded groups to `EMPTY_LIVE_STATUS` (the shape this
1617
+ * had before the tree-wide aggregator existed, harmless while nothing aggregated) makes
1618
+ * every collapsed parent undercount by the size of its expanded descendants.
1619
+ *
1620
+ * @param groups - derived groups, each with its member rows already populated.
1621
+ * @param waitingReviewSummary - dsh-gitea-dispatch waiting map, when that plugin is loaded.
1622
+ * @param scheduleSummary - dsh-later schedule map, when that plugin is loaded.
1623
+ */
1624
+ function directStatusesForGroups(groups, waitingReviewSummary, scheduleSummary) {
1625
+ const map = /* @__PURE__ */ new Map();
1626
+ for (const group of groups) map.set(group.key, summarizeGroupStatus(group.sessions, group.sessions.map((s) => s.id), waitingReviewSummary, scheduleSummary));
1627
+ return map;
1628
+ }
1629
+ /**
1630
+ * Fold every group's DIRECT status up the workspace tree, so a collapsed folder
1631
+ * counts every descendant session. A group's own row shows no badge while it is
1632
+ * expanded, but its sessions still need attention and its collapsed ancestor must
1633
+ * keep counting them — so the walk never depends on a group's expanded state.
1634
+ *
1635
+ * @param direct - each group's own-member status, keyed by group key.
1636
+ * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
1637
+ * @returns aggregated status per group key: own members plus the whole subtree.
1638
+ */
1639
+ function aggregateGroupStatuses(direct, childrenByParent) {
1640
+ const aggregated = /* @__PURE__ */ new Map();
1641
+ const visiting = /* @__PURE__ */ new Set();
1642
+ const fold = (key) => {
1643
+ const cached = aggregated.get(key);
1644
+ if (cached !== void 0) return cached;
1645
+ if (visiting.has(key)) return direct.get(key) ?? EMPTY_LIVE_STATUS;
1646
+ visiting.add(key);
1647
+ let total = direct.get(key) ?? EMPTY_LIVE_STATUS;
1648
+ for (const child of childrenByParent.get(key) ?? []) total = mergeLiveStatus(total, fold(child.key));
1649
+ visiting.delete(key);
1650
+ aggregated.set(key, total);
1651
+ return total;
1652
+ };
1653
+ for (const children of childrenByParent.values()) for (const child of children) fold(child.key);
1654
+ return aggregated;
1655
+ }
1656
+ /**
1657
+ * Attach each group's subtree-wide status, in derivation order. This is the producer
1658
+ * the sidebar renders from.
1659
+ *
1660
+ * Expanded groups carry their status too: their own row draws no badge (a decision
1661
+ * inside `groupLiveBadge`), but a collapsed ancestor still has to count them, so
1662
+ * expansion never suppresses the measurement.
1663
+ *
1664
+ * @param groups - derived rows, in the order they render.
1665
+ * @param direct - each group's own-member status, from {@link directStatusesForGroups}.
1666
+ * @param childrenByParent - the rendered tree: parent key (`undefined` = roots) → children.
1667
+ */
1668
+ function withAggregatedStatus(groups, direct, childrenByParent) {
1669
+ const aggregated = aggregateGroupStatuses(direct, childrenByParent);
1670
+ return groups.map((group) => ({
1671
+ ...group,
1672
+ liveStatus: aggregated.get(group.key) ?? EMPTY_LIVE_STATUS
1673
+ }));
1674
+ }
1675
+ //#endregion
1248
1676
  //#region \0dsh-css:/home/wujue/workspace/dsh-plugins/dsh-group-status/src/client/rows/Rows.module.css.mjs
1249
1677
  const css$3 = ".eCE9Ta_projectRow,.eCE9Ta_sessionRow{border-radius:var(--dsw-radius-md);padding:0 8px;cursor:pointer;user-select:none;color:var(--dsw-alias-label-primary);align-items:center;gap:6px;padding-inline-start:calc(8px + var(--dsh-workspace-indent,0px));display:flex}.eCE9Ta_projectRow:hover,.eCE9Ta_sessionRow:hover,.eCE9Ta_sessionRow.eCE9Ta_selected{background:var(--dsw-alias-interactive-bg-hover)}.eCE9Ta_searchResultRow{box-sizing:border-box;border-radius:var(--dsw-radius-lg);cursor:pointer;text-align:left;width:100%;min-height:48px;color:var(--dsw-alias-label-primary);background:0 0;border:none;flex-direction:column;align-items:stretch;padding:4px 8px;display:flex}.eCE9Ta_searchResultRow:hover,.eCE9Ta_searchResultRow.eCE9Ta_selected{background:var(--dsw-alias-interactive-bg-hover)}.eCE9Ta_searchResultHeading{align-items:center;min-width:0;display:flex}.eCE9Ta_searchResultTitle{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;margin-left:4px;font-size:14px;line-height:20px;overflow:hidden}.eCE9Ta_searchResultMeta{align-items:center;gap:6px;min-width:0;margin-left:20px;display:flex}.eCE9Ta_searchResultWorkspace,.eCE9Ta_searchResultSnippet{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px;overflow:hidden}.eCE9Ta_searchResultWorkspace{max-width:40%;color:var(--dsw-alias-label-tertiary);flex:none}.eCE9Ta_searchResultWorkspace:only-child{max-width:100%}.eCE9Ta_searchResultSnippet{min-width:0;color:var(--dsw-alias-label-secondary);flex:1}.eCE9Ta_projectRow{box-sizing:border-box;align-items:center;height:34px}.eCE9Ta_projectRow .eCE9Ta_rowActions{height:20px}.eCE9Ta_sessionRow{gap:0;height:32px}.eCE9Ta_sessionRow .eCE9Ta_title{margin:0 6px 0 4px}.eCE9Ta_slot{width:16px;height:20px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.eCE9Ta_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.eCE9Ta_folderActive{color:var(--dsw-alias-state-business-primary)}.eCE9Ta_projectRow .eCE9Ta_chevron{display:none}.eCE9Ta_projectRow:hover .eCE9Ta_chevron{display:inline-flex}.eCE9Ta_projectRow:hover .eCE9Ta_folder{display:none}.eCE9Ta_arrow{transition:transform .15s var(--ds-ease-in-out)}.eCE9Ta_arrowOpen{transform:rotate(90deg)}.eCE9Ta_projectText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.eCE9Ta_title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:20px;overflow:hidden}.eCE9Ta_renameInput{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-button-elevated-fill);min-width:0;color:inherit;outline:none;padding:0 2px;font-size:14px;line-height:20px}.eCE9Ta_sessionRow .eCE9Ta_title{flex:1}.eCE9Ta_sessionRow .eCE9Ta_title[data-scrolled]{mask-image:linear-gradient(90deg,#0000,#000 12px)}.eCE9Ta_sessionRow .eCE9Ta_title[data-clipped]{mask-image:linear-gradient(270deg,#0000,#000 12px)}.eCE9Ta_sessionRow .eCE9Ta_title[data-scrolled][data-clipped]{mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000)}.eCE9Ta_meta{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden}.eCE9Ta_time{color:var(--dsw-alias-label-tertiary);flex:none;font-size:10px;line-height:16px}.eCE9Ta_pinIndicator{width:16px;height:20px;color:var(--dsw-alias-label-caption);flex:none;justify-content:center;align-items:center;margin-left:6px;display:inline-flex}.eCE9Ta_sessionRow.eCE9Ta_archived .eCE9Ta_title,.eCE9Ta_searchResultRow.eCE9Ta_archived .eCE9Ta_searchResultTitle,.eCE9Ta_searchResultRow.eCE9Ta_archived .eCE9Ta_searchResultWorkspace,.eCE9Ta_searchResultRow.eCE9Ta_archived .eCE9Ta_searchResultSnippet{color:var(--dsw-alias-label-caption)}.eCE9Ta_dot{flex:none}.eCE9Ta_rowActions{flex:none;align-items:center;gap:10px;display:none}.eCE9Ta_projectRow:hover .eCE9Ta_rowActions,.eCE9Ta_sessionRow:hover .eCE9Ta_rowActions,.eCE9Ta_searchResultRow:hover .eCE9Ta_rowActions,.eCE9Ta_projectRow.eCE9Ta_menuOpen .eCE9Ta_rowActions,.eCE9Ta_sessionRow.eCE9Ta_menuOpen .eCE9Ta_rowActions{display:inline-flex}.eCE9Ta_searchResultHeading .eCE9Ta_rowActions{margin-left:auto}.eCE9Ta_sessionRow:hover .eCE9Ta_time,.eCE9Ta_sessionRow.eCE9Ta_menuOpen .eCE9Ta_time,.eCE9Ta_sessionRow:hover .eCE9Ta_pinIndicator,.eCE9Ta_sessionRow.eCE9Ta_menuOpen .eCE9Ta_pinIndicator{display:none}@media (hover:hover){.eCE9Ta_sessionRow:hover .eCE9Ta_title,.eCE9Ta_sessionRow.eCE9Ta_menuOpen .eCE9Ta_title{text-overflow:clip}}.eCE9Ta_projectRow.eCE9Ta_menuOpen,.eCE9Ta_sessionRow.eCE9Ta_menuOpen{background:var(--dsw-alias-interactive-bg-hover)}.eCE9Ta_sessionRow.eCE9Ta_dropBefore,.eCE9Ta_sessionRow.eCE9Ta_dropAfter{position:relative}.eCE9Ta_sessionRow.eCE9Ta_dropBefore:before,.eCE9Ta_sessionRow.eCE9Ta_dropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:4px}.eCE9Ta_sessionRow.eCE9Ta_dropBefore:before{top:-7px}.eCE9Ta_sessionRow.eCE9Ta_dropAfter:after{bottom:-7px}.eCE9Ta_hoverContent{flex-direction:column;gap:8px;display:flex}.eCE9Ta_hoverTitle{color:var(--dsw-alias-label-primary);overflow-wrap:break-word;font-size:14px;line-height:20px}.eCE9Ta_hoverPath{color:var(--dsw-alias-label-secondary);word-break:break-all;font-size:12px;line-height:16px}.eCE9Ta_hoverTime{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px}.eCE9Ta_hoverStatus{color:var(--dsw-alias-label-tertiary);align-items:center;gap:8px;font-size:12px;line-height:20px;display:flex}.eCE9Ta_groupBadge{flex:none;align-items:center;gap:3px;margin-inline-start:4px;display:inline-flex}.eCE9Ta_groupBadgeCount{color:var(--dsw-alias-label-tertiary);border:1px solid;border-radius:6px;padding:0 3px;font-size:9px;line-height:12px}@keyframes eCE9Ta_groupBadgePulse{0%,to{opacity:1}50%{opacity:.6}}.eCE9Ta_groupBadgeUrgent{animation:2s ease-in-out infinite eCE9Ta_groupBadgePulse}.eCE9Ta_hoverArchived svg{flex-shrink:0;margin:0 -4px}.eCE9Ta_iconButton{border-radius:var(--dsw-radius-xs);cursor:pointer;width:16px;height:16px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.eCE9Ta_iconButton:hover{color:var(--dsw-alias-label-primary)}.eCE9Ta_chevron{color:var(--dsw-alias-label-caption)}@media (prefers-reduced-motion:reduce){.eCE9Ta_arrow{transition:none;animation:none}}";
1250
1678
  const tagId$3 = "dsh-group-status/Rows.module.css";
@@ -1301,17 +1729,23 @@ window.__ModuleLoader__.load({
1301
1729
  /**
1302
1730
  * Accessible label for a folded group's aggregated status, reusing the
1303
1731
  * session-row locale keys so both surfaces read identically.
1732
+ *
1733
+ * 按设置过滤:关掉的来源不出现在标签里(aria-label 与徽标视觉一致)。
1304
1734
  */
1305
- function groupLiveLabel(s, t) {
1735
+ function groupLiveLabel(s, t, settings) {
1306
1736
  const parts = [];
1307
- if (s.approval > 0) parts.push(`${s.approval} ${t("status.waitingApproval")}`);
1308
- if (s.planReview > 0) parts.push(`${s.planReview} ${t("status.planReview")}`);
1309
- if (s.question > 0) parts.push(`${s.question} ${t("status.waitingAnswer")}`);
1310
- if (s.running > 0) parts.push(`${s.running} ${t("status.running")}`);
1311
- if (s.subagents > 0) parts.push(t(s.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: s.subagents }));
1312
- if (s.waitingReviewCount > 0) parts.push(`${s.waitingReviewCount} ${t("status.waitingReview")}`);
1313
- if (s.scheduledSessions > 0) parts.push(`${s.scheduledSessions} ${t("status.scheduled")}`);
1314
- if (s.done > 0) parts.push(`${s.done} ${t("status.completed")}`);
1737
+ if (settings.showPendingInBadge) {
1738
+ if (s.approval > 0) parts.push(`${s.approval} ${t("status.waitingApproval")}`);
1739
+ if (s.planReview > 0) parts.push(`${s.planReview} ${t("status.planReview")}`);
1740
+ if (s.question > 0) parts.push(`${s.question} ${t("status.waitingAnswer")}`);
1741
+ }
1742
+ if (settings.showRunningInBadge) {
1743
+ if (s.running > 0) parts.push(`${s.running} ${t("status.running")}`);
1744
+ if (s.subagents > 0) parts.push(t(s.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: s.subagents }));
1745
+ }
1746
+ if (settings.showWaitingReviewInBadge && s.waitingReviewCount > 0) parts.push(`${s.waitingReviewCount} ${t("status.waitingReview")}`);
1747
+ if (settings.showScheduledInBadge && s.scheduledSessions > 0) parts.push(`${s.scheduledSessions} ${t("status.scheduled")}`);
1748
+ if (settings.showCompletedInBadge && s.done > 0) parts.push(`${s.done} ${t("status.completed")}`);
1315
1749
  return parts.join(" · ");
1316
1750
  }
1317
1751
  /**
@@ -1321,20 +1755,19 @@ window.__ModuleLoader__.load({
1321
1755
  *
1322
1756
  * Count matches the dots: pending + active + completed + waiting-review + scheduled sessions.
1323
1757
  *
1324
- * `showCompleted` (设置项 `showCompletedInBadge`) 关闭时,`done` 完全退出这个聚合
1325
- * 提示——不画绿点、不计入数字、不参与「是否显示徽标」的判断,等价于旧行为。会话行
1326
- * 自身的绿点不受影响:那是行级状态,不是分组提示。
1758
+ * `settings` 按来源过滤:关掉的来源不画点、不计数、不进 aria-label、不参与「是否显示徽标」
1759
+ * 的判断。会话行自身的状态不受影响:那是行级状态,不是分组提示。
1327
1760
  */
1328
- function groupLiveBadge(group, t, showCompleted) {
1761
+ function groupLiveBadge(group, t, settings) {
1329
1762
  const s = group.liveStatus;
1330
1763
  if (group.expanded || s === void 0) return null;
1331
- const pending = s.approval + s.planReview + s.question;
1332
- const active = s.running + s.subagents;
1333
- const scheduled = s.scheduledSessions;
1334
- const hasWaitingReview = s.waitingReviewCount > 0;
1335
- const completed = showCompleted ? s.done : 0;
1764
+ const pending = settings.showPendingInBadge ? s.approval + s.planReview + s.question : 0;
1765
+ const active = settings.showRunningInBadge ? s.running + s.subagents : 0;
1766
+ const scheduled = settings.showScheduledInBadge ? s.scheduledSessions : 0;
1767
+ const hasWaitingReview = settings.showWaitingReviewInBadge && s.waitingReviewCount > 0;
1768
+ const completed = settings.showCompletedInBadge ? s.done : 0;
1336
1769
  if (pending === 0 && active === 0 && completed === 0 && scheduled === 0 && !hasWaitingReview) return null;
1337
- const label = groupLiveLabel(s, t);
1770
+ const label = groupLiveLabel(s, t, settings);
1338
1771
  const dots = [];
1339
1772
  if (pending > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" }, "warn"));
1340
1773
  if (active > 0) dots.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }, "run"));
@@ -1361,8 +1794,8 @@ window.__ModuleLoader__.load({
1361
1794
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconListPenOutlineRegular, { size: 14 })
1362
1795
  }, "waiting-review"));
1363
1796
  }
1364
- const count = pending + active + completed + scheduled + s.waitingReviewCount;
1365
- const countLabel = count > 99 ? "99+" : count > 1 ? String(count) : "";
1797
+ const count = pending + active + completed + scheduled + (hasWaitingReview ? s.waitingReviewCount : 0);
1798
+ const countLabel = count > 99 ? "99+" : count >= settings.countThreshold ? String(count) : "";
1366
1799
  const badgeClass = s.scheduleState === "overdue" || s.scheduleState === "urgent" || s.waitingReviewState === "overdue" ? `${Rows_module_css_default.groupBadge} ${Rows_module_css_default.groupBadgeUrgent}` : Rows_module_css_default.groupBadge;
1367
1800
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1368
1801
  className: badgeClass,
@@ -1380,39 +1813,45 @@ window.__ModuleLoader__.load({
1380
1813
  *
1381
1814
  * Extracted so `WorkspaceHoverContent` in `Rows.tsx` only needs to map the
1382
1815
  * returned array — the fork-specific status→line logic lives here.
1816
+ *
1817
+ * 按设置过滤:关掉的来源不出现在明细行里;`showHoverDetails=false` 时整段返回空。
1383
1818
  */
1384
- function hoverStatusLines(status, t) {
1819
+ function hoverStatusLines(status, t, settings) {
1385
1820
  const lines = [];
1386
- if (status === void 0) return lines;
1387
- if (status.approval > 0) lines.push({
1388
- state: "warning",
1389
- text: `${status.approval} ${t("status.waitingApproval")}`
1390
- });
1391
- if (status.planReview > 0) lines.push({
1392
- state: "warning",
1393
- text: `${status.planReview} ${t("status.planReview")}`
1394
- });
1395
- if (status.question > 0) lines.push({
1396
- state: "warning",
1397
- text: `${status.question} ${t("status.waitingAnswer")}`
1398
- });
1399
- if (status.running > 0) lines.push({
1400
- state: "ongoing",
1401
- text: `${status.running} ${t("status.running")}`
1402
- });
1403
- if (status.subagents > 0) lines.push({
1404
- state: "ongoing",
1405
- text: t(status.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: status.subagents })
1406
- });
1407
- if (status.waitingReviewCount > 0) lines.push({
1821
+ if (status === void 0 || !settings.showHoverDetails) return lines;
1822
+ if (settings.showPendingInBadge) {
1823
+ if (status.approval > 0) lines.push({
1824
+ state: "warning",
1825
+ text: `${status.approval} ${t("status.waitingApproval")}`
1826
+ });
1827
+ if (status.planReview > 0) lines.push({
1828
+ state: "warning",
1829
+ text: `${status.planReview} ${t("status.planReview")}`
1830
+ });
1831
+ if (status.question > 0) lines.push({
1832
+ state: "warning",
1833
+ text: `${status.question} ${t("status.waitingAnswer")}`
1834
+ });
1835
+ }
1836
+ if (settings.showRunningInBadge) {
1837
+ if (status.running > 0) lines.push({
1838
+ state: "ongoing",
1839
+ text: `${status.running} ${t("status.running")}`
1840
+ });
1841
+ if (status.subagents > 0) lines.push({
1842
+ state: "ongoing",
1843
+ text: t(status.subagents === 1 ? "status.subagentsRunning.one" : "status.subagentsRunning.other", { n: status.subagents })
1844
+ });
1845
+ }
1846
+ if (settings.showWaitingReviewInBadge && status.waitingReviewCount > 0) lines.push({
1408
1847
  state: "warning",
1409
1848
  text: `${status.waitingReviewCount} ${t("status.waitingReview")}`
1410
1849
  });
1411
- if (status.scheduledSessions > 0) lines.push({
1850
+ if (settings.showScheduledInBadge && status.scheduledSessions > 0) lines.push({
1412
1851
  state: "warning",
1413
1852
  text: `${status.scheduledSessions} ${t("status.scheduled")}`
1414
1853
  });
1415
- if (status.done > 0) lines.push({
1854
+ if (settings.showCompletedInBadge && status.done > 0) lines.push({
1416
1855
  state: "done",
1417
1856
  text: `${status.done} ${t("status.completed")}`
1418
1857
  });
@@ -1543,8 +1982,8 @@ window.__ModuleLoader__.load({
1543
1982
  })} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` });
1544
1983
  }
1545
1984
  /** Hover-card body: workspace title, display directory path, absolute creation time. */
1546
- function WorkspaceHoverContent({ label, cwd, createdAt, status, t }) {
1547
- const lines = hoverStatusLines(status, t);
1985
+ function WorkspaceHoverContent({ label, cwd, createdAt, status, settings, t }) {
1986
+ const lines = hoverStatusLines(status, t, settings);
1548
1987
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1549
1988
  className: Rows_module_css_default.hoverContent,
1550
1989
  children: [
@@ -1586,7 +2025,7 @@ window.__ModuleLoader__.load({
1586
2025
  * @param props.t - the browser root's locale seat.
1587
2026
  * @returns the row element.
1588
2027
  */
1589
- function ProjectRowItem({ group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag, home, newShortcut, showCompleted = true, t }) {
2028
+ function ProjectRowItem({ group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag, home, newShortcut, settings, t }) {
1590
2029
  const row = group;
1591
2030
  const label = row.workspaceId === void 0 ? t("group.ungrouped") : row.label;
1592
2031
  const active = containsCurrentDescendant || group.expanded && group.containsCurrent;
@@ -1630,7 +2069,7 @@ window.__ModuleLoader__.load({
1630
2069
  children: label
1631
2070
  })
1632
2071
  }),
1633
- groupLiveBadge(row, t, showCompleted),
2072
+ groupLiveBadge(group, t, settings),
1634
2073
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1635
2074
  className: Rows_module_css_default.rowActions,
1636
2075
  children: [actions !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
@@ -1687,6 +2126,7 @@ window.__ModuleLoader__.load({
1687
2126
  cwd: row.cwd === void 0 ? void 0 : abbreviateHomePath(row.cwd, home),
1688
2127
  createdAt: row.createdAt,
1689
2128
  status: row.expanded ? void 0 : row.liveStatus,
2129
+ settings,
1690
2130
  t
1691
2131
  }),
1692
2132
  openDelayMs: 800,
@@ -2645,7 +3085,7 @@ window.__ModuleLoader__.load({
2645
3085
  /** The scrolling session tree; unmounting drops the sessions subscription and local row limits. */
2646
3086
  function SessionTree({ list, useSessionStatus, startSession, open, workspaces, ungroupedSessionIds, rowState, onLeaveArchivedOnly, workspaceReady, animationResetKey, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRenameRequest, renderSlot, insertWorkspaceBefore, nestWorkspaces, groupExpansion, setGroupExpanded, setSessionOrder, home, t, revealSessionId, onSessionRevealed, shortcuts, useBadgeSettings }) {
2647
3087
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
2648
- const showCompleted = useBadgeSettings((settings) => settings.showCompletedInBadge);
3088
+ const badgeSettings = useBadgeSettings((settings) => settings);
2649
3089
  const statuses = useSessionStatus((s) => s);
2650
3090
  const scheduleSummary = useScheduleSummaryHook?.();
2651
3091
  const waitingReviewSummary = useWaitingSummaryHook?.();
@@ -2690,20 +3130,16 @@ window.__ModuleLoader__.load({
2690
3130
  parents,
2691
3131
  workspaces
2692
3132
  ]);
2693
- const allGroupsExpanded = expandedGroups.length >= workspaces.length + 1 && workspaces.every((w) => expandedGroups.includes(w.workspaceId)) && expandedGroups.includes("");
2694
3133
  const groups = (0, react.useMemo)(() => deriveGroups(list, workspaces, rowState, statuses, {
2695
3134
  expandedGroups,
2696
3135
  ungroupedOrder: ungroupedSessionIds
2697
- }, allGroupsExpanded ? void 0 : waitingReviewSummary, allGroupsExpanded ? void 0 : scheduleSummary), [
3136
+ }), [
2698
3137
  list,
2699
3138
  workspaces,
2700
3139
  rowState,
2701
3140
  statuses,
2702
3141
  expandedGroups,
2703
- ungroupedSessionIds,
2704
- scheduleSummary,
2705
- waitingReviewSummary,
2706
- allGroupsExpanded
3142
+ ungroupedSessionIds
2707
3143
  ]);
2708
3144
  (0, react.useEffect)(() => {
2709
3145
  for (let key = revealGroup; key !== void 0; key = parents.get(key)) if (groupExpansion[key] === false || key === revealGroup && groupExpansion[key] !== true) setGroupExpanded(key, true);
@@ -2758,7 +3194,7 @@ window.__ModuleLoader__.load({
2758
3194
  console.warn("workspace reorder rejected:", reason);
2759
3195
  });
2760
3196
  };
2761
- const childrenByParent = (0, react.useMemo)(() => {
3197
+ const childrenByParentForAggregation = (0, react.useMemo)(() => {
2762
3198
  const rendered = new Set(groups.map((group) => group.key));
2763
3199
  const children = /* @__PURE__ */ new Map();
2764
3200
  for (const group of groups) {
@@ -2770,9 +3206,31 @@ window.__ModuleLoader__.load({
2770
3206
  }
2771
3207
  return children;
2772
3208
  }, [groups, parents]);
3209
+ const directStatusByGroup = (0, react.useMemo)(() => directStatusesForGroups(groups, waitingReviewSummary, scheduleSummary), [
3210
+ groups,
3211
+ waitingReviewSummary,
3212
+ scheduleSummary
3213
+ ]);
3214
+ const groupsWithAggregatedStatus = (0, react.useMemo)(() => withAggregatedStatus(groups, directStatusByGroup, childrenByParentForAggregation), [
3215
+ groups,
3216
+ directStatusByGroup,
3217
+ childrenByParentForAggregation
3218
+ ]);
3219
+ const childrenByParent = (0, react.useMemo)(() => {
3220
+ const rendered = new Set(groupsWithAggregatedStatus.map((group) => group.key));
3221
+ const children = /* @__PURE__ */ new Map();
3222
+ for (const group of groupsWithAggregatedStatus) {
3223
+ let parent = parents.get(group.key);
3224
+ while (parent !== void 0 && !rendered.has(parent)) parent = parents.get(parent);
3225
+ const siblings = children.get(parent);
3226
+ if (siblings === void 0) children.set(parent, [group]);
3227
+ else siblings.push(group);
3228
+ }
3229
+ return children;
3230
+ }, [groupsWithAggregatedStatus, parents]);
2773
3231
  const rootGroups = childrenByParent.get(void 0) ?? [];
2774
3232
  const workspaceDropAtListStart = rootGroups[0]?.workspaceId !== void 0 && workspaceDrag?.over?.id === rootGroups[0].workspaceId && workspaceDrag.over.half === "before";
2775
- const rowKeys = groups.length === 0 ? ["empty"] : [];
3233
+ const rowKeys = groupsWithAggregatedStatus.length === 0 ? ["empty"] : [];
2776
3234
  const renderGroup = (group, depth) => {
2777
3235
  const workspaceId = group.workspaceId;
2778
3236
  const children = childrenByParent.get(group.key) ?? [];
@@ -2848,7 +3306,7 @@ window.__ModuleLoader__.load({
2848
3306
  group,
2849
3307
  containsCurrentDescendant: currentAncestors.has(group.key),
2850
3308
  home,
2851
- showCompleted,
3309
+ settings: badgeSettings,
2852
3310
  t,
2853
3311
  onToggle: () => {
2854
3312
  if (group.expanded) setSessionLimits((limits) => ({
@@ -2879,7 +3337,7 @@ window.__ModuleLoader__.load({
2879
3337
  role: "group",
2880
3338
  children: childRows
2881
3339
  }),
2882
- sessions.map((node) => {
3340
+ group.expanded && sessions.map((node) => {
2883
3341
  const sameGroupDrag = drag !== null && drag.accountKey === group.key;
2884
3342
  const compatibleTarget = sameGroupDrag && drag.pinned === node.pinned;
2885
3343
  const normalizeHalf = (half) => node.blank ? "after" : half;
@@ -2933,7 +3391,7 @@ window.__ModuleLoader__.load({
2933
3391
  t
2934
3392
  }, node.id);
2935
3393
  }),
2936
- collapsed.hiddenCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3394
+ group.expanded && collapsed.hiddenCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2937
3395
  type: "button",
2938
3396
  className: WorkspaceBrowser_module_css_default.sessionOverflowButton,
2939
3397
  "data-row-key": `overflow:${group.key}`,
@@ -2962,8 +3420,8 @@ window.__ModuleLoader__.load({
2962
3420
  label: t("section.sessions"),
2963
3421
  rowKeys,
2964
3422
  ready: list.phase === "ready" && workspaceReady && !nativeDragActive,
2965
- resetKey: JSON.stringify([animationResetKey, sessionLimits]),
2966
- children: [groups.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptySessions, {
3423
+ resetKey: animationResetKey,
3424
+ children: [groupsWithAggregatedStatus.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptySessions, {
2967
3425
  rowState,
2968
3426
  onLeaveArchivedOnly,
2969
3427
  t
@@ -4319,7 +4777,29 @@ window.__ModuleLoader__.load({
4319
4777
  "time.days": "{n}天",
4320
4778
  "time.months": "{n}个月",
4321
4779
  "time.years": "{n}年",
4322
- "time.ago": "{t}前"
4780
+ "time.ago": "{t}前",
4781
+ "settings.badge.section": "折叠徽标",
4782
+ "settings.badge.showRunning": "显示「进行中」转圈",
4783
+ "settings.badge.showRunningHint": "关闭后,进行中的会话(含子智能体)不再画转圈,也不再计入折叠徽标的数字与悬停明细。",
4784
+ "settings.badge.showPending": "显示「待审批」黄点",
4785
+ "settings.badge.showPendingHint": "关闭后,等待审批 / 计划评审 / 等待回答的会话不再画黄点,也不再计入折叠徽标的数字与悬停明细。",
4786
+ "settings.badge.showCompleted": "显示「已完成」绿点",
4787
+ "settings.badge.showCompletedHint": "关闭后,「已完成」的会话不再画绿点,也不再计入折叠徽标的数字与悬停明细。会话行自身的绿点不受影响。",
4788
+ "settings.badge.showScheduled": "显示「定时任务」时钟",
4789
+ "settings.badge.showScheduledHint": "关闭后,带定时提醒的会话(来自 dsh-later)不再画时钟图标,也不再计入折叠徽标的数字与悬停明细。",
4790
+ "settings.badge.showWaitingReview": "显示「等待评审」列表笔",
4791
+ "settings.badge.showWaitingReviewHint": "关闭后,等待代码评审的会话(来自 dsh-gitea-dispatch)不再画列表笔图标,也不再计入折叠徽标的数字与悬停明细。",
4792
+ "settings.hover.section": "悬停明细",
4793
+ "settings.hover.showDetails": "显示状态明细行",
4794
+ "settings.hover.showDetailsHint": "关闭后,悬停卡只显示工作区标题、路径和创建时间——不再显示聚合状态明细行。",
4795
+ "settings.count.section": "数字显示",
4796
+ "settings.count.threshold": "数字显示阈值",
4797
+ "settings.count.thresholdHint": "数量 >= 此值时才在徽标上显示数字。默认 2(即 count=1 时省略,因为状态点已表达\"有东西\")。设为 1 则始终显示。",
4798
+ "settings.save": "保存",
4799
+ "settings.saving": "保存中…",
4800
+ "settings.saveFailed": "本部署没有接受这些值,已保留供你修改。",
4801
+ "settings.readOnly": "本部署的设置为只读。",
4802
+ "settings.unavailable": "该插件当前未加载,暂时无法配置。"
4323
4803
  };
4324
4804
  /** English dictionary, checked complete against the zh key set. */
4325
4805
  const en = {
@@ -4436,12 +4916,41 @@ window.__ModuleLoader__.load({
4436
4916
  "time.days": "{n}d",
4437
4917
  "time.months": "{n}mo",
4438
4918
  "time.years": "{n}y",
4439
- "time.ago": "{t} ago"
4919
+ "time.ago": "{t} ago",
4920
+ "settings.badge.section": "Collapsed badge",
4921
+ "settings.badge.showRunning": "Show the running dot",
4922
+ "settings.badge.showRunningHint": "When off, running Sessions (and subagents) stop drawing the spinning dot and drop out of the collapsed badge count and its hover details.",
4923
+ "settings.badge.showPending": "Show the pending dot",
4924
+ "settings.badge.showPendingHint": "When off, Sessions waiting for approval / plan review / answer stop drawing the yellow dot and drop out of the collapsed badge count and its hover details.",
4925
+ "settings.badge.showCompleted": "Show the completed dot",
4926
+ "settings.badge.showCompletedHint": "When off, completed Sessions stop drawing the dot and drop out of the collapsed badge count and its hover details. The Session row's own dot is unaffected.",
4927
+ "settings.badge.showScheduled": "Show the scheduled icon",
4928
+ "settings.badge.showScheduledHint": "When off, Sessions with scheduled reminders (from dsh-later) stop drawing the clock icon and drop out of the collapsed badge count and its hover details.",
4929
+ "settings.badge.showWaitingReview": "Show the waiting-for-review icon",
4930
+ "settings.badge.showWaitingReviewHint": "When off, Sessions waiting for code review (from dsh-gitea-dispatch) stop drawing the list-pen icon and drop out of the collapsed badge count and its hover details.",
4931
+ "settings.hover.section": "Hover details",
4932
+ "settings.hover.showDetails": "Show status detail lines",
4933
+ "settings.hover.showDetailsHint": "When off, the hover card shows only the workspace title, path, and creation time — no aggregated status lines.",
4934
+ "settings.count.section": "Count display",
4935
+ "settings.count.threshold": "Count display threshold",
4936
+ "settings.count.thresholdHint": "The count is shown on the badge only when the total is >= this value. Default 2 (omits the count when it equals 1 — the dots already convey \"something is happening\"). Set to 1 to always show.",
4937
+ "settings.save": "Save",
4938
+ "settings.saving": "Saving…",
4939
+ "settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
4940
+ "settings.readOnly": "This deployment stores settings read-only.",
4941
+ "settings.unavailable": "This plugin is not loaded, so it cannot be configured right now."
4440
4942
  };
4441
4943
  //#endregion
4442
4944
  //#region src/client/index.ts
4443
4945
  /** Dictionary namespace owned by this plugin. */
4444
4946
  const NS = "workspace";
4947
+ /** 插件详情页配置表单的槽位名(按包名寻址;契约见 `plugin-manager-slot.d.ts`)。 */
4948
+ const CONFIG_CARD_SLOT = "plugins.bundle.config";
4949
+ /**
4950
+ * 插件管理器分发 `plugins.bundle.config` 用的键 = **包名**(不是条目 id)。
4951
+ * 本包的补丁把行 id 也写成同名,但两者是不同概念,别改成 ENTRY_ID 的引用。
4952
+ */
4953
+ const CONFIG_CARD_KEY = "dsh-group-status";
4445
4954
  /**
4446
4955
  * Required services (cordis fiber inject). The target slots are declared by
4447
4956
  * the ui-sidebar / ui-conversation applies, whose activation order relative
@@ -4494,12 +5003,21 @@ window.__ModuleLoader__.load({
4494
5003
  ctx.slots.provideRoot({ hooks: { badgeSettings } });
4495
5004
  ctx.inject(["configForms"], (configCtx) => {
4496
5005
  const forms = configCtx.get("configForms");
4497
- const scope = forms !== void 0 && typeof forms.get === "function" ? forms.get(ENTRY_ID) : void 0;
5006
+ if (forms === void 0 || typeof forms.get !== "function") return;
5007
+ const scope = forms.get(ENTRY_ID);
4498
5008
  if (scope === void 0) return;
4499
5009
  badgeSettings.publish(scope.getSnapshot());
4500
5010
  configCtx.effect(() => scope.subscribe(() => {
4501
5011
  badgeSettings.publish(scope.getSnapshot());
4502
5012
  }), "dsh-group-status: badge settings");
5013
+ const whileServed = forms.whileServed?.bind(forms);
5014
+ if (whileServed === void 0) return;
5015
+ configCtx.effect(() => whileServed([ENTRY_ID], () => ctx.slots.inject(CONFIG_CARD_SLOT, () => ctx.slots.register({
5016
+ name: CONFIG_CARD_SLOT,
5017
+ key: CONFIG_CARD_KEY,
5018
+ locale: NS,
5019
+ inject: () => ({ scope })
5020
+ }, BadgeSettingsCard))), "dsh-group-status: plugins-page card");
4503
5021
  });
4504
5022
  const searchSessions = async (query, signal) => {
4505
5023
  const result = await sessions.search(query, signal);