nx-kv 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,294 @@
1
+ // 实时任务页:主题(按工作目录划分)总览 + 选中某个主题回填 + 该主题的任务队列。
2
+ // 本质是「agent 提交结果 → 阻塞 → 用户在 web 针对主题回填 → agent 接收」。
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { api } from '../../web/frontend/api/client.js';
5
+ import { useToast, useGuard } from '../../web/frontend/components/ui.jsx';
6
+ import { CliHints } from '../../web/frontend/components/CliHints.jsx';
7
+
8
+ // “YYYY-MM-DD HH:mm:ss” → 本地 Date
9
+ function parseStamp(s) {
10
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(s || '');
11
+ if (!m) return null;
12
+ return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
13
+ }
14
+ function countdown(s) {
15
+ const d = parseStamp(s);
16
+ if (!d) return '';
17
+ const ms = d.getTime() - Date.now();
18
+ if (ms <= 0) return '等待中 0:00';
19
+ const mm = Math.floor(ms / 60000);
20
+ const ss = Math.floor((ms % 60000) / 1000);
21
+ return `等待中 ${mm}:${String(ss).padStart(2, '0')}`;
22
+ }
23
+
24
+ export default function SessionTaskView() {
25
+ const toast = useToast();
26
+ const guard = useGuard();
27
+
28
+ const [overview, setOverview] = useState(null);
29
+ const [activeKey, setActiveKey] = useState(null);
30
+ const [text, setText] = useState('');
31
+ const [saving, setSaving] = useState(false);
32
+ const [timeoutSec, setTimeoutSec] = useState(180);
33
+ const [cfgSaving, setCfgSaving] = useState(false);
34
+
35
+ const load = useCallback(async () => {
36
+ try {
37
+ setOverview(await api('/api/sessiontasks'));
38
+ } catch (e) {
39
+ toast(e.message);
40
+ }
41
+ }, [toast]);
42
+
43
+ useEffect(() => {
44
+ load();
45
+ }, [load]);
46
+
47
+ // 实时刷新:主题状态/倒计时、任务是否被取走,每 1.5s 轮询
48
+ useEffect(() => {
49
+ const t = setInterval(load, 1500);
50
+ return () => clearInterval(t);
51
+ }, [load]);
52
+
53
+ const topics = overview ? overview.topics : [];
54
+
55
+ // 同步页面上的超时配置(来源:后端 settings)
56
+ useEffect(() => {
57
+ if (overview && overview.settings) setTimeoutSec(overview.settings.timeoutSec);
58
+ }, [overview]);
59
+
60
+ const saveTimeout = (sec) =>
61
+ guard(async () => {
62
+ setTimeoutSec(Number(sec));
63
+ setCfgSaving(true);
64
+ try {
65
+ const s = await api('/api/sessiontasks/setting', {
66
+ method: 'PATCH',
67
+ body: { timeout: Number(sec) },
68
+ });
69
+ setTimeoutSec(s.timeoutSec);
70
+ toast(`等待超时已配置为 ${s.timeoutSec} 秒`);
71
+ } finally {
72
+ setCfgSaving(false);
73
+ }
74
+ });
75
+
76
+ // 默认选中:优先等待中的,否则第一个;用户已选且仍存在则保留
77
+ useEffect(() => {
78
+ if (!topics.length) {
79
+ setActiveKey(null);
80
+ return;
81
+ }
82
+ if (activeKey && topics.some((t) => t.key === activeKey)) return;
83
+ const w = topics.find((t) => t.waiting) || topics[0];
84
+ setActiveKey(w.key);
85
+ }, [topics, activeKey]);
86
+
87
+ const active = topics.find((t) => t.key === activeKey) || null;
88
+
89
+ const submit = () =>
90
+ guard(async () => {
91
+ const body = text.trim();
92
+ if (!body) {
93
+ toast('请输入任务内容');
94
+ return;
95
+ }
96
+ if (!active) {
97
+ toast('请先选择一个主题(工作目录)');
98
+ return;
99
+ }
100
+ setSaving(true);
101
+ try {
102
+ const r = await api('/api/sessiontasks', {
103
+ method: 'POST',
104
+ body: { cwd: active.cwd, text: body },
105
+ });
106
+ setText('');
107
+ toast(r.delivered ? '已返回给等待中的 agent' : '已排入该主题队列,等待 agent 下一次 wait');
108
+ await load();
109
+ } finally {
110
+ setSaving(false);
111
+ }
112
+ });
113
+
114
+ const remove = (id) =>
115
+ guard(async () => {
116
+ await api('/api/sessiontasks/item', {
117
+ method: 'DELETE',
118
+ body: { cwd: active.cwd, id },
119
+ });
120
+ toast('已删除');
121
+ await load();
122
+ });
123
+
124
+ const tasks = active ? active.tasks.slice().reverse() : [];
125
+
126
+ return (
127
+ <>
128
+ <div className="card" style={{ padding: '10px 14px' }}>
129
+ <div className="muted" style={{ fontSize: 12 }}>
130
+ agent 用法:完成当前任务后,把
131
+ <code> nx-kv sessiontask wait --cwd "&lt;工作目录&gt;" --summary "&lt;当前现状&gt;" --json </code>
132
+ 作为本轮<strong>最后一个动作</strong>调用——工作目录即<strong>主题</strong>、主题拥有任务队列,
133
+ 同目录共用队列、不同目录互不串,任务可在队列里堆积;命令阻塞并自动打开浏览器弹窗,
134
+ 用户回填后解除阻塞、返回新任务;超时未回填则返回 <code>status:"timeout"</code>,agent 收尾结束本轮。
135
+ </div>
136
+ </div>
137
+
138
+ <div className="card" style={{ padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
139
+ <span style={{ fontWeight: 700 }}>等待超时</span>
140
+ <select
141
+ value={timeoutSec}
142
+ disabled={cfgSaving}
143
+ onChange={(e) => saveTimeout(e.target.value)}
144
+ style={{ font: 'inherit', padding: '5px 8px' }}
145
+ >
146
+ <option value={60}>1 分钟</option>
147
+ <option value={120}>2 分钟</option>
148
+ <option value={180}>3 分钟(推荐)</option>
149
+ <option value={240}>4 分钟</option>
150
+ <option value={300}>5 分钟</option>
151
+ <option value={600}>10 分钟(最大)</option>
152
+ </select>
153
+ <span className="muted" style={{ fontSize: 12 }}>
154
+ agent wait 未带 --timeout 时使用此值;推荐 3 分钟、最大 10 分钟。
155
+ </span>
156
+ </div>
157
+
158
+ <div className="colhead">
159
+ <h3>主题总览(按工作目录)</h3>
160
+ <span className="muted">{overview ? `${topics.length} 个 · 等待中 ${overview.waiting}` : ''}</span>
161
+ </div>
162
+
163
+ <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 14 }}>
164
+ {topics.length ? (
165
+ topics.map((t) => {
166
+ const isActive = t.key === activeKey;
167
+ return (
168
+ <button
169
+ key={t.key}
170
+ onClick={() => setActiveKey(t.key)}
171
+ className="card"
172
+ style={{
173
+ textAlign: 'left',
174
+ width: 270,
175
+ padding: '10px 12px',
176
+ cursor: 'pointer',
177
+ font: 'inherit',
178
+ color: 'inherit',
179
+ border: isActive ? '1px solid var(--ink,#14161a)' : '1px solid transparent',
180
+ boxShadow: isActive
181
+ ? '0 0 0 2px rgba(20,22,26,.12)'
182
+ : '0 1px 2px rgba(20,22,26,.06), 0 6px 16px rgba(20,22,26,.05)',
183
+ }}
184
+ >
185
+ <div style={{ fontWeight: 700, marginBottom: 2 }}>{t.name || '(未命名)'}</div>
186
+ <div
187
+ className="mono muted"
188
+ title={t.cwd}
189
+ style={{ fontSize: 11, wordBreak: 'break-all', marginBottom: 6, minHeight: 15 }}
190
+ >
191
+ {t.cwd || '(旧数据,无目录)'}
192
+ </div>
193
+ {t.summary && (
194
+ <div className="muted" style={{ fontSize: 11, whiteSpace: 'pre-wrap', marginBottom: 6 }}>
195
+ 现状:{t.summary}
196
+ </div>
197
+ )}
198
+ <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
199
+ {t.waiting ? (
200
+ <span className="tag strong">{countdown(t.waitingUntil)}</span>
201
+ ) : (
202
+ <span className="tag">已结束</span>
203
+ )}
204
+ <span className="muted" style={{ fontSize: 11 }}>
205
+ 待领取 {t.pending}/{t.total}
206
+ </span>
207
+ </div>
208
+ </button>
209
+ );
210
+ })
211
+ ) : (
212
+ <div className="card muted" style={{ padding: '20px 16px', width: '100%', textAlign: 'center' }}>
213
+ (暂无主题;agent 调用 sessiontask wait 后会出现在这里)
214
+ </div>
215
+ )}
216
+ </div>
217
+
218
+ {active && (
219
+ <>
220
+ <div className="toolbar">
221
+ <span style={{ fontWeight: 700, maxWidth: 240 }} title={active.cwd}>
222
+ {active.name}
223
+ </span>
224
+ <span className="sep"></span>
225
+ <textarea
226
+ style={{ flex: 1, minWidth: 240, minHeight: 40, resize: 'vertical' }}
227
+ spellCheck="false"
228
+ value={text}
229
+ onChange={(e) => setText(e.target.value)}
230
+ onKeyDown={(e) => {
231
+ if (e.key === 'Enter' && !e.shiftKey) {
232
+ e.preventDefault();
233
+ submit();
234
+ }
235
+ }}
236
+ placeholder={`向主题「${active.name}」的队列回填下一条任务(Enter 提交,Shift+Enter 换行)`}
237
+ />
238
+ <button className="btn" onClick={submit} disabled={saving}>
239
+ {saving ? '提交中…' : '回填新任务'}
240
+ </button>
241
+ </div>
242
+
243
+ <div className="colhead">
244
+ <h3>该主题的任务队列</h3>
245
+ <span className="muted">待领取 {active.pending} · 共 {active.total} 条</span>
246
+ </div>
247
+
248
+ <div className="card" style={{ padding: 4 }}>
249
+ <table>
250
+ <thead>
251
+ <tr>
252
+ <th style={{ width: 56 }}>#</th>
253
+ <th>任务</th>
254
+ <th style={{ width: 80 }}>状态</th>
255
+ <th style={{ width: 160 }}>时间</th>
256
+ <th style={{ width: 76 }}>操作</th>
257
+ </tr>
258
+ </thead>
259
+ <tbody>
260
+ {tasks.length ? (
261
+ tasks.map((t) => (
262
+ <tr key={t.id}>
263
+ <td className="mono">#{t.id}</td>
264
+ <td style={{ whiteSpace: 'pre-wrap' }}>{t.text}</td>
265
+ <td>
266
+ {t.status === 'consumed' ? (
267
+ <span className="tag strong">已领取</span>
268
+ ) : (
269
+ <span className="tag">待领取</span>
270
+ )}
271
+ </td>
272
+ <td className="mono">{t.consumedAt || t.createdAt}</td>
273
+ <td className="ops">
274
+ <button className="btn small ghost" onClick={() => remove(t.id)}>删除</button>
275
+ </td>
276
+ </tr>
277
+ ))
278
+ ) : (
279
+ <tr>
280
+ <td colSpan="5" className="muted" style={{ textAlign: 'center', padding: '22px 0' }}>
281
+ (暂无任务,在上方回填第一条)
282
+ </td>
283
+ </tr>
284
+ )}
285
+ </tbody>
286
+ </table>
287
+ </div>
288
+ </>
289
+ )}
290
+
291
+ <CliHints module="sessiontask" />
292
+ </>
293
+ );
294
+ }
@@ -12,10 +12,11 @@ import { cliPathsOf } from './spec.js';
12
12
  import system from '../modules/system/index.js';
