dsh-tree-task-flow 0.3.14 → 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 +119 -98
- package/client.js +590 -675
- package/config.example.yml +8 -7
- package/cordis.patch.yml +4 -4
- package/lib/commands.js +57 -25
- package/lib/http.js +181 -46
- package/lib/index.js +1 -1
- package/lib/notes.js +173 -0
- package/lib/pause.js +44 -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/store.js +50 -7
- package/lib/tools.js +588 -144
- 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,28 +143,23 @@ 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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
* 满不了。丢弃的证据在树里(划掉那一行),不需要分数再替它记一笔。
|
|
182
|
-
*/
|
|
183
|
-
function countNodes(goal) {
|
|
184
|
-
if (goal.status === "dropped") return { total: 0, done: 0 };
|
|
185
|
-
let total = 1;
|
|
186
|
-
let done = goal.status === "done" ? 1 : 0;
|
|
187
|
-
for (const task of goal.tasks || []) {
|
|
158
|
+
/** 一条需求的进度分数:任务与步骤都算,放弃的两边都不算。 */
|
|
159
|
+
function countOf(row) {
|
|
160
|
+
let total = 0;
|
|
161
|
+
let done = 0;
|
|
162
|
+
for (const task of row.tasks || []) {
|
|
188
163
|
if (task.status === "dropped") continue;
|
|
189
164
|
total += 1;
|
|
190
165
|
if (task.status === "done") done += 1;
|
|
@@ -197,60 +172,12 @@ window.__ModuleLoader__.load({
|
|
|
197
172
|
return { total, done };
|
|
198
173
|
}
|
|
199
174
|
|
|
200
|
-
/**
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
function focusGoal(data) {
|
|
207
|
-
const goals = goalsOf(data);
|
|
208
|
-
if (goals.length === 0) return null;
|
|
209
|
-
const here = goals.find((each) => each.id === data?.active?.goalId);
|
|
210
|
-
if (here) return here;
|
|
211
|
-
return (
|
|
212
|
-
goals.find((each) => each.status !== "done" && each.status !== "dropped") ||
|
|
213
|
-
goals[goals.length - 1]
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** 当前活动节点的标题:子任务优先,其次任务,最后目标。 */
|
|
218
|
-
function currentLabel(data) {
|
|
219
|
-
const active = data.active;
|
|
220
|
-
if (!active) return "";
|
|
221
|
-
const goal = focusGoal(data);
|
|
222
|
-
if (!goal) return "";
|
|
223
|
-
const task = (goal.tasks || []).find((each) => each.id === active.taskId);
|
|
224
|
-
if (!task) return goal.title;
|
|
225
|
-
const step = (task.steps || []).find((each) => each.id === active.stepId);
|
|
226
|
-
return step ? step.title : task.title;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** 活动路径上三层各自的 id。节点 id 全局唯一,所以一个集合就够判"是不是当前节点"。 */
|
|
230
|
-
function currentIds(data) {
|
|
231
|
-
const active = data.active;
|
|
232
|
-
return new Set(
|
|
233
|
-
[active?.goalId, active?.taskId, active?.stepId].filter(
|
|
234
|
-
(each) => typeof each === "string" && each !== "",
|
|
235
|
-
),
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* 面板一打开该摊开哪些节点:活动路径上的三层,加上检查点节点以及它的祖先。
|
|
241
|
-
*
|
|
242
|
-
* 其余节点一律收起——正在走的那条路径本来就窄,摊平整棵树只会让人翻半天。
|
|
243
|
-
* 检查点落在某个任务上时,目标也得一起摊开,否则连那一行都看不见。
|
|
244
|
-
*/
|
|
245
|
-
function defaultOpenIds(data) {
|
|
246
|
-
const ids = currentIds(data);
|
|
247
|
-
const checkpoint = data.checkpoint;
|
|
248
|
-
if (checkpoint !== null && checkpoint !== undefined && typeof checkpoint.id === "string") {
|
|
249
|
-
ids.add(checkpoint.id);
|
|
250
|
-
const goalId = focusGoal(data)?.id;
|
|
251
|
-
if (checkpoint.level !== "goal" && typeof goalId === "string") ids.add(goalId);
|
|
252
|
-
}
|
|
253
|
-
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";
|
|
254
181
|
}
|
|
255
182
|
|
|
256
183
|
// ---------------------------------------------------------------- 文案
|
|
@@ -259,23 +186,42 @@ window.__ModuleLoader__.load({
|
|
|
259
186
|
nav: "树形任务流",
|
|
260
187
|
"state.running": "进行中",
|
|
261
188
|
"state.paused": "已暂停",
|
|
262
|
-
"state.
|
|
263
|
-
"
|
|
264
|
-
"
|
|
265
|
-
"
|
|
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": "总结",
|
|
266
203
|
"action.pause": "暂停",
|
|
267
204
|
"action.resume": "继续",
|
|
268
|
-
"action.reset": "清空计划",
|
|
269
205
|
"action.expand": "展开",
|
|
270
206
|
"action.collapse": "收起",
|
|
271
|
-
"action.
|
|
272
|
-
"action.
|
|
273
|
-
"action.
|
|
274
|
-
"action.
|
|
275
|
-
"action.
|
|
276
|
-
"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": "记下来",
|
|
277
215
|
"action.cancel": "取消",
|
|
278
|
-
"
|
|
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": "详情,可以很长",
|
|
279
225
|
"settings.loading": "正在读设置…",
|
|
280
226
|
"settings.unavailable": "读不到设置:宿主那一半没在跑,或者接口不通。",
|
|
281
227
|
"settings.off": "插件在 profile 配置里被关掉了(enabled: false),这两个开关现在不起作用。",
|
|
@@ -283,45 +229,49 @@ window.__ModuleLoader__.load({
|
|
|
283
229
|
"setting.autoContinue": "任务没做完、模型自己停下时,自动把它叫起来接着跑",
|
|
284
230
|
"setting.autoContinueNote": "会往会话里推一条消息。默认关。",
|
|
285
231
|
"setting.interruptRecovery": "工具出错或网络断了,自动重试把会话拉起来",
|
|
286
|
-
"setting.interruptRecoveryNote":
|
|
287
|
-
"按 1 / 5 / 10 / 30 分钟各拉一次,一直拉不起来就停手。你按过停止的绝不拉起。",
|
|
288
|
-
"label.result": "结果",
|
|
289
|
-
"label.noResult": "(还没提交结果)",
|
|
290
|
-
"label.current": "当前",
|
|
291
|
-
"label.checkpointHere": "检查点",
|
|
292
|
-
"label.goals": "{count} 个目标",
|
|
293
|
-
"placeholder.result":
|
|
294
|
-
"写清这个节点产出了什么。它整段执行过程会被这条结果顶替,所以要能独立看懂。",
|
|
295
|
-
"hint.noChildren": "(还没有子节点 → 让模型调用 tree_task_plan)",
|
|
296
|
-
"confirm.reset": "确定要清空这个会话的计划树吗?",
|
|
297
|
-
"confirm.drop": "确定要丢弃「{title}」吗?丢弃会把它标成「不做了」,节点留在树里可查。",
|
|
298
|
-
"confirm.delete":
|
|
299
|
-
"确定要删除「{title}」吗?它会连同下面的任务或子任务一起从树里消失,找不回来;已经提交的结果也会一起没掉。如果只是「这件事不做了」,请改用丢弃。",
|
|
232
|
+
"setting.interruptRecoveryNote": "按 1 / 5 / 10 / 30 分钟各拉一次,一直拉不起来就停手。你按过停止的绝不拉起。",
|
|
300
233
|
};
|
|
301
234
|
|
|
302
235
|
const en = {
|
|
303
|
-
nav: "
|
|
236
|
+
nav: "Task tree",
|
|
304
237
|
"state.running": "In progress",
|
|
305
238
|
"state.paused": "Paused",
|
|
306
|
-
"state.
|
|
307
|
-
"
|
|
308
|
-
"
|
|
309
|
-
|
|
310
|
-
"
|
|
311
|
-
|
|
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",
|
|
312
253
|
"action.pause": "Pause",
|
|
313
254
|
"action.resume": "Resume",
|
|
314
|
-
"action.reset": "Clear plan",
|
|
315
255
|
"action.expand": "Expand",
|
|
316
256
|
"action.collapse": "Collapse",
|
|
317
|
-
"action.
|
|
318
|
-
"action.
|
|
319
|
-
"action.
|
|
320
|
-
"action.
|
|
321
|
-
"action.
|
|
322
|
-
"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",
|
|
323
265
|
"action.cancel": "Cancel",
|
|
324
|
-
"
|
|
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",
|
|
325
275
|
"settings.loading": "Reading settings…",
|
|
326
276
|
"settings.unavailable": "Settings are unreachable: the host half is not running, or the endpoint is blocked.",
|
|
327
277
|
"settings.off": "The plugin is switched off in the profile config (enabled: false), so these switches do nothing right now.",
|
|
@@ -329,37 +279,21 @@ window.__ModuleLoader__.load({
|
|
|
329
279
|
"setting.autoContinue": "When the plan is unfinished and the model stops, wake it up to carry on",
|
|
330
280
|
"setting.autoContinueNote": "This pushes a message into the session. Off by default.",
|
|
331
281
|
"setting.interruptRecovery": "After a tool error or a dropped connection, retry to get the session going",
|
|
332
|
-
"setting.interruptRecoveryNote":
|
|
333
|
-
"Retries at 1 / 5 / 10 / 30 minutes, then gives up. A session you stopped is never revived.",
|
|
334
|
-
"label.result": "Result",
|
|
335
|
-
"label.noResult": "(no result submitted yet)",
|
|
336
|
-
"label.current": "current",
|
|
337
|
-
"label.checkpointHere": "checkpoint",
|
|
338
|
-
"label.goals": "{count} goals",
|
|
339
|
-
"placeholder.result":
|
|
340
|
-
"State what this node produced. Its whole execution range is replaced by this result, so it must stand on its own.",
|
|
341
|
-
"hint.noChildren": "(no children yet → ask the model to call tree_task_plan)",
|
|
342
|
-
"confirm.reset": "Clear this session's plan tree?",
|
|
343
|
-
"confirm.drop": "Drop “{title}”? It is marked as not-to-do and stays in the tree.",
|
|
344
|
-
"confirm.delete":
|
|
345
|
-
"Delete “{title}”? It disappears from the tree together with its tasks or steps, and any submitted result goes with it. If you only mean “not doing this”, use Drop instead.",
|
|
282
|
+
"setting.interruptRecoveryNote": "Retries at 1 / 5 / 10 / 30 minutes, then gives up. A session you stopped is never revived.",
|
|
346
283
|
};
|
|
347
284
|
|
|
348
|
-
|
|
285
|
+
/** 插件配置页上的两个开关。 */
|
|
286
|
+
const SETTING_ROWS = [
|
|
287
|
+
{ key: "autoContinue", note: "setting.autoContinueNote" },
|
|
288
|
+
{ key: "interruptRecovery", note: "setting.interruptRecoveryNote" },
|
|
289
|
+
];
|
|
349
290
|
|
|
350
|
-
|
|
351
|
-
* 样式照内置的 dock 条目写:宽高、间距、圆角、颜色变量都取
|
|
352
|
-
* `dsh-client-ui-goal` 的 GoalBar 与内置待办面板 TodoPanel 的同一套值。
|
|
353
|
-
*
|
|
354
|
-
* 注入方式也照它们:一个带 `data-plugin-css` 的 <style> 标签——
|
|
355
|
-
* 客户端 HMR 就是靠这个属性在插件卸载时收走样式的。
|
|
356
|
-
*/
|
|
357
|
-
const CSS_TAG = "dsh-tree-task-flow/Dock.module.css";
|
|
291
|
+
// ---------------------------------------------------------------- 样式
|
|
358
292
|
|
|
359
293
|
const CSS = [
|
|
360
|
-
//
|
|
294
|
+
// 外壳与宽度:**必须照抄内置条目的宽度公式**。漏掉它,这一条会按父容器的
|
|
295
|
+
// 宽度铺满整个输入区(实测就是这样占满全屏的)。
|
|
361
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)}",
|
|
362
|
-
// 卡片:背景与圆角同内置条目,描边用 ::after 画(.5px 在缩放屏上才不糊)。
|
|
363
297
|
".dsh-ttf-panel{background:var(--dsw-specific-tip);border-radius:12px;position:relative;overflow:hidden}",
|
|
364
298
|
'.dsh-ttf-panel:after{border:.5px solid var(--dsw-alias-border-l1);border-radius:inherit;content:"";pointer-events:none;position:absolute;inset:0}',
|
|
365
299
|
".dsh-ttf-bar{box-sizing:border-box;width:100%;height:36px;display:flex;align-items:center;gap:10px;padding:4px 5px 4px 12px}",
|
|
@@ -370,490 +304,308 @@ window.__ModuleLoader__.load({
|
|
|
370
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}",
|
|
371
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}",
|
|
372
306
|
".dsh-ttf-actions{flex:none;display:flex;align-items:center;gap:2px}",
|
|
373
|
-
".dsh-ttf-iconBtn{
|
|
374
|
-
".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)}",
|
|
375
309
|
".dsh-ttf-iconBtn:disabled{opacity:.4;cursor:default}",
|
|
376
|
-
".dsh-ttf-iconBtn:disabled:hover{background:0 0;color:var(--dsw-alias-label-tertiary)}",
|
|
377
|
-
// 取不到内置图标时退回文字:固定 28px 宽装不下几个字,文字会溢出到相邻
|
|
378
|
-
// 按钮上,看着就是几个按钮叠在一起。这里按文字宽度撑开、留出边距,
|
|
379
|
-
// 并给一个上限,长了用省略号收住——几个按钮始终各占各的位置。
|
|
380
310
|
".dsh-ttf-iconBtnText{width:auto;min-width:28px;max-width:112px;height:24px;padding:0 8px}",
|
|
381
311
|
".dsh-ttf-iconText{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;font-size:12px;line-height:16px;white-space:nowrap}",
|
|
382
|
-
// 展开体:限高滚动——这是输入框旁边的位置,不能让它把会话挤没了。
|
|
383
312
|
".dsh-ttf-body{max-height:264px;overflow-y:auto;padding:2px 8px 8px}",
|
|
384
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)}",
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
".dsh-ttf-node{position:relative}",
|
|
388
|
-
// 同一层的相邻节点之间画一条分界线——任务与任务的分界就靠它。
|
|
389
|
-
".dsh-ttf-node+.dsh-ttf-node{border-top:1px solid var(--dsw-alias-border-l1);margin-top:2px;padding-top:2px}",
|
|
390
|
-
".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}",
|
|
391
|
-
".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}",
|
|
392
316
|
".dsh-ttf-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
393
|
-
|
|
394
|
-
".dsh-ttf-rowTitle{min-width:0;flex:1;white-space:pre-wrap;word-break:break-word}",
|
|
395
|
-
".dsh-ttf-
|
|
396
|
-
".dsh-ttf-
|
|
397
|
-
".dsh-ttf-
|
|
398
|
-
".dsh-ttf-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
"@keyframes dsh-ttf-spin{to{transform:rotate(360deg)}}",
|
|
402
|
-
".dsh-ttf-markSpin{animation:dsh-ttf-spin 1s linear infinite;transform-origin:50% 50%}",
|
|
403
|
-
".dsh-ttf-done{color:var(--dsw-alias-label-tertiary);text-decoration:line-through}",
|
|
404
|
-
// 三层缩进。
|
|
405
|
-
// 子节点:一条竖线 + 每个子节点一个拐角,把三层画成看得见的树。
|
|
406
|
-
".dsh-ttf-depth1,.dsh-ttf-depth2{margin-left:11px;padding-left:9px;border-left:1px solid var(--dsw-alias-border-l2)}",
|
|
407
|
-
'.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)}',
|
|
408
|
-
// 详情里的每一块(说明 / 结果 / 表单)自成一块,块间留白 + 描边,分得开。
|
|
409
|
-
".dsh-ttf-detail>*+*{border-top:.5px solid var(--dsw-alias-border-l1);padding-top:6px}",
|
|
410
|
-
".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)}",
|
|
411
325
|
".dsh-ttf-tagWarn{color:var(--dsw-alias-state-warning-primary,#a06800);background:var(--dsw-alias-state-warning-bg,#ffcc002e)}",
|
|
412
|
-
// 展开详情:说明、结果、完成表单。
|
|
413
326
|
".dsh-ttf-detail{display:flex;flex-direction:column;gap:6px;padding:2px 4px 4px}",
|
|
414
|
-
".dsh-ttf-
|
|
327
|
+
".dsh-ttf-detail>*+*{border-top:.5px solid var(--dsw-alias-border-l1);padding-top:6px}",
|
|
415
328
|
".dsh-ttf-detailText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}",
|
|
416
|
-
|
|
417
|
-
".dsh-ttf-
|
|
418
|
-
".dsh-ttf-
|
|
419
|
-
".dsh-ttf-resultOpen{display:block;max-height:220px;overflow-y:auto}",
|
|
420
|
-
".dsh-ttf-form{margin:6px 0 2px}",
|
|
421
|
-
".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}",
|
|
422
|
-
".dsh-ttf-input:focus{border-color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
423
|
-
".dsh-ttf-input::placeholder{color:var(--dsw-alias-label-caption,#00000066)}",
|
|
424
|
-
".dsh-ttf-formBar{margin-top:6px;display:flex;align-items:center;gap:6px}",
|
|
425
|
-
".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}",
|
|
426
|
-
".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)}",
|
|
427
332
|
".dsh-ttf-btn:disabled{opacity:.4;cursor:default}",
|
|
428
|
-
".dsh-ttf-
|
|
429
|
-
".dsh-ttf-
|
|
430
|
-
".dsh-ttf-
|
|
431
|
-
|
|
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}",
|
|
432
339
|
".dsh-ttf-settingsPage{display:flex;flex-direction:column;gap:10px;padding:4px 2px;max-width:640px}",
|
|
433
|
-
".dsh-ttf-settings{display:flex;flex-direction:column;gap:
|
|
434
|
-
".dsh-ttf-setting{display:flex;align-items:flex-start;gap:
|
|
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}",
|
|
435
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}",
|
|
436
|
-
".dsh-ttf-
|
|
437
|
-
".dsh-ttf-switchOn{border-color:var(--dsw-alias-state-business-primary,#4d6bfe);background:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
346
|
+
".dsh-ttf-switchOn{background:var(--dsw-alias-state-business-primary,#4d6bfe);border-color:transparent}",
|
|
438
347
|
".dsh-ttf-switchKnob{position:absolute;top:1px;left:1px;width:14px;height:14px;border-radius:999px;background:#fff;transition:left .12s}",
|
|
439
348
|
".dsh-ttf-switchOn .dsh-ttf-switchKnob{left:15px}",
|
|
440
|
-
|
|
441
|
-
".dsh-ttf-settingName{color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px}",
|
|
442
|
-
".dsh-ttf-settingNote{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
443
|
-
].join("");
|
|
349
|
+
].join("\n");
|
|
444
350
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
*
|
|
461
|
-
* 正在运行的那个圈还会匀速转起来——虚线一转就能看出这条路径还在推进。
|
|
462
|
-
*/
|
|
463
|
-
function StatusMark({ node, isCurrent }) {
|
|
464
|
-
if (node.status === "done") {
|
|
465
|
-
const check = icon("IconCheckOutline", 14);
|
|
466
|
-
return h("span", { className: "dsh-ttf-markDone" }, check === null ? "✓" : check);
|
|
467
|
-
}
|
|
468
|
-
if (node.status === "dropped") {
|
|
469
|
-
const close = icon("IconCloseOutline", 14);
|
|
470
|
-
return h("span", { className: "dsh-ttf-markMuted" }, close === null ? "×" : close);
|
|
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
|
+
// 样式注入失败不该拖垮条目:没有样式也还能用。
|
|
471
366
|
}
|
|
472
|
-
const circle = { cx: 7, cy: 7, r: 5.2, stroke: "currentColor", strokeWidth: 1.2 };
|
|
473
|
-
if (isCurrent) circle.strokeDasharray = "2.4 2.4";
|
|
474
|
-
return h(
|
|
475
|
-
"span",
|
|
476
|
-
{ className: "dsh-ttf-markCell" },
|
|
477
|
-
h(
|
|
478
|
-
"svg",
|
|
479
|
-
{
|
|
480
|
-
width: 14,
|
|
481
|
-
height: 14,
|
|
482
|
-
viewBox: "0 0 14 14",
|
|
483
|
-
fill: "none",
|
|
484
|
-
"aria-hidden": true,
|
|
485
|
-
className: isCurrent ? "dsh-ttf-markSpin" : undefined,
|
|
486
|
-
},
|
|
487
|
-
h("circle", circle),
|
|
488
|
-
),
|
|
489
|
-
);
|
|
490
367
|
}
|
|
491
368
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
className:
|
|
505
|
-
"dsh-ttf-result" +
|
|
506
|
-
(expandable ? " dsh-ttf-resultClick" : "") +
|
|
507
|
-
(open ? " dsh-ttf-resultOpen" : ""),
|
|
508
|
-
role: expandable ? "button" : undefined,
|
|
509
|
-
tabIndex: expandable ? 0 : undefined,
|
|
510
|
-
title: expandable ? t(open ? "action.collapseResult" : "action.expandResult") : undefined,
|
|
511
|
-
onClick: expandable
|
|
512
|
-
? () => setOpen(!open)
|
|
513
|
-
: undefined,
|
|
514
|
-
},
|
|
515
|
-
body,
|
|
516
|
-
);
|
|
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));
|
|
517
381
|
}
|
|
518
382
|
|
|
519
|
-
/**
|
|
520
|
-
function
|
|
521
|
-
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("");
|
|
522
389
|
return h(
|
|
523
390
|
"div",
|
|
524
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
|
+
}),
|
|
525
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", {
|
|
526
413
|
className: "dsh-ttf-input",
|
|
527
|
-
value:
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
onChange: (event) => setText(event.target.value),
|
|
414
|
+
value: change,
|
|
415
|
+
placeholder: "这次改了什么(会写进版本历史)",
|
|
416
|
+
onChange: (event) => setChange(event.target.value),
|
|
531
417
|
}),
|
|
532
|
-
|
|
418
|
+
h("div", { className: "dsh-ttf-note" }, t("note.edit")),
|
|
533
419
|
h(
|
|
534
420
|
"div",
|
|
535
|
-
{ className: "dsh-ttf-
|
|
421
|
+
{ className: "dsh-ttf-formActions" },
|
|
422
|
+
h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onCancel }, t("action.cancel")),
|
|
536
423
|
h(
|
|
537
424
|
"button",
|
|
538
425
|
{
|
|
539
426
|
type: "button",
|
|
540
427
|
className: "dsh-ttf-btn",
|
|
541
|
-
disabled: busy
|
|
542
|
-
onClick: () =>
|
|
428
|
+
disabled: busy,
|
|
429
|
+
onClick: () =>
|
|
430
|
+
onSubmit({ title, intro, detail, change }),
|
|
543
431
|
},
|
|
544
|
-
t("action.
|
|
432
|
+
t("action.submitChange"),
|
|
545
433
|
),
|
|
546
|
-
h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onCancel }, t("action.cancel")),
|
|
547
434
|
),
|
|
548
435
|
);
|
|
549
436
|
}
|
|
550
437
|
|
|
551
|
-
/**
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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,
|
|
592
488
|
);
|
|
489
|
+
}
|
|
593
490
|
|
|
491
|
+
/** 一条需求。 */
|
|
492
|
+
function RequirementRow({ row, isCurrent, expanded, busy, onToggle, onStart, onArchive, onEdit, t }) {
|
|
493
|
+
const counts = countOf(row);
|
|
594
494
|
return h(
|
|
595
495
|
"div",
|
|
596
|
-
{ className: "dsh-ttf-
|
|
496
|
+
{ className: "dsh-ttf-row" },
|
|
597
497
|
h(
|
|
598
498
|
"div",
|
|
599
|
-
{ className: "dsh-ttf-
|
|
600
|
-
h(
|
|
601
|
-
h(
|
|
602
|
-
"span",
|
|
603
|
-
{ className: "dsh-ttf-rowTitle" + (node.status === "done" ? " dsh-ttf-done" : "") },
|
|
604
|
-
node.title,
|
|
605
|
-
),
|
|
606
|
-
isCurrent ? h("span", { className: "dsh-ttf-tag" }, t("label.current")) : null,
|
|
607
|
-
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),
|
|
608
502
|
h(
|
|
609
503
|
"span",
|
|
610
|
-
{ className: "dsh-ttf-
|
|
611
|
-
|
|
612
|
-
? h(IconButton, {
|
|
613
|
-
iconBase: "IconCheckOutline",
|
|
614
|
-
label: t("action.done"),
|
|
615
|
-
disabled: busy,
|
|
616
|
-
onClick: () => {
|
|
617
|
-
setComposing(true);
|
|
618
|
-
setOpen(true);
|
|
619
|
-
},
|
|
620
|
-
})
|
|
621
|
-
: null,
|
|
622
|
-
canAct && canDrop
|
|
623
|
-
? h(IconButton, {
|
|
624
|
-
// 划掉 = 留个记号说"这件事不做了",和树里已丢弃节点的 × 标记是同一个意思。
|
|
625
|
-
// 垃圾桶留给真正的删除(见下一个按钮)。
|
|
626
|
-
iconBase: "IconCloseOutline",
|
|
627
|
-
label: t("action.drop"),
|
|
628
|
-
disabled: busy,
|
|
629
|
-
onClick: () => {
|
|
630
|
-
if (window.confirm(t("confirm.drop", { title: node.title }))) {
|
|
631
|
-
act({ action: "drop", id: node.id });
|
|
632
|
-
}
|
|
633
|
-
},
|
|
634
|
-
})
|
|
635
|
-
: null,
|
|
636
|
-
// 删除对**任何状态**都开着:它就是用来清理写错/建重的内容的,
|
|
637
|
-
// 已完成的节点恰恰是最需要清掉的那种(结果记错了)。
|
|
638
|
-
canDrop
|
|
639
|
-
? h(IconButton, {
|
|
640
|
-
iconBase: "IconTrashOutline",
|
|
641
|
-
label: t("action.delete"),
|
|
642
|
-
disabled: busy,
|
|
643
|
-
onClick: () => {
|
|
644
|
-
if (window.confirm(t("confirm.delete", { title: node.title }))) {
|
|
645
|
-
act({ action: "delete", id: node.id });
|
|
646
|
-
}
|
|
647
|
-
},
|
|
648
|
-
})
|
|
649
|
-
: null,
|
|
650
|
-
expandable
|
|
651
|
-
? h(IconButton, {
|
|
652
|
-
iconBase: open ? "IconChevronDownOutline" : "IconChevronRightOutline",
|
|
653
|
-
label: open ? t("action.collapse") : t("action.expand"),
|
|
654
|
-
onClick: () => setOpen(!open),
|
|
655
|
-
})
|
|
656
|
-
: null,
|
|
504
|
+
{ className: "dsh-ttf-rowTitle", onClick: onToggle, title: row.title },
|
|
505
|
+
row.title || "(还没有标题)",
|
|
657
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
|
+
}),
|
|
658
527
|
),
|
|
659
|
-
|
|
528
|
+
row.intro ? h("div", { className: "dsh-ttf-rowIntro" }, row.intro) : null,
|
|
529
|
+
expanded
|
|
660
530
|
? h(
|
|
661
531
|
"div",
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
?
|
|
666
|
-
"div",
|
|
667
|
-
null,
|
|
668
|
-
h(
|
|
669
|
-
"div",
|
|
670
|
-
{ className: "dsh-ttf-detailLabel" },
|
|
671
|
-
t("label.result") + "(" + formatTime(node.result.at) + ")",
|
|
672
|
-
),
|
|
673
|
-
h(ResultText, { text: node.result.text, t }),
|
|
674
|
-
)
|
|
532
|
+
null,
|
|
533
|
+
h(Detail, { row, t }),
|
|
534
|
+
row.pending
|
|
535
|
+
? null
|
|
675
536
|
: h(
|
|
676
537
|
"div",
|
|
677
|
-
{ className: "dsh-ttf-
|
|
678
|
-
|
|
538
|
+
{ className: "dsh-ttf-formActions" },
|
|
539
|
+
h("button", { type: "button", className: "dsh-ttf-btn", disabled: busy, onClick: onEdit }, t("action.edit")),
|
|
679
540
|
),
|
|
680
|
-
composing
|
|
681
|
-
? h(CompleteForm, {
|
|
682
|
-
t,
|
|
683
|
-
busy,
|
|
684
|
-
error,
|
|
685
|
-
onSubmit: (text) => act({ action: "done", id: node.id, result: text }),
|
|
686
|
-
onCancel: () => {
|
|
687
|
-
setComposing(false);
|
|
688
|
-
setError(null);
|
|
689
|
-
},
|
|
690
|
-
})
|
|
691
|
-
: null,
|
|
692
|
-
children.map((child) =>
|
|
693
|
-
h(NodeRow, {
|
|
694
|
-
key: child.id,
|
|
695
|
-
node: child,
|
|
696
|
-
depth: depth + 1,
|
|
697
|
-
activeIds,
|
|
698
|
-
openIds,
|
|
699
|
-
checkpointId,
|
|
700
|
-
canDrop: true,
|
|
701
|
-
sessionId,
|
|
702
|
-
reload,
|
|
703
|
-
t,
|
|
704
|
-
}),
|
|
705
|
-
),
|
|
706
|
-
children.length === 0 && depth < 2
|
|
707
|
-
? h("div", { className: "dsh-ttf-hint" }, t("hint.noChildren"))
|
|
708
|
-
: null,
|
|
709
541
|
)
|
|
710
542
|
: null,
|
|
711
543
|
);
|
|
712
544
|
}
|
|
713
545
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
*
|
|
719
|
-
* 键必须与 `lib/settings.js` 的 `SETTING_KEYS` **一模一样**:对不上时接口会
|
|
720
|
-
* 400 拒掉,而界面只会安静地什么都没发生,最难查。自检里有一条断言把两份
|
|
721
|
-
* 名单对起来(`verify-settings.mjs`)。
|
|
722
|
-
*
|
|
723
|
-
* 文案名就按 `setting.<键>` 拼,另配一条 `setting.<键>Note` 讲清楚它到底会做什么。
|
|
724
|
-
*/
|
|
725
|
-
const SETTING_ROWS = [
|
|
726
|
-
{ key: "autoContinue", note: "setting.autoContinueNote" },
|
|
727
|
-
{ key: "interruptRecovery", note: "setting.interruptRecoveryNote" },
|
|
728
|
-
];
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* 设置面板里的那一页:两个开关。
|
|
732
|
-
*
|
|
733
|
-
* ★ 这一页挂在 **DSH 自己的设置面板**里(`settings.section` 槽),不在插件的
|
|
734
|
-
* 任务条上。任务条是"看进度、按暂停"的地方;偏好属于设置,就该待在设置里。
|
|
735
|
-
*
|
|
736
|
-
* 数据走宿主那两个**与会话无关**的接口:读 `/dsh-tree-task-flow/settings`,
|
|
737
|
-
* 写 `action: "setting"`。跟任务条不同,这里没有要盯的东西,所以不轮询:
|
|
738
|
-
* 挂载时读一次,改完拿返回值就地更新。
|
|
739
|
-
*/
|
|
740
|
-
function TaskTreeSettings() {
|
|
741
|
-
const t = useT();
|
|
742
|
-
const [state, setState] = useState({ loading: true, data: null });
|
|
743
|
-
const [busy, setBusy] = useState(false);
|
|
744
|
-
const [note, setNote] = useState("");
|
|
745
|
-
|
|
746
|
-
useEffect(() => {
|
|
747
|
-
let alive = true;
|
|
748
|
-
(async () => {
|
|
749
|
-
try {
|
|
750
|
-
const data = await api("/dsh-tree-task-flow/settings");
|
|
751
|
-
if (alive) setState({ loading: false, data });
|
|
752
|
-
} catch {
|
|
753
|
-
// 读不到就说读不到,别画一排骗人的开关。
|
|
754
|
-
if (alive) setState({ loading: false, data: null });
|
|
755
|
-
}
|
|
756
|
-
})();
|
|
757
|
-
return () => {
|
|
758
|
-
alive = false;
|
|
759
|
-
};
|
|
760
|
-
}, []);
|
|
761
|
-
|
|
762
|
-
if (state.loading) {
|
|
763
|
-
return h("div", { className: "dsh-ttf-settingsPage" }, h("div", { className: "dsh-ttf-settingNote" }, t("settings.loading")));
|
|
764
|
-
}
|
|
765
|
-
if (!state.data) {
|
|
766
|
-
return h(
|
|
767
|
-
"div",
|
|
768
|
-
{ className: "dsh-ttf-settingsPage" },
|
|
769
|
-
h("div", { className: "dsh-ttf-settingNote" }, t("settings.unavailable")),
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const settings = state.data.settings ?? {};
|
|
774
|
-
|
|
775
|
-
async function toggle(key, value) {
|
|
776
|
-
setBusy(true);
|
|
777
|
-
setNote("");
|
|
778
|
-
try {
|
|
779
|
-
const result = await post({ action: "setting", key, value });
|
|
780
|
-
if (result && result.settings) {
|
|
781
|
-
setState({ loading: false, data: Object.assign({}, state.data, { settings: result.settings }) });
|
|
782
|
-
}
|
|
783
|
-
} catch (failure) {
|
|
784
|
-
setNote(String((failure && failure.message) || failure));
|
|
785
|
-
} finally {
|
|
786
|
-
setBusy(false);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
546
|
+
/** 新建需求:只产出草稿。 */
|
|
547
|
+
function DraftForm({ onSubmit, onCancel, busy, t }) {
|
|
548
|
+
const [title, setTitle] = useState("");
|
|
549
|
+
const [intro, setIntro] = useState("");
|
|
790
550
|
return h(
|
|
791
551
|
"div",
|
|
792
|
-
{ className: "dsh-ttf-
|
|
793
|
-
h("
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
:
|
|
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")),
|
|
797
566
|
h(
|
|
798
567
|
"div",
|
|
799
|
-
{ className: "dsh-ttf-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
"aria-label": t("setting." + row.key),
|
|
812
|
-
className: "dsh-ttf-switch" + (on ? " dsh-ttf-switchOn" : ""),
|
|
813
|
-
disabled: busy,
|
|
814
|
-
onClick: () => toggle(row.key, !on),
|
|
815
|
-
},
|
|
816
|
-
h("span", { className: "dsh-ttf-switchKnob" }),
|
|
817
|
-
),
|
|
818
|
-
h(
|
|
819
|
-
"div",
|
|
820
|
-
{ className: "dsh-ttf-settingText" },
|
|
821
|
-
h("span", { className: "dsh-ttf-settingName" }, t("setting." + row.key)),
|
|
822
|
-
h("span", { className: "dsh-ttf-settingNote" }, t(row.note)),
|
|
823
|
-
),
|
|
824
|
-
);
|
|
825
|
-
}),
|
|
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
|
+
),
|
|
826
580
|
),
|
|
827
|
-
note ? h("div", { className: "dsh-ttf-note" }, note) : null,
|
|
828
581
|
);
|
|
829
582
|
}
|
|
830
583
|
|
|
831
|
-
/**
|
|
832
|
-
* 输入框上方的全宽条目。
|
|
833
|
-
*
|
|
834
|
-
* 收起时只有一行:图标 + 阶段标签 + 目标标题 + 进度,动作全在右侧的图标
|
|
835
|
-
* 按钮里(暂停 / 继续、清空、展开)。展开才画整棵树,并限高滚动。
|
|
836
|
-
*
|
|
837
|
-
* 会话没有计划时返回 null:内置的 todo / goal 条目不该因为我而挪位。
|
|
838
|
-
*/
|
|
584
|
+
/** 输入框上方的全宽条目。 */
|
|
839
585
|
function TaskTreeDock(props) {
|
|
840
586
|
const t = useT();
|
|
841
587
|
const sessionId = props.sessionId;
|
|
842
|
-
const [state, setState] = useState({ loading: true });
|
|
588
|
+
const [state, setState] = useState({ loading: true, data: null });
|
|
843
589
|
const [open, setOpen] = useState(false);
|
|
844
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);
|
|
845
598
|
|
|
846
599
|
const reload = useCallback(async () => {
|
|
847
600
|
if (!sessionId) {
|
|
848
|
-
setState({ loading: false,
|
|
601
|
+
setState({ loading: false, data: null });
|
|
849
602
|
return;
|
|
850
603
|
}
|
|
851
604
|
try {
|
|
852
605
|
const data = await api("/dsh-tree-task-flow/tree?sessionId=" + encodeURIComponent(sessionId));
|
|
853
606
|
setState({ loading: false, data });
|
|
854
607
|
} catch {
|
|
855
|
-
|
|
856
|
-
setState({ loading: false, empty: true });
|
|
608
|
+
setState({ loading: false, data: null });
|
|
857
609
|
}
|
|
858
610
|
}, [sessionId]);
|
|
859
611
|
|
|
@@ -861,25 +613,25 @@ window.__ModuleLoader__.load({
|
|
|
861
613
|
reload();
|
|
862
614
|
}, [reload]);
|
|
863
615
|
|
|
864
|
-
// 模型随时会改计划,而条目没有推送通道,所以自己轮询。
|
|
865
|
-
// 4 秒一次,读的是插件自己的一个小 JSON。
|
|
866
616
|
useEffect(() => {
|
|
867
617
|
if (!sessionId) return undefined;
|
|
868
|
-
const timer = setInterval(() =>
|
|
869
|
-
reload();
|
|
870
|
-
}, 4000);
|
|
618
|
+
const timer = setInterval(() => reload(), 4000);
|
|
871
619
|
return () => clearInterval(timer);
|
|
872
620
|
}, [sessionId, reload]);
|
|
873
621
|
|
|
874
|
-
// 这次操作的结果说明(已放行 / 已唤醒 / 为什么没动):恢复失败时用户最需要
|
|
875
|
-
// 的就是这句话,不能只留在接口的返回里。
|
|
876
|
-
const [note, setNote] = useState(null);
|
|
877
622
|
useEffect(() => {
|
|
878
623
|
if (!note) return undefined;
|
|
879
624
|
const timer = setTimeout(() => setNote(null), 8000);
|
|
880
625
|
return () => clearTimeout(timer);
|
|
881
626
|
}, [note]);
|
|
882
627
|
|
|
628
|
+
// 打开面板 = 重新读一次,并把「延迟的归档」结算掉。
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
if (!open) return;
|
|
631
|
+
setOverrides({});
|
|
632
|
+
reload();
|
|
633
|
+
}, [open, reload]);
|
|
634
|
+
|
|
883
635
|
const act = useCallback(
|
|
884
636
|
async (body) => {
|
|
885
637
|
setBusy(true);
|
|
@@ -890,6 +642,8 @@ window.__ModuleLoader__.load({
|
|
|
890
642
|
setNote(result.message);
|
|
891
643
|
}
|
|
892
644
|
await reload();
|
|
645
|
+
} catch (failure) {
|
|
646
|
+
setNote(String((failure && failure.message) || failure));
|
|
893
647
|
} finally {
|
|
894
648
|
setBusy(false);
|
|
895
649
|
}
|
|
@@ -897,36 +651,72 @@ window.__ModuleLoader__.load({
|
|
|
897
651
|
[sessionId, reload],
|
|
898
652
|
);
|
|
899
653
|
|
|
900
|
-
if (state.loading ||
|
|
654
|
+
if (state.loading || !state.data) return null;
|
|
901
655
|
|
|
902
656
|
const data = state.data;
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
(acc, each) => {
|
|
909
|
-
const one = countNodes(each);
|
|
910
|
-
return { total: acc.total + one.total, done: acc.done + one.done };
|
|
911
|
-
},
|
|
912
|
-
{ total: 0, done: 0 },
|
|
913
|
-
);
|
|
914
|
-
const heading = goals.length === 1 ? goals[0].title : t("label.goals", { count: goals.length });
|
|
915
|
-
const current = currentLabel(data);
|
|
916
|
-
// 「被按住」有两条来源:插件闸门(paused)与人按过界面停止(stopped)。
|
|
917
|
-
// 对用户来说都是"停着呢,点继续就能接着跑",所以合成一个状态展示。
|
|
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
|
+
|
|
918
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");
|
|
919
676
|
const stateLabel = held
|
|
920
677
|
? t("state.paused")
|
|
921
|
-
:
|
|
922
|
-
? t("state.
|
|
923
|
-
:
|
|
924
|
-
? t(
|
|
925
|
-
: t("state.
|
|
926
|
-
|
|
927
|
-
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);
|
|
928
684
|
const listGlyph =
|
|
929
|
-
|
|
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
|
+
};
|
|
930
720
|
|
|
931
721
|
return h(
|
|
932
722
|
"div",
|
|
@@ -940,14 +730,8 @@ window.__ModuleLoader__.load({
|
|
|
940
730
|
h("span", { className: "dsh-ttf-glyph", "aria-hidden": true }, listGlyph),
|
|
941
731
|
h("span", { className: "dsh-ttf-label" + (held ? " dsh-ttf-labelHeld" : "") }, stateLabel),
|
|
942
732
|
h("span", { className: "dsh-ttf-title", title: heading }, heading),
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
stats.total > 0
|
|
946
|
-
? h(
|
|
947
|
-
"span",
|
|
948
|
-
{ className: "dsh-ttf-progress" },
|
|
949
|
-
stats.done + "/" + stats.total + (current ? " · " + current : ""),
|
|
950
|
-
)
|
|
733
|
+
counts !== null && counts.total > 0
|
|
734
|
+
? h("span", { className: "dsh-ttf-progress" }, counts.done + "/" + counts.total)
|
|
951
735
|
: null,
|
|
952
736
|
h(
|
|
953
737
|
"div",
|
|
@@ -958,14 +742,6 @@ window.__ModuleLoader__.load({
|
|
|
958
742
|
disabled: busy,
|
|
959
743
|
onClick: () => act({ action: held ? "resume" : "pause" }),
|
|
960
744
|
}),
|
|
961
|
-
h(IconButton, {
|
|
962
|
-
iconBase: "IconTrashOutline",
|
|
963
|
-
label: t("action.reset"),
|
|
964
|
-
disabled: busy,
|
|
965
|
-
onClick: () => {
|
|
966
|
-
if (window.confirm(t("confirm.reset"))) act({ action: "reset" });
|
|
967
|
-
},
|
|
968
|
-
}),
|
|
969
745
|
h(IconButton, {
|
|
970
746
|
iconBase: open ? "IconChevronDownOutline" : "IconChevronUpOutline",
|
|
971
747
|
label: open ? t("action.collapse") : t("action.expand"),
|
|
@@ -973,55 +749,199 @@ window.__ModuleLoader__.load({
|
|
|
973
749
|
}),
|
|
974
750
|
),
|
|
975
751
|
),
|
|
976
|
-
// 这次操作的结果与原因(已放行 / 已唤醒 / 为什么没动)。
|
|
977
752
|
note ? h("div", { className: "dsh-ttf-note" }, note) : null,
|
|
978
753
|
open
|
|
979
754
|
? h(
|
|
980
755
|
"div",
|
|
981
756
|
{ className: "dsh-ttf-body" },
|
|
982
|
-
|
|
757
|
+
held ? h("div", { className: "dsh-ttf-note" }, t("note.paused")) : null,
|
|
758
|
+
|
|
759
|
+
lists.active.length > 0
|
|
983
760
|
? h(
|
|
984
761
|
"div",
|
|
985
|
-
|
|
986
|
-
t("
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
+
),
|
|
993
779
|
)
|
|
994
780
|
: null,
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
h(
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
),
|
|
1011
832
|
)
|
|
1012
833
|
: null,
|
|
1013
834
|
),
|
|
1014
835
|
);
|
|
1015
836
|
}
|
|
1016
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
|
+
|
|
1017
938
|
// ---------------------------------------------------------------- 挂载
|
|
1018
939
|
|
|
1019
940
|
function apply(ctx) {
|
|
1020
941
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-tree-task-flow: dictionaries");
|
|
1021
942
|
translate = ctx.locale.bind(NS);
|
|
943
|
+
ensureStyles();
|
|
1022
944
|
|
|
1023
|
-
// 输入框上方的条目。list 槽 + 自己的 id ⇒ 只会并列加在内置
|
|
1024
|
-
// 的 queue / todo / goal 旁边,不会替换任何一个。
|
|
1025
945
|
ctx.effect(
|
|
1026
946
|
() =>
|
|
1027
947
|
ctx.slots.inject("conversation.input.dock", () =>
|
|
@@ -1038,24 +958,19 @@ window.__ModuleLoader__.load({
|
|
|
1038
958
|
"dsh-tree-task-flow: conversation dock",
|
|
1039
959
|
);
|
|
1040
960
|
|
|
1041
|
-
// 设置面板里的那一页。用 `settings.section` 这个**列表槽**,所以它会作为
|
|
1042
|
-
// 一行出现在 DSH 设置面板的导航里(跟「通用」「市场」并排),点开就是
|
|
1043
|
-
// 那两个开关。槽没被组合出来时(比如没有设置面板的宿主)这里什么都不发生。
|
|
1044
961
|
ctx.effect(
|
|
1045
962
|
() =>
|
|
1046
|
-
ctx.slots.inject("
|
|
963
|
+
ctx.slots.inject("plugins.bundle.config", () =>
|
|
1047
964
|
ctx.slots.register(
|
|
1048
965
|
{
|
|
1049
|
-
name: "
|
|
1050
|
-
|
|
1051
|
-
order: 60,
|
|
1052
|
-
label: () => translate("settings.nav"),
|
|
966
|
+
name: "plugins.bundle.config",
|
|
967
|
+
key: BUNDLE_PACKAGE,
|
|
1053
968
|
locale: NS,
|
|
1054
969
|
},
|
|
1055
|
-
|
|
970
|
+
TaskTreeConfigEntry,
|
|
1056
971
|
),
|
|
1057
972
|
),
|
|
1058
|
-
"dsh-tree-task-flow:
|
|
973
|
+
"dsh-tree-task-flow: 插件配置页",
|
|
1059
974
|
);
|
|
1060
975
|
}
|
|
1061
976
|
|