nx-kv 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +3 -2
- package/assets/kv-sessiontask/SKILL.md +7 -1
- package/assets/kv-sessiontask/references/sessiontask-workflow.md +18 -2
- package/package.json +1 -1
- package/src/modules/sessiontask/index.js +65 -0
- package/src/modules/sessiontask/service.js +132 -1
- package/src/modules/sessiontask/view.jsx +249 -28
- package/src/web/public/assets/{CliHints-ChSGOGe2.js → CliHints-BcGuVv3q.js} +1 -1
- package/src/web/public/assets/{index-BRHRrC1Y.js → index-BC4hXOgL.js} +2 -2
- package/src/web/public/assets/{view-BkFTel2t.js → view-Bq7gBnSO.js} +1 -1
- package/src/web/public/assets/view-DpLqgnYO.js +1 -0
- package/src/web/public/assets/{view-Bz43nKKp.js → view-Xx2TV723.js} +1 -1
- package/src/web/public/assets/{view-DgoHpORS.js → view-hrrRARfz.js} +1 -1
- package/src/web/public/index.html +1 -1
- package/src/web/public/assets/view-DMK3MwZZ.js +0 -1
|
@@ -36,6 +36,15 @@ export default function SessionTaskView() {
|
|
|
36
36
|
const [editText, setEditText] = useState('');
|
|
37
37
|
const [editSaving, setEditSaving] = useState(false);
|
|
38
38
|
|
|
39
|
+
// 拖拽重排(仅待领取任务)
|
|
40
|
+
const [dragId, setDragId] = useState(null);
|
|
41
|
+
const [over, setOver] = useState(null); // { id, pos:'top'|'bottom' }
|
|
42
|
+
const [reordering, setReordering] = useState(false);
|
|
43
|
+
|
|
44
|
+
// 主题勾选(批量删除)
|
|
45
|
+
const [selected, setSelected] = useState(() => new Set());
|
|
46
|
+
const [deleting, setDeleting] = useState(false);
|
|
47
|
+
|
|
39
48
|
const load = useCallback(async () => {
|
|
40
49
|
try {
|
|
41
50
|
setOverview(await api('/api/sessiontasks'));
|
|
@@ -101,8 +110,27 @@ export default function SessionTaskView() {
|
|
|
101
110
|
setActiveKey(w.key);
|
|
102
111
|
}, [topics, activeKey]);
|
|
103
112
|
|
|
113
|
+
// 主题被删除后,从勾选集合里剔除失效 key
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
setSelected((s) => {
|
|
116
|
+
const valid = new Set([...s].filter((k) => topics.some((t) => t.key === k)));
|
|
117
|
+
return valid.size === s.size ? s : valid;
|
|
118
|
+
});
|
|
119
|
+
}, [topics]);
|
|
120
|
+
|
|
104
121
|
const active = topics.find((t) => t.key === activeKey) || null;
|
|
105
122
|
|
|
123
|
+
const toggleSelect = (key) =>
|
|
124
|
+
setSelected((s) => {
|
|
125
|
+
const n = new Set(s);
|
|
126
|
+
if (n.has(key)) n.delete(key);
|
|
127
|
+
else n.add(key);
|
|
128
|
+
return n;
|
|
129
|
+
});
|
|
130
|
+
const allSelected = topics.length > 0 && selected.size === topics.length;
|
|
131
|
+
const toggleAll = () =>
|
|
132
|
+
setSelected(allSelected ? new Set() : new Set(topics.map((t) => t.key)));
|
|
133
|
+
|
|
106
134
|
const submit = () =>
|
|
107
135
|
guard(async () => {
|
|
108
136
|
const body = text.trim();
|
|
@@ -138,6 +166,56 @@ export default function SessionTaskView() {
|
|
|
138
166
|
await load();
|
|
139
167
|
});
|
|
140
168
|
|
|
169
|
+
// 删除整个主题(卡片上的「删除主题」);等待中需二次确认并 force
|
|
170
|
+
const deleteTopic = (t) =>
|
|
171
|
+
guard(async () => {
|
|
172
|
+
const msg = t.waiting
|
|
173
|
+
? `主题「${t.name}」正在等待中,删除后其 agent 将超时收尾,确认删除?`
|
|
174
|
+
: `删除主题「${t.name}」及其全部 ${t.total} 条任务?`;
|
|
175
|
+
if (!window.confirm(msg)) return;
|
|
176
|
+
setDeleting(true);
|
|
177
|
+
try {
|
|
178
|
+
await api('/api/sessiontasks/topic', {
|
|
179
|
+
method: 'DELETE',
|
|
180
|
+
body: t.waiting ? { key: t.key, force: true } : { key: t.key },
|
|
181
|
+
});
|
|
182
|
+
setSelected((s) => {
|
|
183
|
+
const n = new Set(s);
|
|
184
|
+
n.delete(t.key);
|
|
185
|
+
return n;
|
|
186
|
+
});
|
|
187
|
+
toast('已删除主题');
|
|
188
|
+
await load();
|
|
189
|
+
} finally {
|
|
190
|
+
setDeleting(false);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 批量删除勾选主题;含等待中时 force 并提示
|
|
195
|
+
const batchDelete = () =>
|
|
196
|
+
guard(async () => {
|
|
197
|
+
if (!selected.size) return;
|
|
198
|
+
const sel = topics.filter((t) => selected.has(t.key));
|
|
199
|
+
const waitingCount = sel.filter((t) => t.waiting).length;
|
|
200
|
+
let msg = `确认删除选中的 ${sel.length} 个主题?`;
|
|
201
|
+
if (waitingCount) msg += `(其中 ${waitingCount} 个等待中,删除后其 agent 将超时收尾)`;
|
|
202
|
+
if (!window.confirm(msg)) return;
|
|
203
|
+
setDeleting(true);
|
|
204
|
+
try {
|
|
205
|
+
const r = await api('/api/sessiontasks/topics/remove', {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
body: { keys: [...selected], force: waitingCount > 0 },
|
|
208
|
+
});
|
|
209
|
+
toast(
|
|
210
|
+
`已删除 ${r.count} 个主题` + (r.skipped.length ? `,跳过 ${r.skipped.length} 个` : '')
|
|
211
|
+
);
|
|
212
|
+
setSelected(new Set());
|
|
213
|
+
await load();
|
|
214
|
+
} finally {
|
|
215
|
+
setDeleting(false);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
141
219
|
const startEdit = (t) => {
|
|
142
220
|
setEditingId(t.id);
|
|
143
221
|
setEditText(t.text);
|
|
@@ -169,6 +247,55 @@ export default function SessionTaskView() {
|
|
|
169
247
|
|
|
170
248
|
const tasks = active ? active.tasks.slice().reverse() : [];
|
|
171
249
|
|
|
250
|
+
// 待领取任务按「领取优先级」(数组真实顺序,未反转)排列;上移/置后据此定位相邻项
|
|
251
|
+
const pendingOrder = active ? active.tasks.filter((t) => t.status !== 'consumed') : [];
|
|
252
|
+
|
|
253
|
+
// 把 dragId 移到 beforeId 之前 / afterId 之后(数组顺序)
|
|
254
|
+
const applyReorder = (id, { beforeId, afterId }) =>
|
|
255
|
+
guard(async () => {
|
|
256
|
+
setReordering(true);
|
|
257
|
+
try {
|
|
258
|
+
await api('/api/sessiontasks/reorder', {
|
|
259
|
+
method: 'PATCH',
|
|
260
|
+
body: { cwd: active.cwd, id, beforeId, afterId },
|
|
261
|
+
});
|
|
262
|
+
toast('已调整领取优先级');
|
|
263
|
+
await load();
|
|
264
|
+
} finally {
|
|
265
|
+
setReordering(false);
|
|
266
|
+
setDragId(null);
|
|
267
|
+
setOver(null);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// 提前/置后一步(在 pending 子序列里移动),作为拖拽之外的兜底(窄屏/触屏友好)
|
|
272
|
+
const stepPriority = (t, dir) => {
|
|
273
|
+
const i = pendingOrder.findIndex((x) => x.id === t.id);
|
|
274
|
+
if (dir === 'up' && i > 0) applyReorder(t.id, { beforeId: pendingOrder[i - 1].id });
|
|
275
|
+
if (dir === 'down' && i < pendingOrder.length - 1) applyReorder(t.id, { afterId: pendingOrder[i + 1].id });
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// 表格反转显示:视觉上「上方」= 数组里更靠后。落点在目标行上半 → 拖到它数组之后;
|
|
279
|
+
// 下半 → 拖到它数组之前。
|
|
280
|
+
const onRowDragOver = (e, t) => {
|
|
281
|
+
if (dragId === null || t.status === 'consumed' || dragId === t.id) return;
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
e.dataTransfer.dropEffect = 'move';
|
|
284
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
285
|
+
const pos = e.clientY < rect.top + rect.height / 2 ? 'top' : 'bottom';
|
|
286
|
+
setOver((o) => (o && o.id === t.id && o.pos === pos ? o : { id: t.id, pos }));
|
|
287
|
+
};
|
|
288
|
+
const onRowDrop = (e, t) => {
|
|
289
|
+
if (dragId === null || t.status === 'consumed' || dragId === t.id) return;
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
292
|
+
const top = e.clientY < rect.top + rect.height / 2;
|
|
293
|
+
const moved = dragId;
|
|
294
|
+
setDragId(null);
|
|
295
|
+
setOver(null);
|
|
296
|
+
applyReorder(moved, top ? { afterId: t.id } : { beforeId: t.id });
|
|
297
|
+
};
|
|
298
|
+
|
|
172
299
|
return (
|
|
173
300
|
<>
|
|
174
301
|
<div className="card" style={{ padding: '10px 14px' }}>
|
|
@@ -202,17 +329,34 @@ export default function SessionTaskView() {
|
|
|
202
329
|
</div>
|
|
203
330
|
|
|
204
331
|
<div className="colhead">
|
|
332
|
+
<input
|
|
333
|
+
type="checkbox"
|
|
334
|
+
checked={allSelected}
|
|
335
|
+
onChange={toggleAll}
|
|
336
|
+
disabled={!topics.length}
|
|
337
|
+
title="全选 / 取消全选"
|
|
338
|
+
style={{ margin: 0 }}
|
|
339
|
+
/>
|
|
205
340
|
<h3>主题总览(按工作目录)</h3>
|
|
206
341
|
<span className="muted">{overview ? `${topics.length} 个 · 等待中 ${overview.waiting}` : ''}</span>
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
342
|
+
<span style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
|
343
|
+
<button
|
|
344
|
+
className="btn small ghost"
|
|
345
|
+
onClick={batchDelete}
|
|
346
|
+
disabled={!selected.size || deleting}
|
|
347
|
+
title="删除勾选的主题(含其全部任务)"
|
|
348
|
+
>
|
|
349
|
+
{deleting ? '删除中…' : `批量删除${selected.size ? ` (${selected.size})` : ''}`}
|
|
350
|
+
</button>
|
|
351
|
+
<button
|
|
352
|
+
className="btn small ghost"
|
|
353
|
+
onClick={prune}
|
|
354
|
+
disabled={pruning}
|
|
355
|
+
title="清理旧模型无目录主题与空主题(等待中一律保留)"
|
|
356
|
+
>
|
|
357
|
+
{pruning ? '清理中…' : '清理旧数据'}
|
|
358
|
+
</button>
|
|
359
|
+
</span>
|
|
216
360
|
</div>
|
|
217
361
|
|
|
218
362
|
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 14 }}>
|
|
@@ -220,7 +364,7 @@ export default function SessionTaskView() {
|
|
|
220
364
|
topics.map((t) => {
|
|
221
365
|
const isActive = t.key === activeKey;
|
|
222
366
|
return (
|
|
223
|
-
<
|
|
367
|
+
<div
|
|
224
368
|
key={t.key}
|
|
225
369
|
onClick={() => setActiveKey(t.key)}
|
|
226
370
|
className="card"
|
|
@@ -229,21 +373,31 @@ export default function SessionTaskView() {
|
|
|
229
373
|
width: 270,
|
|
230
374
|
padding: '10px 12px',
|
|
231
375
|
cursor: 'pointer',
|
|
232
|
-
font: 'inherit',
|
|
233
|
-
color: 'inherit',
|
|
234
376
|
border: isActive ? '1px solid var(--ink,#14161a)' : '1px solid transparent',
|
|
235
377
|
boxShadow: isActive
|
|
236
378
|
? '0 0 0 2px rgba(20,22,26,.12)'
|
|
237
379
|
: '0 1px 2px rgba(20,22,26,.06), 0 6px 16px rgba(20,22,26,.05)',
|
|
238
380
|
}}
|
|
239
381
|
>
|
|
240
|
-
<div style={{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
382
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
|
|
383
|
+
<input
|
|
384
|
+
type="checkbox"
|
|
385
|
+
checked={selected.has(t.key)}
|
|
386
|
+
onChange={() => toggleSelect(t.key)}
|
|
387
|
+
onClick={(e) => e.stopPropagation()}
|
|
388
|
+
title="选择以批量删除"
|
|
389
|
+
style={{ margin: '3px 0 0' }}
|
|
390
|
+
/>
|
|
391
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
392
|
+
<div style={{ fontWeight: 700, marginBottom: 2 }}>{t.name || '(未命名)'}</div>
|
|
393
|
+
<div
|
|
394
|
+
className="mono muted"
|
|
395
|
+
title={t.cwd}
|
|
396
|
+
style={{ fontSize: 11, wordBreak: 'break-all', marginBottom: 6, minHeight: 15 }}
|
|
397
|
+
>
|
|
398
|
+
{t.cwd || '(旧数据,无目录)'}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
247
401
|
</div>
|
|
248
402
|
{t.summary && (
|
|
249
403
|
<div className="muted" style={{ fontSize: 11, whiteSpace: 'pre-wrap', marginBottom: 6 }}>
|
|
@@ -260,7 +414,20 @@ export default function SessionTaskView() {
|
|
|
260
414
|
待领取 {t.pending}/{t.total}
|
|
261
415
|
</span>
|
|
262
416
|
</div>
|
|
263
|
-
|
|
417
|
+
<div
|
|
418
|
+
style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 8 }}
|
|
419
|
+
onClick={(e) => e.stopPropagation()}
|
|
420
|
+
>
|
|
421
|
+
<button
|
|
422
|
+
className="btn small ghost"
|
|
423
|
+
onClick={() => deleteTopic(t)}
|
|
424
|
+
disabled={deleting}
|
|
425
|
+
title="删除整个主题(含全部任务)"
|
|
426
|
+
>
|
|
427
|
+
删除主题
|
|
428
|
+
</button>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
264
431
|
);
|
|
265
432
|
})
|
|
266
433
|
) : (
|
|
@@ -298,23 +465,55 @@ export default function SessionTaskView() {
|
|
|
298
465
|
<div className="colhead">
|
|
299
466
|
<h3>该主题的任务队列</h3>
|
|
300
467
|
<span className="muted">待领取 {active.pending} · 共 {active.total} 条</span>
|
|
468
|
+
<span className="muted" style={{ fontSize: 12, marginLeft: 'auto' }}>
|
|
469
|
+
拖拽待领取行(或用「提前/置后」)调整领取优先级
|
|
470
|
+
</span>
|
|
301
471
|
</div>
|
|
302
472
|
|
|
303
473
|
<div className="card" style={{ padding: 4 }}>
|
|
304
474
|
<table>
|
|
305
475
|
<thead>
|
|
306
476
|
<tr>
|
|
477
|
+
<th style={{ width: 34 }}></th>
|
|
307
478
|
<th style={{ width: 56 }}>#</th>
|
|
308
479
|
<th>任务</th>
|
|
309
480
|
<th style={{ width: 80 }}>状态</th>
|
|
310
481
|
<th style={{ width: 160 }}>时间</th>
|
|
311
|
-
<th style={{ width:
|
|
482
|
+
<th style={{ width: 150 }}>操作</th>
|
|
312
483
|
</tr>
|
|
313
484
|
</thead>
|
|
314
485
|
<tbody>
|
|
315
486
|
{tasks.length ? (
|
|
316
|
-
tasks.map((t) =>
|
|
317
|
-
|
|
487
|
+
tasks.map((t) => {
|
|
488
|
+
const isPending = t.status !== 'consumed';
|
|
489
|
+
const isDrag = dragId === t.id;
|
|
490
|
+
const isOver = over && over.id === t.id;
|
|
491
|
+
return (
|
|
492
|
+
<tr
|
|
493
|
+
key={t.id}
|
|
494
|
+
draggable={isPending && editingId !== t.id}
|
|
495
|
+
onDragStart={(e) => {
|
|
496
|
+
setDragId(t.id);
|
|
497
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
498
|
+
try { e.dataTransfer.setData('text/plain', String(t.id)); } catch { /* 某些浏览器需 setData 才能拖拽 */ }
|
|
499
|
+
}}
|
|
500
|
+
onDragOver={(e) => onRowDragOver(e, t)}
|
|
501
|
+
onDrop={(e) => onRowDrop(e, t)}
|
|
502
|
+
onDragEnd={() => { setDragId(null); setOver(null); }}
|
|
503
|
+
style={{
|
|
504
|
+
cursor: isPending && editingId !== t.id ? 'grab' : 'default',
|
|
505
|
+
opacity: isDrag ? 0.4 : 1,
|
|
506
|
+
boxShadow: isOver
|
|
507
|
+
? over.pos === 'top'
|
|
508
|
+
? 'inset 0 2px 0 var(--ink,#14161a)'
|
|
509
|
+
: 'inset 0 -2px 0 var(--ink,#14161a)'
|
|
510
|
+
: 'none',
|
|
511
|
+
background: isOver ? 'rgba(20,22,26,.04)' : undefined,
|
|
512
|
+
}}
|
|
513
|
+
>
|
|
514
|
+
<td className="mono muted" title="按住拖拽调整优先级" style={{ textAlign: 'center', userSelect: 'none' }}>
|
|
515
|
+
{isPending ? '⠿' : ''}
|
|
516
|
+
</td>
|
|
318
517
|
<td className="mono">#{t.id}</td>
|
|
319
518
|
<td style={{ whiteSpace: 'pre-wrap' }}>
|
|
320
519
|
{editingId === t.id ? (
|
|
@@ -369,16 +568,38 @@ export default function SessionTaskView() {
|
|
|
369
568
|
</td>
|
|
370
569
|
<td className="mono">{t.consumedAt || t.createdAt}</td>
|
|
371
570
|
<td className="ops">
|
|
372
|
-
{
|
|
373
|
-
|
|
374
|
-
|
|
571
|
+
{isPending && editingId !== t.id && (() => {
|
|
572
|
+
const pi = pendingOrder.findIndex((x) => x.id === t.id);
|
|
573
|
+
return (
|
|
574
|
+
<>
|
|
575
|
+
<button className="btn small ghost" onClick={() => startEdit(t)}>编辑</button>
|
|
576
|
+
<button
|
|
577
|
+
className="btn small ghost"
|
|
578
|
+
title="提高优先级(更早被领取)"
|
|
579
|
+
disabled={reordering || pi <= 0}
|
|
580
|
+
onClick={() => stepPriority(t, 'up')}
|
|
581
|
+
>
|
|
582
|
+
提前
|
|
583
|
+
</button>
|
|
584
|
+
<button
|
|
585
|
+
className="btn small ghost"
|
|
586
|
+
title="降低优先级(更晚被领取)"
|
|
587
|
+
disabled={reordering || pi >= pendingOrder.length - 1}
|
|
588
|
+
onClick={() => stepPriority(t, 'down')}
|
|
589
|
+
>
|
|
590
|
+
置后
|
|
591
|
+
</button>
|
|
592
|
+
</>
|
|
593
|
+
);
|
|
594
|
+
})()}
|
|
375
595
|
<button className="btn small ghost" onClick={() => remove(t.id)}>删除</button>
|
|
376
596
|
</td>
|
|
377
597
|
</tr>
|
|
378
|
-
|
|
598
|
+
);
|
|
599
|
+
})
|
|
379
600
|
) : (
|
|
380
601
|
<tr>
|
|
381
|
-
<td colSpan="
|
|
602
|
+
<td colSpan="6" className="muted" style={{ textAlign: 'center', padding: '22px 0' }}>
|
|
382
603
|
(暂无任务,在上方回填第一条)
|
|
383
604
|
</td>
|
|
384
605
|
</tr>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,s as t,t as n}from"./index-
|
|
1
|
+
import{o as e,s as t,t as n}from"./index-BC4hXOgL.js";var r=t();function i({module:t}){let{boot:i}=e(),a=(i?.commands||[]).filter(e=>e.module===t);return a.length?(0,r.jsxs)(`div`,{className:`cli-hint`,children:[(0,r.jsx)(`span`,{className:`cli-hint-label`,children:`CLI 等价(同构命令,加 --json 得机器可读输出):`}),a.map(e=>(0,r.jsx)(n,{className:`cli-cmd`,text:e.command,title:`点击复制:${e.usage}`,children:e.command},e.id))]}):null}export{i as t};
|