pi-kanban 0.2.0 → 0.3.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/README.md +9 -4
- package/lib/pi-parsers.js +186 -87
- package/lib/task-store.js +19 -3
- package/package.json +8 -2
- package/public/app.js +5854 -5750
- package/public/style.css +144 -0
- package/server.js +125 -46
package/public/style.css
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
--accent-glow: rgba(232, 111, 51, 0.55);
|
|
21
21
|
--success: #3ecf8e;
|
|
22
22
|
--success-dim: rgba(62, 207, 142, 0.18);
|
|
23
|
+
--error: #e5534b;
|
|
23
24
|
--warning: #f0b429;
|
|
24
25
|
--warning-dim: rgba(240, 180, 41, 0.18);
|
|
25
26
|
--plan: #86a886;
|
|
@@ -761,6 +762,13 @@ body::before {
|
|
|
761
762
|
display: flex;
|
|
762
763
|
align-items: center;
|
|
763
764
|
justify-content: space-between;
|
|
765
|
+
flex-wrap: wrap;
|
|
766
|
+
gap: 12px;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.view-header > div:first-child {
|
|
770
|
+
min-width: 0;
|
|
771
|
+
flex: 1 1 auto;
|
|
764
772
|
}
|
|
765
773
|
|
|
766
774
|
.view-title {
|
|
@@ -789,6 +797,12 @@ body::before {
|
|
|
789
797
|
display: flex;
|
|
790
798
|
align-items: center;
|
|
791
799
|
gap: 16px;
|
|
800
|
+
flex-wrap: wrap;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
@media (max-width: 720px) {
|
|
804
|
+
.view-progress { display: none; }
|
|
805
|
+
.view-actions { gap: 8px; }
|
|
792
806
|
}
|
|
793
807
|
|
|
794
808
|
.view-progress {
|
|
@@ -2534,6 +2548,7 @@ body.light {
|
|
|
2534
2548
|
--accent-glow: rgba(232, 111, 51, 0.5);
|
|
2535
2549
|
--success: #1a8a5a;
|
|
2536
2550
|
--success-dim: rgba(26, 138, 90, 0.15);
|
|
2551
|
+
--error: #c0392b;
|
|
2537
2552
|
--warning: #b07d0a;
|
|
2538
2553
|
--warning-dim: rgba(176, 125, 10, 0.15);
|
|
2539
2554
|
--plan: #5a7a5a;
|
|
@@ -3234,6 +3249,7 @@ select.form-input option:checked {
|
|
|
3234
3249
|
--accent-glow: rgba(232, 111, 51, 0.5);
|
|
3235
3250
|
--success: #1a8a5a;
|
|
3236
3251
|
--success-dim: rgba(26, 138, 90, 0.15);
|
|
3252
|
+
--error: #c0392b;
|
|
3237
3253
|
--warning: #b07d0a;
|
|
3238
3254
|
--warning-dim: rgba(176, 125, 10, 0.15);
|
|
3239
3255
|
--plan: #5a7a5a;
|
|
@@ -3614,3 +3630,131 @@ pre.mermaid svg {
|
|
|
3614
3630
|
border: 1px solid var(--text-muted);
|
|
3615
3631
|
}
|
|
3616
3632
|
/* #endregion */
|
|
3633
|
+
|
|
3634
|
+
/* #region AskUserQuestion card */
|
|
3635
|
+
.ask-user-questions {
|
|
3636
|
+
display: flex;
|
|
3637
|
+
flex-direction: column;
|
|
3638
|
+
gap: 10px;
|
|
3639
|
+
margin-bottom: 12px;
|
|
3640
|
+
}
|
|
3641
|
+
.ask-user-card {
|
|
3642
|
+
border: 1px solid var(--border);
|
|
3643
|
+
border-radius: 6px;
|
|
3644
|
+
padding: 10px 12px;
|
|
3645
|
+
background: var(--bg-secondary, var(--bg-elevated, rgba(127,127,127,0.06)));
|
|
3646
|
+
}
|
|
3647
|
+
.ask-user-header {
|
|
3648
|
+
font-size: 0.72rem;
|
|
3649
|
+
font-weight: 600;
|
|
3650
|
+
text-transform: uppercase;
|
|
3651
|
+
letter-spacing: 0.05em;
|
|
3652
|
+
color: var(--accent);
|
|
3653
|
+
margin-bottom: 4px;
|
|
3654
|
+
}
|
|
3655
|
+
.ask-user-question-text {
|
|
3656
|
+
font-size: 0.9rem;
|
|
3657
|
+
font-weight: 500;
|
|
3658
|
+
color: var(--text-primary);
|
|
3659
|
+
margin-bottom: 6px;
|
|
3660
|
+
}
|
|
3661
|
+
.ask-user-hint {
|
|
3662
|
+
font-size: 0.72rem;
|
|
3663
|
+
color: var(--text-muted);
|
|
3664
|
+
margin-bottom: 6px;
|
|
3665
|
+
}
|
|
3666
|
+
.ask-user-options {
|
|
3667
|
+
list-style: none;
|
|
3668
|
+
margin: 0;
|
|
3669
|
+
padding: 0;
|
|
3670
|
+
display: flex;
|
|
3671
|
+
flex-direction: column;
|
|
3672
|
+
gap: 3px;
|
|
3673
|
+
}
|
|
3674
|
+
.ask-user-option {
|
|
3675
|
+
display: flex;
|
|
3676
|
+
align-items: flex-start;
|
|
3677
|
+
gap: 6px;
|
|
3678
|
+
font-size: 0.85rem;
|
|
3679
|
+
color: var(--text-secondary);
|
|
3680
|
+
padding: 3px 6px;
|
|
3681
|
+
border-radius: 4px;
|
|
3682
|
+
}
|
|
3683
|
+
.ask-user-option--checked {
|
|
3684
|
+
color: var(--text-primary);
|
|
3685
|
+
background: var(--bg-hover);
|
|
3686
|
+
}
|
|
3687
|
+
.ask-user-check {
|
|
3688
|
+
flex-shrink: 0;
|
|
3689
|
+
width: 14px;
|
|
3690
|
+
color: var(--success);
|
|
3691
|
+
font-weight: 700;
|
|
3692
|
+
}
|
|
3693
|
+
.ask-user-option:not(.ask-user-option--checked) .ask-user-check {
|
|
3694
|
+
color: var(--text-muted);
|
|
3695
|
+
}
|
|
3696
|
+
.ask-user-option-label {
|
|
3697
|
+
font-weight: 500;
|
|
3698
|
+
}
|
|
3699
|
+
.ask-user-option-desc {
|
|
3700
|
+
color: var(--text-muted);
|
|
3701
|
+
font-size: 0.78rem;
|
|
3702
|
+
margin-left: 4px;
|
|
3703
|
+
}
|
|
3704
|
+
.ask-user-pending {
|
|
3705
|
+
color: var(--text-muted);
|
|
3706
|
+
font-size: 0.85rem;
|
|
3707
|
+
font-style: italic;
|
|
3708
|
+
margin-top: 6px;
|
|
3709
|
+
}
|
|
3710
|
+
.ask-user-cancelled {
|
|
3711
|
+
color: var(--text-muted);
|
|
3712
|
+
font-size: 0.85rem;
|
|
3713
|
+
font-style: italic;
|
|
3714
|
+
padding: 8px 0;
|
|
3715
|
+
}
|
|
3716
|
+
/* #endregion */
|
|
3717
|
+
|
|
3718
|
+
/* #region parallel-subagent */
|
|
3719
|
+
.psa-mode-label {
|
|
3720
|
+
font-size: 11px;
|
|
3721
|
+
color: var(--text-secondary);
|
|
3722
|
+
text-transform: uppercase;
|
|
3723
|
+
letter-spacing: 0.05em;
|
|
3724
|
+
margin-bottom: 6px;
|
|
3725
|
+
}
|
|
3726
|
+
.psa-row {
|
|
3727
|
+
display: flex;
|
|
3728
|
+
align-items: baseline;
|
|
3729
|
+
gap: 8px;
|
|
3730
|
+
margin: 6px 0;
|
|
3731
|
+
}
|
|
3732
|
+
.psa-task-row {
|
|
3733
|
+
align-items: flex-start;
|
|
3734
|
+
}
|
|
3735
|
+
.psa-key {
|
|
3736
|
+
font-size: 11px;
|
|
3737
|
+
color: var(--text-secondary);
|
|
3738
|
+
flex-shrink: 0;
|
|
3739
|
+
min-width: 40px;
|
|
3740
|
+
}
|
|
3741
|
+
.psa-task-text {
|
|
3742
|
+
white-space: pre-wrap;
|
|
3743
|
+
word-break: break-word;
|
|
3744
|
+
font-size: 12px;
|
|
3745
|
+
color: var(--text-primary);
|
|
3746
|
+
line-height: 1.5;
|
|
3747
|
+
}
|
|
3748
|
+
.psa-status {
|
|
3749
|
+
margin-top: 8px;
|
|
3750
|
+
font-size: 12px;
|
|
3751
|
+
font-weight: 500;
|
|
3752
|
+
}
|
|
3753
|
+
.psa-status-ok {
|
|
3754
|
+
color: var(--success);
|
|
3755
|
+
}
|
|
3756
|
+
.psa-status-err {
|
|
3757
|
+
color: var(--error);
|
|
3758
|
+
}
|
|
3759
|
+
/* #endregion */
|
|
3760
|
+
|
package/server.js
CHANGED
|
@@ -10,6 +10,16 @@ const chokidar = require('chokidar');
|
|
|
10
10
|
const open = require('open').default || require('open');
|
|
11
11
|
const parsers = require('./lib/pi-parsers');
|
|
12
12
|
const taskStore = require('./lib/task-store');
|
|
13
|
+
const pkg = require('./package.json');
|
|
14
|
+
|
|
15
|
+
function sendWithETag(req, res, data) {
|
|
16
|
+
const body = typeof data === 'string' ? data : JSON.stringify(data);
|
|
17
|
+
const etag = `"${crypto.createHash('md5').update(body).digest('hex').slice(0, 16)}"`;
|
|
18
|
+
res.set('ETag', etag);
|
|
19
|
+
res.set('Cache-Control', 'no-cache');
|
|
20
|
+
if (req.headers['if-none-match'] === etag) return res.status(304).end();
|
|
21
|
+
res.type('application/json').send(body);
|
|
22
|
+
}
|
|
13
23
|
|
|
14
24
|
function enrichTask(t, sessionId, project) {
|
|
15
25
|
return {
|
|
@@ -23,23 +33,27 @@ function enrichTask(t, sessionId, project) {
|
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
async function tasksForSession(sessionId, project) {
|
|
26
|
-
const tasks = taskStore.
|
|
36
|
+
const tasks = await taskStore.listTasksAsync(sessionId);
|
|
27
37
|
return tasks.map((t) => enrichTask(t, sessionId, project));
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
async function allStoredTasks() {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} catch {}
|
|
40
|
-
for (const t of tasks) out.push(enrichTask(t, sid, project));
|
|
41
|
+
const [sids, sessionFiles] = await Promise.all([
|
|
42
|
+
taskStore.listSessionIdsAsync(),
|
|
43
|
+
parsers.listSessionFiles(),
|
|
44
|
+
]);
|
|
45
|
+
const projectBySid = new Map();
|
|
46
|
+
for (const meta of sessionFiles) {
|
|
47
|
+
const slug = parsers.slugFromFile(meta.file);
|
|
48
|
+
if (!projectBySid.has(slug)) projectBySid.set(slug, meta.cwd);
|
|
41
49
|
}
|
|
42
|
-
|
|
50
|
+
const groups = await Promise.all(sids.map(async (sid) => {
|
|
51
|
+
const tasks = await taskStore.listTasksAsync(sid);
|
|
52
|
+
if (!tasks.length) return [];
|
|
53
|
+
const project = projectBySid.get(sid) || null;
|
|
54
|
+
return tasks.map((t) => enrichTask(t, sid, project));
|
|
55
|
+
}));
|
|
56
|
+
return groups.flat();
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
|
|
@@ -112,7 +126,7 @@ app.use(express.static(path.join(__dirname, 'public')));
|
|
|
112
126
|
const empty = (_req, res) => res.json([]);
|
|
113
127
|
const emptyObj = (_req, res) => res.json({});
|
|
114
128
|
|
|
115
|
-
app.get('/api/version', (_req, res) => res.json({ version:
|
|
129
|
+
app.get('/api/version', (_req, res) => res.json({ version: pkg.version, name: pkg.name }));
|
|
116
130
|
|
|
117
131
|
function resolveActiveTheme(mode) {
|
|
118
132
|
const requestedId = mode === 'light' ? LIGHT_THEME_ID : DARK_THEME_ID;
|
|
@@ -137,7 +151,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
137
151
|
try {
|
|
138
152
|
const limit = req.query.limit ? Number(req.query.limit) : 50;
|
|
139
153
|
const all = await parsers.listSessions();
|
|
140
|
-
res
|
|
154
|
+
sendWithETag(req, res, all.slice(0, limit));
|
|
141
155
|
} catch (err) {
|
|
142
156
|
console.error('listSessions error', err);
|
|
143
157
|
res.status(500).json({ error: String(err) });
|
|
@@ -158,7 +172,7 @@ app.get('/api/sessions/:id/summary', async (req, res) => {
|
|
|
158
172
|
const meta = await parsers.findSessionFileById(req.params.id);
|
|
159
173
|
if (!meta) return res.status(404).json({ error: 'not found' });
|
|
160
174
|
try {
|
|
161
|
-
res
|
|
175
|
+
sendWithETag(req, res, await parsers.buildSessionSummary(meta));
|
|
162
176
|
} catch (err) {
|
|
163
177
|
res.status(500).json({ error: String(err) });
|
|
164
178
|
}
|
|
@@ -172,18 +186,44 @@ app.get('/api/sessions/:id/messages', async (req, res) => {
|
|
|
172
186
|
const before = req.query.before ? String(req.query.before) : null;
|
|
173
187
|
const out = await parsers.buildMessages(meta.file, limit, before);
|
|
174
188
|
out.sessionId = req.params.id;
|
|
175
|
-
res
|
|
189
|
+
sendWithETag(req, res, out);
|
|
176
190
|
} catch (err) {
|
|
177
191
|
res.status(500).json({ messages: [], error: String(err) });
|
|
178
192
|
}
|
|
179
193
|
});
|
|
180
194
|
|
|
195
|
+
app.get('/api/sessions/:id/tool-result/:toolUseId', async (req, res) => {
|
|
196
|
+
const meta = await parsers.findSessionFileById(req.params.id);
|
|
197
|
+
if (!meta) return res.status(404).json({ error: 'not found' });
|
|
198
|
+
try {
|
|
199
|
+
const entries = await parsers.readSessionEntries(meta.file);
|
|
200
|
+
const toolUseId = req.params.toolUseId;
|
|
201
|
+
for (const e of entries) {
|
|
202
|
+
if (e.type !== 'message') continue;
|
|
203
|
+
const m = e.message;
|
|
204
|
+
if (m && m.role === 'toolResult' && m.toolCallId === toolUseId) {
|
|
205
|
+
const content = parsers.flattenContentToText(m.content);
|
|
206
|
+
return res.json({ content });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
res.status(404).json({ error: 'tool result not found' });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
res.status(500).json({ error: String(err) });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
|
|
181
216
|
app.get('/api/sessions/:id/agents', async (req, res) => {
|
|
182
217
|
const meta = await parsers.findSessionFileById(req.params.id);
|
|
183
218
|
if (!meta) return res.status(404).json({ agents: [], error: 'not found' });
|
|
184
219
|
try {
|
|
185
|
-
const
|
|
186
|
-
|
|
220
|
+
const entries = await parsers.readSessionEntries(meta.file);
|
|
221
|
+
const s = parsers.summarize(entries);
|
|
222
|
+
const agents = await parsers.listAgentsForSession(meta, s);
|
|
223
|
+
const waitingForUser = s.pendingAskUser
|
|
224
|
+
? { kind: 'question', toolName: 'AskUserQuestion', timestamp: s.pendingAskUser.timestamp }
|
|
225
|
+
: null;
|
|
226
|
+
res.json({ agents, waitingForUser });
|
|
187
227
|
} catch (err) {
|
|
188
228
|
res.status(500).json({ agents: [], error: String(err) });
|
|
189
229
|
}
|
|
@@ -237,8 +277,8 @@ app.post('/api/session/plan', async (req, res) => {
|
|
|
237
277
|
});
|
|
238
278
|
|
|
239
279
|
// Stubbed endpoints (later phases may fill these).
|
|
240
|
-
app.get('/api/tasks/all', async (
|
|
241
|
-
try { res
|
|
280
|
+
app.get('/api/tasks/all', async (req, res) => {
|
|
281
|
+
try { sendWithETag(req, res, await allStoredTasks()); }
|
|
242
282
|
catch (err) { res.status(500).json({ error: String(err) }); }
|
|
243
283
|
});
|
|
244
284
|
app.get('/api/projects', async (_req, res) => {
|
|
@@ -410,6 +450,49 @@ app.get('/api/events', (req, res) => {
|
|
|
410
450
|
req.on('close', () => { clearInterval(ka); sseClients.delete(res); });
|
|
411
451
|
});
|
|
412
452
|
|
|
453
|
+
// First call for a key arms a timer; further calls inside the window are
|
|
454
|
+
// dropped (leading-edge debounce). Used for SSE coalescing.
|
|
455
|
+
function debounceByKey(ms, fn) {
|
|
456
|
+
const pending = new Map();
|
|
457
|
+
return (key) => {
|
|
458
|
+
if (pending.has(key)) return;
|
|
459
|
+
pending.set(key, setTimeout(() => {
|
|
460
|
+
pending.delete(key);
|
|
461
|
+
fn(key);
|
|
462
|
+
}, ms));
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Per-file subscriber set. Notified by the global watcher; parses once per
|
|
467
|
+
// change and fans out to all connected clients (replaces per-client chokidar).
|
|
468
|
+
const agentLogStreams = (() => {
|
|
469
|
+
const byFile = new Map();
|
|
470
|
+
async function broadcast(file) {
|
|
471
|
+
const subs = byFile.get(file);
|
|
472
|
+
if (!subs || !subs.size) return;
|
|
473
|
+
let payload;
|
|
474
|
+
try { payload = await parsers.buildMessages(file, 50); } catch { return; }
|
|
475
|
+
for (const { res, agentId } of subs) {
|
|
476
|
+
payload.agentId = agentId;
|
|
477
|
+
try { res.write(`event: agent-log-update\ndata: ${JSON.stringify(payload)}\n\n`); } catch {}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const flush = debounceByKey(75, broadcast);
|
|
481
|
+
return {
|
|
482
|
+
subscribe(file, res, agentId) {
|
|
483
|
+
let set = byFile.get(file);
|
|
484
|
+
if (!set) { set = new Set(); byFile.set(file, set); }
|
|
485
|
+
const sub = { res, agentId };
|
|
486
|
+
set.add(sub);
|
|
487
|
+
return () => {
|
|
488
|
+
set.delete(sub);
|
|
489
|
+
if (!set.size) byFile.delete(file);
|
|
490
|
+
};
|
|
491
|
+
},
|
|
492
|
+
notify(file) { if (byFile.has(file)) flush(file); },
|
|
493
|
+
};
|
|
494
|
+
})();
|
|
495
|
+
|
|
413
496
|
app.get('/api/sessions/:id/agents/:agentId/messages/stream', async (req, res) => {
|
|
414
497
|
res.set({
|
|
415
498
|
'Content-Type': 'text/event-stream',
|
|
@@ -418,11 +501,11 @@ app.get('/api/sessions/:id/agents/:agentId/messages/stream', async (req, res) =>
|
|
|
418
501
|
});
|
|
419
502
|
res.write(': connected\n\n');
|
|
420
503
|
|
|
421
|
-
let
|
|
504
|
+
let unsubscribe = null;
|
|
422
505
|
const ka = setInterval(() => { try { res.write(': ka\n\n'); } catch {} }, 30000);
|
|
423
506
|
req.on('close', () => {
|
|
424
507
|
clearInterval(ka);
|
|
425
|
-
if (
|
|
508
|
+
if (unsubscribe) unsubscribe();
|
|
426
509
|
});
|
|
427
510
|
|
|
428
511
|
let meta;
|
|
@@ -430,19 +513,16 @@ app.get('/api/sessions/:id/agents/:agentId/messages/stream', async (req, res) =>
|
|
|
430
513
|
const rec = meta ? await parsers.findAgentRecord(meta, req.params.agentId).catch(() => null) : null;
|
|
431
514
|
if (!rec || !rec._sessionFile) return;
|
|
432
515
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
|
444
|
-
});
|
|
445
|
-
watcherRef.on('change', send).on('add', send);
|
|
516
|
+
// Subscribe before sending initial payload: any change during buildMessages
|
|
517
|
+
// will fire notify -> broadcast and be picked up. Otherwise the gap between
|
|
518
|
+
// initial send and subscribe could drop events.
|
|
519
|
+
unsubscribe = agentLogStreams.subscribe(rec._sessionFile, res, req.params.agentId);
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const out = await parsers.buildMessages(rec._sessionFile, 50);
|
|
523
|
+
out.agentId = req.params.agentId;
|
|
524
|
+
res.write(`event: agent-log-update\ndata: ${JSON.stringify(out)}\n\n`);
|
|
525
|
+
} catch {}
|
|
446
526
|
});
|
|
447
527
|
|
|
448
528
|
const watcher = chokidar.watch(path.join(parsers.getSessionsDir(), '**/*.jsonl'), {
|
|
@@ -453,9 +533,15 @@ parsers.setOnBranchResolved((cwd, branch) => {
|
|
|
453
533
|
sseSend({ type: 'branch:resolved', cwd, branch });
|
|
454
534
|
});
|
|
455
535
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
536
|
+
const queueSessionUpdate = debounceByKey(75, (sid) => sseSend({ type: 'update', sessionId: sid }));
|
|
537
|
+
|
|
538
|
+
for (const ev of ['add', 'change', 'unlink']) {
|
|
539
|
+
watcher.on(ev, (f) => {
|
|
540
|
+
parsers.invalidateSessionCache(f);
|
|
541
|
+
agentLogStreams.notify(f);
|
|
542
|
+
queueSessionUpdate(parsers.slugFromFile(f));
|
|
543
|
+
});
|
|
544
|
+
}
|
|
459
545
|
|
|
460
546
|
const taskWatcher = chokidar.watch(taskStore.getTasksDir(), {
|
|
461
547
|
ignoreInitial: true,
|
|
@@ -463,14 +549,7 @@ const taskWatcher = chokidar.watch(taskStore.getTasksDir(), {
|
|
|
463
549
|
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
464
550
|
});
|
|
465
551
|
const tasksRoot = path.basename(taskStore.getTasksDir());
|
|
466
|
-
const
|
|
467
|
-
function queueTaskUpdate(sid) {
|
|
468
|
-
if (pendingTaskSse.has(sid)) return;
|
|
469
|
-
pendingTaskSse.set(sid, setTimeout(() => {
|
|
470
|
-
pendingTaskSse.delete(sid);
|
|
471
|
-
sseSend({ type: 'task:update', sessionId: sid });
|
|
472
|
-
}, 75));
|
|
473
|
-
}
|
|
552
|
+
const queueTaskUpdate = debounceByKey(75, (sid) => sseSend({ type: 'task:update', sessionId: sid }));
|
|
474
553
|
taskWatcher.on('all', (_evt, file) => {
|
|
475
554
|
if (!file || !file.endsWith('.json')) return;
|
|
476
555
|
const sid = path.basename(path.dirname(file));
|