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.
@@ -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
- <button
208
- className="btn small ghost"
209
- onClick={prune}
210
- disabled={pruning}
211
- style={{ marginLeft: 'auto' }}
212
- title="清理旧模型无目录主题与空主题(等待中一律保留)"
213
- >
214
- {pruning ? '清理中…' : '清理旧数据'}
215
- </button>
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
- <button
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={{ fontWeight: 700, marginBottom: 2 }}>{t.name || '(未命名)'}</div>
241
- <div
242
- className="mono muted"
243
- title={t.cwd}
244
- style={{ fontSize: 11, wordBreak: 'break-all', marginBottom: 6, minHeight: 15 }}
245
- >
246
- {t.cwd || '(旧数据,无目录)'}
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
- </button>
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: 76 }}>操作</th>
482
+ <th style={{ width: 150 }}>操作</th>
312
483
  </tr>
313
484
  </thead>
314
485
  <tbody>
315
486
  {tasks.length ? (
316
- tasks.map((t) => (
317
- <tr key={t.id}>
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
- {t.status !== 'consumed' && editingId !== t.id && (
373
- <button className="btn small ghost" onClick={() => startEdit(t)}>编辑</button>
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="5" className="muted" style={{ textAlign: 'center', padding: '22px 0' }}>
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-BRHRrC1Y.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};
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};