dsh-tree-task-flow 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dsh-tree-task-flow 的客户端:输入框上方的树形任务流条目 + 设置页里的插件开关说明。
2
+ * dsh-tree-task-flow 的客户端:输入框上方的树形任务流条目。
3
3
  *
4
4
  * 这是**手写**的模块包装格式(和 dsh-purge 的 client.js 一样),不经过打包,
5
5
  * 所以用 `h()` 而不是 JSX。
@@ -8,16 +8,23 @@
8
8
  * (`todo`)与目标(`goal`)就在那里。这是一个 `list` 槽:**用自己的 id 注册就
9
9
  * 会并列加在它们旁边**,不会替换任何一个;用内置的 id 才会顶掉它。
10
10
  *
11
+ * 外观照内置条目来:收起时是目标条(GoalBar)那样的 36px 单行条,展开后是待办
12
+ * 面板(TodoPanel)那样的卡片。图标与 Tooltip 直接用外壳播种的 UI 基座
13
+ * `@deepseek-ai/dsh-client-ui-primitives`,尺寸与颜色变量也都取它那一套,
14
+ * 这样并排放在输入框上方时看起来是一家。
15
+ *
11
16
  * 没有计划的会话**一个像素都不占**:读不到计划就返回 null,免得平白挤掉内置条目。
12
17
  *
13
- * 数据全部来自 host 端的 `/dsh-tree-task-flow/*` 接口;条目本身不持有任何状态,
14
- * 动作(完成 / 丢弃 / 停止 / 恢复 / 重置)都是 POST 回 host,由 host 改真正的文件。
18
+ * 数据全部来自 host 端的 `/dsh-tree-task-flow/*` 接口;动作(暂停 / 继续 / 完成 /
19
+ * 丢弃 / 清空)都是 POST 回 host,由 host 改真正的文件和 agent 状态。
15
20
  *
16
- * 面板只展示两样东西:**每个节点提交的结果**,以及**现在是不是停在检查点**。
21
+ * 「暂停」不是只停掉自动续行,而是**真的把会话按住**:host 侧在模型发起下一次
22
+ * 请求之前等一个闸门,所以当前正在执行的工具会正常跑完,之后什么都不干;点
23
+ * 「继续」就放行,模型从原处接着跑,整个过程不往会话里插任何消息。
17
24
  *
18
- * 「完成」必须带结果,所以它不是一步按钮:先在节点下展开一个输入框,
19
- * 写清产出了什么,再提交。结果一旦提交,这个节点整段执行过程就会被它顶替,
20
- * 所以输入框的提示语要把这一点讲明白。
25
+ * 「完成」必须带结果,所以它不是一步按钮:先在节点下展开一个输入框,写清产出了
26
+ * 什么,再提交。结果一旦提交,这个节点整段执行过程就会被它顶替,所以输入框的
27
+ * 提示语要把这一点讲明白。
21
28
  */
