dsh-tree-task-flow 0.3.13 → 0.3.15
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 +157 -91
- package/client.js +629 -512
- package/config.example.yml +13 -6
- package/cordis.patch.yml +5 -3
- package/lib/commands.js +91 -18
- package/lib/http.js +228 -47
- package/lib/index.js +24 -16
- package/lib/interrupt.js +18 -0
- package/lib/notes.js +173 -0
- package/lib/pause.js +84 -15
- package/lib/plan.js +48 -0
- package/lib/prompt.js +20 -4
- package/lib/region.js +375 -58
- package/lib/render.js +311 -0
- package/lib/requirement.js +914 -0
- package/lib/settings.js +53 -0
- package/lib/store.js +91 -7
- package/lib/tools.js +602 -111
- package/package.json +2 -2
package/client.js
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-tree-task-flow
|
|
2
|
+
* dsh-tree-task-flow 的客户端:输入框上方的需求条。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 所以用 `h()` 而不是 JSX。
|
|
4
|
+
* 这是**手写**的模块包装格式,不经过打包,所以用 `h()` 而不是 JSX。
|
|
6
5
|
*
|
|
7
6
|
* 挂载点是 `conversation.input.dock`——输入框上方的全宽条目区,内置的任务清单
|
|
8
|
-
* (`todo`)与目标(`goal`)就在那里。这是一个 `list`
|
|
9
|
-
*
|
|
7
|
+
* (`todo`)与目标(`goal`)就在那里。这是一个 `list` 槽:用自己的 id 注册就会
|
|
8
|
+
* 并列加在它们旁边,不会替换任何一个。
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
* 面板(TodoPanel)那样的卡片。图标与 Tooltip 直接用外壳播种的 UI 基座
|
|
13
|
-
* `@deepseek-ai/dsh-client-ui-primitives`,尺寸与颜色变量也都取它那一套,
|
|
14
|
-
* 这样并排放在输入框上方时看起来是一家。
|
|
10
|
+
* 这一条只管三件事(见 `docs/09-需求与任务模型.md` §5):
|
|
15
11
|
*
|
|
16
|
-
*
|
|
12
|
+
* 1. **看**:需求两个列表(活跃 / 已归档)+ 待确认的草稿,当前进行中的那条就地
|
|
13
|
+
* 展开成任务与步骤;
|
|
14
|
+
* 2. **改**:用户改需求正文(标题 / 简介 / 详情)→ 点「确认变更」→ 落成**暂定版本**,
|
|
15
|
+
* 等 AI 确认。改的时候**不动**正在生效的正文;
|
|
16
|
+
* 3. **开始**:点某条需求的「开始」——这是能叫醒 AI 的三种动作之一(另外两种是
|
|
17
|
+
* 点「继续」与直接发消息)。
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* **归档按钮有一个刻意的延迟**:点了之后这一行**不动**,按钮变成「取消归档」,
|
|
20
|
+
* 反悔点一下就回去了;下次打开面板才按新状态重新分列表。防误点优先于即时一致。
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
* 请求之前等一个闸门,所以当前正在执行的工具会正常跑完,之后什么都不干;点
|
|
23
|
-
* 「继续」就放行,模型从原处接着跑,整个过程不往会话里插任何消息。
|
|
22
|
+
* 没有需求的会话**一个像素都不占**:读不到就返回 null,免得平白挤掉内置条目。
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* 另外挂在插件管理的**本插件自己的配置页** `plugins.bundle.config`(按 bundle 的
|
|
25
|
+
* 包名 `dsh-tree-task-flow` 作 key)——不是 DSH 设置面板里的一页标签:设置属于
|
|
26
|
+
* 这个插件,就该长在插件自己身上,从「插件」页点进这个 bundle 才看得到。
|
|
28
27
|
*/
|
|
29
28
|
window.__ModuleLoader__.load({
|
|
30
29
|
id: "dsh-tree-task-flow",
|
|
@@ -42,6 +41,11 @@ window.__ModuleLoader__.load({
|
|
|
42
41
|
const NS = "dsh-tree-task-flow";
|
|
43
42
|
/** 条目 id。用自己的 id,所以只会加在内置条目旁边。 */
|
|
44
43
|
const ENTRY_ID = "dsh-tree-task-flow";
|
|
44
|
+
/**
|
|
45
|
+
* 本插件作为 bundle 的包名——`plugins.bundle.config` 的 key 用它,
|
|
46
|
+
* 跟 locale 命名空间 `NS` 是两个不同的字符串(那是文案的命名空间)。
|
|
47
|
+
*/
|
|
48
|
+
const BUNDLE_PACKAGE = "dsh-tree-task-flow";
|
|
45
49
|
|
|
46
50
|
let translate = (key) => key;
|
|
47
51
|
const useT = () => translate;
|
|
@@ -49,10 +53,8 @@ window.__ModuleLoader__.load({
|
|
|
49
53
|
// ------------------------------------------------------------ 内置基座
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
|
-
* 内置 UI
|
|
53
|
-
*
|
|
54
|
-
* 仍然兜一层 try——万一这版外壳没有 primitives,条目该降级成文字,
|
|
55
|
-
* 而不是整块白掉。
|
|
56
|
+
* 内置 UI 基座。外壳启动时把这张表播种进模块表,所以这里的 require 正常一定
|
|
57
|
+
* 成功;仍然兜一层 try——万一这版外壳没有 primitives,条目该降级成文字。
|
|
56
58
|
*/
|
|
57
59
|
let primitives = {};
|
|
58
60
|
try {
|
|
@@ -61,14 +63,6 @@ window.__ModuleLoader__.load({
|
|
|
61
63
|
primitives = {};
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
/**
|
|
65
|
-
* 内置图标名有两套写法,这里两套都认。
|
|
66
|
-
*
|
|
67
|
-
* 旧外壳把尺寸写进名字:`IconCheckOutline16`、`IconChevronDownOutline14`;
|
|
68
|
-
* 升级后的外壳把名字里的尺寸换成笔画粗细:`IconCheckOutlineRegular`、
|
|
69
|
-
* `IconCheckOutlineMedium`,尺寸改由 `size` 属性给。按「尺寸后缀 → 粗细后缀
|
|
70
|
-
* → 裸名」的顺序找,两套外壳都取得回图标,不会平白降级成文字。
|
|
71
|
-
*/
|
|
72
66
|
function findIcon(base, size) {
|
|
73
67
|
const candidates = [base + size, base + "14", base + "16", base + "Regular", base + "Medium", base];
|
|
74
68
|
for (const candidate of candidates) {
|
|
@@ -78,26 +72,18 @@ window.__ModuleLoader__.load({
|
|
|
78
72
|
return null;
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
/** 取一个内置图标;这版外壳没有就返回 null,由调用方决定怎么退。 */
|
|
82
75
|
function icon(base, size) {
|
|
83
76
|
const component = findIcon(base, size);
|
|
84
77
|
if (component === null) return null;
|
|
85
78
|
return h(component, { size });
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
/** 用内置 Tooltip 包一层;没有 Tooltip 就原样返回。 */
|
|
89
81
|
function withTooltip(label, node) {
|
|
90
82
|
const Tooltip = primitives.Tooltip;
|
|
91
83
|
if (typeof Tooltip !== "function") return node;
|
|
92
84
|
return h(Tooltip, { label, side: "bottom", delayMs: 500 }, node);
|
|
93
85
|
}
|
|
94
86
|
|
|
95
|
-
/**
|
|
96
|
-
* 图标按钮:有图标用图标,没图标退回短文字,保证永远点得到。
|
|
97
|
-
*
|
|
98
|
-
* 退回文字时换成 `dsh-ttf-iconBtnText`:按钮按文字宽度自己撑开、文字长了
|
|
99
|
-
* 用省略号收住,所以并排的几个按钮不会挤成一团互相压。
|
|
100
|
-
*/
|
|
101
87
|
function IconButton({ iconBase, label, size, disabled, onClick }) {
|
|
102
88
|
const glyph = icon(iconBase, typeof size === "number" ? size : 16);
|
|
103
89
|
return withTooltip(
|
|
@@ -136,23 +122,17 @@ window.__ModuleLoader__.load({
|
|
|
136
122
|
try {
|
|
137
123
|
data = text ? JSON.parse(text) : null;
|
|
138
124
|
} catch {
|
|
139
|
-
throw new Error(
|
|
140
|
-
response.status + " 返回了非 JSON:" + String(text).slice(0, 120),
|
|
141
|
-
);
|
|
125
|
+
throw new Error(response.status + " 返回了非 JSON:" + String(text).slice(0, 120));
|
|
142
126
|
}
|
|
143
127
|
if (!response.ok) {
|
|
144
128
|
const error = new Error((data && data.error) || response.status + " " + response.statusText);
|
|
145
|
-
// 调用方要能区分「这个会话没有计划」(正常)与真出错。
|
|
146
129
|
error.status = response.status;
|
|
147
130
|
throw error;
|
|
148
131
|
}
|
|
149
132
|
return data;
|
|
150
133
|
}
|
|
151
134
|
|
|
152
|
-
/**
|
|
153
|
-
* 写操作。host 用 HTTP 200 携带 `{ ok: false, error }` 表示业务拒绝
|
|
154
|
-
* (比如完成时没给结果),所以这里要把 `ok` 也当错误判一次。
|
|
155
|
-
*/
|
|
135
|
+
/** 写操作。host 用 HTTP 200 携带 `{ ok: false, error }` 表示业务拒绝,所以要把 ok 也判一次。 */
|
|
156
136
|
async function post(body) {
|
|
157
137
|
const data = await api("/dsh-tree-task-flow/action", {
|
|
158
138
|
method: "POST",
|
|
@@ -163,24 +143,28 @@ window.__ModuleLoader__.load({
|
|
|
163
143
|
return data;
|
|
164
144
|
}
|
|
165
145
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
146
|
+
// ---------------------------------------------------------------- 数据
|
|
147
|
+
|
|
148
|
+
/** 面板要的三组数据;接口缺字段时一律当空,不让界面炸。 */
|
|
149
|
+
function listsOf(data) {
|
|
150
|
+
const lists = (data && data.requirements) || {};
|
|
151
|
+
return {
|
|
152
|
+
active: Array.isArray(lists.active) ? lists.active : [],
|
|
153
|
+
archived: Array.isArray(lists.archived) ? lists.archived : [],
|
|
154
|
+
drafts: Array.isArray(lists.drafts) ? lists.drafts : [],
|
|
155
|
+
};
|
|
174
156
|
}
|
|
175
157
|
|
|
176
|
-
/**
|
|
177
|
-
function
|
|
178
|
-
let total =
|
|
179
|
-
let done =
|
|
180
|
-
for (const task of
|
|
158
|
+
/** 一条需求的进度分数:任务与步骤都算,放弃的两边都不算。 */
|
|
159
|
+
function countOf(row) {
|
|
160
|
+
let total = 0;
|
|
161
|
+
let done = 0;
|
|
162
|
+
for (const task of row.tasks || []) {
|
|
163
|
+
if (task.status === "dropped") continue;
|
|
181
164
|
total += 1;
|
|
182
165
|
if (task.status === "done") done += 1;
|
|
183
166
|
for (const step of task.steps || []) {
|
|
167
|
+
if (step.status === "dropped") continue;
|
|
184
168
|
total += 1;
|
|
185
169
|
if (step.status === "done") done += 1;
|
|
186
170
|
}
|
|
@@ -188,60 +172,12 @@ window.__ModuleLoader__.load({
|
|
|
188
172
|
return { total, done };
|
|
189
173
|
}
|
|
190
174
|
|
|
191
|
-
/**
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
function focusGoal(data) {
|
|
198
|
-
const goals = goalsOf(data);
|
|
199
|
-
if (goals.length === 0) return null;
|
|
200
|
-
const here = goals.find((each) => each.id === data?.active?.goalId);
|
|
201
|
-
if (here) return here;
|
|
202
|
-
return (
|
|
203
|
-
goals.find((each) => each.status !== "done" && each.status !== "dropped") ||
|
|
204
|
-
goals[goals.length - 1]
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** 当前活动节点的标题:子任务优先,其次任务,最后目标。 */
|
|
209
|
-
function currentLabel(data) {
|
|
210
|
-
const active = data.active;
|
|
211
|
-
if (!active) return "";
|
|
212
|
-
const goal = focusGoal(data);
|
|
213
|
-
if (!goal) return "";
|
|
214
|
-
const task = (goal.tasks || []).find((each) => each.id === active.taskId);
|
|
215
|
-
if (!task) return goal.title;
|
|
216
|
-
const step = (task.steps || []).find((each) => each.id === active.stepId);
|
|
217
|
-
return step ? step.title : task.title;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** 活动路径上三层各自的 id。节点 id 全局唯一,所以一个集合就够判"是不是当前节点"。 */
|
|
221
|
-
function currentIds(data) {
|
|
222
|
-
const active = data.active;
|
|
223
|
-
return new Set(
|
|
224
|
-
[active?.goalId, active?.taskId, active?.stepId].filter(
|
|
225
|
-
(each) => typeof each === "string" && each !== "",
|
|
226
|
-
),
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 面板一打开该摊开哪些节点:活动路径上的三层,加上检查点节点以及它的祖先。
|
|
232
|
-
*
|
|
233
|
-
* 其余节点一律收起——正在走的那条路径本来就窄,摊平整棵树只会让人翻半天。
|
|
234
|
-
* 检查点落在某个任务上时,目标也得一起摊开,否则连那一行都看不见。
|
|
235
|
-
*/
|
|
236
|
-
function defaultOpenIds(data) {
|
|
237
|
-
const ids = currentIds(data);
|
|
238
|
-
const checkpoint = data.checkpoint;
|
|
239
|
-
if (checkpoint !== null && checkpoint !== undefined && typeof checkpoint.id === "string") {
|
|
240
|
-
ids.add(checkpoint.id);
|
|
241
|
-
const goalId = focusGoal(data)?.id;
|
|
242
|
-
if (checkpoint.level !== "goal" && typeof goalId === "string") ids.add(goalId);
|
|
243
|
-
}
|
|
244
|
-
return ids;
|
|
175
|
+
/** 状态的显示名。 */
|
|
176
|
+
function statusKey(status) {
|
|
177
|
+
if (status === "draft") return "status.draft";
|
|
178
|
+
if (status === "done") return "status.done";
|
|
179
|
+
if (status === "dropped") return "status.dropped";
|
|
180
|
+
return "status.open";
|
|
245
181
|
}
|
|
246
182
|
|
|
247
183
|
// ---------------------------------------------------------------- 文案
|
|
@@ -250,81 +186,114 @@ window.__ModuleLoader__.load({
|
|
|
250
186
|
nav: "树形任务流",
|
|
251
187
|
"state.running": "进行中",
|
|
252
188
|
"state.paused": "已暂停",
|
|
253
|
-
"state.
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
189
|
+
"state.idle": "待开始",
|
|
190
|
+
"status.open": "未完成",
|
|
191
|
+
"status.done": "已完成",
|
|
192
|
+
"status.dropped": "已放弃",
|
|
193
|
+
"status.draft": "草稿",
|
|
194
|
+
"label.requirements": "{count} 条需求",
|
|
195
|
+
"label.archived": "已归档",
|
|
196
|
+
"label.drafts": "待确认",
|
|
197
|
+
"label.current": "当前",
|
|
198
|
+
"label.missing": "缺前置",
|
|
199
|
+
"label.pending": "待确认变动",
|
|
200
|
+
"label.detail": "详情",
|
|
201
|
+
"label.tasks": "任务",
|
|
202
|
+
"label.summary": "总结",
|
|
257
203
|
"action.pause": "暂停",
|
|
258
204
|
"action.resume": "继续",
|
|
259
|
-
"action.reset": "清空计划",
|
|
260
205
|
"action.expand": "展开",
|
|
261
206
|
"action.collapse": "收起",
|
|
262
|
-
"action.
|
|
263
|
-
"action.
|
|
264
|
-
"action.
|
|
265
|
-
"action.
|
|
266
|
-
"action.
|
|
207
|
+
"action.start": "开始这条需求",
|
|
208
|
+
"action.archive": "归档",
|
|
209
|
+
"action.unarchive": "取消归档",
|
|
210
|
+
"action.open": "看详情",
|
|
211
|
+
"action.edit": "改需求",
|
|
212
|
+
"action.submitChange": "确认变更",
|
|
213
|
+
"action.newRequirement": "新建需求",
|
|
214
|
+
"action.add": "记下来",
|
|
267
215
|
"action.cancel": "取消",
|
|
268
|
-
"
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
216
|
+
"note.paused": "已暂停:正在跑的工具会跑完,之后不再往下走。点「继续」从原处接着跑。",
|
|
217
|
+
"note.archiveDeferred": "点了不会立刻移动:这一行留在原地,下次打开面板才按新状态分开。反悔就再点一下。",
|
|
218
|
+
"note.draft": "草稿还不算真需求。跟 AI 把「要什么」说清楚,它确认之后才会转正。",
|
|
219
|
+
"note.pending": "这条需求有用户提交的改动,等 AI 确认。确认之前仍按旧版本走。",
|
|
220
|
+
"note.edit": "改完点「确认变更」提交成暂定版本,AI 确认之后才生效。",
|
|
221
|
+
"note.noTasks": "还没有任务,让 AI 拆。",
|
|
222
|
+
"placeholder.title": "一句话说清要做什么(不超过 50 字)",
|
|
223
|
+
"placeholder.intro": "简介(不超过 200 字)",
|
|
224
|
+
"placeholder.detail": "详情,可以很长",
|
|
225
|
+
"settings.loading": "正在读设置…",
|
|
226
|
+
"settings.unavailable": "读不到设置:宿主那一半没在跑,或者接口不通。",
|
|
227
|
+
"settings.off": "插件在 profile 配置里被关掉了(enabled: false),这两个开关现在不起作用。",
|
|
228
|
+
"setting.note": "改完立刻生效,也会写进状态目录,重启 dsh web 之后还在。",
|
|
229
|
+
"setting.autoContinue": "任务没做完、模型自己停下时,自动把它叫起来接着跑",
|
|
230
|
+
"setting.autoContinueNote": "会往会话里推一条消息。默认关。",
|
|
231
|
+
"setting.interruptRecovery": "工具出错或网络断了,自动重试把会话拉起来",
|
|
232
|
+
"setting.interruptRecoveryNote": "按 1 / 5 / 10 / 30 分钟各拉一次,一直拉不起来就停手。你按过停止的绝不拉起。",
|
|
278
233
|
};
|
|
279
234
|
|
|
280
235
|
const en = {
|
|
281
|
-
nav: "
|
|
236
|
+
nav: "Task tree",
|
|
282
237
|
"state.running": "In progress",
|
|
283
238
|
"state.paused": "Paused",
|
|
284
|
-
"state.
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
|
|
288
|
-
"
|
|
289
|
-
|
|
239
|
+
"state.idle": "Not started",
|
|
240
|
+
"status.open": "Open",
|
|
241
|
+
"status.done": "Done",
|
|
242
|
+
"status.dropped": "Dropped",
|
|
243
|
+
"status.draft": "Draft",
|
|
244
|
+
"label.requirements": "{count} requirements",
|
|
245
|
+
"label.archived": "Archived",
|
|
246
|
+
"label.drafts": "Waiting for you",
|
|
247
|
+
"label.current": "current",
|
|
248
|
+
"label.missing": "waiting on",
|
|
249
|
+
"label.pending": "pending change",
|
|
250
|
+
"label.detail": "Detail",
|
|
251
|
+
"label.tasks": "Tasks",
|
|
252
|
+
"label.summary": "Summary",
|
|
290
253
|
"action.pause": "Pause",
|
|
291
254
|
"action.resume": "Resume",
|
|
292
|
-
"action.reset": "Clear plan",
|
|
293
255
|
"action.expand": "Expand",
|
|
294
256
|
"action.collapse": "Collapse",
|
|
295
|
-
"action.
|
|
296
|
-
"action.
|
|
297
|
-
"action.
|
|
298
|
-
"action.
|
|
299
|
-
"action.
|
|
257
|
+
"action.start": "Start this requirement",
|
|
258
|
+
"action.archive": "Archive",
|
|
259
|
+
"action.unarchive": "Unarchive",
|
|
260
|
+
"action.open": "Show detail",
|
|
261
|
+
"action.edit": "Edit",
|
|
262
|
+
"action.submitChange": "Submit change",
|
|
263
|
+
"action.newRequirement": "New requirement",
|
|
264
|
+
"action.add": "Save",
|
|
300
265
|
"action.cancel": "Cancel",
|
|
301
|
-
"
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
266
|
+
"note.paused": "Paused: the running tool finishes, then nothing else happens. Resume picks up exactly where it stopped.",
|
|
267
|
+
"note.archiveDeferred": "Nothing moves right away: the row stays put and the next time you open the panel it shows up in the other list. Click again to undo.",
|
|
268
|
+
"note.draft": "A draft is not a requirement yet. Describe what you want to the model; it becomes real once confirmed.",
|
|
269
|
+
"note.pending": "This requirement has a change submitted by you, waiting for the model to confirm. The old version is still in force.",
|
|
270
|
+
"note.edit": "Submit the change and it becomes a provisional version; it takes effect once the model confirms it.",
|
|
271
|
+
"note.noTasks": "No tasks yet — the model breaks it down.",
|
|
272
|
+
"placeholder.title": "One line: what should be done (<= 50 chars)",
|
|
273
|
+
"placeholder.intro": "Short intro (<= 200 chars)",
|
|
274
|
+
"placeholder.detail": "Detail, may be long",
|
|
275
|
+
"settings.loading": "Reading settings…",
|
|
276
|
+
"settings.unavailable": "Settings are unreachable: the host half is not running, or the endpoint is blocked.",
|
|
277
|
+
"settings.off": "The plugin is switched off in the profile config (enabled: false), so these switches do nothing right now.",
|
|
278
|
+
"setting.note": "Takes effect right away and is written to the state directory, so it survives a restart.",
|
|
279
|
+
"setting.autoContinue": "When the plan is unfinished and the model stops, wake it up to carry on",
|
|
280
|
+
"setting.autoContinueNote": "This pushes a message into the session. Off by default.",
|
|
281
|
+
"setting.interruptRecovery": "After a tool error or a dropped connection, retry to get the session going",
|
|
282
|
+
"setting.interruptRecoveryNote": "Retries at 1 / 5 / 10 / 30 minutes, then gives up. A session you stopped is never revived.",
|
|
311
283
|
};
|
|
312
284
|
|
|
313
|
-
|
|
285
|
+
/** 插件配置页上的两个开关。 */
|
|
286
|
+
const SETTING_ROWS = [
|
|
287
|
+
{ key: "autoContinue", note: "setting.autoContinueNote" },
|
|
288
|
+
{ key: "interruptRecovery", note: "setting.interruptRecoveryNote" },
|
|
289
|
+
];
|
|
314
290
|
|
|
315
|
-
|
|
316
|
-
* 样式照内置的 dock 条目写:宽高、间距、圆角、颜色变量都取
|
|
317
|
-
* `dsh-client-ui-goal` 的 GoalBar 与内置待办面板 TodoPanel 的同一套值。
|
|
318
|
-
*
|
|
319
|
-
* 注入方式也照它们:一个带 `data-plugin-css` 的 <style> 标签——
|
|
320
|
-
* 客户端 HMR 就是靠这个属性在插件卸载时收走样式的。
|
|
321
|
-
*/
|
|
322
|
-
const CSS_TAG = "dsh-tree-task-flow/Dock.module.css";
|
|
291
|
+
// ---------------------------------------------------------------- 样式
|
|
323
292
|
|
|
324
293
|
const CSS = [
|
|
325
|
-
//
|
|
294
|
+
// 外壳与宽度:**必须照抄内置条目的宽度公式**。漏掉它,这一条会按父容器的
|
|
295
|
+
// 宽度铺满整个输入区(实测就是这样占满全屏的)。
|
|
326
296
|
".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)}",
|
|
327
|
-
// 卡片:背景与圆角同内置条目,描边用 ::after 画(.5px 在缩放屏上才不糊)。
|
|
328
297
|
".dsh-ttf-panel{background:var(--dsw-specific-tip);border-radius:12px;position:relative;overflow:hidden}",
|
|
329
298
|
'.dsh-ttf-panel:after{border:.5px solid var(--dsw-alias-border-l1);border-radius:inherit;content:"";pointer-events:none;position:absolute;inset:0}',
|
|
330
299
|
".dsh-ttf-bar{box-sizing:border-box;width:100%;height:36px;display:flex;align-items:center;gap:10px;padding:4px 5px 4px 12px}",
|
|
@@ -335,347 +304,308 @@ window.__ModuleLoader__.load({
|
|
|
335
304
|
".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}",
|
|
336
305
|
".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}",
|
|
337
306
|
".dsh-ttf-actions{flex:none;display:flex;align-items:center;gap:2px}",
|
|
338
|
-
".dsh-ttf-iconBtn{
|
|
339
|
-
".dsh-ttf-iconBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
|
|
307
|
+
".dsh-ttf-iconBtn{width:28px;height:28px;border:none;background:0 0;border-radius:999px;color:var(--dsw-alias-label-tertiary);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0;font:inherit;flex:none}",
|
|
308
|
+
".dsh-ttf-iconBtn:hover:enabled{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
|
|
340
309
|
".dsh-ttf-iconBtn:disabled{opacity:.4;cursor:default}",
|
|
341
|
-
".dsh-ttf-iconBtn:disabled:hover{background:0 0;color:var(--dsw-alias-label-tertiary)}",
|
|
342
|
-
// 取不到内置图标时退回文字:固定 28px 宽装不下几个字,文字会溢出到相邻
|
|
343
|
-
// 按钮上,看着就是几个按钮叠在一起。这里按文字宽度撑开、留出边距,
|
|
344
|
-
// 并给一个上限,长了用省略号收住——几个按钮始终各占各的位置。
|
|
345
310
|
".dsh-ttf-iconBtnText{width:auto;min-width:28px;max-width:112px;height:24px;padding:0 8px}",
|
|
346
311
|
".dsh-ttf-iconText{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;font-size:12px;line-height:16px;white-space:nowrap}",
|
|
347
|
-
// 展开体:限高滚动——这是输入框旁边的位置,不能让它把会话挤没了。
|
|
348
312
|
".dsh-ttf-body{max-height:264px;overflow-y:auto;padding:2px 8px 8px}",
|
|
349
313
|
".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)}",
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
".dsh-ttf-node{position:relative}",
|
|
353
|
-
// 同一层的相邻节点之间画一条分界线——任务与任务的分界就靠它。
|
|
354
|
-
".dsh-ttf-node+.dsh-ttf-node{border-top:1px solid var(--dsw-alias-border-l1);margin-top:2px;padding-top:2px}",
|
|
355
|
-
".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}",
|
|
356
|
-
".dsh-ttf-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
314
|
+
".dsh-ttf-section{margin:8px 4px 2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
315
|
+
".dsh-ttf-row{position:relative;box-sizing:border-box;display:flex;flex-direction:column;gap:2px;padding:6px 4px;border-radius:8px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}",
|
|
357
316
|
".dsh-ttf-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
358
|
-
|
|
359
|
-
".dsh-ttf-rowTitle{min-width:0;flex:1;white-space:pre-wrap;word-break:break-word}",
|
|
360
|
-
".dsh-ttf-
|
|
361
|
-
".dsh-ttf-
|
|
362
|
-
".dsh-ttf-
|
|
363
|
-
".dsh-ttf-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
"@keyframes dsh-ttf-spin{to{transform:rotate(360deg)}}",
|
|
367
|
-
".dsh-ttf-markSpin{animation:dsh-ttf-spin 1s linear infinite;transform-origin:50% 50%}",
|
|
368
|
-
".dsh-ttf-done{color:var(--dsw-alias-label-tertiary);text-decoration:line-through}",
|
|
369
|
-
// 三层缩进。
|
|
370
|
-
// 子节点:一条竖线 + 每个子节点一个拐角,把三层画成看得见的树。
|
|
371
|
-
".dsh-ttf-depth1,.dsh-ttf-depth2{margin-left:11px;padding-left:9px;border-left:1px solid var(--dsw-alias-border-l2)}",
|
|
372
|
-
'.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)}',
|
|
373
|
-
// 详情里的每一块(说明 / 结果 / 表单)自成一块,块间留白 + 描边,分得开。
|
|
374
|
-
".dsh-ttf-detail>*+*{border-top:.5px solid var(--dsw-alias-border-l1);padding-top:6px}",
|
|
375
|
-
".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)}",
|
|
317
|
+
".dsh-ttf-rowHead{display:flex;align-items:center;gap:8px;min-width:0}",
|
|
318
|
+
".dsh-ttf-rowTitle{min-width:0;flex:1;white-space:pre-wrap;word-break:break-word;cursor:pointer}",
|
|
319
|
+
".dsh-ttf-rowIntro{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}",
|
|
320
|
+
".dsh-ttf-pill{flex:none;border:.5px solid var(--dsw-alias-border-l4);border-radius:999px;padding:0 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}",
|
|
321
|
+
".dsh-ttf-pillDone{border-color:var(--dsw-alias-state-success-primary,#1a7f37);color:var(--dsw-alias-state-success-primary,#1a7f37)}",
|
|
322
|
+
".dsh-ttf-pillDropped{opacity:.6;text-decoration:line-through}",
|
|
323
|
+
".dsh-ttf-pillDraft{color:var(--dsw-alias-state-warning-primary,#a06800);border-color:var(--dsw-alias-state-warning-primary,#a06800)}",
|
|
324
|
+
".dsh-ttf-tag{flex:none;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)}",
|
|
376
325
|
".dsh-ttf-tagWarn{color:var(--dsw-alias-state-warning-primary,#a06800);background:var(--dsw-alias-state-warning-bg,#ffcc002e)}",
|
|
377
|
-
// 展开详情:说明、结果、完成表单。
|
|
378
326
|
".dsh-ttf-detail{display:flex;flex-direction:column;gap:6px;padding:2px 4px 4px}",
|
|
379
|
-
".dsh-ttf-
|
|
327
|
+
".dsh-ttf-detail>*+*{border-top:.5px solid var(--dsw-alias-border-l1);padding-top:6px}",
|
|
380
328
|
".dsh-ttf-detailText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}",
|
|
381
|
-
|
|
382
|
-
".dsh-ttf-
|
|
383
|
-
".dsh-ttf-
|
|
384
|
-
".dsh-ttf-resultOpen{display:block;max-height:220px;overflow-y:auto}",
|
|
385
|
-
".dsh-ttf-form{margin:6px 0 2px}",
|
|
386
|
-
".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}",
|
|
387
|
-
".dsh-ttf-input:focus{border-color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
388
|
-
".dsh-ttf-input::placeholder{color:var(--dsw-alias-label-caption,#00000066)}",
|
|
389
|
-
".dsh-ttf-formBar{margin-top:6px;display:flex;align-items:center;gap:6px}",
|
|
390
|
-
".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}",
|
|
391
|
-
".dsh-ttf-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
329
|
+
".dsh-ttf-list{margin:2px 0;padding-left:18px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}",
|
|
330
|
+
".dsh-ttf-btn{box-sizing:border-box;height:26px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer;flex:none}",
|
|
331
|
+
".dsh-ttf-btn:hover:enabled{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
392
332
|
".dsh-ttf-btn:disabled{opacity:.4;cursor:default}",
|
|
393
|
-
".dsh-ttf-
|
|
394
|
-
".dsh-ttf-
|
|
395
|
-
".dsh-ttf-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
333
|
+
".dsh-ttf-form{display:flex;flex-direction:column;gap:6px;margin:6px 0 2px}",
|
|
334
|
+
".dsh-ttf-input{box-sizing:border-box;width:100%;height:28px;padding:0 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:inherit;font-size:12px;outline:none}",
|
|
335
|
+
".dsh-ttf-textarea{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}",
|
|
336
|
+
".dsh-ttf-input:focus,.dsh-ttf-textarea:focus{border-color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
337
|
+
".dsh-ttf-input::placeholder,.dsh-ttf-textarea::placeholder{color:var(--dsw-alias-label-caption,#00000066)}",
|
|
338
|
+
".dsh-ttf-formActions{margin-top:2px;display:flex;align-items:center;justify-content:flex-end;gap:6px}",
|
|
339
|
+
".dsh-ttf-settingsPage{display:flex;flex-direction:column;gap:10px;padding:4px 2px;max-width:640px}",
|
|
340
|
+
".dsh-ttf-settings{display:flex;flex-direction:column;gap:12px}",
|
|
341
|
+
".dsh-ttf-setting{display:flex;align-items:flex-start;gap:10px}",
|
|
342
|
+
".dsh-ttf-settingText{display:flex;flex-direction:column;gap:2px;min-width:0}",
|
|
343
|
+
".dsh-ttf-settingName{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}",
|
|
344
|
+
".dsh-ttf-settingNote{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
345
|
+
".dsh-ttf-switch{box-sizing:border-box;flex:none;position:relative;width:32px;height:18px;margin-top:1px;padding:0;border:.5px solid var(--dsw-alias-border-l4);border-radius:999px;background:var(--dsw-alias-bg-module-platform);cursor:pointer}",
|
|
346
|
+
".dsh-ttf-switchOn{background:var(--dsw-alias-state-business-primary,#4d6bfe);border-color:transparent}",
|
|
347
|
+
".dsh-ttf-switchKnob{position:absolute;top:1px;left:1px;width:14px;height:14px;border-radius:999px;background:#fff;transition:left .12s}",
|
|
348
|
+
".dsh-ttf-switchOn .dsh-ttf-switchKnob{left:15px}",
|
|
349
|
+
].join("\n");
|
|
350
|
+
|
|
351
|
+
function ensureStyles() {
|
|
352
|
+
const id = "dsh-tree-task-flow:css";
|
|
353
|
+
if (typeof document === "undefined" || document === null) return;
|
|
354
|
+
if (typeof document.createElement !== "function") return;
|
|
355
|
+
try {
|
|
356
|
+
// 自检里的假 document 只有 createElement:取不到已有节点就当作没有。
|
|
357
|
+
if (typeof document.getElementById === "function" && document.getElementById(id) !== null) return;
|
|
358
|
+
const style = document.createElement("style");
|
|
359
|
+
style.id = id;
|
|
360
|
+
style.textContent = CSS;
|
|
361
|
+
if (document.head && typeof document.head.appendChild === "function") {
|
|
362
|
+
document.head.appendChild(style);
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
// 样式注入失败不该拖垮条目:没有样式也还能用。
|
|
424
366
|
}
|
|
425
|
-
const circle = { cx: 7, cy: 7, r: 5.2, stroke: "currentColor", strokeWidth: 1.2 };
|
|
426
|
-
if (isCurrent) circle.strokeDasharray = "2.4 2.4";
|
|
427
|
-
return h(
|
|
428
|
-
"span",
|
|
429
|
-
{ className: "dsh-ttf-markCell" },
|
|
430
|
-
h(
|
|
431
|
-
"svg",
|
|
432
|
-
{
|
|
433
|
-
width: 14,
|
|
434
|
-
height: 14,
|
|
435
|
-
viewBox: "0 0 14 14",
|
|
436
|
-
fill: "none",
|
|
437
|
-
"aria-hidden": true,
|
|
438
|
-
className: isCurrent ? "dsh-ttf-markSpin" : undefined,
|
|
439
|
-
},
|
|
440
|
-
h("circle", circle),
|
|
441
|
-
),
|
|
442
|
-
);
|
|
443
367
|
}
|
|
444
368
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
className:
|
|
458
|
-
"dsh-ttf-result" +
|
|
459
|
-
(expandable ? " dsh-ttf-resultClick" : "") +
|
|
460
|
-
(open ? " dsh-ttf-resultOpen" : ""),
|
|
461
|
-
role: expandable ? "button" : undefined,
|
|
462
|
-
tabIndex: expandable ? 0 : undefined,
|
|
463
|
-
title: expandable ? t(open ? "action.collapseResult" : "action.expandResult") : undefined,
|
|
464
|
-
onClick: expandable
|
|
465
|
-
? () => setOpen(!open)
|
|
466
|
-
: undefined,
|
|
467
|
-
},
|
|
468
|
-
body,
|
|
469
|
-
);
|
|
369
|
+
// ---------------------------------------------------------------- 组件
|
|
370
|
+
|
|
371
|
+
/** 状态那枚小药丸。 */
|
|
372
|
+
function StatusPill({ row, t }) {
|
|
373
|
+
const key = statusKey(row.status);
|
|
374
|
+
const extra =
|
|
375
|
+
row.status === "done"
|
|
376
|
+
? " dsh-ttf-pillDone"
|
|
377
|
+
: row.status === "dropped"
|
|
378
|
+
? " dsh-ttf-pillDropped"
|
|
379
|
+
: "";
|
|
380
|
+
return h("span", { className: "dsh-ttf-pill" + extra }, t(key));
|
|
470
381
|
}
|
|
471
382
|
|
|
472
|
-
/**
|
|
473
|
-
function
|
|
474
|
-
const [
|
|
383
|
+
/** 编辑态:标题 / 简介 / 详情 + 「确认变更」。 */
|
|
384
|
+
function EditForm({ row, onSubmit, onCancel, busy, t }) {
|
|
385
|
+
const [title, setTitle] = useState(row.title || "");
|
|
386
|
+
const [intro, setIntro] = useState(row.intro || "");
|
|
387
|
+
const [detail, setDetail] = useState(row.detail || "");
|
|
388
|
+
const [change, setChange] = useState("");
|
|
475
389
|
return h(
|
|
476
390
|
"div",
|
|
477
391
|
{ className: "dsh-ttf-form" },
|
|
392
|
+
h("input", {
|
|
393
|
+
className: "dsh-ttf-input",
|
|
394
|
+
value: title,
|
|
395
|
+
maxLength: 80,
|
|
396
|
+
placeholder: t("placeholder.title"),
|
|
397
|
+
onChange: (event) => setTitle(event.target.value),
|
|
398
|
+
}),
|
|
399
|
+
h("input", {
|
|
400
|
+
className: "dsh-ttf-input",
|
|
401
|
+
value: intro,
|
|
402
|
+
maxLength: 240,
|
|
403
|
+
placeholder: t("placeholder.intro"),
|
|
404
|
+
onChange: (event) => setIntro(event.target.value),
|
|
405
|
+
}),
|
|
478
406
|
h("textarea", {
|
|
407
|
+
className: "dsh-ttf-textarea",
|
|
408
|
+
value: detail,
|
|
409
|
+
placeholder: t("placeholder.detail"),
|
|
410
|
+
onChange: (event) => setDetail(event.target.value),
|
|
411
|
+
}),
|
|
412
|
+
h("input", {
|
|
479
413
|
className: "dsh-ttf-input",
|
|
480
|
-
value:
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
onChange: (event) => setText(event.target.value),
|
|
414
|
+
value: change,
|
|
415
|
+
placeholder: "这次改了什么(会写进版本历史)",
|
|
416
|
+
onChange: (event) => setChange(event.target.value),
|
|
484
417
|
}),
|
|
485
|
-
|
|
418
|
+
h("div", { className: "dsh-ttf-note" }, t("note.edit")),
|
|
486
419
|
h(
|
|
487
420
|
"div",
|
|
488
|
-
{ className: "dsh-ttf-
|
|
421
|
+
{ className: "dsh-ttf-formActions" },
|
|
422
|
+
h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onCancel }, t("action.cancel")),
|
|
489
423
|
h(
|
|
490
424
|
"button",
|
|
491
425
|
{
|
|
492
426
|
type: "button",
|
|
493
427
|
className: "dsh-ttf-btn",
|
|
494
|
-
disabled: busy
|
|
495
|
-
onClick: () =>
|
|
428
|
+
disabled: busy,
|
|
429
|
+
onClick: () =>
|
|
430
|
+
onSubmit({ title, intro, detail, change }),
|
|
496
431
|
},
|
|
497
|
-
t("action.
|
|
432
|
+
t("action.submitChange"),
|
|
498
433
|
),
|
|
499
|
-
h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onCancel }, t("action.cancel")),
|
|
500
434
|
),
|
|
501
435
|
);
|
|
502
436
|
}
|
|
503
437
|
|
|
504
|
-
/**
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
438
|
+
/** 一条需求的详情(只读)。 */
|
|
439
|
+
function Detail({ row, t }) {
|
|
440
|
+
const tasks = row.tasks || [];
|
|
441
|
+
return h(
|
|
442
|
+
"div",
|
|
443
|
+
{ className: "dsh-ttf-detail" },
|
|
444
|
+
row.intro ? h("div", { className: "dsh-ttf-detailText" }, row.intro) : null,
|
|
445
|
+
row.detail
|
|
446
|
+
? h("div", null, h("div", { className: "dsh-ttf-section" }, t("label.detail")), h("div", { className: "dsh-ttf-detailText" }, row.detail))
|
|
447
|
+
: null,
|
|
448
|
+
row.missing && row.missing.length > 0
|
|
449
|
+
? h("div", { className: "dsh-ttf-note" }, t("label.missing") + ":" + row.missing.map((each) => each.id).join("、"))
|
|
450
|
+
: null,
|
|
451
|
+
row.pending
|
|
452
|
+
? h("div", { className: "dsh-ttf-note" }, t("label.pending") + ":" + (row.pending.change || "(未说明改了什么)"))
|
|
453
|
+
: null,
|
|
454
|
+
tasks.length > 0
|
|
455
|
+
? h(
|
|
456
|
+
"div",
|
|
457
|
+
null,
|
|
458
|
+
h("div", { className: "dsh-ttf-section" }, t("label.tasks")),
|
|
459
|
+
h(
|
|
460
|
+
"ul",
|
|
461
|
+
{ className: "dsh-ttf-list" },
|
|
462
|
+
tasks.map((task) =>
|
|
463
|
+
h(
|
|
464
|
+
"li",
|
|
465
|
+
{ key: task.id },
|
|
466
|
+
(task.status === "done" ? "✓ " : task.status === "dropped" ? "✗ " : "· ") + task.title,
|
|
467
|
+
(task.steps || []).length > 0
|
|
468
|
+
? h(
|
|
469
|
+
"ul",
|
|
470
|
+
{ className: "dsh-ttf-list" },
|
|
471
|
+
task.steps.map((step) =>
|
|
472
|
+
h(
|
|
473
|
+
"li",
|
|
474
|
+
{ key: step.id },
|
|
475
|
+
(step.status === "done" ? "✓ " : step.status === "dropped" ? "✗ " : "· ") + step.title,
|
|
476
|
+
),
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
: null,
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
),
|
|
483
|
+
)
|
|
484
|
+
: h("div", { className: "dsh-ttf-note" }, t("note.noTasks")),
|
|
485
|
+
row.result
|
|
486
|
+
? h("div", null, h("div", { className: "dsh-ttf-section" }, t("label.summary")), h("div", { className: "dsh-ttf-detailText" }, row.result))
|
|
487
|
+
: null,
|
|
545
488
|
);
|
|
489
|
+
}
|
|
546
490
|
|
|
491
|
+
/** 一条需求。 */
|
|
492
|
+
function RequirementRow({ row, isCurrent, expanded, busy, onToggle, onStart, onArchive, onEdit, t }) {
|
|
493
|
+
const counts = countOf(row);
|
|
547
494
|
return h(
|
|
548
495
|
"div",
|
|
549
|
-
{ className: "dsh-ttf-
|
|
496
|
+
{ className: "dsh-ttf-row" },
|
|
550
497
|
h(
|
|
551
498
|
"div",
|
|
552
|
-
{ className: "dsh-ttf-
|
|
553
|
-
h(
|
|
554
|
-
h(
|
|
555
|
-
"span",
|
|
556
|
-
{ className: "dsh-ttf-rowTitle" + (node.status === "done" ? " dsh-ttf-done" : "") },
|
|
557
|
-
node.title,
|
|
558
|
-
),
|
|
559
|
-
isCurrent ? h("span", { className: "dsh-ttf-tag" }, t("label.current")) : null,
|
|
560
|
-
isCheckpoint ? h("span", { className: "dsh-ttf-tag dsh-ttf-tagWarn" }, t("label.checkpointHere")) : null,
|
|
499
|
+
{ className: "dsh-ttf-rowHead" },
|
|
500
|
+
h(StatusPill, { row, t }),
|
|
501
|
+
h("span", { className: "dsh-ttf-tag" }, row.id),
|
|
561
502
|
h(
|
|
562
503
|
"span",
|
|
563
|
-
{ className: "dsh-ttf-
|
|
564
|
-
|
|
565
|
-
? h(IconButton, {
|
|
566
|
-
iconBase: "IconCheckOutline",
|
|
567
|
-
label: t("action.done"),
|
|
568
|
-
disabled: busy,
|
|
569
|
-
onClick: () => {
|
|
570
|
-
setComposing(true);
|
|
571
|
-
setOpen(true);
|
|
572
|
-
},
|
|
573
|
-
})
|
|
574
|
-
: null,
|
|
575
|
-
canAct && canDrop
|
|
576
|
-
? h(IconButton, {
|
|
577
|
-
iconBase: "IconTrashOutline",
|
|
578
|
-
label: t("action.drop"),
|
|
579
|
-
disabled: busy,
|
|
580
|
-
onClick: () => {
|
|
581
|
-
if (window.confirm(t("confirm.drop", { title: node.title }))) {
|
|
582
|
-
act({ action: "drop", id: node.id });
|
|
583
|
-
}
|
|
584
|
-
},
|
|
585
|
-
})
|
|
586
|
-
: null,
|
|
587
|
-
expandable
|
|
588
|
-
? h(IconButton, {
|
|
589
|
-
iconBase: open ? "IconChevronDownOutline" : "IconChevronRightOutline",
|
|
590
|
-
label: open ? t("action.collapse") : t("action.expand"),
|
|
591
|
-
onClick: () => setOpen(!open),
|
|
592
|
-
})
|
|
593
|
-
: null,
|
|
504
|
+
{ className: "dsh-ttf-rowTitle", onClick: onToggle, title: row.title },
|
|
505
|
+
row.title || "(还没有标题)",
|
|
594
506
|
),
|
|
507
|
+
isCurrent ? h("span", { className: "dsh-ttf-tag dsh-ttf-tagWarn" }, t("label.current")) : null,
|
|
508
|
+
counts.total > 0
|
|
509
|
+
? h("span", { className: "dsh-ttf-progress" }, counts.done + "/" + counts.total)
|
|
510
|
+
: null,
|
|
511
|
+
row.status === "pending" && !row.pending
|
|
512
|
+
? h(IconButton, { iconBase: "IconPlayOutline", label: t("action.start"), disabled: busy, onClick: onStart })
|
|
513
|
+
: null,
|
|
514
|
+
row.pending
|
|
515
|
+
? null
|
|
516
|
+
: h(IconButton, {
|
|
517
|
+
iconBase: row.archived ? "IconUndoOutline" : "IconArchiveOutline",
|
|
518
|
+
label: row.archived ? t("action.unarchive") : t("action.archive"),
|
|
519
|
+
disabled: busy,
|
|
520
|
+
onClick: onArchive,
|
|
521
|
+
}),
|
|
522
|
+
h(IconButton, {
|
|
523
|
+
iconBase: expanded ? "IconChevronUpOutline" : "IconChevronDownOutline",
|
|
524
|
+
label: expanded ? t("action.collapse") : t("action.open"),
|
|
525
|
+
onClick: onToggle,
|
|
526
|
+
}),
|
|
595
527
|
),
|
|
596
|
-
|
|
528
|
+
row.intro ? h("div", { className: "dsh-ttf-rowIntro" }, row.intro) : null,
|
|
529
|
+
expanded
|
|
597
530
|
? h(
|
|
598
531
|
"div",
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
?
|
|
603
|
-
"div",
|
|
604
|
-
null,
|
|
605
|
-
h(
|
|
606
|
-
"div",
|
|
607
|
-
{ className: "dsh-ttf-detailLabel" },
|
|
608
|
-
t("label.result") + "(" + formatTime(node.result.at) + ")",
|
|
609
|
-
),
|
|
610
|
-
h(ResultText, { text: node.result.text, t }),
|
|
611
|
-
)
|
|
532
|
+
null,
|
|
533
|
+
h(Detail, { row, t }),
|
|
534
|
+
row.pending
|
|
535
|
+
? null
|
|
612
536
|
: h(
|
|
613
537
|
"div",
|
|
614
|
-
{ className: "dsh-ttf-
|
|
615
|
-
|
|
538
|
+
{ className: "dsh-ttf-formActions" },
|
|
539
|
+
h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onEdit }, t("action.edit")),
|
|
616
540
|
),
|
|
617
|
-
composing
|
|
618
|
-
? h(CompleteForm, {
|
|
619
|
-
t,
|
|
620
|
-
busy,
|
|
621
|
-
error,
|
|
622
|
-
onSubmit: (text) => act({ action: "done", id: node.id, result: text }),
|
|
623
|
-
onCancel: () => {
|
|
624
|
-
setComposing(false);
|
|
625
|
-
setError(null);
|
|
626
|
-
},
|
|
627
|
-
})
|
|
628
|
-
: null,
|
|
629
|
-
children.map((child) =>
|
|
630
|
-
h(NodeRow, {
|
|
631
|
-
key: child.id,
|
|
632
|
-
node: child,
|
|
633
|
-
depth: depth + 1,
|
|
634
|
-
activeIds,
|
|
635
|
-
openIds,
|
|
636
|
-
checkpointId,
|
|
637
|
-
canDrop: true,
|
|
638
|
-
sessionId,
|
|
639
|
-
reload,
|
|
640
|
-
t,
|
|
641
|
-
}),
|
|
642
|
-
),
|
|
643
|
-
children.length === 0 && depth < 2
|
|
644
|
-
? h("div", { className: "dsh-ttf-hint" }, t("hint.noChildren"))
|
|
645
|
-
: null,
|
|
646
541
|
)
|
|
647
542
|
: null,
|
|
648
543
|
);
|
|
649
544
|
}
|
|
650
545
|
|
|
651
|
-
|
|
546
|
+
/** 新建需求:只产出草稿。 */
|
|
547
|
+
function DraftForm({ onSubmit, onCancel, busy, t }) {
|
|
548
|
+
const [title, setTitle] = useState("");
|
|
549
|
+
const [intro, setIntro] = useState("");
|
|
550
|
+
return h(
|
|
551
|
+
"div",
|
|
552
|
+
{ className: "dsh-ttf-form" },
|
|
553
|
+
h("input", {
|
|
554
|
+
className: "dsh-ttf-input",
|
|
555
|
+
value: title,
|
|
556
|
+
placeholder: t("placeholder.title"),
|
|
557
|
+
onChange: (event) => setTitle(event.target.value),
|
|
558
|
+
}),
|
|
559
|
+
h("input", {
|
|
560
|
+
className: "dsh-ttf-input",
|
|
561
|
+
value: intro,
|
|
562
|
+
placeholder: t("placeholder.intro"),
|
|
563
|
+
onChange: (event) => setIntro(event.target.value),
|
|
564
|
+
}),
|
|
565
|
+
h("div", { className: "dsh-ttf-note" }, t("note.draft")),
|
|
566
|
+
h(
|
|
567
|
+
"div",
|
|
568
|
+
{ className: "dsh-ttf-formActions" },
|
|
569
|
+
h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onCancel }, t("action.cancel")),
|
|
570
|
+
h(
|
|
571
|
+
"button",
|
|
572
|
+
{
|
|
573
|
+
type: "button",
|
|
574
|
+
className: "dsh-ttf-btn",
|
|
575
|
+
disabled: busy || title.trim() === "",
|
|
576
|
+
onClick: () => onSubmit({ title, intro }),
|
|
577
|
+
},
|
|
578
|
+
t("action.add"),
|
|
579
|
+
),
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
}
|
|
652
583
|
|
|
653
|
-
/**
|
|
654
|
-
* 输入框上方的全宽条目。
|
|
655
|
-
*
|
|
656
|
-
* 收起时只有一行:图标 + 阶段标签 + 目标标题 + 进度,动作全在右侧的图标
|
|
657
|
-
* 按钮里(暂停 / 继续、清空、展开)。展开才画整棵树,并限高滚动。
|
|
658
|
-
*
|
|
659
|
-
* 会话没有计划时返回 null:内置的 todo / goal 条目不该因为我而挪位。
|
|
660
|
-
*/
|
|
584
|
+
/** 输入框上方的全宽条目。 */
|
|
661
585
|
function TaskTreeDock(props) {
|
|
662
586
|
const t = useT();
|
|
663
587
|
const sessionId = props.sessionId;
|
|
664
|
-
const [state, setState] = useState({ loading: true });
|
|
588
|
+
const [state, setState] = useState({ loading: true, data: null });
|
|
665
589
|
const [open, setOpen] = useState(false);
|
|
666
590
|
const [busy, setBusy] = useState(false);
|
|
591
|
+
const [note, setNote] = useState(null);
|
|
592
|
+
// 归档的**延迟渲染**:这里记的是"这一次打开面板期间点过的开关",
|
|
593
|
+
// 它只改按钮文案,不改行所在的列表。下次打开面板时清空,行才真的搬家。
|
|
594
|
+
const [overrides, setOverrides] = useState({});
|
|
595
|
+
const [expandedId, setExpandedId] = useState(null);
|
|
596
|
+
const [editingId, setEditingId] = useState(null);
|
|
597
|
+
const [drafting, setDrafting] = useState(false);
|
|
667
598
|
|
|
668
599
|
const reload = useCallback(async () => {
|
|
669
600
|
if (!sessionId) {
|
|
670
|
-
setState({ loading: false,
|
|
601
|
+
setState({ loading: false, data: null });
|
|
671
602
|
return;
|
|
672
603
|
}
|
|
673
604
|
try {
|
|
674
605
|
const data = await api("/dsh-tree-task-flow/tree?sessionId=" + encodeURIComponent(sessionId));
|
|
675
606
|
setState({ loading: false, data });
|
|
676
607
|
} catch {
|
|
677
|
-
|
|
678
|
-
setState({ loading: false, empty: true });
|
|
608
|
+
setState({ loading: false, data: null });
|
|
679
609
|
}
|
|
680
610
|
}, [sessionId]);
|
|
681
611
|
|
|
@@ -683,25 +613,25 @@ window.__ModuleLoader__.load({
|
|
|
683
613
|
reload();
|
|
684
614
|
}, [reload]);
|
|
685
615
|
|
|
686
|
-
// 模型随时会改计划,而条目没有推送通道,所以自己轮询。
|
|
687
|
-
// 4 秒一次,读的是插件自己的一个小 JSON。
|
|
688
616
|
useEffect(() => {
|
|
689
617
|
if (!sessionId) return undefined;
|
|
690
|
-
const timer = setInterval(() =>
|
|
691
|
-
reload();
|
|
692
|
-
}, 4000);
|
|
618
|
+
const timer = setInterval(() => reload(), 4000);
|
|
693
619
|
return () => clearInterval(timer);
|
|
694
620
|
}, [sessionId, reload]);
|
|
695
621
|
|
|
696
|
-
// 这次操作的结果说明(已放行 / 已唤醒 / 为什么没动):恢复失败时用户最需要
|
|
697
|
-
// 的就是这句话,不能只留在接口的返回里。
|
|
698
|
-
const [note, setNote] = useState(null);
|
|
699
622
|
useEffect(() => {
|
|
700
623
|
if (!note) return undefined;
|
|
701
624
|
const timer = setTimeout(() => setNote(null), 8000);
|
|
702
625
|
return () => clearTimeout(timer);
|
|
703
626
|
}, [note]);
|
|
704
627
|
|
|
628
|
+
// 打开面板 = 重新读一次,并把「延迟的归档」结算掉。
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
if (!open) return;
|
|
631
|
+
setOverrides({});
|
|
632
|
+
reload();
|
|
633
|
+
}, [open, reload]);
|
|
634
|
+
|
|
705
635
|
const act = useCallback(
|
|
706
636
|
async (body) => {
|
|
707
637
|
setBusy(true);
|
|
@@ -712,6 +642,8 @@ window.__ModuleLoader__.load({
|
|
|
712
642
|
setNote(result.message);
|
|
713
643
|
}
|
|
714
644
|
await reload();
|
|
645
|
+
} catch (failure) {
|
|
646
|
+
setNote(String((failure && failure.message) || failure));
|
|
715
647
|
} finally {
|
|
716
648
|
setBusy(false);
|
|
717
649
|
}
|
|
@@ -719,36 +651,72 @@ window.__ModuleLoader__.load({
|
|
|
719
651
|
[sessionId, reload],
|
|
720
652
|
);
|
|
721
653
|
|
|
722
|
-
if (state.loading ||
|
|
654
|
+
if (state.loading || !state.data) return null;
|
|
723
655
|
|
|
724
656
|
const data = state.data;
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
(acc, each) => {
|
|
731
|
-
const one = countNodes(each);
|
|
732
|
-
return { total: acc.total + one.total, done: acc.done + one.done };
|
|
733
|
-
},
|
|
734
|
-
{ total: 0, done: 0 },
|
|
735
|
-
);
|
|
736
|
-
const heading = goals.length === 1 ? goals[0].title : t("label.goals", { count: goals.length });
|
|
737
|
-
const current = currentLabel(data);
|
|
738
|
-
// 「被按住」有两条来源:插件闸门(paused)与人按过界面停止(stopped)。
|
|
739
|
-
// 对用户来说都是"停着呢,点继续就能接着跑",所以合成一个状态展示。
|
|
657
|
+
const lists = listsOf(data);
|
|
658
|
+
const total = lists.active.length + lists.archived.length + lists.drafts.length;
|
|
659
|
+
// 一条需求都没有的会话,一个像素都不占。
|
|
660
|
+
if (total === 0) return null;
|
|
661
|
+
|
|
740
662
|
const held = data.paused === true || data.stopped === true;
|
|
663
|
+
const currentId = data.current || null;
|
|
664
|
+
const currentRow = lists.active.find((row) => row.id === currentId) || null;
|
|
665
|
+
// 没有进行中的需求时,条上照**最近的那一条**显,状态也照它自己的来。
|
|
666
|
+
// 以前这里直接退回「待开始」,于是一条已经做完的需求在条上写着待开始、
|
|
667
|
+
// 点开明明是已完成——两处对不上(用户报的就是这个)。
|
|
668
|
+
const shownRow =
|
|
669
|
+
currentRow !== null ? currentRow : lists.active.length > 0 ? lists.active[lists.active.length - 1] : null;
|
|
670
|
+
const heading =
|
|
671
|
+
shownRow !== null
|
|
672
|
+
? shownRow.title
|
|
673
|
+
: lists.archived.length > 0
|
|
674
|
+
? t("label.archived")
|
|
675
|
+
: t("label.drafts");
|
|
741
676
|
const stateLabel = held
|
|
742
677
|
? t("state.paused")
|
|
743
|
-
:
|
|
744
|
-
? t("state.
|
|
745
|
-
:
|
|
746
|
-
? t(
|
|
747
|
-
: t("state.
|
|
748
|
-
|
|
749
|
-
const listIcon = icon("IconChecklistOutline", 14);
|
|
678
|
+
: currentRow !== null
|
|
679
|
+
? t("state.running")
|
|
680
|
+
: shownRow !== null
|
|
681
|
+
? t(statusKey(shownRow.status))
|
|
682
|
+
: t("state.idle");
|
|
683
|
+
const counts = currentRow === null ? null : countOf(currentRow);
|
|
750
684
|
const listGlyph =
|
|
751
|
-
|
|
685
|
+
icon("IconChecklistOutline", 14) === null
|
|
686
|
+
? h("span", { className: "dsh-ttf-glyphFallback" }, "☰")
|
|
687
|
+
: icon("IconChecklistOutline", 14);
|
|
688
|
+
|
|
689
|
+
const rowOf = (row) => {
|
|
690
|
+
const shown = overrides[row.id];
|
|
691
|
+
return Object.assign({}, row, {
|
|
692
|
+
archived: typeof shown === "boolean" ? shown : row.archived === true,
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const renderRow = (row) => {
|
|
697
|
+
const view = rowOf(row);
|
|
698
|
+
const expanded = expandedId === row.id || editingId === row.id;
|
|
699
|
+
return h(RequirementRow, {
|
|
700
|
+
key: row.id,
|
|
701
|
+
row: view,
|
|
702
|
+
isCurrent: row.id === currentId,
|
|
703
|
+
expanded,
|
|
704
|
+
busy,
|
|
705
|
+
t,
|
|
706
|
+
onToggle: () => {
|
|
707
|
+
setEditingId(null);
|
|
708
|
+
setExpandedId(expandedId === row.id ? null : row.id);
|
|
709
|
+
},
|
|
710
|
+
onStart: () => act({ action: "start", id: row.id }),
|
|
711
|
+
onArchive: () => {
|
|
712
|
+
const next = !(typeof overrides[row.id] === "boolean" ? overrides[row.id] : row.archived === true);
|
|
713
|
+
setOverrides(Object.assign({}, overrides, { [row.id]: next }));
|
|
714
|
+
setNote(t("note.archiveDeferred"));
|
|
715
|
+
act({ action: next ? "archive" : "unarchive", id: row.id });
|
|
716
|
+
},
|
|
717
|
+
onEdit: () => setEditingId(row.id),
|
|
718
|
+
});
|
|
719
|
+
};
|
|
752
720
|
|
|
753
721
|
return h(
|
|
754
722
|
"div",
|
|
@@ -762,11 +730,9 @@ window.__ModuleLoader__.load({
|
|
|
762
730
|
h("span", { className: "dsh-ttf-glyph", "aria-hidden": true }, listGlyph),
|
|
763
731
|
h("span", { className: "dsh-ttf-label" + (held ? " dsh-ttf-labelHeld" : "") }, stateLabel),
|
|
764
732
|
h("span", { className: "dsh-ttf-title", title: heading }, heading),
|
|
765
|
-
|
|
766
|
-
"span",
|
|
767
|
-
|
|
768
|
-
stats.done + "/" + stats.total + (current ? " · " + current : ""),
|
|
769
|
-
),
|
|
733
|
+
counts !== null && counts.total > 0
|
|
734
|
+
? h("span", { className: "dsh-ttf-progress" }, counts.done + "/" + counts.total)
|
|
735
|
+
: null,
|
|
770
736
|
h(
|
|
771
737
|
"div",
|
|
772
738
|
{ className: "dsh-ttf-actions" },
|
|
@@ -776,14 +742,6 @@ window.__ModuleLoader__.load({
|
|
|
776
742
|
disabled: busy,
|
|
777
743
|
onClick: () => act({ action: held ? "resume" : "pause" }),
|
|
778
744
|
}),
|
|
779
|
-
h(IconButton, {
|
|
780
|
-
iconBase: "IconTrashOutline",
|
|
781
|
-
label: t("action.reset"),
|
|
782
|
-
disabled: busy,
|
|
783
|
-
onClick: () => {
|
|
784
|
-
if (window.confirm(t("confirm.reset"))) act({ action: "reset" });
|
|
785
|
-
},
|
|
786
|
-
}),
|
|
787
745
|
h(IconButton, {
|
|
788
746
|
iconBase: open ? "IconChevronDownOutline" : "IconChevronUpOutline",
|
|
789
747
|
label: open ? t("action.collapse") : t("action.expand"),
|
|
@@ -791,55 +749,199 @@ window.__ModuleLoader__.load({
|
|
|
791
749
|
}),
|
|
792
750
|
),
|
|
793
751
|
),
|
|
794
|
-
// 这次操作的结果与原因(已放行 / 已唤醒 / 为什么没动)。
|
|
795
752
|
note ? h("div", { className: "dsh-ttf-note" }, note) : null,
|
|
796
753
|
open
|
|
797
754
|
? h(
|
|
798
755
|
"div",
|
|
799
756
|
{ className: "dsh-ttf-body" },
|
|
800
|
-
|
|
757
|
+
held ? h("div", { className: "dsh-ttf-note" }, t("note.paused")) : null,
|
|
758
|
+
|
|
759
|
+
lists.active.length > 0
|
|
801
760
|
? h(
|
|
802
761
|
"div",
|
|
803
|
-
|
|
804
|
-
t("
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
762
|
+
null,
|
|
763
|
+
h("div", { className: "dsh-ttf-section" }, t("label.requirements", { count: lists.active.length })),
|
|
764
|
+
lists.active.map((row) =>
|
|
765
|
+
editingId === row.id
|
|
766
|
+
? h(EditForm, {
|
|
767
|
+
key: row.id,
|
|
768
|
+
row,
|
|
769
|
+
busy,
|
|
770
|
+
t,
|
|
771
|
+
onCancel: () => setEditingId(null),
|
|
772
|
+
onSubmit: async (patch) => {
|
|
773
|
+
await act(Object.assign({ action: "pending", id: row.id }, patch));
|
|
774
|
+
setEditingId(null);
|
|
775
|
+
},
|
|
776
|
+
})
|
|
777
|
+
: renderRow(row),
|
|
778
|
+
),
|
|
811
779
|
)
|
|
812
780
|
: null,
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
h(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
781
|
+
|
|
782
|
+
lists.drafts.length > 0
|
|
783
|
+
? h(
|
|
784
|
+
"div",
|
|
785
|
+
null,
|
|
786
|
+
h("div", { className: "dsh-ttf-section" }, t("label.drafts") + " · " + lists.drafts.length),
|
|
787
|
+
lists.drafts.map((row) =>
|
|
788
|
+
h(
|
|
789
|
+
"div",
|
|
790
|
+
{ className: "dsh-ttf-row", key: row.id },
|
|
791
|
+
h(
|
|
792
|
+
"div",
|
|
793
|
+
{ className: "dsh-ttf-rowHead" },
|
|
794
|
+
h(StatusPill, { row, t }),
|
|
795
|
+
h("span", { className: "dsh-ttf-tag" }, row.id),
|
|
796
|
+
h("span", { className: "dsh-ttf-rowTitle" }, row.title || "(还没有标题)"),
|
|
797
|
+
),
|
|
798
|
+
h("div", { className: "dsh-ttf-note" }, t("note.draft")),
|
|
799
|
+
),
|
|
800
|
+
),
|
|
801
|
+
)
|
|
802
|
+
: null,
|
|
803
|
+
|
|
804
|
+
lists.archived.length > 0
|
|
805
|
+
? h(
|
|
806
|
+
"div",
|
|
807
|
+
null,
|
|
808
|
+
h("div", { className: "dsh-ttf-section" }, t("label.archived") + " · " + lists.archived.length),
|
|
809
|
+
lists.archived.map((row) => renderRow(row)),
|
|
810
|
+
)
|
|
811
|
+
: null,
|
|
812
|
+
|
|
813
|
+
drafting
|
|
814
|
+
? h(DraftForm, {
|
|
815
|
+
busy,
|
|
816
|
+
t,
|
|
817
|
+
onCancel: () => setDrafting(false),
|
|
818
|
+
onSubmit: async (patch) => {
|
|
819
|
+
await act(Object.assign({ action: "draft" }, patch));
|
|
820
|
+
setDrafting(false);
|
|
821
|
+
},
|
|
822
|
+
})
|
|
823
|
+
: h(
|
|
824
|
+
"div",
|
|
825
|
+
{ className: "dsh-ttf-formActions" },
|
|
826
|
+
h(
|
|
827
|
+
"button",
|
|
828
|
+
{ type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: () => setDrafting(true) },
|
|
829
|
+
t("action.newRequirement"),
|
|
830
|
+
),
|
|
831
|
+
),
|
|
829
832
|
)
|
|
830
833
|
: null,
|
|
831
834
|
),
|
|
832
835
|
);
|
|
833
836
|
}
|
|
834
837
|
|
|
838
|
+
/**
|
|
839
|
+
* 插件管理里本插件自己那一页(`plugins.bundle.config`)。
|
|
840
|
+
*
|
|
841
|
+
* 宿主给两类视图:`page` 是页面主体,`summary` 是这个 bundle 在列表里的
|
|
842
|
+
* 一句话。bundle 的配置宿主只渲染 `page`,`summary` 一律返回 null——标题与
|
|
843
|
+
* 描述由宿主从包的元数据里取,这里不重复一遍。
|
|
844
|
+
*
|
|
845
|
+
* 判断视图与用 hook 的两件事分在两个组件里:同一个组件里先按 props 早退、
|
|
846
|
+
* 再调 hook,视图一变 hook 数量就变,React 会直接抛错。
|
|
847
|
+
*/
|
|
848
|
+
function TaskTreeConfigEntry(ownerProps) {
|
|
849
|
+
if (ownerProps && ownerProps.view === "summary") return null;
|
|
850
|
+
return h(TaskTreeSettings);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** 配置页的正文:两个开关 + 读不到设置时的说明。 */
|
|
854
|
+
function TaskTreeSettings() {
|
|
855
|
+
const t = useT();
|
|
856
|
+
const [state, setState] = useState({ loading: true, data: null });
|
|
857
|
+
const [busy, setBusy] = useState(false);
|
|
858
|
+
const [note, setNote] = useState("");
|
|
859
|
+
|
|
860
|
+
useEffect(() => {
|
|
861
|
+
let alive = true;
|
|
862
|
+
(async () => {
|
|
863
|
+
try {
|
|
864
|
+
const data = await api("/dsh-tree-task-flow/settings");
|
|
865
|
+
if (alive) setState({ loading: false, data });
|
|
866
|
+
} catch {
|
|
867
|
+
if (alive) setState({ loading: false, data: null });
|
|
868
|
+
}
|
|
869
|
+
})();
|
|
870
|
+
return () => {
|
|
871
|
+
alive = false;
|
|
872
|
+
};
|
|
873
|
+
}, []);
|
|
874
|
+
|
|
875
|
+
if (state.loading) {
|
|
876
|
+
return h("div", { className: "dsh-ttf-settingsPage" }, h("div", { className: "dsh-ttf-settingNote" }, t("settings.loading")));
|
|
877
|
+
}
|
|
878
|
+
if (!state.data) {
|
|
879
|
+
return h("div", { className: "dsh-ttf-settingsPage" }, h("div", { className: "dsh-ttf-settingNote" }, t("settings.unavailable")));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const settings = state.data.settings ?? {};
|
|
883
|
+
|
|
884
|
+
async function toggle(key, value) {
|
|
885
|
+
setBusy(true);
|
|
886
|
+
setNote("");
|
|
887
|
+
try {
|
|
888
|
+
const result = await post({ action: "setting", key, value });
|
|
889
|
+
if (result && result.settings) {
|
|
890
|
+
setState({ loading: false, data: Object.assign({}, state.data, { settings: result.settings }) });
|
|
891
|
+
}
|
|
892
|
+
} catch (failure) {
|
|
893
|
+
setNote(String((failure && failure.message) || failure));
|
|
894
|
+
} finally {
|
|
895
|
+
setBusy(false);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return h(
|
|
900
|
+
"div",
|
|
901
|
+
{ className: "dsh-ttf-settingsPage" },
|
|
902
|
+
h("div", { className: "dsh-ttf-settingNote" }, t("setting.note")),
|
|
903
|
+
state.data.enabled === false ? h("div", { className: "dsh-ttf-note" }, t("settings.off")) : null,
|
|
904
|
+
h(
|
|
905
|
+
"div",
|
|
906
|
+
{ className: "dsh-ttf-settings" },
|
|
907
|
+
SETTING_ROWS.map((row) => {
|
|
908
|
+
const on = settings[row.key] === true;
|
|
909
|
+
return h(
|
|
910
|
+
"div",
|
|
911
|
+
{ className: "dsh-ttf-setting", key: row.key },
|
|
912
|
+
h(
|
|
913
|
+
"button",
|
|
914
|
+
{
|
|
915
|
+
type: "button",
|
|
916
|
+
role: "switch",
|
|
917
|
+
"aria-checked": on,
|
|
918
|
+
"aria-label": t("setting." + row.key),
|
|
919
|
+
className: "dsh-ttf-switch" + (on ? " dsh-ttf-switchOn" : ""),
|
|
920
|
+
disabled: busy,
|
|
921
|
+
onClick: () => toggle(row.key, !on),
|
|
922
|
+
},
|
|
923
|
+
h("span", { className: "dsh-ttf-switchKnob" }),
|
|
924
|
+
),
|
|
925
|
+
h(
|
|
926
|
+
"div",
|
|
927
|
+
{ className: "dsh-ttf-settingText" },
|
|
928
|
+
h("span", { className: "dsh-ttf-settingName" }, t("setting." + row.key)),
|
|
929
|
+
h("span", { className: "dsh-ttf-settingNote" }, t(row.note)),
|
|
930
|
+
),
|
|
931
|
+
);
|
|
932
|
+
}),
|
|
933
|
+
),
|
|
934
|
+
note ? h("div", { className: "dsh-ttf-note" }, note) : null,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
835
938
|
// ---------------------------------------------------------------- 挂载
|
|
836
939
|
|
|
837
940
|
function apply(ctx) {
|
|
838
941
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-tree-task-flow: dictionaries");
|
|
839
942
|
translate = ctx.locale.bind(NS);
|
|
943
|
+
ensureStyles();
|
|
840
944
|
|
|
841
|
-
// 输入框上方的条目。list 槽 + 自己的 id ⇒ 只会并列加在内置
|
|
842
|
-
// 的 queue / todo / goal 旁边,不会替换任何一个。
|
|
843
945
|
ctx.effect(
|
|
844
946
|
() =>
|
|
845
947
|
ctx.slots.inject("conversation.input.dock", () =>
|
|
@@ -855,6 +957,21 @@ window.__ModuleLoader__.load({
|
|
|
855
957
|
),
|
|
856
958
|
"dsh-tree-task-flow: conversation dock",
|
|
857
959
|
);
|
|
960
|
+
|
|
961
|
+
ctx.effect(
|
|
962
|
+
() =>
|
|
963
|
+
ctx.slots.inject("plugins.bundle.config", () =>
|
|
964
|
+
ctx.slots.register(
|
|
965
|
+
{
|
|
966
|
+
name: "plugins.bundle.config",
|
|
967
|
+
key: BUNDLE_PACKAGE,
|
|
968
|
+
locale: NS,
|
|
969
|
+
},
|
|
970
|
+
TaskTreeConfigEntry,
|
|
971
|
+
),
|
|
972
|
+
),
|
|
973
|
+
"dsh-tree-task-flow: 插件配置页",
|
|
974
|
+
);
|
|
858
975
|
}
|
|
859
976
|
|
|
860
977
|
exports.name = name;
|