dsh-tree-task-flow 0.3.10 → 0.3.12

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/client.js CHANGED
@@ -61,10 +61,27 @@ window.__ModuleLoader__.load({
61
61
  primitives = {};
62
62
  }
63
63
 
64
+ /**
65
+ * 内置图标名有两套写法,这里两套都认。
66
+ *
67
+ * 旧外壳把尺寸写进名字:`IconCheckOutline16`、`IconChevronDownOutline14`;
68
+ * 升级后的外壳把名字里的尺寸换成笔画粗细:`IconCheckOutlineRegular`、
69
+ * `IconCheckOutlineMedium`,尺寸改由 `size` 属性给。按「尺寸后缀 → 粗细后缀
70
+ * → 裸名」的顺序找,两套外壳都取得回图标,不会平白降级成文字。
71
+ */
72
+ function findIcon(base, size) {
73
+ const candidates = [base + size, base + "14", base + "16", base + "Regular", base + "Medium", base];
74
+ for (const candidate of candidates) {
75
+ const component = primitives[candidate];
76
+ if (typeof component === "function") return component;
77
+ }
78
+ return null;
79
+ }
80
+
64
81
  /** 取一个内置图标;这版外壳没有就返回 null,由调用方决定怎么退。 */
65
- function icon(iconName, size) {
66
- const component = primitives[iconName];
67
- if (typeof component !== "function") return null;
82
+ function icon(base, size) {
83
+ const component = findIcon(base, size);
84
+ if (component === null) return null;
68
85
  return h(component, { size });
69
86
  }
70
87
 
@@ -75,16 +92,21 @@ window.__ModuleLoader__.load({
75
92
  return h(Tooltip, { label, side: "bottom", delayMs: 500 }, node);
76
93
  }
77
94
 
78
- /** 图标按钮:有图标用图标,没图标退回短文字,保证永远点得到。 */
79
- function IconButton({ iconName, label, disabled, onClick }) {
80
- const glyph = icon(iconName, 16);
95
+ /**
96
+ * 图标按钮:有图标用图标,没图标退回短文字,保证永远点得到。
97
+ *
98
+ * 退回文字时换成 `dsh-ttf-iconBtnText`:按钮按文字宽度自己撑开、文字长了
99
+ * 用省略号收住,所以并排的几个按钮不会挤成一团互相压。
100
+ */
101
+ function IconButton({ iconBase, label, size, disabled, onClick }) {
102
+ const glyph = icon(iconBase, typeof size === "number" ? size : 16);
81
103
  return withTooltip(
82
104
  label,
83
105
  h(
84
106
  "button",
85
107
  {
86
108
  type: "button",
87
- className: "dsh-ttf-iconBtn",
109
+ className: glyph === null ? "dsh-ttf-iconBtn dsh-ttf-iconBtnText" : "dsh-ttf-iconBtn",
88
110
  disabled: disabled === true,
89
111
  "aria-label": label,
90
112
  onClick,
@@ -307,16 +329,21 @@ window.__ModuleLoader__.load({
307
329
  '.dsh-ttf-panel:after{border:.5px solid var(--dsw-alias-border-l1);border-radius:inherit;content:"";pointer-events:none;position:absolute;inset:0}',
308
330
  ".dsh-ttf-bar{box-sizing:border-box;width:100%;height:36px;display:flex;align-items:center;gap:10px;padding:4px 5px 4px 12px}",
309
331
  ".dsh-ttf-glyph{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex;align-items:center}",
332
+ ".dsh-ttf-glyphFallback{font-size:13px;line-height:16px}",
310
333
  ".dsh-ttf-label{color:var(--dsw-alias-label-primary);flex:none;font-size:13px;font-weight:500;line-height:24px;white-space:nowrap}",
311
334
  ".dsh-ttf-labelHeld{color:var(--dsw-alias-state-warning-primary,#a06800)}",
312
335
  ".dsh-ttf-title{min-width:0;flex:1;color:var(--dsw-alias-label-primary-dimmed);font-size:13px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
313
336
  ".dsh-ttf-progress{flex:none;max-width:45%;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
314
337
  ".dsh-ttf-actions{flex:none;display:flex;align-items:center;gap:2px}",
315
- ".dsh-ttf-iconBtn{corner-shape:round;width:28px;height:28px;border:none;background:0 0;border-radius:999px;color:var(--dsw-alias-label-tertiary);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0;font:inherit}",
338
+ ".dsh-ttf-iconBtn{corner-shape:round;width:28px;height:28px;border:none;background:0 0;border-radius:999px;color:var(--dsw-alias-label-tertiary);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0;font:inherit;flex:none}",
316
339
  ".dsh-ttf-iconBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
317
340
  ".dsh-ttf-iconBtn:disabled{opacity:.4;cursor:default}",
318
341
  ".dsh-ttf-iconBtn:disabled:hover{background:0 0;color:var(--dsw-alias-label-tertiary)}",
319
- ".dsh-ttf-iconText{font-size:12px;line-height:16px;padding:0 4px;white-space:nowrap}",
342
+ // 取不到内置图标时退回文字:固定 28px 宽装不下几个字,文字会溢出到相邻
343
+ // 按钮上,看着就是几个按钮叠在一起。这里按文字宽度撑开、留出边距,
344
+ // 并给一个上限,长了用省略号收住——几个按钮始终各占各的位置。
345
+ ".dsh-ttf-iconBtnText{width:auto;min-width:28px;max-width:112px;height:24px;padding:0 8px}",
346
+ ".dsh-ttf-iconText{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;font-size:12px;line-height:16px;white-space:nowrap}",
320
347
  // 展开体:限高滚动——这是输入框旁边的位置,不能让它把会话挤没了。
321
348
  ".dsh-ttf-body{max-height:264px;overflow-y:auto;padding:2px 8px 8px}",
322
349
  ".dsh-ttf-note{margin:4px 4px 6px;padding:6px 8px;border-radius:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform)}",
@@ -388,11 +415,11 @@ window.__ModuleLoader__.load({
388
415
  */
389
416
  function StatusMark({ node, isCurrent }) {
390
417
  if (node.status === "done") {
391
- const check = icon("IconCheckOutline14", 14);
418
+ const check = icon("IconCheckOutline", 14);
392
419
  return h("span", { className: "dsh-ttf-markDone" }, check === null ? "✓" : check);
393
420
  }
394
421
  if (node.status === "dropped") {
395
- const close = icon("IconCloseOutline16", 14);
422
+ const close = icon("IconCloseOutline", 14);
396
423
  return h("span", { className: "dsh-ttf-markMuted" }, close === null ? "×" : close);
397
424
  }
398
425
  const circle = { cx: 7, cy: 7, r: 5.2, stroke: "currentColor", strokeWidth: 1.2 };
@@ -536,7 +563,7 @@ window.__ModuleLoader__.load({
536
563
  { className: "dsh-ttf-rowActions" },
537
564
  canAct
538
565
  ? h(IconButton, {
539
- iconName: "IconCheckOutline16",
566
+ iconBase: "IconCheckOutline",
540
567
  label: t("action.done"),
541
568
  disabled: busy,
542
569
  onClick: () => {
@@ -547,7 +574,7 @@ window.__ModuleLoader__.load({
547
574
  : null,
548
575
  canAct && canDrop
549
576
  ? h(IconButton, {
550
- iconName: "IconTrashOutline16",
577
+ iconBase: "IconTrashOutline",
551
578
  label: t("action.drop"),
552
579
  disabled: busy,
553
580
  onClick: () => {
@@ -559,7 +586,7 @@ window.__ModuleLoader__.load({
559
586
  : null,
560
587
  expandable
561
588
  ? h(IconButton, {
562
- iconName: open ? "IconChevronDownOutline14" : "IconChevronRightOutline14",
589
+ iconBase: open ? "IconChevronDownOutline" : "IconChevronRightOutline",
563
590
  label: open ? t("action.collapse") : t("action.expand"),
564
591
  onClick: () => setOpen(!open),
565
592
  })
@@ -718,6 +745,10 @@ window.__ModuleLoader__.load({
718
745
  : data.active === null
719
746
  ? t("state.done")
720
747
  : t("state.running");
748
+ // 条首的清单图标;外壳连这个图标都没有时退回一个同类字符,别留空白。
749
+ const listIcon = icon("IconChecklistOutline", 14);
750
+ const listGlyph =
751
+ listIcon === null ? h("span", { className: "dsh-ttf-glyphFallback" }, "☰") : listIcon;
721
752
 
722
753
  return h(
723
754
  "div",
@@ -728,7 +759,7 @@ window.__ModuleLoader__.load({
728
759
  h(
729
760
  "div",
730
761
  { className: "dsh-ttf-bar" },
731
- h("span", { className: "dsh-ttf-glyph", "aria-hidden": true }, icon("IconChecklistOutline14", 14)),
762
+ h("span", { className: "dsh-ttf-glyph", "aria-hidden": true }, listGlyph),
732
763
  h("span", { className: "dsh-ttf-label" + (held ? " dsh-ttf-labelHeld" : "") }, stateLabel),
733
764
  h("span", { className: "dsh-ttf-title", title: heading }, heading),
734
765
  h(
@@ -740,13 +771,13 @@ window.__ModuleLoader__.load({
740
771
  "div",
741
772
  { className: "dsh-ttf-actions" },
742
773
  h(IconButton, {
743
- iconName: held ? "IconPlayOutline16" : "IconPauseOutline16",
774
+ iconBase: held ? "IconPlayOutline" : "IconPauseOutline",
744
775
  label: held ? t("action.resume") : t("action.pause"),
745
776
  disabled: busy,
746
777
  onClick: () => act({ action: held ? "resume" : "pause" }),
747
778
  }),
748
779
  h(IconButton, {
749
- iconName: "IconTrashOutline16",
780
+ iconBase: "IconTrashOutline",
750
781
  label: t("action.reset"),
751
782
  disabled: busy,
752
783
  onClick: () => {
@@ -754,7 +785,7 @@ window.__ModuleLoader__.load({
754
785
  },
755
786
  }),
756
787
  h(IconButton, {
757
- iconName: open ? "IconChevronDownOutline14" : "IconChevronUpOutline14",
788
+ iconBase: open ? "IconChevronDownOutline" : "IconChevronUpOutline",
758
789
  label: open ? t("action.collapse") : t("action.expand"),
759
790
  onClick: () => setOpen(!open),
760
791
  }),
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import { activePath, renderContinue } from "./plan.js";
19
+ import { pluginSource } from "./region.js";
19
20
 
20
21
  /**
21
22
  * 注册续行兜底。
@@ -57,7 +58,8 @@ export function registerAutoContinue(ctx, { store, cfg }) {
57
58
  // 必须按来源区分,不能按"这一步有没有消息":`agent.followup()` 推的消息同样会
58
59
  // 从 inbox 进入下一步的 `payload.messages`,那样每一轮都会把自己刚用掉的配额
59
60
  // 重置回去,`maxAutoRounds` 也就失去了意义。
60
- // `source.kind === 'user'` 才是真人,插件推的是 `'plugin'`。
61
+ // `source.kind === 'user'` 才是真人,插件推的是 `plugin:dsh-tree-task-flow`
62
+ // (v3 老会话里是 `{ kind: 'plugin', plugin: 'dsh-tree-task-flow' }`;两种都不是 `'user'`)。
61
63
  //
62
64
  // 注意这是 waterfall:必须 await next() 并把它返回,否则会截断监听器链。
63
65
  ctx.on("agent/pre-step", async (payload, next) => {
@@ -106,7 +108,7 @@ export function registerAutoContinue(ctx, { store, cfg }) {
106
108
  id: `tt-continue-${sessionId}-${Date.now().toString(36)}`,
107
109
  role: "user",
108
110
  content: [{ type: "text", text: renderContinue(plan) }],
109
- source: { kind: "plugin", plugin: "dsh-tree-task-flow", form: "notice", summary: "计划续行" },
111
+ source: pluginSource("计划续行"),
110
112
  });
111
113
  ctx.logger?.info?.(
112
114
  `dsh-tree-task-flow: 自动续行 ${used + 1}/${cfg.maxAutoRounds}(步骤 ${active.step.id})`,
package/lib/compaction.js CHANGED
@@ -10,8 +10,15 @@
10
10
  * 又明确要求"不要提及这次压缩",模型不会自己想到去查。
11
11
  *
12
12
  * 怎么认:压缩留下的表层节点是一条 `user/message`,它的 `source` 由
13
- * `compactCheckpointSource()` 构造,标记固定为 `plugin: "compact"`
14
- * (见 `@deepseek-ai/dsh-compaction` 的 `isCompactCheckpointSource`)。
13
+ * `compactCheckpointSource()` 构造。**两个形状都要认**:
14
+ *
15
+ * - v4 原生:`{ kind: "compact-checkpoint", compactionId, … }`
16
+ * (`COMPACT_CHECKPOINT_MARKER` 就是 `{ kind: "compact-checkpoint" }`);
17
+ * - v3 老写法:`{ kind: "plugin", plugin: "compact" }`——它只在 v3 → v4 迁移里
18
+ * 被改写成前者,迁移前的会话里仍是这个样子。
19
+ *
20
+ * 只认老写法,会让**已经迁到 v4 的会话彻底收不到压缩提示**(实测:本机
21
+ * session-2083c383 在 v4 里连续压缩两次,一条提示都没有)。
15
22
  *
16
23
  * **不要认 `compaction/summary` 事件**:那是压缩自己的记录,不带 `surfaceOp`,
17
24
  * 因此根本不在表层里;真正替代被收走那段的是紧随其后那条带
@@ -35,14 +42,21 @@
35
42
  import { summaryMessage } from "./region.js";
36
43
 
37
44
  /**
38
- * 压缩检查点的插件标记。
45
+ * 压缩检查点的插件标记——**只有** v3 老写法用到它。
39
46
  *
40
- * 与 `@deepseek-ai/dsh-compaction` 的 `COMPACT_CHECKPOINT_MARKER` 保持一致的
41
- * 字面量。这里按形状判断而不是 import 那个包:插件不该为了认一个标记,
42
- * 就把一个 DSH 内部包写进自己的依赖里。
47
+ * 与 `@deepseek-ai/dsh-compaction` 的 V3 侧字面量保持一致。这里按形状判断而不是
48
+ * import 那个包:插件不该为了认一个标记,就把一个 DSH 内部包写进自己的依赖里。
43
49
  */
44
50
  const COMPACTION_PLUGIN = "compact";
45
51
 
52
+ /**
53
+ * v4 原生压缩检查点的 source kind。
54
+ *
55
+ * 与 `@deepseek-ai/dsh-compaction` 的 `COMPACT_CHECKPOINT_MARKER` 一致
56
+ * (`{ kind: "compact-checkpoint" }`)。
57
+ */
58
+ const COMPACTION_KIND = "compact-checkpoint";
59
+
46
60
  /**
47
61
  * 提示正文。
48
62
  *
@@ -64,11 +78,13 @@ function noticeId() {
64
78
  return `tt-compaction-${Date.now().toString(36)}-${noticeSeq.toString(36)}`;
65
79
  }
66
80
 
67
- /** 这条表层节点是不是压缩落下的检查点。 */
81
+ /** 这条表层节点是不是压缩落下的检查点(v4 新写法与 v3 老写法都认)。 */
68
82
  function isCompactionCheckpoint(event) {
69
83
  if (event?.type !== "user/message") return false;
70
84
  const source = event.data?.source;
71
- return source?.kind === "plugin" && source.plugin === COMPACTION_PLUGIN;
85
+ if (source === undefined || source === null) return false;
86
+ if (source.kind === COMPACTION_KIND) return true;
87
+ return source.kind === "plugin" && source.plugin === COMPACTION_PLUGIN;
72
88
  }
73
89
 
74
90
  /**
package/lib/pause.js CHANGED
@@ -39,9 +39,7 @@
39
39
  */
40
40
 
41
41
  import { activePath, renderContinue } from "./plan.js";
42
-
43
- /** 本插件放下的消息都带这个身份。 */
44
- const PLUGIN = "dsh-tree-task-flow";
42
+ import { pluginSource } from "./region.js";
45
43
 
46
44
  /**
47
45
  * 注册暂停闸门。
@@ -152,7 +150,7 @@ export function registerPause(ctx, { store = null, autoContinue = null } = {}) {
152
150
  id: `tt-resume-${sessionId}-${Date.now().toString(36)}`,
153
151
  role: "user",
154
152
  content: [{ type: "text", text: renderContinue(plan) }],
155
- source: { kind: "plugin", plugin: PLUGIN, form: "notice", summary: "恢复续行" },
153
+ source: pluginSource("恢复续行"),
156
154
  });
157
155
  } catch (error) {
158
156
  return {
package/lib/region.js CHANGED
@@ -42,7 +42,55 @@
42
42
  import { activePath } from "./plan.js";
43
43
 
44
44
  /** 本插件放下的消息都带这个身份。 */
45
- const PLUGIN = "dsh-tree-task-flow";
45
+ export const PLUGIN = "dsh-tree-task-flow";
46
+
47
+ /**
48
+ * 本插件消息的 source kind。
49
+ *
50
+ * DSH 的会话格式 v4 要求消息来源带**生产者自有的** kind:老写法
51
+ * `{ kind: "plugin", plugin: "包名" }` 只在 v3 → v4 迁移那一刻被改写成
52
+ * `plugin:<包名>`(第三方插件一律加前缀,见 `dsh-session-format-v3-to-v4`
53
+ * 的转换表)。**已经是 v4 的会话不再经过迁移**,写入时直接按 v4 校验,
54
+ * 于是老写法被拒:
55
+ *
56
+ * format v4 message requires a producer-owned source kind
57
+ *
58
+ * 这条错误来自 DSH 自己的编码路径(`releasedV4SessionFormatCodec.encodeEvent`
59
+ * → `assertV4RowAdmission` → `assertV4SourceRowAdmission`),位置在会话落盘那一段,
60
+ * 插件自己的 try/catch 拦不住。所以从这里起**直接写迁移后的那个 kind**——
61
+ * 它在 v4 里合法,在 v3 里是"非空且非 plugin 的直接来源",原样保留、同样合法。
62
+ */
63
+ export const PLUGIN_SOURCE_KIND = `plugin:${PLUGIN}`;
64
+
65
+ /**
66
+ * 本插件一条 notice 消息的 source。
67
+ *
68
+ * 字段与老写法保持一致:`form: "notice"` 让界面渲染成可展开的一行说明,
69
+ * `summary` 是那一行的文字。
70
+ *
71
+ * @param summary - 界面上显示的一行摘要。
72
+ * @returns 消息 source。
73
+ */
74
+ export function pluginSource(summary) {
75
+ return { kind: PLUGIN_SOURCE_KIND, form: "notice", summary };
76
+ }
77
+
78
+ /**
79
+ * 这条 source 是不是本插件留下的。
80
+ *
81
+ * **两种写法都认**:v4 起写 `plugin:dsh-tree-task-flow`,而旧版留在 v3 会话里、
82
+ * 或压缩折叠区间里带着的老消息仍是 `{ kind: "plugin", plugin: "dsh-tree-task-flow" }`。
83
+ * 只认新写法会让老会话里的历史通告被当成普通过程,只认老写法会让新会话里的通告
84
+ * 被反复折叠。
85
+ *
86
+ * @param source - 消息的 source。
87
+ * @returns 是否本插件所留。
88
+ */
89
+ export function isPluginSource(source) {
90
+ if (source === undefined || source === null) return false;
91
+ if (source.kind === PLUGIN_SOURCE_KIND) return true;
92
+ return source.kind === "plugin" && source.plugin === PLUGIN;
93
+ }
46
94
 
47
95
  /** 三层固定的名字。只给界面用的游标同步按这个顺序走。 */
48
96
  const LEVELS = ["goal", "task", "step"];
@@ -100,7 +148,7 @@ function isHumanMessage(event) {
100
148
 
101
149
  /** 这条消息是不是本插件自己落下的折叠通告。 */
102
150
  function isPluginNotice(event) {
103
- return event?.type === "user/message" && event.data?.source?.plugin === PLUGIN;
151
+ return event?.type === "user/message" && isPluginSource(event.data?.source);
104
152
  }
105
153
 
106
154
  /** 从一次工具调用的参数里取它指向的文件路径;不是文件类调用就返回 null。 */
@@ -149,18 +197,16 @@ export function touchedFiles(session, shadowed) {
149
197
  * 形态是 `notice` + `summary`:界面把它渲染成一行可读的说明,展开才是正文。
150
198
  * 刻意**不用** `snapshot`——那个形态是"运行时上下文快照"的专属标签,
151
199
  * 借用它会让任务汇总在界面上冒充系统提示词/运行环境快照。
200
+ *
201
+ * source 走 {@link pluginSource}:**必须是生产者自有的 kind**,否则 v4 会话
202
+ * 会拒绝这条消息(见 `PLUGIN_SOURCE_KIND` 的说明)。
152
203
  */
153
204
  export function summaryMessage(id, text, summary) {
154
205
  return {
155
206
  id,
156
207
  role: "user",
157
208
  content: [{ type: "text", text }],
158
- source: {
159
- kind: "plugin",
160
- plugin: PLUGIN,
161
- form: "notice",
162
- summary,
163
- },
209
+ source: pluginSource(summary),
164
210
  };
165
211
  }
166
212
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tree-task-flow",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "树形任务流:把多步任务组织成目标→任务→子任务三级树,节点完成时由 AI 提交结果,插件随即把该节点的执行过程从上下文里折叠掉,只留下结果。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",