pi-kanban 0.2.0 → 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.
- package/README.md +51 -46
- package/extensions/kanban.ts +465 -465
- package/lib/git-branch.js +112 -112
- package/lib/pi-parsers.js +1000 -901
- package/lib/task-store.js +152 -136
- package/package.json +66 -59
- package/public/app.js +5859 -5750
- package/public/icons/icon-svg.svg +6 -6
- package/public/index.html +674 -674
- package/public/manifest.json +26 -26
- package/public/style.css +3760 -3616
- package/public/sw.js +60 -60
- package/server.js +602 -491
- package/themes/kanban-dark-default.json +21 -21
- package/themes/kanban-default.json +21 -21
- package/themes/pi-dark.json +21 -21
- package/themes/pi-light.json +21 -21
package/lib/pi-parsers.js
CHANGED
|
@@ -1,901 +1,1000 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const readline = require('node:readline');
|
|
5
|
-
const gitBranchModule = require('./git-branch');
|
|
6
|
-
const taskStore = require('./task-store');
|
|
7
|
-
|
|
8
|
-
// Optional callback (cwd, branch) -> void, set by server.js to push SSE updates when branches resolve.
|
|
9
|
-
let onBranchResolved = null;
|
|
10
|
-
function setOnBranchResolved(fn) { onBranchResolved = typeof fn === 'function' ? fn : null; }
|
|
11
|
-
|
|
12
|
-
const PI_DIR = process.env.PI_DIR || path.join(os.homedir(), '.pi');
|
|
13
|
-
const SESSIONS_DIR = path.join(PI_DIR, 'agent', 'sessions');
|
|
14
|
-
|
|
15
|
-
function getPiDir() {
|
|
16
|
-
return PI_DIR;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function getSessionsDir() {
|
|
20
|
-
return SESSIONS_DIR;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// "--C--Users-nikiforovall-dev-pi--" -> "C:\Users\nikiforovall\dev\pi"
|
|
24
|
-
function decodeProjectDir(name) {
|
|
25
|
-
let s = name;
|
|
26
|
-
if (s.startsWith('--')) s = s.slice(2);
|
|
27
|
-
if (s.endsWith('--')) s = s.slice(0, -2);
|
|
28
|
-
// First "C" (or any single letter) at start represents drive letter, then dashes are path separators.
|
|
29
|
-
// The convention is: drive letter + "-" then path with "/" replaced by "-".
|
|
30
|
-
// e.g. "C--Users-nikiforovall-dev-pi" -> "C:\Users\nikiforovall\dev\pi"
|
|
31
|
-
const m = s.match(/^([A-Za-z])--(.+)$/);
|
|
32
|
-
if (m) {
|
|
33
|
-
return `${m[1]}:\\${m[2].replace(/-/g, '\\')}`;
|
|
34
|
-
}
|
|
35
|
-
return s.replace(/-/g, path.sep);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function listSessionFiles() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
projectDir: projDir,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
rl.on('
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
rec.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
if (startMs &&
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
rec.
|
|
479
|
-
rec.
|
|
480
|
-
rec.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
let
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
[
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
//
|
|
562
|
-
const
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const readline = require('node:readline');
|
|
5
|
+
const gitBranchModule = require('./git-branch');
|
|
6
|
+
const taskStore = require('./task-store');
|
|
7
|
+
|
|
8
|
+
// Optional callback (cwd, branch) -> void, set by server.js to push SSE updates when branches resolve.
|
|
9
|
+
let onBranchResolved = null;
|
|
10
|
+
function setOnBranchResolved(fn) { onBranchResolved = typeof fn === 'function' ? fn : null; }
|
|
11
|
+
|
|
12
|
+
const PI_DIR = process.env.PI_DIR || path.join(os.homedir(), '.pi');
|
|
13
|
+
const SESSIONS_DIR = path.join(PI_DIR, 'agent', 'sessions');
|
|
14
|
+
|
|
15
|
+
function getPiDir() {
|
|
16
|
+
return PI_DIR;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getSessionsDir() {
|
|
20
|
+
return SESSIONS_DIR;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// "--C--Users-nikiforovall-dev-pi--" -> "C:\Users\nikiforovall\dev\pi"
|
|
24
|
+
function decodeProjectDir(name) {
|
|
25
|
+
let s = name;
|
|
26
|
+
if (s.startsWith('--')) s = s.slice(2);
|
|
27
|
+
if (s.endsWith('--')) s = s.slice(0, -2);
|
|
28
|
+
// First "C" (or any single letter) at start represents drive letter, then dashes are path separators.
|
|
29
|
+
// The convention is: drive letter + "-" then path with "/" replaced by "-".
|
|
30
|
+
// e.g. "C--Users-nikiforovall-dev-pi" -> "C:\Users\nikiforovall\dev\pi"
|
|
31
|
+
const m = s.match(/^([A-Za-z])--(.+)$/);
|
|
32
|
+
if (m) {
|
|
33
|
+
return `${m[1]}:\\${m[2].replace(/-/g, '\\')}`;
|
|
34
|
+
}
|
|
35
|
+
return s.replace(/-/g, path.sep);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function listSessionFiles() {
|
|
39
|
+
let projDirs;
|
|
40
|
+
try { projDirs = await fs.promises.readdir(SESSIONS_DIR, { withFileTypes: true }); }
|
|
41
|
+
catch { return []; }
|
|
42
|
+
const projects = projDirs.filter((d) => d.isDirectory());
|
|
43
|
+
const perProject = await Promise.all(projects.map(async (d) => {
|
|
44
|
+
const projDir = d.name;
|
|
45
|
+
const projPath = path.join(SESSIONS_DIR, projDir);
|
|
46
|
+
let files;
|
|
47
|
+
try { files = await fs.promises.readdir(projPath, { withFileTypes: true }); }
|
|
48
|
+
catch { return []; }
|
|
49
|
+
const jsonls = files.filter((f) => f.isFile() && f.name.endsWith('.jsonl'));
|
|
50
|
+
const stats = await Promise.all(jsonls.map(async (f) => {
|
|
51
|
+
const full = path.join(projPath, f.name);
|
|
52
|
+
try {
|
|
53
|
+
const s = await fs.promises.stat(full);
|
|
54
|
+
return { file: full, projectDir: projDir, cwd: decodeProjectDir(projDir), mtime: s.mtime, mtimeMs: s.mtimeMs, size: s.size };
|
|
55
|
+
} catch { return null; }
|
|
56
|
+
}));
|
|
57
|
+
return stats.filter(Boolean);
|
|
58
|
+
}));
|
|
59
|
+
return perProject.flat();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readJsonlLines(file) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const lines = [];
|
|
65
|
+
const stream = fs.createReadStream(file, { encoding: 'utf8' });
|
|
66
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
67
|
+
rl.on('line', (l) => { if (l.trim()) lines.push(l); });
|
|
68
|
+
rl.on('close', () => resolve(lines));
|
|
69
|
+
rl.on('error', reject);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseLine(line) {
|
|
74
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readSessionEntriesUncached(file) {
|
|
78
|
+
const lines = await readJsonlLines(file);
|
|
79
|
+
const entries = [];
|
|
80
|
+
for (const l of lines) {
|
|
81
|
+
const e = parseLine(l);
|
|
82
|
+
if (e) entries.push(e);
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parsed entries + summary + tool/subagent indices keyed by file path; (mtimeMs, size) is
|
|
88
|
+
// the validity tag. Invalidated by chokidar events via invalidateSessionCache().
|
|
89
|
+
const sessionCache = new Map();
|
|
90
|
+
|
|
91
|
+
function buildToolMaps(entries) {
|
|
92
|
+
const toolResultByCallId = new Map();
|
|
93
|
+
const subagentInfoByCallId = new Map();
|
|
94
|
+
for (const e of entries) {
|
|
95
|
+
if (e.type !== 'message') continue;
|
|
96
|
+
const m = e.message;
|
|
97
|
+
if (m && m.role === 'toolResult' && m.toolCallId) {
|
|
98
|
+
toolResultByCallId.set(m.toolCallId, m);
|
|
99
|
+
if (m.toolName === 'subagent' && m.details && Array.isArray(m.details.results) && m.details.results[0]) {
|
|
100
|
+
const r = m.details.results[0];
|
|
101
|
+
subagentInfoByCallId.set(m.toolCallId, {
|
|
102
|
+
runId: m.details.runId || null,
|
|
103
|
+
agent: r.agent || null,
|
|
104
|
+
runIndex: 0,
|
|
105
|
+
outputPath: r.artifactPaths && r.artifactPaths.outputPath ? r.artifactPaths.outputPath : null,
|
|
106
|
+
inputPath: r.artifactPaths && r.artifactPaths.inputPath ? r.artifactPaths.inputPath : null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { toolResultByCallId, subagentInfoByCallId };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function getCachedSession(file, knownStat = null) {
|
|
115
|
+
const mtimeMs = knownStat ? knownStat.mtimeMs : null;
|
|
116
|
+
const size = knownStat ? knownStat.size : null;
|
|
117
|
+
const cached = sessionCache.get(file);
|
|
118
|
+
if (cached && knownStat && cached.mtimeMs === mtimeMs && cached.size === size) return cached;
|
|
119
|
+
const stat = knownStat || await fs.promises.stat(file);
|
|
120
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached;
|
|
121
|
+
const entries = await readSessionEntriesUncached(file);
|
|
122
|
+
const summary = summarize(entries);
|
|
123
|
+
const { toolResultByCallId, subagentInfoByCallId } = buildToolMaps(entries);
|
|
124
|
+
const entry = { mtimeMs: stat.mtimeMs, size: stat.size, entries, summary, toolResultByCallId, subagentInfoByCallId };
|
|
125
|
+
sessionCache.set(file, entry);
|
|
126
|
+
return entry;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readSessionEntries(file) {
|
|
130
|
+
return (await getCachedSession(file)).entries;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function invalidateSessionCache(file) {
|
|
134
|
+
if (file) sessionCache.delete(file);
|
|
135
|
+
else sessionCache.clear();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Extract concrete agent spawns from a `subagent` toolCall arguments.
|
|
139
|
+
// Returns [] for management calls (action:"list" etc) so they don't pollute the agent list.
|
|
140
|
+
// Single spawn: {agent, task} -> [{agent, task}]
|
|
141
|
+
// Parallel: {tasks: [{agent, task}, ...]} -> one rec per task
|
|
142
|
+
// Chain: {chain: [step, ...]} where each step is either {agent, task} or {parallel: [{agent, task}, ...]}
|
|
143
|
+
function expandSubagentSpawns(args) {
|
|
144
|
+
if (!args || typeof args !== 'object') return [];
|
|
145
|
+
if (args.task && typeof args.task === 'string') {
|
|
146
|
+
return [{ agent: args.agent || null, task: args.task }];
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(args.tasks)) {
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const item of args.tasks) {
|
|
151
|
+
if (item && item.task) out.push({ agent: item.agent || null, task: item.task });
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(args.chain)) {
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const step of args.chain) {
|
|
158
|
+
if (!step || typeof step !== 'object') continue;
|
|
159
|
+
if (Array.isArray(step.parallel)) {
|
|
160
|
+
for (const item of step.parallel) {
|
|
161
|
+
if (item && item.task) out.push({ agent: item.agent || null, task: item.task });
|
|
162
|
+
}
|
|
163
|
+
} else if (step.task) {
|
|
164
|
+
out.push({ agent: step.agent || null, task: step.task });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function lastMapValue(map) {
|
|
173
|
+
let v;
|
|
174
|
+
for (v of map.values());
|
|
175
|
+
return v;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function summarize(entries) {
|
|
179
|
+
let sessionEntry = null;
|
|
180
|
+
let firstModel = null;
|
|
181
|
+
let lastTimestamp = null;
|
|
182
|
+
let totalCost = 0;
|
|
183
|
+
let totalInput = 0;
|
|
184
|
+
let totalOutput = 0;
|
|
185
|
+
let totalCacheRead = 0;
|
|
186
|
+
let totalCacheWrite = 0;
|
|
187
|
+
let userCount = 0;
|
|
188
|
+
let assistantCount = 0;
|
|
189
|
+
let toolCalls = 0;
|
|
190
|
+
let toolResults = 0;
|
|
191
|
+
let messageCount = 0;
|
|
192
|
+
let provider = null;
|
|
193
|
+
let model = null;
|
|
194
|
+
let customTitle = null;
|
|
195
|
+
let lastAssistantUsage = null;
|
|
196
|
+
let lastTodoTasks = null;
|
|
197
|
+
const subagents = [];
|
|
198
|
+
// toolCallId -> array of pending recs (chain mode produces one rec per child)
|
|
199
|
+
const subagentByToolCallId = new Map();
|
|
200
|
+
// `${runId}_${index}` -> rec, for /run slash-invoked subagents (custom_message)
|
|
201
|
+
const subagentByRunKey = new Map();
|
|
202
|
+
// toolCallId -> { timestamp } for unanswered ask_user_question calls
|
|
203
|
+
const pendingAskUsers = new Map();
|
|
204
|
+
|
|
205
|
+
for (const e of entries) {
|
|
206
|
+
if (e.type === 'session') sessionEntry = e;
|
|
207
|
+
else if (e.type === 'session_info') {
|
|
208
|
+
if (typeof e.name === 'string' && e.name.trim()) customTitle = e.name.trim();
|
|
209
|
+
}
|
|
210
|
+
else if (e.type === 'model_change') {
|
|
211
|
+
if (!firstModel) firstModel = e;
|
|
212
|
+
provider = e.provider;
|
|
213
|
+
model = e.modelId;
|
|
214
|
+
} else if (e.type === 'message') {
|
|
215
|
+
messageCount++;
|
|
216
|
+
lastTimestamp = e.timestamp || lastTimestamp;
|
|
217
|
+
const m = e.message || {};
|
|
218
|
+
if (m.role === 'user') userCount++;
|
|
219
|
+
else if (m.role === 'assistant') {
|
|
220
|
+
assistantCount++;
|
|
221
|
+
const u = m.usage;
|
|
222
|
+
if (u) {
|
|
223
|
+
totalInput += u.input || 0;
|
|
224
|
+
totalOutput += u.output || 0;
|
|
225
|
+
totalCacheRead += u.cacheRead || 0;
|
|
226
|
+
totalCacheWrite += u.cacheWrite || 0;
|
|
227
|
+
totalCost += (u.cost && u.cost.total) || 0;
|
|
228
|
+
if ((u.input || 0) + (u.cacheRead || 0) + (u.cacheWrite || 0) + (u.output || 0) > 0) {
|
|
229
|
+
lastAssistantUsage = u;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (m.provider) provider = m.provider;
|
|
233
|
+
if (m.model) model = m.model;
|
|
234
|
+
if (Array.isArray(m.content)) {
|
|
235
|
+
for (const c of m.content) {
|
|
236
|
+
if (c && c.type === 'toolCall') {
|
|
237
|
+
toolCalls++;
|
|
238
|
+
if (c.name === 'ask_user_question' && c.id) {
|
|
239
|
+
pendingAskUsers.set(c.id, { timestamp: e.timestamp || null });
|
|
240
|
+
} else if (c.name === 'subagent') {
|
|
241
|
+
const items = expandSubagentSpawns(c.arguments);
|
|
242
|
+
if (items.length) {
|
|
243
|
+
const recs = items.map((it, i) => ({
|
|
244
|
+
toolCallId: c.id || null,
|
|
245
|
+
agent: it.agent || null,
|
|
246
|
+
prompt: it.task || null,
|
|
247
|
+
startedAt: e.timestamp || null,
|
|
248
|
+
runId: null,
|
|
249
|
+
exitCode: null,
|
|
250
|
+
durationMs: null,
|
|
251
|
+
usage: null,
|
|
252
|
+
progressSummary: null,
|
|
253
|
+
sessionFile: null,
|
|
254
|
+
artifactPaths: null,
|
|
255
|
+
stoppedAt: null,
|
|
256
|
+
runIndex: i,
|
|
257
|
+
}));
|
|
258
|
+
for (const rec of recs) subagents.push(rec);
|
|
259
|
+
if (c.id) subagentByToolCallId.set(c.id, recs);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else if (m.role === 'toolResult') {
|
|
266
|
+
toolResults++;
|
|
267
|
+
if (m.toolName === 'ask_user_question' && m.toolCallId) {
|
|
268
|
+
pendingAskUsers.delete(m.toolCallId);
|
|
269
|
+
} else if (m.toolName === 'todo' && m.details && Array.isArray(m.details.tasks)) {
|
|
270
|
+
lastTodoTasks = m.details.tasks;
|
|
271
|
+
} else if (m.toolName === 'subagent') {
|
|
272
|
+
const pendingArr = m.toolCallId ? subagentByToolCallId.get(m.toolCallId) : null;
|
|
273
|
+
const results = m.details && Array.isArray(m.details.results) ? m.details.results : null;
|
|
274
|
+
if (!results && pendingArr && pendingArr.length) {
|
|
275
|
+
// Validation/spawn-time failure: pi wrote an error toolResult with no results.
|
|
276
|
+
const errText = Array.isArray(m.content)
|
|
277
|
+
? (m.content.find((c) => c && c.type === 'text')?.text || '')
|
|
278
|
+
: '';
|
|
279
|
+
for (const rec of pendingArr) {
|
|
280
|
+
rec.stoppedAt = e.timestamp || null;
|
|
281
|
+
rec._error = rec._error || (errText ? errText.split('\n')[0].slice(0, 200) : 'Spawn failed');
|
|
282
|
+
if (rec.exitCode == null) rec.exitCode = -1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
(results || []).forEach((r, idx) => {
|
|
286
|
+
let target = pendingArr && pendingArr[idx];
|
|
287
|
+
if (!target) {
|
|
288
|
+
const base = pendingArr && pendingArr[0];
|
|
289
|
+
target = base ? { ...base, runIndex: idx } : {
|
|
290
|
+
toolCallId: m.toolCallId || null,
|
|
291
|
+
agent: null,
|
|
292
|
+
prompt: null,
|
|
293
|
+
startedAt: null,
|
|
294
|
+
runIndex: idx,
|
|
295
|
+
};
|
|
296
|
+
subagents.push(target);
|
|
297
|
+
}
|
|
298
|
+
target.runId = (m.details && m.details.runId) || target.runId;
|
|
299
|
+
target.runIndex = idx;
|
|
300
|
+
target.agent = r.agent || target.agent;
|
|
301
|
+
target.prompt = target.prompt || r.task || null;
|
|
302
|
+
target.exitCode = r.exitCode ?? null;
|
|
303
|
+
target.durationMs = r.durationMs ?? null;
|
|
304
|
+
target.usage = r.usage || null;
|
|
305
|
+
target.progressSummary = r.progressSummary || null;
|
|
306
|
+
target.sessionFile = r.sessionFile || null;
|
|
307
|
+
target.artifactPaths = r.artifactPaths || null;
|
|
308
|
+
target.stoppedAt = e.timestamp || null;
|
|
309
|
+
if (r.exitCode != null && r.exitCode !== 0 && !r.error && (r.finalOutput == null || r.finalOutput === '')) {
|
|
310
|
+
target._cancelled = true;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
if (results && results.length === 0 && pendingArr && pendingArr.length) {
|
|
314
|
+
const text = Array.isArray(m.content) ? (m.content.find((c) => c && c.type === 'text')?.text || '') : '';
|
|
315
|
+
const cancelled = /cancel/i.test(text);
|
|
316
|
+
for (const rec of pendingArr) {
|
|
317
|
+
rec.stoppedAt = e.timestamp || null;
|
|
318
|
+
if (cancelled) rec._error = rec._error || 'Chain cancelled';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} else if (e.type === 'custom_message' && e.customType === 'subagent-slash-result') {
|
|
324
|
+
lastTimestamp = e.timestamp || lastTimestamp;
|
|
325
|
+
const det = e.details && e.details.result && e.details.result.details;
|
|
326
|
+
const results = det && Array.isArray(det.results) ? det.results : null;
|
|
327
|
+
if (results) {
|
|
328
|
+
const runId = det.runId || null;
|
|
329
|
+
const requestId = (e.details && e.details.requestId) || null;
|
|
330
|
+
// Progress records lack runId; key by (requestId, agent, idx) so the later
|
|
331
|
+
// final record (same requestId) upserts the same target.
|
|
332
|
+
results.forEach((r, idx) => {
|
|
333
|
+
const key = `${requestId || runId || 'noid'}_${r.agent || ''}_${idx}`;
|
|
334
|
+
let target = subagentByRunKey.get(key);
|
|
335
|
+
if (!target) {
|
|
336
|
+
target = {
|
|
337
|
+
toolCallId: null,
|
|
338
|
+
agent: r.agent || null,
|
|
339
|
+
prompt: r.task || null,
|
|
340
|
+
startedAt: e.timestamp || null,
|
|
341
|
+
runId,
|
|
342
|
+
runIndex: idx,
|
|
343
|
+
exitCode: null,
|
|
344
|
+
durationMs: null,
|
|
345
|
+
usage: null,
|
|
346
|
+
progressSummary: null,
|
|
347
|
+
sessionFile: null,
|
|
348
|
+
artifactPaths: null,
|
|
349
|
+
stoppedAt: null,
|
|
350
|
+
model: null,
|
|
351
|
+
};
|
|
352
|
+
subagents.push(target);
|
|
353
|
+
subagentByRunKey.set(key, target);
|
|
354
|
+
}
|
|
355
|
+
if (runId) target.runId = runId;
|
|
356
|
+
if (r.agent) target.agent = r.agent;
|
|
357
|
+
if (!target.prompt && r.task) target.prompt = r.task;
|
|
358
|
+
if (r.exitCode !== undefined) target.exitCode = r.exitCode;
|
|
359
|
+
if (r.durationMs !== undefined) target.durationMs = r.durationMs;
|
|
360
|
+
if (r.usage) target.usage = r.usage;
|
|
361
|
+
if (r.progressSummary) target.progressSummary = r.progressSummary;
|
|
362
|
+
if (r.sessionFile) target.sessionFile = r.sessionFile;
|
|
363
|
+
if (r.artifactPaths) target.artifactPaths = r.artifactPaths;
|
|
364
|
+
if (r.model) target.model = r.model;
|
|
365
|
+
if (r.error) target._error = r.error;
|
|
366
|
+
// pi emits progress events without these fields; their presence marks completion.
|
|
367
|
+
const isFinal = !!(r.model || r.artifactPaths || r.sessionFile || r.error);
|
|
368
|
+
if (isFinal) target.stoppedAt = e.timestamp || target.stoppedAt;
|
|
369
|
+
if (isFinal && r.exitCode != null && r.exitCode !== 0 && !r.error && (r.finalOutput == null || r.finalOutput === '')) {
|
|
370
|
+
target._cancelled = true;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
sessionEntry,
|
|
379
|
+
firstModel,
|
|
380
|
+
lastTimestamp,
|
|
381
|
+
totalCost,
|
|
382
|
+
totalInput,
|
|
383
|
+
totalOutput,
|
|
384
|
+
totalCacheRead,
|
|
385
|
+
totalCacheWrite,
|
|
386
|
+
totalTokens: totalInput + totalOutput,
|
|
387
|
+
userCount,
|
|
388
|
+
assistantCount,
|
|
389
|
+
toolCalls,
|
|
390
|
+
toolResults,
|
|
391
|
+
messageCount,
|
|
392
|
+
provider,
|
|
393
|
+
model,
|
|
394
|
+
customTitle,
|
|
395
|
+
lastAssistantUsage,
|
|
396
|
+
lastTodoTasks,
|
|
397
|
+
subagents,
|
|
398
|
+
pendingAskUser: pendingAskUsers.size > 0 ? lastMapValue(pendingAskUsers) : null,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function agentIdFor(rec) {
|
|
403
|
+
if (rec.toolCallId) return `tc_${rec.toolCallId}_${rec.runIndex ?? 0}`;
|
|
404
|
+
if (rec.runId && rec.agent) return `${rec.runId}_${rec.agent}_${rec.runIndex ?? 0}`;
|
|
405
|
+
if (rec.runId) return rec.runId;
|
|
406
|
+
return `pending_${Math.random().toString(36).slice(2, 10)}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function subagentToApi(rec, lastMessage) {
|
|
410
|
+
const stopped = !!rec.stoppedAt;
|
|
411
|
+
const cancelled = !!rec._cancelled;
|
|
412
|
+
let desc = rec.progressSummary
|
|
413
|
+
? `${rec.progressSummary.toolCount ?? 0} tools · ${rec.progressSummary.tokens ?? 0} tokens`
|
|
414
|
+
: null;
|
|
415
|
+
if (rec._error) desc = `error · ${String(rec._error).split('\n')[0].slice(0, 80)}`;
|
|
416
|
+
else if (cancelled) desc = desc ? `cancelled · ${desc}` : 'cancelled';
|
|
417
|
+
return {
|
|
418
|
+
agentId: agentIdFor(rec),
|
|
419
|
+
type: rec.agent || null,
|
|
420
|
+
agentName: rec.agent || null,
|
|
421
|
+
status: stopped ? (cancelled ? 'cancelled' : 'stopped') : 'active',
|
|
422
|
+
cancelled,
|
|
423
|
+
startedAt: rec.startedAt || null,
|
|
424
|
+
stoppedAt: rec.stoppedAt || null,
|
|
425
|
+
updatedAt: rec.stoppedAt || rec.startedAt || null,
|
|
426
|
+
prompt: rec.prompt || null,
|
|
427
|
+
description: desc,
|
|
428
|
+
model: rec.model || null,
|
|
429
|
+
color: null,
|
|
430
|
+
lastMessage: lastMessage || null,
|
|
431
|
+
_sessionFile: rec.sessionFile || null,
|
|
432
|
+
_exitCode: rec.exitCode ?? null,
|
|
433
|
+
_durationMs: rec.durationMs ?? null,
|
|
434
|
+
_usage: rec.usage || null,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function readSubagentOutput(rec) {
|
|
439
|
+
if (!rec || !rec.artifactPaths || !rec.artifactPaths.outputPath) return null;
|
|
440
|
+
try { return await fs.promises.readFile(rec.artifactPaths.outputPath, 'utf8'); } catch { return null; }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function enrichPendingSubagents(meta, subagents) {
|
|
444
|
+
let pending = subagents.filter((r) => !r.runId && !r.stoppedAt);
|
|
445
|
+
if (pending.length === 0) return;
|
|
446
|
+
const artifactsDir = path.join(path.dirname(meta.file), 'subagent-artifacts');
|
|
447
|
+
let files = null;
|
|
448
|
+
try { files = await fs.promises.readdir(artifactsDir); } catch {}
|
|
449
|
+
const knownRunIds = new Set(subagents.map((r) => r.runId).filter(Boolean));
|
|
450
|
+
const metas = [];
|
|
451
|
+
if (files) {
|
|
452
|
+
const metaFiles = files.filter((f) => f.endsWith('_meta.json'));
|
|
453
|
+
for (const f of metaFiles) {
|
|
454
|
+
try {
|
|
455
|
+
const data = JSON.parse(await fs.promises.readFile(path.join(artifactsDir, f), 'utf8'));
|
|
456
|
+
if (data && data.runId && !knownRunIds.has(data.runId)) metas.push({ ...data, _file: f });
|
|
457
|
+
} catch {}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
metas.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
461
|
+
const used = new Set();
|
|
462
|
+
for (const rec of pending) {
|
|
463
|
+
const startMs = rec.startedAt ? new Date(rec.startedAt).getTime() : 0;
|
|
464
|
+
const idx = metas.findIndex((m, i) => {
|
|
465
|
+
if (used.has(i)) return false;
|
|
466
|
+
if (m.agent !== rec.agent) return false;
|
|
467
|
+
if (rec.prompt && m.task && !m.task.includes(rec.prompt.slice(0, 40))) return false;
|
|
468
|
+
if (startMs && m.timestamp && m.timestamp < startMs - 1000) return false;
|
|
469
|
+
return true;
|
|
470
|
+
});
|
|
471
|
+
if (idx < 0) continue;
|
|
472
|
+
used.add(idx);
|
|
473
|
+
const m = metas[idx];
|
|
474
|
+
const base = m._file.replace(/_meta\.json$/, '');
|
|
475
|
+
rec.runId = m.runId;
|
|
476
|
+
rec.runIndex = 0;
|
|
477
|
+
rec.exitCode = m.exitCode;
|
|
478
|
+
rec.durationMs = m.durationMs;
|
|
479
|
+
rec.usage = m.usage;
|
|
480
|
+
rec.stoppedAt = m.timestamp ? new Date(m.timestamp).toISOString() : (rec.startedAt || null);
|
|
481
|
+
rec.artifactPaths = {
|
|
482
|
+
inputPath: path.join(artifactsDir, `${base}_input.md`),
|
|
483
|
+
outputPath: path.join(artifactsDir, `${base}_output.md`),
|
|
484
|
+
jsonlPath: path.join(artifactsDir, `${base}.jsonl`),
|
|
485
|
+
metadataPath: path.join(artifactsDir, m._file),
|
|
486
|
+
};
|
|
487
|
+
rec.sessionFile = rec.artifactPaths.jsonlPath;
|
|
488
|
+
rec._error = m.error || null;
|
|
489
|
+
if (m.exitCode != null && m.exitCode !== 0 && !m.error) rec._cancelled = true;
|
|
490
|
+
if (m.toolCount != null) rec.progressSummary = { toolCount: m.toolCount, tokens: (m.usage?.input || 0) + (m.usage?.output || 0) };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
pending = subagents.filter((r) => !r.runId && !r.stoppedAt);
|
|
494
|
+
if (pending.length === 0) return;
|
|
495
|
+
await matchLiveSubagentRuns(meta, pending, subagents);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function matchLiveSubagentRuns(meta, pending, allSubagents) {
|
|
499
|
+
const sessionDir = meta.file.replace(/\.jsonl$/, '');
|
|
500
|
+
let entries = [];
|
|
501
|
+
try { entries = await fs.promises.readdir(sessionDir, { withFileTypes: true }); } catch {}
|
|
502
|
+
const knownRunIds = new Set(allSubagents.map((r) => r.runId).filter(Boolean));
|
|
503
|
+
const liveRuns = [];
|
|
504
|
+
for (const d of entries) {
|
|
505
|
+
if (!d.isDirectory()) continue;
|
|
506
|
+
if (d.name === 'subagent-artifacts') continue;
|
|
507
|
+
if (knownRunIds.has(d.name)) continue;
|
|
508
|
+
const runDirPath = path.join(sessionDir, d.name);
|
|
509
|
+
let runs;
|
|
510
|
+
try { runs = await fs.promises.readdir(runDirPath); } catch { continue; }
|
|
511
|
+
for (const r of runs) {
|
|
512
|
+
const m = /^run-(\d+)$/.exec(r);
|
|
513
|
+
if (!m) continue;
|
|
514
|
+
const jsonl = path.join(runDirPath, r, 'session.jsonl');
|
|
515
|
+
try {
|
|
516
|
+
const stat = await fs.promises.stat(jsonl);
|
|
517
|
+
const raw = await fs.promises.readFile(jsonl, 'utf8');
|
|
518
|
+
const lines = raw.split('\n').filter(Boolean).slice(0, 10);
|
|
519
|
+
let firstTs = null;
|
|
520
|
+
let firstUserText = null;
|
|
521
|
+
for (const line of lines) {
|
|
522
|
+
try {
|
|
523
|
+
const ev = JSON.parse(line);
|
|
524
|
+
if (ev.type === 'session' && !firstTs) firstTs = ev.timestamp;
|
|
525
|
+
if (!firstUserText && ev.type === 'message' && ev.message && ev.message.role === 'user' && Array.isArray(ev.message.content)) {
|
|
526
|
+
const txt = ev.message.content.find((c) => c && c.type === 'text');
|
|
527
|
+
if (txt && typeof txt.text === 'string') firstUserText = txt.text;
|
|
528
|
+
}
|
|
529
|
+
} catch {}
|
|
530
|
+
}
|
|
531
|
+
liveRuns.push({ runId: d.name, runIndex: parseInt(m[1], 10), jsonl, firstTs, firstUserText, mtime: stat.mtimeMs });
|
|
532
|
+
} catch {}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
liveRuns.sort((a, b) => (a.firstTs ? new Date(a.firstTs).getTime() : 0) - (b.firstTs ? new Date(b.firstTs).getTime() : 0));
|
|
536
|
+
const used = new Set();
|
|
537
|
+
for (const rec of (liveRuns.length ? pending : [])) {
|
|
538
|
+
const startMs = rec.startedAt ? new Date(rec.startedAt).getTime() : 0;
|
|
539
|
+
const idx = liveRuns.findIndex((lr, i) => {
|
|
540
|
+
if (used.has(i)) return false;
|
|
541
|
+
const runMs = lr.firstTs ? new Date(lr.firstTs).getTime() : 0;
|
|
542
|
+
if (startMs && runMs && runMs < startMs - 5000) return false;
|
|
543
|
+
if (rec.prompt && lr.firstUserText) {
|
|
544
|
+
const probe = rec.prompt.slice(0, 60);
|
|
545
|
+
if (probe && !lr.firstUserText.includes(probe)) return false;
|
|
546
|
+
}
|
|
547
|
+
return true;
|
|
548
|
+
});
|
|
549
|
+
if (idx < 0) continue;
|
|
550
|
+
used.add(idx);
|
|
551
|
+
const lr = liveRuns[idx];
|
|
552
|
+
rec.runId = lr.runId;
|
|
553
|
+
rec.runIndex = lr.runIndex;
|
|
554
|
+
rec.sessionFile = lr.jsonl;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Structural staleness: while a subagent is running, the parent assistant is blocked
|
|
558
|
+
// waiting on its toolResult, so the parent jsonl can't have any later message other
|
|
559
|
+
// than that toolResult. If we see a newer message that isn't this subagent's own
|
|
560
|
+
// toolResult, pi has moved on and the spawn is orphaned. Tradeoff: a real pi crash
|
|
561
|
+
// with no further parent activity stays as "active" until the user restarts pi.
|
|
562
|
+
const parentEntries = (await getCachedSession(meta.file)).entries;
|
|
563
|
+
for (const rec of pending) {
|
|
564
|
+
if (rec.stoppedAt) continue;
|
|
565
|
+
if (!rec.startedAt) continue;
|
|
566
|
+
const startMs = new Date(rec.startedAt).getTime();
|
|
567
|
+
if (!startMs) continue;
|
|
568
|
+
const movedOn = parentEntries.some((e) => {
|
|
569
|
+
if (e.type !== 'message') return false;
|
|
570
|
+
const t = e.timestamp ? new Date(e.timestamp).getTime() : 0;
|
|
571
|
+
if (t <= startMs) return false;
|
|
572
|
+
const m = e.message;
|
|
573
|
+
if (m && m.role === 'toolResult' && m.toolName === 'subagent'
|
|
574
|
+
&& rec.toolCallId && m.toolCallId === rec.toolCallId) return false;
|
|
575
|
+
return true;
|
|
576
|
+
});
|
|
577
|
+
if (movedOn) {
|
|
578
|
+
rec.stoppedAt = new Date().toISOString();
|
|
579
|
+
rec._error = rec._error || 'Interrupted (no result recorded)';
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function listAgentsForSession(meta, summary = null) {
|
|
585
|
+
const s = summary || (await getCachedSession(meta.file)).summary;
|
|
586
|
+
const recs = s.subagents || [];
|
|
587
|
+
await enrichPendingSubagents(meta, recs);
|
|
588
|
+
const out = [];
|
|
589
|
+
for (const rec of recs) {
|
|
590
|
+
let lm = await readSubagentOutput(rec);
|
|
591
|
+
if ((!lm || !lm.trim()) && rec._error) lm = `**Error:**\n\n\`\`\`\n${rec._error}\n\`\`\``;
|
|
592
|
+
out.push(subagentToApi(rec, lm));
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function findAgentRecord(meta, agentId) {
|
|
598
|
+
const list = await listAgentsForSession(meta);
|
|
599
|
+
return list.find((a) => a.agentId === agentId) || null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function tallyTasks(tasks) {
|
|
603
|
+
let pending = 0, inProgress = 0, completed = 0, total = 0;
|
|
604
|
+
for (const t of tasks) {
|
|
605
|
+
if (t.status === 'deleted') continue;
|
|
606
|
+
total++;
|
|
607
|
+
if (t.status === 'completed') completed++;
|
|
608
|
+
else if (t.status === 'in_progress') inProgress++;
|
|
609
|
+
else pending++;
|
|
610
|
+
}
|
|
611
|
+
return { taskCount: total, pending, inProgress, completed };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const MODEL_CONTEXT_WINDOWS = [
|
|
615
|
+
[/claude/i, 200000],
|
|
616
|
+
[/kimi/i, 256000],
|
|
617
|
+
[/glm-?4\.[67]/i, 128000],
|
|
618
|
+
[/gpt-?5/i, 400000],
|
|
619
|
+
[/gpt-?4/i, 128000],
|
|
620
|
+
[/gemini/i, 1000000],
|
|
621
|
+
[/deepseek/i, 128000],
|
|
622
|
+
];
|
|
623
|
+
function modelContextWindow(model) {
|
|
624
|
+
if (!model) return 200000;
|
|
625
|
+
const m = String(model);
|
|
626
|
+
for (const [re, cap] of MODEL_CONTEXT_WINDOWS) if (re.test(m)) return cap;
|
|
627
|
+
return 200000;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function shortIdFromUuid(uuid) {
|
|
631
|
+
if (!uuid) return '';
|
|
632
|
+
return uuid.split('-')[0];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function basenameWithoutExt(file) {
|
|
636
|
+
return path.basename(file, '.jsonl');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Build a slug like "<short-uuid>" from filename.
|
|
640
|
+
function slugFromFile(file) {
|
|
641
|
+
const base = basenameWithoutExt(file);
|
|
642
|
+
// 2026-05-07T08-49-06-833Z_019e01a0-5b0d-7139-9740-bc883238364d
|
|
643
|
+
const idx = base.indexOf('_');
|
|
644
|
+
if (idx >= 0) return base.slice(idx + 1);
|
|
645
|
+
return base;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const RECENT_MS = 5 * 60 * 1000;
|
|
649
|
+
|
|
650
|
+
async function buildSessionSummary(meta) {
|
|
651
|
+
const cached = await getCachedSession(meta.file, meta.mtimeMs != null ? { mtimeMs: meta.mtimeMs, size: meta.size } : null);
|
|
652
|
+
const s = cached.summary;
|
|
653
|
+
const id = (s.sessionEntry && s.sessionEntry.id) || slugFromFile(meta.file);
|
|
654
|
+
const cwd = (s.sessionEntry && s.sessionEntry.cwd) || meta.cwd;
|
|
655
|
+
const projectName = path.basename(cwd) || cwd;
|
|
656
|
+
const ageMs = Date.now() - meta.mtime.getTime();
|
|
657
|
+
const isRecent = ageMs <= RECENT_MS;
|
|
658
|
+
return {
|
|
659
|
+
id,
|
|
660
|
+
name: s.customTitle || shortIdFromUuid(id),
|
|
661
|
+
slug: shortIdFromUuid(id),
|
|
662
|
+
project: cwd,
|
|
663
|
+
cwd,
|
|
664
|
+
description: null,
|
|
665
|
+
gitBranch: gitBranchModule.getBranch(cwd, (branch) => {
|
|
666
|
+
if (onBranchResolved) onBranchResolved(cwd, branch);
|
|
667
|
+
}),
|
|
668
|
+
customTitle: s.customTitle || null,
|
|
669
|
+
parentSession: (s.sessionEntry && s.sessionEntry.parentSession) || null,
|
|
670
|
+
...tallyTasks(await taskStore.listTasksAsync(id)),
|
|
671
|
+
createdAt: s.sessionEntry && s.sessionEntry.timestamp,
|
|
672
|
+
modifiedAt: meta.mtime.toISOString(),
|
|
673
|
+
hasMessages: s.messageCount > 0,
|
|
674
|
+
hasActiveAgents: false,
|
|
675
|
+
hasRunningAgents: false,
|
|
676
|
+
hasWaitingForUser: !!s.pendingAskUser,
|
|
677
|
+
hasRecentLog: isRecent,
|
|
678
|
+
jsonlPath: meta.file,
|
|
679
|
+
tasksDir: null,
|
|
680
|
+
projectDir: meta.projectDir,
|
|
681
|
+
contextStatus: {
|
|
682
|
+
session_id: id,
|
|
683
|
+
transcript_path: meta.file,
|
|
684
|
+
cwd,
|
|
685
|
+
session_name: projectName,
|
|
686
|
+
model: { id: s.model || 'unknown', display_name: s.model || 'unknown' },
|
|
687
|
+
provider: s.provider,
|
|
688
|
+
workspace: { current_dir: cwd, project_dir: cwd, added_dirs: [cwd] },
|
|
689
|
+
version: s.sessionEntry && s.sessionEntry.version,
|
|
690
|
+
cost: {
|
|
691
|
+
total_cost_usd: s.totalCost,
|
|
692
|
+
total_duration_ms: 0,
|
|
693
|
+
total_api_duration_ms: 0,
|
|
694
|
+
total_lines_added: 0,
|
|
695
|
+
total_lines_removed: 0,
|
|
696
|
+
},
|
|
697
|
+
context_window: (() => {
|
|
698
|
+
const lu = s.lastAssistantUsage || {};
|
|
699
|
+
const cap = modelContextWindow(s.model);
|
|
700
|
+
const curIn = (lu.input || 0) + (lu.cacheRead || 0) + (lu.cacheWrite || 0);
|
|
701
|
+
const curOut = lu.output || 0;
|
|
702
|
+
const used = Math.min(100, Math.round((curIn / cap) * 100));
|
|
703
|
+
return {
|
|
704
|
+
total_input_tokens: s.totalInput,
|
|
705
|
+
total_output_tokens: s.totalOutput,
|
|
706
|
+
context_window_size: cap,
|
|
707
|
+
current_usage: {
|
|
708
|
+
input_tokens: curIn,
|
|
709
|
+
output_tokens: curOut,
|
|
710
|
+
cache_creation_input_tokens: lu.cacheWrite || 0,
|
|
711
|
+
cache_read_input_tokens: lu.cacheRead || 0,
|
|
712
|
+
},
|
|
713
|
+
used_percentage: used,
|
|
714
|
+
remaining_percentage: Math.max(0, 100 - used),
|
|
715
|
+
};
|
|
716
|
+
})(),
|
|
717
|
+
_stats: {
|
|
718
|
+
userCount: s.userCount,
|
|
719
|
+
assistantCount: s.assistantCount,
|
|
720
|
+
toolCalls: s.toolCalls,
|
|
721
|
+
toolResults: s.toolResults,
|
|
722
|
+
messageCount: s.messageCount,
|
|
723
|
+
},
|
|
724
|
+
_updatedAt: meta.mtime.getTime(),
|
|
725
|
+
},
|
|
726
|
+
hasPlan: false,
|
|
727
|
+
planTitle: null,
|
|
728
|
+
planPath: null,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function listSessions() {
|
|
733
|
+
const files = await listSessionFiles();
|
|
734
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
735
|
+
const summaries = await Promise.all(files.map(async (f) => {
|
|
736
|
+
try { return await buildSessionSummary(f); }
|
|
737
|
+
catch { return null; }
|
|
738
|
+
}));
|
|
739
|
+
const out = [];
|
|
740
|
+
const seen = new Set();
|
|
741
|
+
for (const summary of summaries) {
|
|
742
|
+
if (!summary) continue;
|
|
743
|
+
if (summary.parentSession) continue;
|
|
744
|
+
if (seen.has(summary.id)) continue;
|
|
745
|
+
seen.add(summary.id);
|
|
746
|
+
out.push(summary);
|
|
747
|
+
}
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function findSessionFileById(id) {
|
|
752
|
+
const files = await listSessionFiles();
|
|
753
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
754
|
+
// First try filename match (faster)
|
|
755
|
+
for (const f of files) {
|
|
756
|
+
if (f.file.includes(id)) return f;
|
|
757
|
+
}
|
|
758
|
+
// Then read header to match session id
|
|
759
|
+
for (const f of files) {
|
|
760
|
+
try {
|
|
761
|
+
const entries = await readSessionEntries(f.file);
|
|
762
|
+
const sess = entries.find((e) => e.type === 'session');
|
|
763
|
+
if (sess && sess.id === id) return f;
|
|
764
|
+
} catch {}
|
|
765
|
+
}
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function flattenContentToText(content, opts = {}) {
|
|
770
|
+
const { includeThinking = false } = opts;
|
|
771
|
+
if (typeof content === 'string') return content;
|
|
772
|
+
if (!Array.isArray(content)) return '';
|
|
773
|
+
return content
|
|
774
|
+
.map((c) => {
|
|
775
|
+
if (!c) return '';
|
|
776
|
+
if (c.type === 'text') return c.text || '';
|
|
777
|
+
if (includeThinking && c.type === 'thinking') return c.thinking || '';
|
|
778
|
+
return '';
|
|
779
|
+
})
|
|
780
|
+
.filter(Boolean)
|
|
781
|
+
.join('\n\n');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Map pi tool name -> display name (matches cck conventions).
|
|
785
|
+
const TOOL_NAME_MAP = {
|
|
786
|
+
read: 'Read',
|
|
787
|
+
write: 'Write',
|
|
788
|
+
edit: 'Edit',
|
|
789
|
+
bash: 'Bash',
|
|
790
|
+
todo: 'TodoWrite',
|
|
791
|
+
subagent: 'Agent',
|
|
792
|
+
ask_user: 'AskUser',
|
|
793
|
+
ask_user_question: 'AskUserQuestion',
|
|
794
|
+
web_search: 'WebSearch',
|
|
795
|
+
code_search: 'Grep',
|
|
796
|
+
ast_grep_search: 'AstGrep',
|
|
797
|
+
lsp: 'LSP',
|
|
798
|
+
lsp_navigation: 'LSP',
|
|
799
|
+
interactive_shell: 'Shell',
|
|
800
|
+
mark_step_done: 'MarkStepDone',
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
function computeToolDetail(name, params) {
|
|
804
|
+
if (!params || typeof params !== 'object') {
|
|
805
|
+
const s = String(params ?? '');
|
|
806
|
+
return { detail: s.slice(0, 80), fullDetail: s.length > 80 ? s : null };
|
|
807
|
+
}
|
|
808
|
+
let detail = null;
|
|
809
|
+
let full = null;
|
|
810
|
+
const trunc = (s, n = 80) => (s.length > n ? s.slice(0, n) + '...' : s);
|
|
811
|
+
if (params.file_path) { detail = path.basename(params.file_path); full = params.file_path; }
|
|
812
|
+
else if (params.path) { detail = path.basename(params.path); full = params.path; }
|
|
813
|
+
else if (params.command) { full = params.command; detail = trunc(full); }
|
|
814
|
+
else if (params.pattern) { full = params.pattern; detail = full; }
|
|
815
|
+
else if (params.query) { full = params.query; detail = full; }
|
|
816
|
+
else if (params.queries) { full = Array.isArray(params.queries) ? params.queries.join(' | ') : String(params.queries); detail = trunc(full); }
|
|
817
|
+
else if (params.url) { full = params.url; detail = trunc(full); }
|
|
818
|
+
else if (params.task) { full = params.task; detail = trunc(full); }
|
|
819
|
+
else if (params.subject) { full = params.subject; detail = full; }
|
|
820
|
+
else if (params.description) { full = params.description; detail = trunc(full); }
|
|
821
|
+
else if (params.tasks && Array.isArray(params.tasks)) {
|
|
822
|
+
const agents = [...new Set(params.tasks.map((t) => t.agent).filter(Boolean))];
|
|
823
|
+
const n = params.tasks.length;
|
|
824
|
+
const agentStr = agents.length ? ` · ${agents.join(', ')}` : '';
|
|
825
|
+
detail = `${n} task${n > 1 ? 's' : ''}${agentStr}`;
|
|
826
|
+
full = detail;
|
|
827
|
+
}
|
|
828
|
+
else if (params.question) { full = params.question; detail = trunc(full); }
|
|
829
|
+
else if (params.questions && Array.isArray(params.questions)) {
|
|
830
|
+
const first = params.questions[0]?.question || '';
|
|
831
|
+
const extra = params.questions.length > 1 ? ` (+${params.questions.length - 1} more)` : '';
|
|
832
|
+
full = params.questions.map((q) => q.question).join(' | ');
|
|
833
|
+
detail = trunc(first, 60) + extra;
|
|
834
|
+
}
|
|
835
|
+
else if (params.action) { detail = params.action; full = detail; }
|
|
836
|
+
else if (params.operation) { detail = params.operation; full = detail; }
|
|
837
|
+
else {
|
|
838
|
+
try { full = JSON.stringify(params); } catch { full = ''; }
|
|
839
|
+
detail = trunc(full);
|
|
840
|
+
}
|
|
841
|
+
return { detail: detail || '', fullDetail: full && full !== detail ? full : null };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function normalizeToolCall(name, args) {
|
|
845
|
+
const display = TOOL_NAME_MAP[name] || name;
|
|
846
|
+
if (!args || typeof args !== 'object') return { name: display, params: args };
|
|
847
|
+
const p = { ...args };
|
|
848
|
+
if (p.path && !p.file_path) {
|
|
849
|
+
p.file_path = p.path;
|
|
850
|
+
delete p.path;
|
|
851
|
+
}
|
|
852
|
+
if (Array.isArray(p.edits)) {
|
|
853
|
+
if (p.edits.length === 1) {
|
|
854
|
+
const e = p.edits[0];
|
|
855
|
+
if (e && (e.oldText || e.newText)) {
|
|
856
|
+
p.old_string = e.oldText || '';
|
|
857
|
+
p.new_string = e.newText || '';
|
|
858
|
+
delete p.edits;
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
p.edits = p.edits.map((e) => ({
|
|
862
|
+
old_string: e.oldText ?? e.old_string ?? '',
|
|
863
|
+
new_string: e.newText ?? e.new_string ?? '',
|
|
864
|
+
}));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return { name: display, params: p };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Convert pi entries -> cck-style messages array.
|
|
871
|
+
async function readFileSafe(p) {
|
|
872
|
+
if (!p) return null;
|
|
873
|
+
try { return await fs.promises.readFile(p, 'utf8'); } catch { return null; }
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async function buildMessages(file, limit = 50, before = null) {
|
|
877
|
+
const cached = await getCachedSession(file);
|
|
878
|
+
if (cached.allMessages) {
|
|
879
|
+
const filtered = before
|
|
880
|
+
? cached.allMessages.filter((m) => m.timestamp && m.timestamp < before)
|
|
881
|
+
: cached.allMessages;
|
|
882
|
+
const total = filtered.length;
|
|
883
|
+
const sliced = limit && limit < total ? filtered.slice(-limit) : filtered;
|
|
884
|
+
return { messages: sliced, hasMore: sliced.length < total, sessionId: null };
|
|
885
|
+
}
|
|
886
|
+
const entries = cached.entries;
|
|
887
|
+
const toolResultByCallId = cached.toolResultByCallId;
|
|
888
|
+
const subagentInfoByCallId = cached.subagentInfoByCallId;
|
|
889
|
+
|
|
890
|
+
const messages = [];
|
|
891
|
+
for (const e of entries) {
|
|
892
|
+
if (e.type === 'compaction') {
|
|
893
|
+
messages.push({
|
|
894
|
+
type: 'user',
|
|
895
|
+
role: 'user',
|
|
896
|
+
systemLabel: 'compact-summary',
|
|
897
|
+
compactSummary: e.summary || '',
|
|
898
|
+
text: e.summary || '',
|
|
899
|
+
timestamp: e.timestamp,
|
|
900
|
+
});
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (e.type !== 'message') continue;
|
|
904
|
+
const m = e.message || {};
|
|
905
|
+
const ts = e.timestamp;
|
|
906
|
+
|
|
907
|
+
if (m.role === 'user') {
|
|
908
|
+
const text = flattenContentToText(m.content);
|
|
909
|
+
messages.push({ type: 'user', role: 'user', text, timestamp: ts });
|
|
910
|
+
} else if (m.role === 'assistant') {
|
|
911
|
+
const text = flattenContentToText(m.content);
|
|
912
|
+
if (text && text.trim()) {
|
|
913
|
+
messages.push({
|
|
914
|
+
type: 'assistant',
|
|
915
|
+
role: 'assistant',
|
|
916
|
+
text,
|
|
917
|
+
timestamp: ts,
|
|
918
|
+
model: m.model,
|
|
919
|
+
provider: m.provider,
|
|
920
|
+
usage: m.usage,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
if (Array.isArray(m.content)) {
|
|
924
|
+
for (const c of m.content) {
|
|
925
|
+
if (c && c.type === 'toolCall') {
|
|
926
|
+
const { name: toolName, params } = normalizeToolCall(c.name, c.arguments);
|
|
927
|
+
const { detail, fullDetail } = computeToolDetail(toolName, params);
|
|
928
|
+
const tr = toolResultByCallId.get(c.id);
|
|
929
|
+
const trText = tr ? flattenContentToText(tr.content) : null;
|
|
930
|
+
const msg = {
|
|
931
|
+
type: 'tool_use',
|
|
932
|
+
tool: toolName,
|
|
933
|
+
detail,
|
|
934
|
+
fullDetail,
|
|
935
|
+
description: null,
|
|
936
|
+
params,
|
|
937
|
+
timestamp: ts,
|
|
938
|
+
toolUseId: c.id,
|
|
939
|
+
toolResult: trText ? trText.slice(0, 1500) : null,
|
|
940
|
+
toolResultTruncated: trText ? trText.length > 1500 : false,
|
|
941
|
+
};
|
|
942
|
+
if (c.name === 'ask_user_question' && tr && tr.details && !tr.isError) {
|
|
943
|
+
msg.toolResultDetails = tr.details;
|
|
944
|
+
}
|
|
945
|
+
if (c.name === 'subagent') {
|
|
946
|
+
const info = subagentInfoByCallId.get(c.id);
|
|
947
|
+
const agent = (c.arguments && c.arguments.agent) || (info && info.agent) || null;
|
|
948
|
+
const runId = info && info.runId ? info.runId : null;
|
|
949
|
+
const isSpawn = !!(c.arguments && c.arguments.task);
|
|
950
|
+
const isParallel = !!(c.arguments && Array.isArray(c.arguments.tasks));
|
|
951
|
+
if (isSpawn) {
|
|
952
|
+
msg.tool = 'Agent';
|
|
953
|
+
msg.agentId = c.id ? `tc_${c.id}_${(info && info.runIndex) || 0}` : (runId && agent ? `${runId}_${agent}_${(info && info.runIndex) || 0}` : `pending_${Math.random().toString(36).slice(2, 10)}`);
|
|
954
|
+
msg.agentType = agent;
|
|
955
|
+
msg.agentName = agent;
|
|
956
|
+
msg.agentPrompt = c.arguments.task || null;
|
|
957
|
+
msg.agentLastMessage = info && info.outputPath ? await readFileSafe(info.outputPath) : null;
|
|
958
|
+
if (!msg.agentLastMessage && trText) msg.agentLastMessage = trText;
|
|
959
|
+
} else if (isParallel) {
|
|
960
|
+
msg.agentTasks = c.arguments.tasks;
|
|
961
|
+
if (tr && tr.details && Array.isArray(tr.details.results)) {
|
|
962
|
+
msg.parallelResults = tr.details.results;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
messages.push(msg);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// toolResult handled via map above.
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
cached.allMessages = messages;
|
|
975
|
+
const filtered = before
|
|
976
|
+
? messages.filter((m) => m.timestamp && m.timestamp < before)
|
|
977
|
+
: messages;
|
|
978
|
+
const total = filtered.length;
|
|
979
|
+
const sliced = limit && limit < total ? filtered.slice(-limit) : filtered;
|
|
980
|
+
return { messages: sliced, hasMore: sliced.length < total, sessionId: null };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
module.exports = {
|
|
984
|
+
getPiDir,
|
|
985
|
+
getSessionsDir,
|
|
986
|
+
decodeProjectDir,
|
|
987
|
+
listSessionFiles,
|
|
988
|
+
listSessions,
|
|
989
|
+
findSessionFileById,
|
|
990
|
+
buildSessionSummary,
|
|
991
|
+
buildMessages,
|
|
992
|
+
readSessionEntries,
|
|
993
|
+
summarize,
|
|
994
|
+
listAgentsForSession,
|
|
995
|
+
findAgentRecord,
|
|
996
|
+
slugFromFile,
|
|
997
|
+
setOnBranchResolved,
|
|
998
|
+
flattenContentToText,
|
|
999
|
+
invalidateSessionCache,
|
|
1000
|
+
};
|