22
29
  window.__ModuleLoader__.load({
23
30
  id: "dsh-tree-task-flow",
@@ -39,6 +46,54 @@ window.__ModuleLoader__.load({
39
46
  let translate = (key) => key;
40
47
  const useT = () => translate;
41
48
 
49
+ // ------------------------------------------------------------ 内置基座
50
+
51
+ /**
52
+ * 内置 UI 基座。外壳启动时把这张表播种进模块表(react、cordis、store、
53
+ * slots、primitives、dockkit),所以这里的 require 正常一定成功;
54
+ * 仍然兜一层 try——万一这版外壳没有 primitives,条目该降级成文字,
55
+ * 而不是整块白掉。
56
+ */
57
+ let primitives = {};
58
+ try {
59
+ primitives = require("@deepseek-ai/dsh-client-ui-primitives") || {};
60
+ } catch {
61
+ primitives = {};
62
+ }
63
+
64
+ /** 取一个内置图标;这版外壳没有就返回 null,由调用方决定怎么退。 */
65
+ function icon(iconName, size) {
66
+ const component = primitives[iconName];
67
+ if (typeof component !== "function") return null;
68
+ return h(component, { size });
69
+ }
70
+
71
+ /** 用内置 Tooltip 包一层;没有 Tooltip 就原样返回。 */
72
+ function withTooltip(label, node) {
73
+ const Tooltip = primitives.Tooltip;
74
+ if (typeof Tooltip !== "function") return node;
75
+ return h(Tooltip, { label, side: "bottom", delayMs: 500 }, node);
76
+ }
77
+
78
+ /** 图标按钮:有图标用图标,没图标退回短文字,保证永远点得到。 */
79
+ function IconButton({ iconName, label, disabled, onClick }) {
80
+ const glyph = icon(iconName, 16);
81
+ return withTooltip(
82
+ label,
83
+ h(
84
+ "button",
85
+ {
86
+ type: "button",
87
+ className: "dsh-ttf-iconBtn",
88
+ disabled: disabled === true,
89
+ "aria-label": label,
90
+ onClick,
91
+ },
92
+ glyph === null ? h("span", { className: "dsh-ttf-iconText" }, label) : glyph,
93
+ ),
94
+ );
95
+ }
96
+
42
97
  // ---------------------------------------------------------------- 基础
43
98
 
44
99
  function urlOf(path) {
@@ -86,12 +141,6 @@ window.__ModuleLoader__.load({
86
141
  return data;
87
142
  }
88
143
 
89
- /** 取首行并截断。树上只报"产出了什么",全文展开看。 */
90
- function firstLine(text, limit) {
91
- const line = String(text ?? "").split("\n")[0].trim();
92
- return line.length > limit ? line.slice(0, limit) + "…" : line;
93
- }
94
-
95
144
  function formatTime(at) {
96
145
  const n = Number(at);
97
146
  if (!n) return "";
@@ -102,13 +151,7 @@ window.__ModuleLoader__.load({
102
151
  }
103
152
  }
104
153
 
105
- function mark(node) {
106
- if (node.status === "done") return "[x]";
107
- if (node.status === "dropped") return "[-]";
108
- return "[ ]";
109
- }
110
-
111
- /** 三层节点总数与已完成数,给折叠时的摘要用。 */
154
+ /** 三层节点总数与已完成数,给单行条上的进度用。 */
112
155
  function countNodes(goal) {
113
156
  let total = 1;
114
157
  let done = goal.status === "done" ? 1 : 0;
@@ -134,240 +177,216 @@ window.__ModuleLoader__.load({
134
177
  return step ? step.title : task.title;
135
178
  }
136
179
 
180
+ /** 活动路径上三层各自的 id。节点 id 全局唯一,所以一个集合就够判"是不是当前节点"。 */
181
+ function currentIds(data) {
182
+ const active = data.active;
183
+ return new Set(
184
+ [active?.goalId, active?.taskId, active?.stepId].filter(
185
+ (each) => typeof each === "string" && each !== "",
186
+ ),
187
+ );
188
+ }
189
+
137
190
  // ---------------------------------------------------------------- 文案
138
191
 
139
192
  const zh = {
140
193
  nav: "树形任务流",
141
- "empty.title": "这个会话还没有计划",
142
- "empty.hint": "让模型调用 tree_task_create 建立目标与任务,这里就会显示整棵树。",
143
194
  "state.running": "进行中",
144
- "state.stopped": "自动续行已停止",
195
+ "state.paused": "已暂停",
145
196
  "state.done": "已全部完成",
146
197
  "state.checkpoint": "检查点",
147
198
  "checkpoint.note": "「{title}」的子节点已全部结束。要么继续给它拆子节点,要么提交结果并完成它。",
148
- "btn.stop": "停止自动续行",
149
- "btn.resume": "恢复自动续行",
150
- "btn.reset": "清空计划",
151
- "btn.refresh": "刷新",
152
- "btn.done": "完成…",
153
- "btn.drop": "丢弃",
154
- "btn.submit": "提交结果",
155
- "btn.cancel": "取消",
199
+ "note.paused": "已暂停:正在跑的工具会跑完,之后不再往下走。点「继续」从原处接着跑,不会往会话里插消息。",
200
+ "action.pause": "暂停",
201
+ "action.resume": "继续",
202
+ "action.reset": "清空计划",
203
+ "action.expand": "展开",
204
+ "action.collapse": "收起",
205
+ "action.done": "完成…",
206
+ "action.drop": "丢弃",
207
+ "action.submit": "提交结果",
208
+ "action.expandResult": "展开看全文",
209
+ "action.collapseResult": "收起",
210
+ "action.cancel": "取消",
156
211
  "label.result": "结果",
157
212
  "label.noResult": "(还没提交结果)",
158
213
  "label.current": "当前",
159
214
  "label.checkpointHere": "检查点",
160
215
  "placeholder.result":
161
216
  "写清这个节点产出了什么。它整段执行过程会被这条结果顶替,所以要能独立看懂。",
217
+ "hint.noChildren": "(还没有子节点 → 让模型调用 tree_task_plan)",
162
218
  "confirm.reset": "确定要清空这个会话的计划树吗?",
163
219
  "confirm.drop": "确定要丢弃「{title}」吗?",
164
- "settings.title": "树形任务流",
165
- "settings.enabled": "已启用",
166
- "settings.disabled": "未启用",
167
- "settings.impact": "启用后它会做什么",
168
- "settings.impact.1": "在每个会话的系统提示词里加一段固定的工具组说明(会话内逐字节不变)。",
169
- "settings.impact.2": "注册 6 个 tree_task_* 工具。",
170
- "settings.impact.3":
171
- "一个节点完成时,把它那一段执行过程从模型可见的上下文里折叠掉,换成一条携带提交结果的汇总消息。",
172
- "settings.impact.4":
173
- "一个节点的子节点全部结束时转入检查点:那一刻只放行 tree_task_plan 与 tree_task_done。",
174
- "settings.impact.5":
175
- "可选:模型停下来时自动续行(默认关闭;你按下停止后不会再被拉起来)。",
176
- "settings.howto": "怎么开 / 怎么关",
177
- "settings.howto.body":
178
- "改 profile 的 cordis.patch.yml 里 task-tree 那一行的 config.enabled,然后重启 dsh web。装上不等于启用——默认是关的。",
179
- "settings.stateDir": "状态目录",
180
- "settings.sessions": "有计划的会话",
181
- "settings.noSessions": "(还没有)",
182
- "settings.checkpoint": "检查点",
183
220
  };
184
221
 
185
222
  const en = {
186
223
  nav: "Tree Task Flow",
187
- "empty.title": "No plan in this session yet",
188
- "empty.hint": "Ask the model to call tree_task_create to build a goal and tasks.",
189
224
  "state.running": "In progress",
190
- "state.stopped": "Auto-continue stopped",
225
+ "state.paused": "Paused",
191
226
  "state.done": "All done",
192
227
  "state.checkpoint": "Checkpoint",
193
228
  "checkpoint.note":
194
229
  "Every child of “{title}” has finished. Either break out more children, or submit its result and complete it.",
195
- "btn.stop": "Stop auto-continue",
196
- "btn.resume": "Resume auto-continue",
197
- "btn.reset": "Clear plan",
198
- "btn.refresh": "Refresh",
199
- "btn.done": "Complete…",
200
- "btn.drop": "Drop",
201
- "btn.submit": "Submit result",
202
- "btn.cancel": "Cancel",
230
+ "note.paused":
231
+ "Paused: the running tool finishes, then nothing else happens. Resume picks up exactly where it stopped, without inserting any message.",
232
+ "action.pause": "Pause",
233
+ "action.resume": "Resume",
234
+ "action.reset": "Clear plan",
235
+ "action.expand": "Expand",
236
+ "action.collapse": "Collapse",
237
+ "action.done": "Complete…",
238
+ "action.drop": "Drop",
239
+ "action.submit": "Submit result",
240
+ "action.expandResult": "Show the full result",
241
+ "action.collapseResult": "Collapse",
242
+ "action.cancel": "Cancel",
203
243
  "label.result": "Result",
204
244
  "label.noResult": "(no result submitted yet)",
205
245
  "label.current": "current",
206
246
  "label.checkpointHere": "checkpoint",
207
247
  "placeholder.result":
208
248
  "State what this node produced. Its whole execution range is replaced by this result, so it must stand on its own.",
249
+ "hint.noChildren": "(no children yet → ask the model to call tree_task_plan)",
209
250
  "confirm.reset": "Clear this session's plan tree?",
210
251
  "confirm.drop": "Drop “{title}”?",
211
- "settings.title": "Tree Task Flow",
212
- "settings.enabled": "Enabled",
213
- "settings.disabled": "Not enabled",
214
- "settings.impact": "What it does once enabled",
215
- "settings.impact.1": "Adds one static tool-group section to every session's system prompt.",
216
- "settings.impact.2": "Registers six tree_task_* tools.",
217
- "settings.impact.3":
218
- "When a node completes, folds its execution range out of the model-visible context and replaces it with one summary message carrying the submitted result.",
219
- "settings.impact.4":
220
- "When every child of a node finishes, the tree enters a checkpoint: only tree_task_plan and tree_task_done are allowed there.",
221
- "settings.impact.5":
222
- "Optional auto-continue when the model goes idle (off by default; never resumes after you stop it).",
223
- "settings.howto": "How to enable / disable",
224
- "settings.howto.body":
225
- "Set config.enabled on the task-tree row in the profile's cordis.patch.yml, then restart dsh web. Installing is not enabling — it is off by default.",
226
- "settings.stateDir": "State directory",
227
- "settings.sessions": "Sessions with a plan",
228
- "settings.noSessions": "(none yet)",
229
- "settings.checkpoint": "checkpoint",
230
252
  };
231
253
 
232
254
  // ---------------------------------------------------------------- 样式
233
255
 
234
- const S = {
235
- wrap: { padding: "10px 12px", font: "13px/1.6 system-ui, sans-serif", overflow: "auto", height: "100%" },
236
- // 输入框上方的条目:和内置 todo / goal 条目共用同一片区域。
237
- // 宽度必须照它们的算法来——直接写 100% 会顶满整栏,和消息正文对不齐。
238
- dock: {
239
- boxSizing: "border-box",
240
- width:
241
- "calc(100% - var(--dsh-composer-side-clearance) - var(--dsh-composer-side-clearance)" +
242
- " - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset)" +
243
- " - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset))",
244
- maxWidth:
245
- "calc(var(--dsh-composer-card-max-width)" +
246
- " - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset)" +
247
- " - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset))",
248
- border: "0.5px solid var(--dsw-alias-border-l1, #00000018)",
249
- background: "var(--dsw-specific-tip, #00000008)",
250
- borderRadius: "12px",
251
- flex: "none",
252
- margin: "0 auto",
253
- overflow: "hidden",
254
- "--dsh-scrollbar-thumb": "var(--dsw-alias-scrollbar-bg-l2)",
255
- "--dsh-scrollbar-thumb-hover": "var(--dsw-alias-scrollbar-hover-l2)",
256
- },
257
- dockHead: { display: "flex", alignItems: "center", gap: "8px", padding: "0 12px" },
258
- dockToggle: {
259
- display: "flex",
260
- alignItems: "center",
261
- gap: "8px",
262
- flex: "auto",
263
- minWidth: 0,
264
- background: "none",
265
- border: "none",
266
- color: "inherit",
267
- cursor: "pointer",
268
- padding: "6px 0",
269
- font: "inherit",
270
- textAlign: "left",
271
- },
272
- dockTitle: { fontWeight: 500, flex: "none", fontSize: "13px" },
273
- dockProgress: {
274
- color: "var(--dsw-alias-label-tertiary, #00000088)",
275
- fontSize: "13px",
276
- flex: "auto",
277
- minWidth: 0,
278
- overflow: "hidden",
279
- textOverflow: "ellipsis",
280
- whiteSpace: "nowrap",
281
- },
282
- dockChevron: { color: "var(--dsw-alias-label-tertiary, #00000088)", flex: "none" },
283
- dockBody: { padding: "0 12px 8px", maxHeight: "260px", overflow: "auto" },
284
- bar: { display: "flex", gap: "6px", flexWrap: "wrap", alignItems: "center" },
285
- btn: {
286
- border: "1px solid var(--dsh-border, #d0d0d0)",
287
- background: "transparent",
288
- color: "inherit",
289
- borderRadius: "6px",
290
- padding: "3px 8px",
291
- cursor: "pointer",
292
- font: "inherit",
293
- },
294
- badge: { borderRadius: "6px", padding: "2px 6px", background: "var(--dsh-muted, #00000010)" },
295
- badgeWarn: {
296
- borderRadius: "6px",
297
- padding: "2px 6px",
298
- background: "var(--dsh-warn, #ffcc0033)",
299
- border: "1px solid var(--dsh-warn-border, #cc990055)",
300
- },
301
- warn: {
302
- marginTop: "6px",
303
- padding: "6px 8px",
304
- borderRadius: "6px",
305
- fontSize: "13px",
306
- background: "var(--dsh-warn, #ffcc0033)",
307
- },
308
- node: { marginLeft: "0", padding: "3px 0" },
309
- child: { marginLeft: "14px", borderLeft: "1px solid var(--dsh-border, #00000018)", paddingLeft: "8px" },
310
- stepTitle: { display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" },
311
- done: { opacity: 0.55 },
312
- cur: { fontWeight: 600 },
313
- meta: { opacity: 0.7, font: "12px/1.5 ui-monospace, monospace", wordBreak: "break-all" },
314
- resultLine: { opacity: 0.75, marginTop: "1px" },
315
- hint: { opacity: 0.65, marginTop: "6px" },
316
- err: { color: "var(--dsh-danger, #c00)", whiteSpace: "pre-wrap" },
317
- pre: {
318
- margin: "2px 0 0",
319
- padding: "6px 8px",
320
- background: "var(--dsh-muted, #00000010)",
321
- borderRadius: "6px",
322
- whiteSpace: "pre-wrap",
323
- wordBreak: "break-word",
324
- font: "12px/1.5 ui-monospace, monospace",
325
- maxHeight: "220px",
326
- overflow: "auto",
327
- },
328
- form: { marginTop: "6px", border: "1px solid var(--dsh-border, #d0d0d0)", borderRadius: "6px", padding: "6px" },
329
- textarea: {
330
- width: "100%",
331
- boxSizing: "border-box",
332
- minHeight: "64px",
333
- font: "12px/1.5 ui-monospace, monospace",
334
- background: "transparent",
335
- color: "inherit",
336
- border: "1px solid var(--dsh-border, #d0d0d0)",
337
- borderRadius: "4px",
338
- padding: "4px",
339
- resize: "vertical",
340
- },
341
- };
256
+ /**
257
+ * 样式照内置的 dock 条目写:宽高、间距、圆角、颜色变量都取
258
+ * `dsh-client-ui-goal` 的 GoalBar 与内置待办面板 TodoPanel 的同一套值。
259
+ *
260
+ * 注入方式也照它们:一个带 `data-plugin-css` 的 <style> 标签——
261
+ * 客户端 HMR 就是靠这个属性在插件卸载时收走样式的。
262
+ */
263
+ const CSS_TAG = "dsh-tree-task-flow/Dock.module.css";
264
+
265
+ const CSS = [
266
+ // 条目外壳:宽度算法与内置 goal / todo 条目完全一致,否则会和消息正文对不齐。
267
+ ".dsh-ttf-dock{box-sizing:border-box;width:calc(100% - var(--dsh-composer-side-clearance) - var(--dsh-composer-side-clearance) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset));max-width:calc(var(--dsh-composer-card-max-width) - 4 * var(--dsh-composer-dock-inset));margin:0 auto;flex:none;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}",
268
+ // 卡片:背景与圆角同内置条目,描边用 ::after 画(.5px 在缩放屏上才不糊)。
269
+ ".dsh-ttf-panel{background:var(--dsw-specific-tip);border-radius:12px;position:relative;overflow:hidden}",
270
+ '.dsh-ttf-panel:after{border:.5px solid var(--dsw-alias-border-l1);border-radius:inherit;content:"";pointer-events:none;position:absolute;inset:0}',
271
+ ".dsh-ttf-bar{box-sizing:border-box;width:100%;height:36px;display:flex;align-items:center;gap:10px;padding:4px 5px 4px 12px}",
272
+ ".dsh-ttf-glyph{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex;align-items:center}",
273
+ ".dsh-ttf-label{color:var(--dsw-alias-label-primary);flex:none;font-size:13px;font-weight:500;line-height:24px;white-space:nowrap}",
274
+ ".dsh-ttf-labelHeld{color:var(--dsw-alias-state-warning-primary,#a06800)}",
275
+ ".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}",
276
+ ".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}",
277
+ ".dsh-ttf-actions{flex:none;display:flex;align-items:center;gap:2px}",
278
+ ".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}",
279
+ ".dsh-ttf-iconBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
280
+ ".dsh-ttf-iconBtn:disabled{opacity:.4;cursor:default}",
281
+ ".dsh-ttf-iconBtn:disabled:hover{background:0 0;color:var(--dsw-alias-label-tertiary)}",
282
+ ".dsh-ttf-iconText{font-size:12px;line-height:16px;padding:0 4px;white-space:nowrap}",
283
+ // 展开体:限高滚动——这是输入框旁边的位置,不能让它把会话挤没了。
284
+ ".dsh-ttf-body{max-height:264px;overflow-y:auto;padding:2px 8px 8px}",
285
+ ".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)}",
286
+ // 节点行:行高与 hover 背景照内置待办面板。
287
+ // 一个节点。行本身跟着标题长,标记与按钮对齐第一行。
288
+ ".dsh-ttf-node{position:relative}",
289
+ // 同一层的相邻节点之间画一条分界线——任务与任务的分界就靠它。
290
+ ".dsh-ttf-node+.dsh-ttf-node{border-top:1px solid var(--dsw-alias-border-l1);margin-top:2px;padding-top:2px}",
291
+ ".dsh-ttf-row{position:relative;box-sizing:border-box;display:flex;align-items:flex-start;gap:8px;padding:6px 4px;border-radius:8px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}",
292
+ ".dsh-ttf-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
293
+ ".dsh-ttf-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
294
+ // 标题不缩略:有多长写多长,多了换行。
295
+ ".dsh-ttf-rowTitle{min-width:0;flex:1;white-space:pre-wrap;word-break:break-word}",
296
+ ".dsh-ttf-rowActions{flex:none;display:flex;align-items:center;gap:2px;margin-top:-4px}",
297
+ ".dsh-ttf-markCell{flex:none;display:inline-flex;align-items:center;margin-top:3px;color:var(--dsw-alias-label-tertiary)}",
298
+ ".dsh-ttf-markDone{flex:none;display:inline-flex;align-items:center;margin-top:3px;color:var(--dsw-alias-state-success-primary,#1a7f37)}",
299
+ ".dsh-ttf-markMuted{flex:none;display:inline-flex;align-items:center;margin-top:3px;color:var(--dsw-alias-label-caption,#00000066)}",
300
+ ".dsh-ttf-done{color:var(--dsw-alias-label-tertiary);text-decoration:line-through}",
301
+ // 三层缩进。
302
+ // 子节点:一条竖线 + 每个子节点一个拐角,把三层画成看得见的树。
303
+ ".dsh-ttf-depth1,.dsh-ttf-depth2{margin-left:11px;padding-left:9px;border-left:1px solid var(--dsw-alias-border-l2)}",
304
+ '.dsh-ttf-depth1>.dsh-ttf-row:before,.dsh-ttf-depth2>.dsh-ttf-row:before{content:"";position:absolute;left:-9px;top:16px;width:8px;height:1px;background:var(--dsw-alias-border-l2)}',
305
+ // 详情里的每一块(说明 / 结果 / 表单)自成一块,块间留白 + 描边,分得开。
306
+ ".dsh-ttf-detail>*+*{border-top:.5px solid var(--dsw-alias-border-l1);padding-top:6px}",
307
+ ".dsh-ttf-tag{flex:none;margin-top:1px;border-radius:999px;padding:0 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform)}",
308
+ ".dsh-ttf-tagWarn{color:var(--dsw-alias-state-warning-primary,#a06800);background:var(--dsw-alias-state-warning-bg,#ffcc002e)}",
309
+ // 展开详情:说明、结果、完成表单。
310
+ ".dsh-ttf-detail{display:flex;flex-direction:column;gap:6px;padding:2px 4px 4px}",
311
+ ".dsh-ttf-detailLabel{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
312
+ ".dsh-ttf-detailText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}",
313
+ // 提交的结果:默认只占一行,点一下看全文。
314
+ ".dsh-ttf-result{margin-top:2px;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word;overflow:hidden;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical}",
315
+ ".dsh-ttf-resultClick{cursor:pointer}",
316
+ ".dsh-ttf-resultOpen{display:block;max-height:220px;overflow-y:auto}",
317
+ ".dsh-ttf-form{margin:6px 0 2px}",
318
+ ".dsh-ttf-input{box-sizing:border-box;width:100%;min-height:64px;padding:6px 8px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:ui-monospace,monospace;font-size:12px;line-height:18px;resize:vertical;outline:none}",
319
+ ".dsh-ttf-input:focus{border-color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
320
+ ".dsh-ttf-input::placeholder{color:var(--dsw-alias-label-caption,#00000066)}",
321
+ ".dsh-ttf-formBar{margin-top:6px;display:flex;align-items:center;gap:6px}",
322
+ ".dsh-ttf-btn{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border-radius:6px;padding:2px 10px;font-family:inherit;font-size:12px;line-height:20px;cursor:pointer}",
323
+ ".dsh-ttf-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
324
+ ".dsh-ttf-btn:disabled{opacity:.4;cursor:default}",
325
+ ".dsh-ttf-btn:disabled:hover{background:var(--dsw-alias-bg-base)}",
326
+ ".dsh-ttf-error{color:var(--dsw-alias-state-error-primary,#c00);font-size:12px;line-height:18px;white-space:pre-wrap}",
327
+ ".dsh-ttf-hint{padding:4px 4px 6px 24px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
328
+ ].join("");
329
+
330
+ if (
331
+ typeof document !== "undefined" &&
332
+ document.querySelector("style[data-plugin-css=" + JSON.stringify(CSS_TAG) + "]") === null
333
+ ) {
334
+ const tag = document.createElement("style");
335
+ tag.dataset.plugin = "dsh-tree-task-flow";
336
+ tag.dataset.pluginCss = CSS_TAG;
337
+ tag.textContent = CSS;
338
+ document.head.appendChild(tag);
339
+ }
342
340
 
343
341
  // ---------------------------------------------------------------- 零件
344
342
 
345
- /** 一行节点的抬头:标记 + 标题 + 徽章 + id + 操作按钮。 */
346
- function NodeHeader({ node, t, isCurrent, isCheckpoint, onToggle, onDone, onDrop, canDrop, busy }) {
347
- const canAct = node.status === "pending";
343
+ /** 节点状态标记:完成打勾、丢弃划掉、未完成画圈(当前节点用虚线圈,和内置待办一致)。 */
344
+ function StatusMark({ node, isCurrent }) {
345
+ if (node.status === "done") {
346
+ const check = icon("IconCheckOutline14", 14);
347
+ return h("span", { className: "dsh-ttf-markDone" }, check === null ? "✓" : check);
348
+ }
349
+ if (node.status === "dropped") {
350
+ const close = icon("IconCloseOutline16", 14);
351
+ return h("span", { className: "dsh-ttf-markMuted" }, close === null ? "×" : close);
352
+ }
353
+ const circle = { cx: 7, cy: 7, r: 5.2, stroke: "currentColor", strokeWidth: 1.2 };
354
+ if (isCurrent) circle.strokeDasharray = "2.4 2.4";
348
355
  return h(
349
- "div",
350
- { style: S.stepTitle },
351
- h("span", null, mark(node)),
356
+ "span",
357
+ { className: "dsh-ttf-markCell" },
352
358
  h(
353
- "a",
354
- {
355
- href: "#",
356
- style: { color: "inherit" },
357
- onClick: (event) => {
358
- event.preventDefault();
359
- onToggle();
360
- },
361
- },
362
- node.title,
359
+ "svg",
360
+ { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": true },
361
+ h("circle", circle),
363
362
  ),
364
- isCurrent ? h("span", { style: S.badge }, t("label.current")) : null,
365
- isCheckpoint ? h("span", { style: S.badgeWarn }, t("label.checkpointHere")) : null,
366
- h("span", { style: S.meta }, node.id),
367
- canAct ? h("button", { style: S.btn, disabled: busy, onClick: onDone }, t("btn.done")) : null,
368
- canAct && canDrop
369
- ? h("button", { style: S.btn, disabled: busy, onClick: onDrop }, t("btn.drop"))
370
- : null,
363
+ );
364
+ }
365
+
366
+ /**
367
+ * 提交的结果。默认只占一行,点一下摊开看全文。
368
+ *
369
+ * 短到一行放得下的结果不给展开——点了也没变化,只是徒增噪音。
370
+ */
371
+ function ResultText({ text, t }) {
372
+ const [open, setOpen] = useState(false);
373
+ const body = String(text ?? "");
374
+ const expandable = body.includes("\n") || body.length > 60;
375
+ return h(
376
+ "div",
377
+ {
378
+ className:
379
+ "dsh-ttf-result" +
380
+ (expandable ? " dsh-ttf-resultClick" : "") +
381
+ (open ? " dsh-ttf-resultOpen" : ""),
382
+ role: expandable ? "button" : undefined,
383
+ tabIndex: expandable ? 0 : undefined,
384
+ title: expandable ? t(open ? "action.collapseResult" : "action.expandResult") : undefined,
385
+ onClick: expandable
386
+ ? () => setOpen(!open)
387
+ : undefined,
388
+ },
389
+ body,
371
390
  );
372
391
  }
373
392
 
@@ -376,57 +395,55 @@ window.__ModuleLoader__.load({
376
395
  const [text, setText] = useState("");
377
396
  return h(
378
397
  "div",
379
- { style: S.form },
398
+ { className: "dsh-ttf-form" },
380
399
  h("textarea", {
381
- style: S.textarea,
400
+ className: "dsh-ttf-input",
382
401
  value: text,
383
402
  rows: 4,
384
403
  placeholder: t("placeholder.result"),
385
404
  onChange: (event) => setText(event.target.value),
386
405
  }),
387
- error ? h("div", { style: S.err }, error) : null,
406
+ error ? h("div", { className: "dsh-ttf-error" }, error) : null,
388
407
  h(
389
408
  "div",
390
- { style: Object.assign({}, S.bar, { marginTop: "6px" }) },
409
+ { className: "dsh-ttf-formBar" },
391
410
  h(
392
411
  "button",
393
412
  {
394
- style: S.btn,
413
+ type: "button",
414
+ className: "dsh-ttf-btn",
395
415
  disabled: busy || text.trim() === "",
396
416
  onClick: () => onSubmit(text),
397
417
  },
398
- t("btn.submit"),
418
+ t("action.submit"),
399
419
  ),
400
- h("button", { style: S.btn, disabled: busy, onClick: onCancel }, t("btn.cancel")),
420
+ h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onCancel }, t("action.cancel")),
401
421
  ),
402
422
  );
403
423
  }
404
424
 
405
- /** 一个节点提交过的结果。折叠时只报首行,展开时给全文。 */
406
- function ResultBlock({ node, t, expanded }) {
407
- if (!node.result) {
408
- return expanded ? h("div", { style: S.meta }, t("label.result") + ":" + t("label.noResult")) : null;
409
- }
410
- if (!expanded) {
411
- return h("div", { style: S.resultLine }, t("label.result") + ":" + firstLine(node.result.text, 100));
412
- }
413
- return h(
414
- "div",
415
- null,
416
- h("div", { style: S.meta }, t("label.result") + "(" + formatTime(node.result.at) + ")"),
417
- h("pre", { style: S.pre }, node.result.text),
418
- );
419
- }
420
-
421
- function StatusBar({ data, sessionId, reload, t }) {
425
+ /**
426
+ * 一个节点:一行标题 + 可展开的详情。
427
+ *
428
+ * 三层共用同一个组件——它们的字段与动作本来就一样,差别只有缩进和
429
+ * 「能不能丢弃」(整个目标不能丢,plan.js 会拒绝)。
430
+ */
431
+ function NodeRow({ node, depth, activeIds, checkpointId, canDrop, sessionId, reload, t, defaultOpen }) {
432
+ const [open, setOpen] = useState(defaultOpen === true);
433
+ const [composing, setComposing] = useState(false);
422
434
  const [busy, setBusy] = useState(false);
435
+ const [error, setError] = useState(null);
423
436
 
424
437
  const act = useCallback(
425
438
  async (body) => {
426
439
  setBusy(true);
440
+ setError(null);
427
441
  try {
428
442
  await post(Object.assign({ sessionId }, body));
443
+ setComposing(false);
429
444
  await reload();
445
+ } catch (failure) {
446
+ setError(String((failure && failure.message) || failure));
430
447
  } finally {
431
448
  setBusy(false);
432
449
  }
@@ -434,275 +451,113 @@ window.__ModuleLoader__.load({
434
451
  [sessionId, reload],
435
452
  );
436
453
 
437
- const checkpoint = data.checkpoint;
438
- const badge = checkpoint
439
- ? t("state.checkpoint")
440
- : data.stopped
441
- ? t("state.stopped")
442
- : data.active === null
443
- ? t("state.done")
444
- : t("state.running");
454
+ const children = node.tasks || node.steps || [];
455
+ const canAct = node.status === "pending";
456
+ const isCurrent = activeIds.has(node.id);
457
+ const isCheckpoint = checkpointId === node.id;
458
+ const expandable = children.length > 0 || node.detail !== undefined || node.result !== undefined;
445
459
 
446
460
  return h(
447
461
  "div",
448
- null,
462
+ { className: "dsh-ttf-node dsh-ttf-depth" + depth },
449
463
  h(
450
464
  "div",
451
- { style: S.bar },
452
- h("span", { style: checkpoint ? S.badgeWarn : S.badge }, badge),
465
+ { className: "dsh-ttf-row" },
466
+ h(StatusMark, { node, isCurrent }),
453
467
  h(
454
- "button",
455
- { style: S.btn, disabled: busy, onClick: () => act({ action: data.stopped ? "resume" : "stop" }) },
456
- data.stopped ? t("btn.resume") : t("btn.stop"),
468
+ "span",
469
+ { className: "dsh-ttf-rowTitle" + (node.status === "done" ? " dsh-ttf-done" : "") },
470
+ node.title,
457
471
  ),
472
+ isCurrent ? h("span", { className: "dsh-ttf-tag" }, t("label.current")) : null,
473
+ isCheckpoint ? h("span", { className: "dsh-ttf-tag dsh-ttf-tagWarn" }, t("label.checkpointHere")) : null,
458
474
  h(
459
- "button",
460
- {
461
- style: S.btn,
462
- disabled: busy,
463
- onClick: () => {
464
- if (window.confirm(t("confirm.reset"))) act({ action: "reset" });
465
- },
466
- },
467
- t("btn.reset"),
475
+ "span",
476
+ { className: "dsh-ttf-rowActions" },
477
+ canAct
478
+ ? h(IconButton, {
479
+ iconName: "IconCheckOutline16",
480
+ label: t("action.done"),
481
+ disabled: busy,
482
+ onClick: () => {
483
+ setComposing(true);
484
+ setOpen(true);
485
+ },
486
+ })
487
+ : null,
488
+ canAct && canDrop
489
+ ? h(IconButton, {
490
+ iconName: "IconTrashOutline16",
491
+ label: t("action.drop"),
492
+ disabled: busy,
493
+ onClick: () => {
494
+ if (window.confirm(t("confirm.drop", { title: node.title }))) {
495
+ act({ action: "drop", id: node.id });
496
+ }
497
+ },
498
+ })
499
+ : null,
500
+ expandable
501
+ ? h(IconButton, {
502
+ iconName: open ? "IconChevronDownOutline14" : "IconChevronRightOutline14",
503
+ label: open ? t("action.collapse") : t("action.expand"),
504
+ onClick: () => setOpen(!open),
505
+ })
506
+ : null,
468
507
  ),
469
- h("button", { style: S.btn, disabled: busy, onClick: reload }, t("btn.refresh")),
470
508
  ),
471
- checkpoint
472
- ? h(
473
- "div",
474
- { style: S.warn },
475
- t("state.checkpoint") + ":" + checkpoint.label + "「" + checkpoint.title + "」",
476
- h("div", null, t("checkpoint.note", { title: checkpoint.title })),
477
- )
478
- : null,
479
- );
480
- }
481
-
482
- function StepNode({ step, isCurrent, isCheckpoint, sessionId, reload, t, defaultOpen = true }) {
483
- const [open, setOpen] = useState(defaultOpen);
484
- const [composing, setComposing] = useState(false);
485
- const [busy, setBusy] = useState(false);
486
- const [error, setError] = useState(null);
487
-
488
- const act = useCallback(
489
- async (body) => {
490
- setBusy(true);
491
- setError(null);
492
- try {
493
- await post(Object.assign({ sessionId }, body));
494
- setComposing(false);
495
- await reload();
496
- } catch (failure) {
497
- setError(String((failure && failure.message) || failure));
498
- } finally {
499
- setBusy(false);
500
- }
501
- },
502
- [sessionId, reload],
503
- );
504
-
505
- const style = Object.assign({}, S.node, step.status === "done" ? S.done : null);
506
-
507
- return h(
508
- "div",
509
- { style },
510
- h(NodeHeader, {
511
- node: step,
512
- t,
513
- isCurrent,
514
- isCheckpoint,
515
- onToggle: () => setOpen(!open),
516
- onDone: () => {
517
- setComposing(true);
518
- setOpen(true);
519
- },
520
- onDrop: () => {
521
- if (window.confirm(t("confirm.drop", { title: step.title }))) act({ action: "drop", id: step.id });
522
- },
523
- canDrop: true,
524
- busy,
525
- }),
526
- h(ResultBlock, { node: step, t, expanded: false }),
527
509
  open
528
510
  ? h(
529
511
  "div",
530
- { style: S.child },
531
- step.detail ? h("div", { style: S.meta }, step.detail) : null,
532
- h(ResultBlock, { node: step, t, expanded: true }),
533
- composing
534
- ? h(CompleteForm, {
535
- t,
536
- busy,
537
- error,
538
- onSubmit: (text) => act({ action: "done", id: step.id, result: text }),
539
- onCancel: () => {
540
- setComposing(false);
541
- setError(null);
542
- },
543
- })
544
- : null,
545
- )
546
- : null,
547
- );
548
- }
549
-
550
- function TaskNode({ task, active, checkpoint, sessionId, reload, t, defaultOpen = true }) {
551
- const [open, setOpen] = useState(defaultOpen);
552
- const [composing, setComposing] = useState(false);
553
- const [busy, setBusy] = useState(false);
554
- const [error, setError] = useState(null);
555
-
556
- const act = useCallback(
557
- async (body) => {
558
- setBusy(true);
559
- setError(null);
560
- try {
561
- await post(Object.assign({ sessionId }, body));
562
- setComposing(false);
563
- await reload();
564
- } catch (failure) {
565
- setError(String((failure && failure.message) || failure));
566
- } finally {
567
- setBusy(false);
568
- }
569
- },
570
- [sessionId, reload],
571
- );
572
-
573
- const style = Object.assign({}, S.node, task.status === "done" ? S.done : null);
574
- const steps = task.steps || [];
575
-
576
- return h(
577
- "div",
578
- { style },
579
- h(NodeHeader, {
580
- node: task,
581
- t,
582
- isCurrent: active?.taskId === task.id,
583
- isCheckpoint: checkpoint?.id === task.id,
584
- onToggle: () => setOpen(!open),
585
- onDone: () => {
586
- setComposing(true);
587
- setOpen(true);
588
- },
589
- onDrop: () => {
590
- if (window.confirm(t("confirm.drop", { title: task.title }))) act({ action: "drop", id: task.id });
591
- },
592
- canDrop: true,
593
- busy,
594
- }),
595
- h(ResultBlock, { node: task, t, expanded: false }),
596
- open
597
- ? h(
598
- "div",
599
- { style: S.child },
600
- task.detail ? h("div", { style: S.meta }, task.detail) : null,
601
- h(ResultBlock, { node: task, t, expanded: true }),
602
- composing
603
- ? h(CompleteForm, {
604
- t,
605
- busy,
606
- error,
607
- onSubmit: (text) => act({ action: "done", id: task.id, result: text }),
608
- onCancel: () => {
609
- setComposing(false);
610
- setError(null);
611
- },
612
- })
613
- : null,
614
- steps.length === 0
615
- ? h("div", { style: S.hint }, "(还没有子任务 → 让模型调用 tree_task_plan)")
616
- : steps.map((step) =>
617
- h(StepNode, {
618
- key: step.id,
619
- step,
620
- isCurrent: active?.stepId === step.id,
621
- isCheckpoint: checkpoint?.id === step.id,
622
- sessionId,
623
- reload,
624
- t,
625
- defaultOpen,
626
- }),
512
+ { className: "dsh-ttf-detail" },
513
+ node.detail ? h("div", { className: "dsh-ttf-detailText" }, node.detail) : null,
514
+ node.result
515
+ ? h(
516
+ "div",
517
+ null,
518
+ h(
519
+ "div",
520
+ { className: "dsh-ttf-detailLabel" },
521
+ t("label.result") + "(" + formatTime(node.result.at) + ")",
522
+ ),
523
+ h(ResultText, { text: node.result.text, t }),
524
+ )
525
+ : h(
526
+ "div",
527
+ { className: "dsh-ttf-detailLabel" },
528
+ t("label.result") + ":" + t("label.noResult"),
627
529
  ),
628
- )
629
- : null,
630
- );
631
- }
632
-
633
- function GoalNode({ goal, active, checkpoint, sessionId, reload, t, defaultOpen = true }) {
634
- const [open, setOpen] = useState(defaultOpen);
635
- const [composing, setComposing] = useState(false);
636
- const [busy, setBusy] = useState(false);
637
- const [error, setError] = useState(null);
638
-
639
- const act = useCallback(
640
- async (body) => {
641
- setBusy(true);
642
- setError(null);
643
- try {
644
- await post(Object.assign({ sessionId }, body));
645
- setComposing(false);
646
- await reload();
647
- } catch (failure) {
648
- setError(String((failure && failure.message) || failure));
649
- } finally {
650
- setBusy(false);
651
- }
652
- },
653
- [sessionId, reload],
654
- );
655
-
656
- const tasks = goal.tasks || [];
657
-
658
- return h(
659
- "div",
660
- null,
661
- h(NodeHeader, {
662
- node: goal,
663
- t,
664
- isCurrent: active?.goalId === goal.id,
665
- isCheckpoint: checkpoint?.id === goal.id,
666
- onToggle: () => setOpen(!open),
667
- onDone: () => {
668
- setComposing(true);
669
- setOpen(true);
670
- },
671
- // 整个目标不能丢弃:plan.js 会拒绝,这里也不给按钮。
672
- canDrop: false,
673
- busy,
674
- }),
675
- h(ResultBlock, { node: goal, t, expanded: false }),
676
- open
677
- ? h(
678
- "div",
679
- { style: S.child },
680
- goal.detail ? h("div", { style: S.meta }, goal.detail) : null,
681
- h(ResultBlock, { node: goal, t, expanded: true }),
682
530
  composing
683
531
  ? h(CompleteForm, {
684
532
  t,
685
533
  busy,
686
534
  error,
687
- onSubmit: (text) => act({ action: "done", id: goal.id, result: text }),
535
+ onSubmit: (text) => act({ action: "done", id: node.id, result: text }),
688
536
  onCancel: () => {
689
537
  setComposing(false);
690
538
  setError(null);
691
539
  },
692
540
  })
693
541
  : null,
694
- tasks.map((task) =>
695
- h(TaskNode, {
696
- key: task.id,
697
- task,
698
- active,
699
- checkpoint,
542
+ children.map((child) =>
543
+ h(NodeRow, {
544
+ key: child.id,
545
+ node: child,
546
+ depth: depth + 1,
547
+ activeIds,
548
+ checkpointId,
549
+ canDrop: true,
700
550
  sessionId,
701
551
  reload,
702
552
  t,
703
- defaultOpen,
553
+ // 条目一展开就该看见整棵树,所以每一层都摊开;
554
+ // 嫌长的用上面的「收起」逐层折。
555
+ defaultOpen: true,
704
556
  }),
705
557
  ),
558
+ children.length === 0 && depth < 2
559
+ ? h("div", { className: "dsh-ttf-hint" }, t("hint.noChildren"))
560
+ : null,
706
561
  )
707
562
  : null,
708
563
  );
@@ -713,8 +568,8 @@ window.__ModuleLoader__.load({
713
568
  /**
714
569
  * 输入框上方的全宽条目。
715
570
  *
716
- * 折叠时只有一行:标题 + 进度 + 当前节点。展开才画整棵树,并限高滚动——
717
- * 这是输入框旁边的位置,不能让它把会话挤没了。
571
+ * 收起时只有一行:图标 + 阶段标签 + 目标标题 + 进度,动作全在右侧的图标
572
+ * 按钮里(暂停 / 继续、清空、展开)。展开才画整棵树,并限高滚动。
718
573
  *
719
574
  * 会话没有计划时返回 null:内置的 todo / goal 条目不该因为我而挪位。
720
575
  */
@@ -723,6 +578,7 @@ window.__ModuleLoader__.load({
723
578
  const sessionId = props.sessionId;
724
579
  const [state, setState] = useState({ loading: true });
725
580
  const [open, setOpen] = useState(false);
581
+ const [busy, setBusy] = useState(false);
726
582
 
727
583
  const reload = useCallback(async () => {
728
584
  if (!sessionId) {
@@ -742,130 +598,121 @@ window.__ModuleLoader__.load({
742
598
  reload();
743
599
  }, [reload]);
744
600
 
601
+ // 模型随时会改计划,而条目没有推送通道,所以自己轮询。
602
+ // 4 秒一次,读的是插件自己的一个小 JSON。
603
+ useEffect(() => {
604
+ if (!sessionId) return undefined;
605
+ const timer = setInterval(() => {
606
+ reload();
607
+ }, 4000);
608
+ return () => clearInterval(timer);
609
+ }, [sessionId, reload]);
610
+
611
+ const act = useCallback(
612
+ async (body) => {
613
+ setBusy(true);
614
+ try {
615
+ await post(Object.assign({ sessionId }, body));
616
+ await reload();
617
+ } finally {
618
+ setBusy(false);
619
+ }
620
+ },
621
+ [sessionId, reload],
622
+ );
623
+
745
624
  if (state.loading || state.empty || !state.data) return null;
746
625
 
747
626
  const data = state.data;
748
- const stats = countNodes(data.plan.goal);
627
+ const goal = data.plan.goal;
628
+ const stats = countNodes(goal);
749
629
  const current = currentLabel(data);
630
+ // 「被按住」有两条来源:插件闸门(paused)与人按过界面停止(stopped)。
631
+ // 对用户来说都是"停着呢,点继续就能接着跑",所以合成一个状态展示。
632
+ const held = data.paused === true || data.stopped === true;
633
+ const stateLabel = held
634
+ ? t("state.paused")
635
+ : data.checkpoint
636
+ ? t("state.checkpoint")
637
+ : data.active === null
638
+ ? t("state.done")
639
+ : t("state.running");
750
640
 
751
641
  return h(
752
642
  "div",
753
- { style: S.dock },
643
+ { className: "dsh-ttf-dock", "aria-label": t("nav") },
754
644
  h(
755
645
  "div",
756
- { style: S.dockHead },
646
+ { className: "dsh-ttf-panel" },
757
647
  h(
758
- "button",
759
- { style: S.dockToggle, onClick: () => setOpen(!open) },
760
- h("span", { style: S.dockTitle }, t("nav")),
648
+ "div",
649
+ { className: "dsh-ttf-bar" },
650
+ h("span", { className: "dsh-ttf-glyph", "aria-hidden": true }, icon("IconChecklistOutline14", 14)),
651
+ h("span", { className: "dsh-ttf-label" + (held ? " dsh-ttf-labelHeld" : "") }, stateLabel),
652
+ h("span", { className: "dsh-ttf-title", title: goal.title }, goal.title),
761
653
  h(
762
654
  "span",
763
- { style: S.dockProgress },
655
+ { className: "dsh-ttf-progress" },
764
656
  stats.done + "/" + stats.total + (current ? " · " + current : ""),
765
657
  ),
766
- h("span", { style: S.dockChevron }, open ? "▾" : "▸"),
767
- ),
768
- data.checkpoint ? h("span", { style: S.badgeWarn }, t("state.checkpoint")) : null,
769
- ),
770
- open
771
- ? h(
658
+ h(
772
659
  "div",
773
- { style: S.dockBody },
774
- h(StatusBar, { data, sessionId, reload, t }),
775
- h(
660
+ { className: "dsh-ttf-actions" },
661
+ h(IconButton, {
662
+ iconName: held ? "IconPlayOutline16" : "IconPauseOutline16",
663
+ label: held ? t("action.resume") : t("action.pause"),
664
+ disabled: busy,
665
+ onClick: () => act({ action: held ? "resume" : "pause" }),
666
+ }),
667
+ h(IconButton, {
668
+ iconName: "IconTrashOutline16",
669
+ label: t("action.reset"),
670
+ disabled: busy,
671
+ onClick: () => {
672
+ if (window.confirm(t("confirm.reset"))) act({ action: "reset" });
673
+ },
674
+ }),
675
+ h(IconButton, {
676
+ iconName: open ? "IconChevronDownOutline14" : "IconChevronUpOutline14",
677
+ label: open ? t("action.collapse") : t("action.expand"),
678
+ onClick: () => setOpen(!open),
679
+ }),
680
+ ),
681
+ ),
682
+ open
683
+ ? h(
776
684
  "div",
777
- { style: { marginTop: "6px" } },
778
- h(GoalNode, {
779
- goal: data.plan.goal,
780
- active: data.active,
781
- checkpoint: data.checkpoint,
685
+ { className: "dsh-ttf-body" },
686
+ data.checkpoint
687
+ ? h(
688
+ "div",
689
+ { className: "dsh-ttf-note" },
690
+ t("state.checkpoint") +
691
+ ":" +
692
+ data.checkpoint.label +
693
+ "「" +
694
+ data.checkpoint.title +
695
+ "」",
696
+ h("div", null, t("checkpoint.note", { title: data.checkpoint.title })),
697
+ )
698
+ : null,
699
+ held ? h("div", { className: "dsh-ttf-note" }, t("note.paused")) : null,
700
+ h(NodeRow, {
701
+ node: goal,
702
+ depth: 0,
703
+ activeIds: currentIds(data),
704
+ checkpointId: data.checkpoint ? data.checkpoint.id : null,
705
+ // 整个目标不能丢弃:plan.js 会拒绝,这里也不给按钮。
706
+ canDrop: false,
782
707
  sessionId,
783
708
  reload,
784
709
  t,
785
- // 展开整棵树的动作已经由上面那一行承担,这里默认为折叠。
786
- defaultOpen: false,
710
+ // 展开条目就是为了看树,所以第一层默认摊开。
711
+ defaultOpen: true,
787
712
  }),
788
- ),
789
- )
790
- : null,
791
- );
792
- }
793
-
794
- // ---------------------------------------------------------------- 设置页
795
-
796
- function SettingsSection() {
797
- const t = useT();
798
- const [state, setState] = useState({ loading: true });
799
-
800
- useEffect(() => {
801
- api("/dsh-tree-task-flow/sessions")
802
- .then((data) => setState({ loading: false, data }))
803
- .catch((error) => setState({ loading: false, error: String((error && error.message) || error) }));
804
- }, []);
805
-
806
- if (state.loading) return h("div", { style: S.wrap }, "…");
807
- if (state.error) return h("div", { style: S.wrap }, h("div", { style: S.err }, state.error));
808
-
809
- const enabled = state.data && state.data.enabled === true;
810
- const sessions = (state.data && state.data.sessions) || [];
811
-
812
- const row = (label, value) =>
813
- h(
814
- "div",
815
- { style: { display: "flex", gap: "8px", margin: "2px 0" } },
816
- h("span", { style: { minWidth: "9em", opacity: 0.7 } }, label),
817
- h("span", null, value),
818
- );
819
-
820
- return h(
821
- "div",
822
- { style: S.wrap },
823
- h("div", { style: { fontWeight: 600, marginBottom: "6px" } }, t("settings.title")),
824
- row(
825
- "",
826
- h("span", { style: S.badge }, enabled ? t("settings.enabled") : t("settings.disabled")),
827
- ),
828
- h("div", { style: { marginTop: "10px", fontWeight: 600 } }, t("settings.impact")),
829
- h(
830
- "ul",
831
- { style: { margin: "4px 0 10px 1.2em", padding: 0 } },
832
- [
833
- "settings.impact.1",
834
- "settings.impact.2",
835
- "settings.impact.3",
836
- "settings.impact.4",
837
- "settings.impact.5",
838
- ].map((key) => h("li", { key }, t(key))),
713
+ )
714
+ : null,
839
715
  ),
840
- h("div", { style: { fontWeight: 600 } }, t("settings.howto")),
841
- h("div", { style: S.hint }, t("settings.howto.body")),
842
- h("div", { style: { marginTop: "10px", fontWeight: 600 } }, t("settings.stateDir")),
843
- h("div", { style: S.meta }, "$DSH_HOME/dsh-task-tree/"),
844
- h(
845
- "div",
846
- { style: { marginTop: "10px", fontWeight: 600 } },
847
- t("settings.sessions") + ":" + sessions.length,
848
- ),
849
- sessions.length === 0
850
- ? h("div", { style: S.hint }, t("settings.noSessions"))
851
- : h(
852
- "div",
853
- { style: S.child },
854
- sessions.map((item) =>
855
- h(
856
- "div",
857
- { key: item.sessionId, style: S.meta },
858
- item.goal +
859
- " · " +
860
- item.done +
861
- "/" +
862
- item.steps +
863
- (item.checkpoint ? " · " + t("settings.checkpoint") : "") +
864
- " · " +
865
- item.sessionId,
866
- ),
867
- ),
868
- ),
869
716
  );
870
717
  }
871
718
 
@@ -892,25 +739,6 @@ window.__ModuleLoader__.load({
892
739
  ),
893
740
  "dsh-tree-task-flow: conversation dock",
894
741
  );
895
-
896
- // 设置页:只显示状态与影响说明(启用与否由 profile 配置决定,运行时不改)。
897
- ctx.effect(
898
- () =>
899
- ctx.slots.inject("settings.section", () =>
900
- ctx.slots.register(
901
- {
902
- name: "settings.section",
903
- id: ENTRY_ID,
904
- order: 45,
905
- label: () => translate("nav"),
906
- locale: NS,
907
- inject: () => ({}),
908
- },
909
- SettingsSection,
910
- ),
911
- ),
912
- "dsh-tree-task-flow: settings section",
913
- );
914
742
  }
915
743
 
916
744
  exports.name = name;