dsh-tree-task-flow 0.2.0 → 0.3.1
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 +86 -38
- package/client.js +426 -463
- package/config.example.yml +27 -0
- package/cordis.patch.yml +5 -4
- package/lib/auto-continue.js +3 -5
- package/lib/commands.js +26 -26
- package/lib/http.js +12 -5
- package/lib/index.js +23 -16
- 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 +2 -2
- package/enable.example.yml +0 -4
package/client.js
CHANGED
|
@@ -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,202 +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
220
|
};
|
|
165
221
|
|
|
166
222
|
const en = {
|
|
167
223
|
nav: "Tree Task Flow",
|
|
168
|
-
"empty.title": "No plan in this session yet",
|
|
169
|
-
"empty.hint": "Ask the model to call tree_task_create to build a goal and tasks.",
|
|
170
224
|
"state.running": "In progress",
|
|
171
|
-
"state.
|
|
225
|
+
"state.paused": "Paused",
|
|
172
226
|
"state.done": "All done",
|
|
173
227
|
"state.checkpoint": "Checkpoint",
|
|
174
228
|
"checkpoint.note":
|
|
175
229
|
"Every child of “{title}” has finished. Either break out more children, or submit its result and complete it.",
|
|
176
|
-
"
|
|
177
|
-
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
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",
|
|
184
243
|
"label.result": "Result",
|
|
185
244
|
"label.noResult": "(no result submitted yet)",
|
|
186
245
|
"label.current": "current",
|
|
187
246
|
"label.checkpointHere": "checkpoint",
|
|
188
247
|
"placeholder.result":
|
|
189
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)",
|
|
190
250
|
"confirm.reset": "Clear this session's plan tree?",
|
|
191
251
|
"confirm.drop": "Drop “{title}”?",
|
|
192
252
|
};
|
|
193
253
|
|
|
194
254
|
// ---------------------------------------------------------------- 样式
|
|
195
255
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
},
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
},
|
|
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
|
-
margin: "2px 0 0",
|
|
281
|
-
padding: "6px 8px",
|
|
282
|
-
background: "var(--dsh-muted, #00000010)",
|
|
283
|
-
borderRadius: "6px",
|
|
284
|
-
whiteSpace: "pre-wrap",
|
|
285
|
-
wordBreak: "break-word",
|
|
286
|
-
font: "12px/1.5 ui-monospace, monospace",
|
|
287
|
-
maxHeight: "220px",
|
|
288
|
-
overflow: "auto",
|
|
289
|
-
},
|
|
290
|
-
form: { marginTop: "6px", border: "1px solid var(--dsh-border, #d0d0d0)", borderRadius: "6px", padding: "6px" },
|
|
291
|
-
textarea: {
|
|
292
|
-
width: "100%",
|
|
293
|
-
boxSizing: "border-box",
|
|
294
|
-
minHeight: "64px",
|
|
295
|
-
font: "12px/1.5 ui-monospace, monospace",
|
|
296
|
-
background: "transparent",
|
|
297
|
-
color: "inherit",
|
|
298
|
-
border: "1px solid var(--dsh-border, #d0d0d0)",
|
|
299
|
-
borderRadius: "4px",
|
|
300
|
-
padding: "4px",
|
|
301
|
-
resize: "vertical",
|
|
302
|
-
},
|
|
303
|
-
};
|
|
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
|
+
}
|
|
304
340
|
|
|
305
341
|
// ---------------------------------------------------------------- 零件
|
|
306
342
|
|
|
307
|
-
/**
|
|
308
|
-
function
|
|
309
|
-
|
|
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";
|
|
310
355
|
return h(
|
|
311
|
-
"
|
|
312
|
-
{
|
|
313
|
-
h("span", null, mark(node)),
|
|
356
|
+
"span",
|
|
357
|
+
{ className: "dsh-ttf-markCell" },
|
|
314
358
|
h(
|
|
315
|
-
"
|
|
316
|
-
{
|
|
317
|
-
|
|
318
|
-
style: { color: "inherit" },
|
|
319
|
-
onClick: (event) => {
|
|
320
|
-
event.preventDefault();
|
|
321
|
-
onToggle();
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
node.title,
|
|
359
|
+
"svg",
|
|
360
|
+
{ width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": true },
|
|
361
|
+
h("circle", circle),
|
|
325
362
|
),
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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,
|
|
333
390
|
);
|
|
334
391
|
}
|
|
335
392
|
|
|
@@ -338,57 +395,55 @@ window.__ModuleLoader__.load({
|
|
|
338
395
|
const [text, setText] = useState("");
|
|
339
396
|
return h(
|
|
340
397
|
"div",
|
|
341
|
-
{
|
|
398
|
+
{ className: "dsh-ttf-form" },
|
|
342
399
|
h("textarea", {
|
|
343
|
-
|
|
400
|
+
className: "dsh-ttf-input",
|
|
344
401
|
value: text,
|
|
345
402
|
rows: 4,
|
|
346
403
|
placeholder: t("placeholder.result"),
|
|
347
404
|
onChange: (event) => setText(event.target.value),
|
|
348
405
|
}),
|
|
349
|
-
error ? h("div", {
|
|
406
|
+
error ? h("div", { className: "dsh-ttf-error" }, error) : null,
|
|
350
407
|
h(
|
|
351
408
|
"div",
|
|
352
|
-
{
|
|
409
|
+
{ className: "dsh-ttf-formBar" },
|
|
353
410
|
h(
|
|
354
411
|
"button",
|
|
355
412
|
{
|
|
356
|
-
|
|
413
|
+
type: "button",
|
|
414
|
+
className: "dsh-ttf-btn",
|
|
357
415
|
disabled: busy || text.trim() === "",
|
|
358
416
|
onClick: () => onSubmit(text),
|
|
359
417
|
},
|
|
360
|
-
t("
|
|
418
|
+
t("action.submit"),
|
|
361
419
|
),
|
|
362
|
-
h("button", {
|
|
420
|
+
h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onCancel }, t("action.cancel")),
|
|
363
421
|
),
|
|
364
422
|
);
|
|
365
423
|
}
|
|
366
424
|
|
|
367
|
-
/**
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
"div",
|
|
377
|
-
null,
|
|
378
|
-
h("div", { style: S.meta }, t("label.result") + "(" + formatTime(node.result.at) + ")"),
|
|
379
|
-
h("pre", { style: S.pre }, node.result.text),
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
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);
|
|
384
434
|
const [busy, setBusy] = useState(false);
|
|
435
|
+
const [error, setError] = useState(null);
|
|
385
436
|
|
|
386
437
|
const act = useCallback(
|
|
387
438
|
async (body) => {
|
|
388
439
|
setBusy(true);
|
|
440
|
+
setError(null);
|
|
389
441
|
try {
|
|
390
442
|
await post(Object.assign({ sessionId }, body));
|
|
443
|
+
setComposing(false);
|
|
391
444
|
await reload();
|
|
445
|
+
} catch (failure) {
|
|
446
|
+
setError(String((failure && failure.message) || failure));
|
|
392
447
|
} finally {
|
|
393
448
|
setBusy(false);
|
|
394
449
|
}
|
|
@@ -396,275 +451,113 @@ window.__ModuleLoader__.load({
|
|
|
396
451
|
[sessionId, reload],
|
|
397
452
|
);
|
|
398
453
|
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
: data.active === null
|
|
405
|
-
? t("state.done")
|
|
406
|
-
: 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;
|
|
407
459
|
|
|
408
460
|
return h(
|
|
409
461
|
"div",
|
|
410
|
-
|
|
462
|
+
{ className: "dsh-ttf-node dsh-ttf-depth" + depth },
|
|
411
463
|
h(
|
|
412
464
|
"div",
|
|
413
|
-
{
|
|
414
|
-
h(
|
|
465
|
+
{ className: "dsh-ttf-row" },
|
|
466
|
+
h(StatusMark, { node, isCurrent }),
|
|
415
467
|
h(
|
|
416
|
-
"
|
|
417
|
-
{
|
|
418
|
-
|
|
468
|
+
"span",
|
|
469
|
+
{ className: "dsh-ttf-rowTitle" + (node.status === "done" ? " dsh-ttf-done" : "") },
|
|
470
|
+
node.title,
|
|
419
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,
|
|
420
474
|
h(
|
|
421
|
-
"
|
|
422
|
-
{
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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,
|
|
430
507
|
),
|
|
431
|
-
h("button", { style: S.btn, disabled: busy, onClick: reload }, t("btn.refresh")),
|
|
432
508
|
),
|
|
433
|
-
checkpoint
|
|
434
|
-
? h(
|
|
435
|
-
"div",
|
|
436
|
-
{ style: S.warn },
|
|
437
|
-
t("state.checkpoint") + ":" + checkpoint.label + "「" + checkpoint.title + "」",
|
|
438
|
-
h("div", null, t("checkpoint.note", { title: checkpoint.title })),
|
|
439
|
-
)
|
|
440
|
-
: null,
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function StepNode({ step, isCurrent, isCheckpoint, sessionId, reload, t, defaultOpen = true }) {
|
|
445
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
446
|
-
const [composing, setComposing] = useState(false);
|
|
447
|
-
const [busy, setBusy] = useState(false);
|
|
448
|
-
const [error, setError] = useState(null);
|
|
449
|
-
|
|
450
|
-
const act = useCallback(
|
|
451
|
-
async (body) => {
|
|
452
|
-
setBusy(true);
|
|
453
|
-
setError(null);
|
|
454
|
-
try {
|
|
455
|
-
await post(Object.assign({ sessionId }, body));
|
|
456
|
-
setComposing(false);
|
|
457
|
-
await reload();
|
|
458
|
-
} catch (failure) {
|
|
459
|
-
setError(String((failure && failure.message) || failure));
|
|
460
|
-
} finally {
|
|
461
|
-
setBusy(false);
|
|
462
|
-
}
|
|
463
|
-
},
|
|
464
|
-
[sessionId, reload],
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
const style = Object.assign({}, S.node, step.status === "done" ? S.done : null);
|
|
468
|
-
|
|
469
|
-
return h(
|
|
470
|
-
"div",
|
|
471
|
-
{ style },
|
|
472
|
-
h(NodeHeader, {
|
|
473
|
-
node: step,
|
|
474
|
-
t,
|
|
475
|
-
isCurrent,
|
|
476
|
-
isCheckpoint,
|
|
477
|
-
onToggle: () => setOpen(!open),
|
|
478
|
-
onDone: () => {
|
|
479
|
-
setComposing(true);
|
|
480
|
-
setOpen(true);
|
|
481
|
-
},
|
|
482
|
-
onDrop: () => {
|
|
483
|
-
if (window.confirm(t("confirm.drop", { title: step.title }))) act({ action: "drop", id: step.id });
|
|
484
|
-
},
|
|
485
|
-
canDrop: true,
|
|
486
|
-
busy,
|
|
487
|
-
}),
|
|
488
|
-
h(ResultBlock, { node: step, t, expanded: false }),
|
|
489
|
-
open
|
|
490
|
-
? h(
|
|
491
|
-
"div",
|
|
492
|
-
{ style: S.child },
|
|
493
|
-
step.detail ? h("div", { style: S.meta }, step.detail) : null,
|
|
494
|
-
h(ResultBlock, { node: step, t, expanded: true }),
|
|
495
|
-
composing
|
|
496
|
-
? h(CompleteForm, {
|
|
497
|
-
t,
|
|
498
|
-
busy,
|
|
499
|
-
error,
|
|
500
|
-
onSubmit: (text) => act({ action: "done", id: step.id, result: text }),
|
|
501
|
-
onCancel: () => {
|
|
502
|
-
setComposing(false);
|
|
503
|
-
setError(null);
|
|
504
|
-
},
|
|
505
|
-
})
|
|
506
|
-
: null,
|
|
507
|
-
)
|
|
508
|
-
: null,
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function TaskNode({ task, active, checkpoint, sessionId, reload, t, defaultOpen = true }) {
|
|
513
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
514
|
-
const [composing, setComposing] = useState(false);
|
|
515
|
-
const [busy, setBusy] = useState(false);
|
|
516
|
-
const [error, setError] = useState(null);
|
|
517
|
-
|
|
518
|
-
const act = useCallback(
|
|
519
|
-
async (body) => {
|
|
520
|
-
setBusy(true);
|
|
521
|
-
setError(null);
|
|
522
|
-
try {
|
|
523
|
-
await post(Object.assign({ sessionId }, body));
|
|
524
|
-
setComposing(false);
|
|
525
|
-
await reload();
|
|
526
|
-
} catch (failure) {
|
|
527
|
-
setError(String((failure && failure.message) || failure));
|
|
528
|
-
} finally {
|
|
529
|
-
setBusy(false);
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
[sessionId, reload],
|
|
533
|
-
);
|
|
534
|
-
|
|
535
|
-
const style = Object.assign({}, S.node, task.status === "done" ? S.done : null);
|
|
536
|
-
const steps = task.steps || [];
|
|
537
|
-
|
|
538
|
-
return h(
|
|
539
|
-
"div",
|
|
540
|
-
{ style },
|
|
541
|
-
h(NodeHeader, {
|
|
542
|
-
node: task,
|
|
543
|
-
t,
|
|
544
|
-
isCurrent: active?.taskId === task.id,
|
|
545
|
-
isCheckpoint: checkpoint?.id === task.id,
|
|
546
|
-
onToggle: () => setOpen(!open),
|
|
547
|
-
onDone: () => {
|
|
548
|
-
setComposing(true);
|
|
549
|
-
setOpen(true);
|
|
550
|
-
},
|
|
551
|
-
onDrop: () => {
|
|
552
|
-
if (window.confirm(t("confirm.drop", { title: task.title }))) act({ action: "drop", id: task.id });
|
|
553
|
-
},
|
|
554
|
-
canDrop: true,
|
|
555
|
-
busy,
|
|
556
|
-
}),
|
|
557
|
-
h(ResultBlock, { node: task, t, expanded: false }),
|
|
558
509
|
open
|
|
559
510
|
? h(
|
|
560
511
|
"div",
|
|
561
|
-
{
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
: steps.map((step) =>
|
|
579
|
-
h(StepNode, {
|
|
580
|
-
key: step.id,
|
|
581
|
-
step,
|
|
582
|
-
isCurrent: active?.stepId === step.id,
|
|
583
|
-
isCheckpoint: checkpoint?.id === step.id,
|
|
584
|
-
sessionId,
|
|
585
|
-
reload,
|
|
586
|
-
t,
|
|
587
|
-
defaultOpen,
|
|
588
|
-
}),
|
|
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"),
|
|
589
529
|
),
|
|
590
|
-
)
|
|
591
|
-
: null,
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function GoalNode({ goal, active, checkpoint, sessionId, reload, t, defaultOpen = true }) {
|
|
596
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
597
|
-
const [composing, setComposing] = useState(false);
|
|
598
|
-
const [busy, setBusy] = useState(false);
|
|
599
|
-
const [error, setError] = useState(null);
|
|
600
|
-
|
|
601
|
-
const act = useCallback(
|
|
602
|
-
async (body) => {
|
|
603
|
-
setBusy(true);
|
|
604
|
-
setError(null);
|
|
605
|
-
try {
|
|
606
|
-
await post(Object.assign({ sessionId }, body));
|
|
607
|
-
setComposing(false);
|
|
608
|
-
await reload();
|
|
609
|
-
} catch (failure) {
|
|
610
|
-
setError(String((failure && failure.message) || failure));
|
|
611
|
-
} finally {
|
|
612
|
-
setBusy(false);
|
|
613
|
-
}
|
|
614
|
-
},
|
|
615
|
-
[sessionId, reload],
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
const tasks = goal.tasks || [];
|
|
619
|
-
|
|
620
|
-
return h(
|
|
621
|
-
"div",
|
|
622
|
-
null,
|
|
623
|
-
h(NodeHeader, {
|
|
624
|
-
node: goal,
|
|
625
|
-
t,
|
|
626
|
-
isCurrent: active?.goalId === goal.id,
|
|
627
|
-
isCheckpoint: checkpoint?.id === goal.id,
|
|
628
|
-
onToggle: () => setOpen(!open),
|
|
629
|
-
onDone: () => {
|
|
630
|
-
setComposing(true);
|
|
631
|
-
setOpen(true);
|
|
632
|
-
},
|
|
633
|
-
// 整个目标不能丢弃:plan.js 会拒绝,这里也不给按钮。
|
|
634
|
-
canDrop: false,
|
|
635
|
-
busy,
|
|
636
|
-
}),
|
|
637
|
-
h(ResultBlock, { node: goal, t, expanded: false }),
|
|
638
|
-
open
|
|
639
|
-
? h(
|
|
640
|
-
"div",
|
|
641
|
-
{ style: S.child },
|
|
642
|
-
goal.detail ? h("div", { style: S.meta }, goal.detail) : null,
|
|
643
|
-
h(ResultBlock, { node: goal, t, expanded: true }),
|
|
644
530
|
composing
|
|
645
531
|
? h(CompleteForm, {
|
|
646
532
|
t,
|
|
647
533
|
busy,
|
|
648
534
|
error,
|
|
649
|
-
onSubmit: (text) => act({ action: "done", id:
|
|
535
|
+
onSubmit: (text) => act({ action: "done", id: node.id, result: text }),
|
|
650
536
|
onCancel: () => {
|
|
651
537
|
setComposing(false);
|
|
652
538
|
setError(null);
|
|
653
539
|
},
|
|
654
540
|
})
|
|
655
541
|
: null,
|
|
656
|
-
|
|
657
|
-
h(
|
|
658
|
-
key:
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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,
|
|
662
550
|
sessionId,
|
|
663
551
|
reload,
|
|
664
552
|
t,
|
|
665
|
-
|
|
553
|
+
// 条目一展开就该看见整棵树,所以每一层都摊开;
|
|
554
|
+
// 嫌长的用上面的「收起」逐层折。
|
|
555
|
+
defaultOpen: true,
|
|
666
556
|
}),
|
|
667
557
|
),
|
|
558
|
+
children.length === 0 && depth < 2
|
|
559
|
+
? h("div", { className: "dsh-ttf-hint" }, t("hint.noChildren"))
|
|
560
|
+
: null,
|
|
668
561
|
)
|
|
669
562
|
: null,
|
|
670
563
|
);
|
|
@@ -675,8 +568,8 @@ window.__ModuleLoader__.load({
|
|
|
675
568
|
/**
|
|
676
569
|
* 输入框上方的全宽条目。
|
|
677
570
|
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
571
|
+
* 收起时只有一行:图标 + 阶段标签 + 目标标题 + 进度,动作全在右侧的图标
|
|
572
|
+
* 按钮里(暂停 / 继续、清空、展开)。展开才画整棵树,并限高滚动。
|
|
680
573
|
*
|
|
681
574
|
* 会话没有计划时返回 null:内置的 todo / goal 条目不该因为我而挪位。
|
|
682
575
|
*/
|
|
@@ -685,6 +578,7 @@ window.__ModuleLoader__.load({
|
|
|
685
578
|
const sessionId = props.sessionId;
|
|
686
579
|
const [state, setState] = useState({ loading: true });
|
|
687
580
|
const [open, setOpen] = useState(false);
|
|
581
|
+
const [busy, setBusy] = useState(false);
|
|
688
582
|
|
|
689
583
|
const reload = useCallback(async () => {
|
|
690
584
|
if (!sessionId) {
|
|
@@ -704,52 +598,121 @@ window.__ModuleLoader__.load({
|
|
|
704
598
|
reload();
|
|
705
599
|
}, [reload]);
|
|
706
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
|
+
|
|
707
624
|
if (state.loading || state.empty || !state.data) return null;
|
|
708
625
|
|
|
709
626
|
const data = state.data;
|
|
710
|
-
const
|
|
627
|
+
const goal = data.plan.goal;
|
|
628
|
+
const stats = countNodes(goal);
|
|
711
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");
|
|
712
640
|
|
|
713
641
|
return h(
|
|
714
642
|
"div",
|
|
715
|
-
{
|
|
643
|
+
{ className: "dsh-ttf-dock", "aria-label": t("nav") },
|
|
716
644
|
h(
|
|
717
645
|
"div",
|
|
718
|
-
{
|
|
646
|
+
{ className: "dsh-ttf-panel" },
|
|
719
647
|
h(
|
|
720
|
-
"
|
|
721
|
-
{
|
|
722
|
-
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),
|
|
723
653
|
h(
|
|
724
654
|
"span",
|
|
725
|
-
{
|
|
655
|
+
{ className: "dsh-ttf-progress" },
|
|
726
656
|
stats.done + "/" + stats.total + (current ? " · " + current : ""),
|
|
727
657
|
),
|
|
728
|
-
h(
|
|
729
|
-
),
|
|
730
|
-
data.checkpoint ? h("span", { style: S.badgeWarn }, t("state.checkpoint")) : null,
|
|
731
|
-
),
|
|
732
|
-
open
|
|
733
|
-
? h(
|
|
658
|
+
h(
|
|
734
659
|
"div",
|
|
735
|
-
{
|
|
736
|
-
h(
|
|
737
|
-
|
|
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(
|
|
738
684
|
"div",
|
|
739
|
-
{
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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,
|
|
744
707
|
sessionId,
|
|
745
708
|
reload,
|
|
746
709
|
t,
|
|
747
|
-
//
|
|
748
|
-
defaultOpen:
|
|
710
|
+
// 展开条目就是为了看树,所以第一层默认摊开。
|
|
711
|
+
defaultOpen: true,
|
|
749
712
|
}),
|
|
750
|
-
)
|
|
751
|
-
|
|
752
|
-
|
|
713
|
+
)
|
|
714
|
+
: null,
|
|
715
|
+
),
|
|
753
716
|
);
|
|
754
717
|
}
|
|
755
718
|
|