13
13
  import auth from '../modules/auth/index.js';
14
14
  import group from '../modules/group/index.js';
15
+ import sessiontask from '../modules/sessiontask/index.js';
15
16
  import todo from '../modules/todo/index.js';
16
17
  import bundled from '../modules/bundled/index.js';
17
18
 
18
- export const MODULES = [system, auth, group, todo, bundled].sort(
19
+ export const MODULES = [system, auth, group, sessiontask, todo, bundled].sort(
19
20
  (a, b) => (a.order ?? 100) - (b.order ?? 100)
20
21
  );
21
22
 
@@ -6,6 +6,7 @@
6
6
  import { lazy } from 'react';
7
7
 
8
8
  export const VIEWS = [
9
+ { id: 'sessiontask', title: '实时任务', component: lazy(() => import('../../modules/sessiontask/view.jsx')) },
9
10
  { id: 'todo', title: '清单', component: lazy(() => import('../../modules/todo/view.jsx')) },
10
11
  { id: 'group', title: '工作空间', component: lazy(() => import('../../modules/group/view.jsx')) },
11
12
  { id: 'auth', title: '登录', component: lazy(() => import('../../modules/auth/view.jsx')) },
@@ -1 +1 @@
1
- import{o as e,s as t,t as n}from"./index-BbN9ozEj.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-C3LDUTKW.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};