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/README.md +68 -25
- package/client.js +426 -598
- package/cordis.patch.yml +1 -1
- package/lib/auto-continue.js +3 -5
- package/lib/commands.js +20 -20
- package/lib/http.js +17 -61
- package/lib/index.js +18 -12
- package/lib/pause.js +138 -0
- package/lib/plan.js +5 -6
- package/lib/prompt.js +28 -30
- package/lib/tools.js +1 -62
- package/package.json +1 -1
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
195
|
+
"state.paused": "已暂停",
|
|
145
196
|
"state.done": "已全部完成",
|
|
146
197
|
"state.checkpoint": "检查点",
|
|
147
198
|
"checkpoint.note": "「{title}」的子节点已全部结束。要么继续给它拆子节点,要么提交结果并完成它。",
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
196
|
-
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
},
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
},
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
/**
|
|
346
|
-
function
|
|
347
|
-
|
|
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
|
-
"
|
|
350
|
-
{
|
|
351
|
-
h("span", null, mark(node)),
|
|
356
|
+
"span",
|
|
357
|
+
{ className: "dsh-ttf-markCell" },
|
|
352
358
|
h(
|
|
353
|
-
"
|
|
354
|
-
{
|
|
355
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
{
|
|
398
|
+
{ className: "dsh-ttf-form" },
|
|
380
399
|
h("textarea", {
|
|
381
|
-
|
|
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", {
|
|
406
|
+
error ? h("div", { className: "dsh-ttf-error" }, error) : null,
|
|
388
407
|
h(
|
|
389
408
|
"div",
|
|
390
|
-
{
|
|
409
|
+
{ className: "dsh-ttf-formBar" },
|
|
391
410
|
h(
|
|
392
411
|
"button",
|
|
393
412
|
{
|
|
394
|
-
|
|
413
|
+
type: "button",
|
|
414
|
+
className: "dsh-ttf-btn",
|
|
395
415
|
disabled: busy || text.trim() === "",
|
|
396
416
|
onClick: () => onSubmit(text),
|
|
397
417
|
},
|
|
398
|
-
t("
|
|
418
|
+
t("action.submit"),
|
|
399
419
|
),
|
|
400
|
-
h("button", {
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
462
|
+
{ className: "dsh-ttf-node dsh-ttf-depth" + depth },
|
|
449
463
|
h(
|
|
450
464
|
"div",
|
|
451
|
-
{
|
|
452
|
-
h(
|
|
465
|
+
{ className: "dsh-ttf-row" },
|
|
466
|
+
h(StatusMark, { node, isCurrent }),
|
|
453
467
|
h(
|
|
454
|
-
"
|
|
455
|
-
{
|
|
456
|
-
|
|
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
|
-
"
|
|
460
|
-
{
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
{
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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:
|
|
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
|
-
|
|
695
|
-
h(
|
|
696
|
-
key:
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
643
|
+
{ className: "dsh-ttf-dock", "aria-label": t("nav") },
|
|
754
644
|
h(
|
|
755
645
|
"div",
|
|
756
|
-
{
|
|
646
|
+
{ className: "dsh-ttf-panel" },
|
|
757
647
|
h(
|
|
758
|
-
"
|
|
759
|
-
{
|
|
760
|
-
h("span", {
|
|
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
|
-
{
|
|
655
|
+
{ className: "dsh-ttf-progress" },
|
|
764
656
|
stats.done + "/" + stats.total + (current ? " · " + current : ""),
|
|
765
657
|
),
|
|
766
|
-
h(
|
|
767
|
-
),
|
|
768
|
-
data.checkpoint ? h("span", { style: S.badgeWarn }, t("state.checkpoint")) : null,
|
|
769
|
-
),
|
|
770
|
-
open
|
|
771
|
-
? h(
|
|
658
|
+
h(
|
|
772
659
|
"div",
|
|
773
|
-
{
|
|
774
|
-
h(
|
|
775
|
-
|
|
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
|
-
{
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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:
|
|
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;
|