pi-cc-extensions 0.8.9 → 0.8.10
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 +3 -2
- package/extensions/agent-autocomplete.ts +11 -16
- package/extensions/ask-user-question/ask-user-question.ts +29 -7
- package/extensions/ask-user-question/config.ts +6 -2
- package/extensions/ask-user-question/index.ts +8 -8
- package/extensions/ask-user-question/rpc-fallback.ts +16 -4
- package/extensions/ask-user-question/state/build-questionnaire.ts +16 -5
- package/extensions/ask-user-question/state/key-router.ts +29 -7
- package/extensions/ask-user-question/state/questionnaire-session.ts +1 -4
- package/extensions/ask-user-question/state/selectors/projections.ts +4 -1
- package/extensions/ask-user-question/state/state-reducer.ts +24 -7
- package/extensions/ask-user-question/tool/response-envelope.ts +4 -1
- package/extensions/ask-user-question/tool/types.ts +5 -1
- package/extensions/ask-user-question/tool/validate-questionnaire.ts +10 -2
- package/extensions/ask-user-question/view/components/inline-input.ts +15 -2
- package/extensions/ask-user-question/view/components/multi-select-view.ts +11 -4
- package/extensions/ask-user-question/view/components/option-list-view.ts +14 -5
- package/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +13 -7
- package/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +16 -4
- package/extensions/ask-user-question/view/components/preview/preview-pane.ts +5 -1
- package/extensions/ask-user-question/view/components/submit-picker.ts +3 -1
- package/extensions/ask-user-question/view/components/wrapping-select.ts +20 -4
- package/extensions/ask-user-question/view/dialog-builder.ts +27 -7
- package/extensions/ask-user-question/view/tab-content-strategy.ts +23 -8
- package/extensions/claude-code-style.ts +1658 -1485
- package/extensions/compact-style.ts +120 -43
- package/extensions/context.ts +502 -394
- package/extensions/session-reference-core.ts +8 -3
- package/extensions/session-reference.ts +475 -448
- package/extensions/tool-diff/ATTRIBUTION.md +28 -0
- package/extensions/tool-diff/ansi-utils.ts +113 -0
- package/extensions/tool-diff/diff-presentation.ts +70 -0
- package/extensions/tool-diff/diff-renderer.ts +2827 -0
- package/extensions/tool-diff/index.ts +106 -0
- package/extensions/tool-diff/line-width-safety.ts +91 -0
- package/extensions/tool-diff/render-utils.ts +5 -0
- package/extensions/tool-diff/types.ts +20 -0
- package/extensions/tool-diff/write-display-utils.ts +29 -0
- package/extensions/tool-diff/write-execution.ts +113 -0
- package/extensions/tool-result-sanitize.ts +4 -4
- package/package.json +64 -61
- package/themes/github-dark-default.json +91 -91
|
@@ -1,1485 +1,1658 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
DynamicBorder,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.
|
|
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
|
-
const
|
|
302
|
-
const
|
|
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
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return
|
|
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
|
-
|
|
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
|
-
function
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (!
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
function
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const
|
|
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
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const
|
|
859
|
-
if (
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
if (
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
toolMouseTui
|
|
945
|
-
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
function
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
return
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
)
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
);
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
prototype.
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
DynamicBorder,
|
|
4
|
+
getSettingsListTheme,
|
|
5
|
+
keyHint,
|
|
6
|
+
ToolExecutionComponent,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
installCompactStyle,
|
|
10
|
+
type CompactStyleHooks,
|
|
11
|
+
type CompactStyleMode,
|
|
12
|
+
} from "./compact-style.ts";
|
|
13
|
+
import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
|
|
14
|
+
import {
|
|
15
|
+
installWriteOverride,
|
|
16
|
+
renderRichToolResult,
|
|
17
|
+
WriteExecutionMetadataStore,
|
|
18
|
+
} from "./tool-diff/index.ts";
|
|
19
|
+
import {
|
|
20
|
+
Container,
|
|
21
|
+
SettingsList,
|
|
22
|
+
Text,
|
|
23
|
+
matchesKey,
|
|
24
|
+
truncateToWidth,
|
|
25
|
+
visibleWidth,
|
|
26
|
+
wrapTextWithAnsi,
|
|
27
|
+
} from "@earendil-works/pi-tui";
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Claude Code Style for pi.
|
|
34
|
+
*
|
|
35
|
+
* This is the package's only entry point. Compact transcript rendering lives in
|
|
36
|
+
* the internal compact-style module and is routed by the mode below.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export type Config = {
|
|
40
|
+
mode: CompactStyleMode;
|
|
41
|
+
excludeRenderers: string[];
|
|
42
|
+
fixedEditorFeatures: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const CONFIG_PATH = join(homedir(), ".pi", "agent", "claude-code-style.json");
|
|
46
|
+
|
|
47
|
+
export function normalizeConfig(input: unknown): Config {
|
|
48
|
+
const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
|
|
49
|
+
const mode = source.mode;
|
|
50
|
+
const migratedMode: CompactStyleMode =
|
|
51
|
+
mode === "on" || mode === "off" || mode === "compact"
|
|
52
|
+
? mode
|
|
53
|
+
: typeof source.enabled === "boolean"
|
|
54
|
+
? source.enabled
|
|
55
|
+
? "on"
|
|
56
|
+
: "off"
|
|
57
|
+
: "on";
|
|
58
|
+
const excludeRenderers = Array.isArray(source.excludeRenderers)
|
|
59
|
+
? [
|
|
60
|
+
...new Set(
|
|
61
|
+
source.excludeRenderers.filter(
|
|
62
|
+
(name): name is string => typeof name === "string" && name.length > 0,
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
: [];
|
|
67
|
+
const fixedEditorFeatures = source.fixedEditorFeatures !== false;
|
|
68
|
+
return { mode: migratedMode, excludeRenderers, fixedEditorFeatures };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let config: Config = loadConfig();
|
|
72
|
+
|
|
73
|
+
function loadConfig(): Config {
|
|
74
|
+
try {
|
|
75
|
+
if (existsSync(CONFIG_PATH)) {
|
|
76
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as unknown;
|
|
77
|
+
const normalized = normalizeConfig(parsed);
|
|
78
|
+
const source = parsed as Record<string, unknown>;
|
|
79
|
+
// Persist the one-time enabled:boolean -> mode migration while retaining
|
|
80
|
+
// the existing exclusion list.
|
|
81
|
+
if (
|
|
82
|
+
typeof source.enabled === "boolean" &&
|
|
83
|
+
source.mode !== "on" &&
|
|
84
|
+
source.mode !== "off" &&
|
|
85
|
+
source.mode !== "compact"
|
|
86
|
+
) {
|
|
87
|
+
try {
|
|
88
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2));
|
|
89
|
+
} catch {
|
|
90
|
+
// A read-only config still uses the migrated in-memory value.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return normalized;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore bad config and fall back to defaults.
|
|
97
|
+
}
|
|
98
|
+
return { mode: "on", excludeRenderers: [], fixedEditorFeatures: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function saveConfig() {
|
|
102
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function oneLine(value: unknown, max = 72): string {
|
|
106
|
+
const text = String(value ?? "")
|
|
107
|
+
.replace(/\s+/g, " ")
|
|
108
|
+
.trim();
|
|
109
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function textFromResult(result: any): string {
|
|
113
|
+
const item = result?.content?.find?.((c: any) => c.type === "text");
|
|
114
|
+
return item?.type === "text" ? sanitizeToolResultText(item.text ?? "") : "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function countLines(text: string): number {
|
|
118
|
+
return text
|
|
119
|
+
.trim()
|
|
120
|
+
.split("\n")
|
|
121
|
+
.filter((line) => line.trim().length > 0).length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hasExpandableResult(text: string): boolean {
|
|
125
|
+
return countLines(text) > 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toolIcon(_name: string): string {
|
|
129
|
+
return "●";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Match the braille loader shown to the left of pi's "Working..." row.
|
|
133
|
+
// Every frame is one cell wide, so tool titles remain horizontally stable.
|
|
134
|
+
const WORKING_LOADER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
135
|
+
const TOOL_PENDING_FRAMES: Record<string, string[]> = {
|
|
136
|
+
read: WORKING_LOADER_FRAMES,
|
|
137
|
+
bash: WORKING_LOADER_FRAMES,
|
|
138
|
+
edit: WORKING_LOADER_FRAMES,
|
|
139
|
+
write: WORKING_LOADER_FRAMES,
|
|
140
|
+
find: WORKING_LOADER_FRAMES,
|
|
141
|
+
grep: WORKING_LOADER_FRAMES,
|
|
142
|
+
ls: WORKING_LOADER_FRAMES,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
function animationFrame(frames: string[], intervalMs = 120): string {
|
|
146
|
+
return frames[Math.floor(Date.now() / intervalMs) % frames.length] ?? frames[0] ?? "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const activeAnimationContexts = new Set<any>();
|
|
150
|
+
let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
151
|
+
|
|
152
|
+
function clearAnimation(context: any) {
|
|
153
|
+
if (!context?.state?.ccstyleAnimationScheduled) return;
|
|
154
|
+
context.state.ccstyleAnimationScheduled = false;
|
|
155
|
+
activeAnimationContexts.delete(context);
|
|
156
|
+
if (activeAnimationContexts.size === 0 && sharedAnimationTimer) {
|
|
157
|
+
clearTimeout(sharedAnimationTimer);
|
|
158
|
+
sharedAnimationTimer = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function clearAllAnimations() {
|
|
163
|
+
for (const ctx of activeAnimationContexts) {
|
|
164
|
+
ctx.state.ccstyleAnimationScheduled = false;
|
|
165
|
+
}
|
|
166
|
+
activeAnimationContexts.clear();
|
|
167
|
+
if (sharedAnimationTimer) {
|
|
168
|
+
clearTimeout(sharedAnimationTimer);
|
|
169
|
+
sharedAnimationTimer = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function scheduleAnimation(context: any, intervalMs = 80) {
|
|
174
|
+
const state = (context.state ??= {});
|
|
175
|
+
if (state.ccstyleAnimationScheduled) return;
|
|
176
|
+
state.ccstyleAnimationScheduled = true;
|
|
177
|
+
activeAnimationContexts.add(context);
|
|
178
|
+
if (!sharedAnimationTimer) {
|
|
179
|
+
sharedAnimationTimer = setTimeout(() => {
|
|
180
|
+
sharedAnimationTimer = null;
|
|
181
|
+
const contexts = Array.from(activeAnimationContexts);
|
|
182
|
+
activeAnimationContexts.clear();
|
|
183
|
+
for (const ctx of contexts) {
|
|
184
|
+
ctx.state.ccstyleAnimationScheduled = false;
|
|
185
|
+
ctx.invalidate?.();
|
|
186
|
+
}
|
|
187
|
+
}, intervalMs);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pendingIcon(name: string): string {
|
|
192
|
+
return animationFrame(TOOL_PENDING_FRAMES[name] ?? [toolIcon(name)], 80);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type ToolVisualState = "pending" | "success" | "error";
|
|
196
|
+
|
|
197
|
+
function settledIcon(name: string, state: ToolVisualState | undefined): string {
|
|
198
|
+
if (state === "success") return "✓";
|
|
199
|
+
if (state === "error") return "✗";
|
|
200
|
+
return toolIcon(name);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function setToolVisualState(context: any, visualState: ToolVisualState) {
|
|
204
|
+
const state = (context.state ??= {});
|
|
205
|
+
if (visualState !== "pending") clearAnimation(context);
|
|
206
|
+
if (state.ccstyleToolVisualState === visualState) return;
|
|
207
|
+
state.ccstyleToolVisualState = visualState;
|
|
208
|
+
// Do not invalidate synchronously from renderResult. Pi is already rendering
|
|
209
|
+
// this tool row; recursively scheduling another render here can retain both
|
|
210
|
+
// the finalized result component and its previous secondary/partial component,
|
|
211
|
+
// which displays the result summary twice. The current render pass also
|
|
212
|
+
// refreshes renderCall, so the settled icon still updates immediately.
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getToolVisualState(context: any): ToolVisualState | undefined {
|
|
216
|
+
return context?.state?.ccstyleToolVisualState as ToolVisualState | undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveToolVisualState(context: any): ToolVisualState | undefined {
|
|
220
|
+
const visualState = getToolVisualState(context);
|
|
221
|
+
if (visualState || context?.isPartial !== false) return visualState;
|
|
222
|
+
const settledState: ToolVisualState = context?.isError ? "error" : "success";
|
|
223
|
+
setToolVisualState(context, settledState);
|
|
224
|
+
return settledState;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function toolIconColor(context: any): "accent" | "error" | "success" | "muted" {
|
|
228
|
+
const visualState = getToolVisualState(context);
|
|
229
|
+
if (context?.isError || visualState === "error") return "error";
|
|
230
|
+
if (visualState === "success") return "success";
|
|
231
|
+
if (context?.isPartial || context?.executionStarted || visualState === "pending") return "accent";
|
|
232
|
+
return "muted";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isToolExpanded(options: any, context: any): boolean {
|
|
236
|
+
const local = context?.state?.ccstyleToolExpanded;
|
|
237
|
+
return typeof local === "boolean" ? local : Boolean(options?.expanded ?? context?.expanded);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Keep the guide aligned when long result lines wrap at the viewport edge. */
|
|
241
|
+
export class ExpandedToolResultText {
|
|
242
|
+
private text: string;
|
|
243
|
+
private prefix: string;
|
|
244
|
+
private normalizedText: string;
|
|
245
|
+
private cachedWidth: number | undefined;
|
|
246
|
+
private cachedLines: string[] | undefined;
|
|
247
|
+
|
|
248
|
+
constructor(text: string, prefix: string) {
|
|
249
|
+
this.text = text;
|
|
250
|
+
this.prefix = prefix;
|
|
251
|
+
this.normalizedText = text.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setText(text: string): void {
|
|
255
|
+
if (this.text === text) return;
|
|
256
|
+
this.text = text;
|
|
257
|
+
this.normalizedText = text.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
258
|
+
this.invalidate();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setPrefix(prefix: string): void {
|
|
262
|
+
if (this.prefix === prefix) return;
|
|
263
|
+
this.prefix = prefix;
|
|
264
|
+
this.invalidate();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
render(width: number): string[] {
|
|
268
|
+
if (this.cachedLines !== undefined && this.cachedWidth === width) return this.cachedLines;
|
|
269
|
+
|
|
270
|
+
const prefixWidth = visibleWidth(this.prefix);
|
|
271
|
+
const contentWidth = Math.max(1, width - prefixWidth);
|
|
272
|
+
const lines = wrapTextWithAnsi(this.normalizedText, contentWidth).map((line) =>
|
|
273
|
+
truncateToWidth(this.prefix + line, width, ""),
|
|
274
|
+
);
|
|
275
|
+
this.cachedWidth = width;
|
|
276
|
+
this.cachedLines = lines;
|
|
277
|
+
return lines;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
invalidate(): void {
|
|
281
|
+
this.cachedWidth = undefined;
|
|
282
|
+
this.cachedLines = undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function renderCollapsedToolResult(body: string, collapsedHint = ""): string {
|
|
287
|
+
return ` ↳ ${body}${collapsedHint}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function renderExpandedToolResult(
|
|
291
|
+
body: string,
|
|
292
|
+
theme: any,
|
|
293
|
+
isError: boolean,
|
|
294
|
+
lastComponent?: unknown,
|
|
295
|
+
): ExpandedToolResultText | Text {
|
|
296
|
+
const color = isError ? "error" : "muted";
|
|
297
|
+
if (!body.trim()) {
|
|
298
|
+
return new Text(theme.fg(color, renderCollapsedToolResult("Done")), 0, 0);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const text = theme.fg(color, body);
|
|
302
|
+
const prefix = theme.fg(color, " │ ");
|
|
303
|
+
if (lastComponent instanceof ExpandedToolResultText) {
|
|
304
|
+
lastComponent.setText(text);
|
|
305
|
+
lastComponent.setPrefix(prefix);
|
|
306
|
+
return lastComponent;
|
|
307
|
+
}
|
|
308
|
+
return new ExpandedToolResultText(text, prefix);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function expandHint(theme: any): string {
|
|
312
|
+
// Keep interaction guidance neutral; it should not inherit success/error
|
|
313
|
+
// coloring from the tool result surrounding it.
|
|
314
|
+
return `${theme.fg("muted", " (")}${keyHint("app.tools.expand", "expand")}${theme.fg("muted", " / click)")}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
type SgrMousePacket = {
|
|
318
|
+
code: number;
|
|
319
|
+
col: number;
|
|
320
|
+
row: number;
|
|
321
|
+
final: "M" | "m";
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
type ToolRenderHit = {
|
|
325
|
+
component: any;
|
|
326
|
+
start: number;
|
|
327
|
+
end: number;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
|
|
331
|
+
const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h";
|
|
332
|
+
const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1000l";
|
|
333
|
+
const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
|
|
334
|
+
const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
|
|
335
|
+
const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
|
|
336
|
+
let toolMouseTui: any = null;
|
|
337
|
+
let toolMouseUi: any = null;
|
|
338
|
+
let toolMouseFixedFeaturesEnabled = false;
|
|
339
|
+
let toolMouseInputUnsubscribe: (() => void) | null = null;
|
|
340
|
+
let toolMouseInputPatchTui: any = null;
|
|
341
|
+
let toolMouseInputPatchOriginalHandle: ((...args: any[]) => any) | null = null;
|
|
342
|
+
let toolMouseInputPatchWrapper: ((...args: any[]) => any) | null = null;
|
|
343
|
+
let scrollButtonVisible = false;
|
|
344
|
+
let scrollButtonWidget: any = null;
|
|
345
|
+
let pendingScrollMessages = 0;
|
|
346
|
+
let assistantMessageActive = false;
|
|
347
|
+
let scrollButtonSyncScheduled = false;
|
|
348
|
+
let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
349
|
+
|
|
350
|
+
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
351
|
+
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
352
|
+
const packets: SgrMousePacket[] = [];
|
|
353
|
+
let offset = 0;
|
|
354
|
+
|
|
355
|
+
for (const match of data.matchAll(pattern)) {
|
|
356
|
+
if (match.index !== offset) return null;
|
|
357
|
+
offset = match.index + match[0].length;
|
|
358
|
+
packets.push({
|
|
359
|
+
code: Number(match[1]),
|
|
360
|
+
col: Number(match[2]),
|
|
361
|
+
row: Number(match[3]),
|
|
362
|
+
final: match[4] as "M" | "m",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return packets.length > 0 && offset === data.length ? packets : null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function isSgrLeftPress(packet: SgrMousePacket): boolean {
|
|
370
|
+
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
371
|
+
return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function stripTerminalSequences(value: string): string {
|
|
375
|
+
return value
|
|
376
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
377
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
378
|
+
.replace(/\s+/g, " ")
|
|
379
|
+
.trim();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function isToolExecutionComponent(value: any): boolean {
|
|
383
|
+
return Boolean(
|
|
384
|
+
value &&
|
|
385
|
+
typeof value === "object" &&
|
|
386
|
+
typeof value.toolCallId === "string" &&
|
|
387
|
+
typeof value.setExpanded === "function" &&
|
|
388
|
+
typeof value.render === "function",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function renderedLineCount(component: any, width: number, cache: WeakMap<object, number>): number {
|
|
393
|
+
if (!component || typeof component !== "object") return 0;
|
|
394
|
+
const cached = cache.get(component);
|
|
395
|
+
if (cached !== undefined) return cached;
|
|
396
|
+
|
|
397
|
+
let count = 0;
|
|
398
|
+
try {
|
|
399
|
+
const lines = component.render(width);
|
|
400
|
+
count = Array.isArray(lines) ? lines.length : 0;
|
|
401
|
+
} catch {
|
|
402
|
+
count = 0;
|
|
403
|
+
}
|
|
404
|
+
cache.set(component, count);
|
|
405
|
+
return count;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function collectToolRenderHits(
|
|
409
|
+
component: any,
|
|
410
|
+
start: number,
|
|
411
|
+
width: number,
|
|
412
|
+
hits: ToolRenderHit[],
|
|
413
|
+
cache: WeakMap<object, number>,
|
|
414
|
+
seen: Set<object>,
|
|
415
|
+
): number {
|
|
416
|
+
if (!component || typeof component !== "object" || seen.has(component)) return start;
|
|
417
|
+
seen.add(component);
|
|
418
|
+
|
|
419
|
+
const count = renderedLineCount(component, width, cache);
|
|
420
|
+
if (isToolExecutionComponent(component)) {
|
|
421
|
+
if (count > 0) hits.push({ component, start, end: start + count });
|
|
422
|
+
return start + count;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let childStart = start;
|
|
426
|
+
if (Array.isArray(component.children)) {
|
|
427
|
+
for (const child of component.children) {
|
|
428
|
+
collectToolRenderHits(child, childStart, width, hits, cache, seen);
|
|
429
|
+
childStart += renderedLineCount(child, width, cache);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return start + count;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function collectToolComponents(component: any, tools: any[], seen = new Set<any>()): void {
|
|
436
|
+
if (!component || typeof component !== "object" || seen.has(component)) return;
|
|
437
|
+
seen.add(component);
|
|
438
|
+
if (isToolExecutionComponent(component)) {
|
|
439
|
+
tools.push(component);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!Array.isArray(component.children)) return;
|
|
443
|
+
for (const child of component.children) collectToolComponents(child, tools, seen);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function fixedEditorLineMatch(rendered: string, visible: string): boolean {
|
|
447
|
+
return (
|
|
448
|
+
rendered === visible ||
|
|
449
|
+
(visible.length >= 8 && (rendered.includes(visible) || visible.includes(rendered)))
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function fixedEditorContextScore(
|
|
454
|
+
renderedLines: string[],
|
|
455
|
+
renderedRow: number,
|
|
456
|
+
visibleLines: string[],
|
|
457
|
+
visibleRow: number,
|
|
458
|
+
): number {
|
|
459
|
+
let score = renderedLines[renderedRow] === visibleLines[visibleRow] ? 100 : 50;
|
|
460
|
+
for (const direction of [-1, 1]) {
|
|
461
|
+
for (let distance = 1; distance <= 4; distance++) {
|
|
462
|
+
const candidate = renderedLines[renderedRow + direction * distance];
|
|
463
|
+
const visible = visibleLines[visibleRow + direction * distance];
|
|
464
|
+
if (candidate === undefined || visible === undefined || candidate !== visible) break;
|
|
465
|
+
score += 5 - distance;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return score;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function findToolAtFixedEditorRow(
|
|
472
|
+
tui: any,
|
|
473
|
+
visibleRow: number,
|
|
474
|
+
previousLines: string[],
|
|
475
|
+
width: number,
|
|
476
|
+
): ToolRenderHit | null {
|
|
477
|
+
if (visibleRow < 0 || visibleRow >= previousLines.length) return null;
|
|
478
|
+
const visibleLines = previousLines.map((line) => stripTerminalSequences(String(line)));
|
|
479
|
+
const clickedLine = visibleLines[visibleRow] ?? "";
|
|
480
|
+
if (!clickedLine) return null;
|
|
481
|
+
|
|
482
|
+
const tools: any[] = [];
|
|
483
|
+
collectToolComponents(tui, tools);
|
|
484
|
+
let best: { hit: ToolRenderHit; score: number } | null = null;
|
|
485
|
+
for (const component of tools) {
|
|
486
|
+
const expanded = Boolean(component.expanded);
|
|
487
|
+
if (!expanded && !/(?:expand|\/ click)/i.test(clickedLine)) continue;
|
|
488
|
+
const renderedLines = renderComponentTree(component, width).map((line) =>
|
|
489
|
+
stripTerminalSequences(String(line)),
|
|
490
|
+
);
|
|
491
|
+
for (let renderedRow = 0; renderedRow < renderedLines.length; renderedRow++) {
|
|
492
|
+
if (!fixedEditorLineMatch(renderedLines[renderedRow] ?? "", clickedLine)) continue;
|
|
493
|
+
const score = fixedEditorContextScore(renderedLines, renderedRow, visibleLines, visibleRow);
|
|
494
|
+
if (!best || score > best.score) {
|
|
495
|
+
best = {
|
|
496
|
+
hit: { component, start: visibleRow, end: visibleRow + renderedLines.length },
|
|
497
|
+
score,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return best?.hit ?? null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function findToolAtScreenRow(tui: any, screenRow: number): ToolRenderHit | null {
|
|
506
|
+
const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
507
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
508
|
+
if (useFixedEditorFeatures(tui)) {
|
|
509
|
+
// Zentui replaces Pi's root render with the already-sliced visible
|
|
510
|
+
// transcript. previousViewportTop remains cursor bookkeeping and must not
|
|
511
|
+
// be added to a physical mouse row here.
|
|
512
|
+
return findToolAtFixedEditorRow(tui, screenRow - 1, previousLines, width);
|
|
513
|
+
}
|
|
514
|
+
const viewportTop = Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0;
|
|
515
|
+
const bufferRow = viewportTop + screenRow - 1;
|
|
516
|
+
if (bufferRow < 0 || bufferRow >= previousLines.length) return null;
|
|
517
|
+
|
|
518
|
+
const hits: ToolRenderHit[] = [];
|
|
519
|
+
const cache = new WeakMap<object, number>();
|
|
520
|
+
collectToolRenderHits(tui, 0, width, hits, cache, new Set<object>());
|
|
521
|
+
|
|
522
|
+
const clickedLine = stripTerminalSequences(String(previousLines[bufferRow] ?? ""));
|
|
523
|
+
for (const hit of hits) {
|
|
524
|
+
if (bufferRow < hit.start || bufferRow >= hit.end) continue;
|
|
525
|
+
const component = hit.component;
|
|
526
|
+
const expanded = Boolean(component.expanded);
|
|
527
|
+
// Match Pi's old click contract: collapsed results expose a click hint;
|
|
528
|
+
// expanded results can be collapsed by clicking any rendered line.
|
|
529
|
+
if (!expanded && !/(?:expand|\/ click)/i.test(clickedLine)) continue;
|
|
530
|
+
return hit;
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function isFixedEditorTui(tui: any): boolean {
|
|
536
|
+
const terminal = tui?.terminal;
|
|
537
|
+
if (!terminal) return false;
|
|
538
|
+
const ownRows = Object.getOwnPropertyDescriptor(terminal, "rows");
|
|
539
|
+
const prototype = Object.getPrototypeOf(terminal);
|
|
540
|
+
const inheritedRows = prototype ? Object.getOwnPropertyDescriptor(prototype, "rows") : undefined;
|
|
541
|
+
return typeof ownRows?.get === "function" && ownRows.get !== inheritedRows?.get;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function useFixedEditorFeatures(tui: any): boolean {
|
|
545
|
+
return toolMouseFixedFeaturesEnabled && isFixedEditorTui(tui);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function formatShortcut(shortcut: string): string {
|
|
549
|
+
return shortcut
|
|
550
|
+
.split("+")
|
|
551
|
+
.map((part) =>
|
|
552
|
+
part.length <= 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`,
|
|
553
|
+
)
|
|
554
|
+
.join("+");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isScrollBottomInput(data: string): boolean {
|
|
558
|
+
return matchesKey(data, SCROLL_BOTTOM_SHORTCUT);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isScrollNavigationInput(data: string): boolean {
|
|
562
|
+
if (
|
|
563
|
+
matchesKey(data, "pageUp") ||
|
|
564
|
+
matchesKey(data, "pageDown") ||
|
|
565
|
+
ZENTUI_PAGE_UP_INPUT.test(data) ||
|
|
566
|
+
ZENTUI_PAGE_DOWN_INPUT.test(data)
|
|
567
|
+
) {
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
const packets = parseSgrMousePackets(data);
|
|
571
|
+
return Boolean(
|
|
572
|
+
packets?.some((packet) => {
|
|
573
|
+
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
574
|
+
return packet.final === "M" && (baseButton === 64 || baseButton === 65);
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function directRenderLines(component: any, width: number): string[] {
|
|
580
|
+
try {
|
|
581
|
+
const lines = component?.render?.(width);
|
|
582
|
+
return Array.isArray(lines) ? lines : [];
|
|
583
|
+
} catch {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Render only the transcript portion that fixed-editor leaves scrollable. */
|
|
589
|
+
function renderFixedScrollableRoot(tui: any, width: number): string[] {
|
|
590
|
+
const children = Array.isArray(tui?.children) ? tui.children : [];
|
|
591
|
+
const editorIndex = children.findIndex((child: any) =>
|
|
592
|
+
containsEditorLike(child, tui.focusedComponent),
|
|
593
|
+
);
|
|
594
|
+
const end = editorIndex >= 2 ? editorIndex - 2 : children.length;
|
|
595
|
+
return children.slice(0, end).flatMap((child: any) => directRenderLines(child, width));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function isFixedEditorAtBottom(tui: any): boolean {
|
|
599
|
+
if (!useFixedEditorFeatures(tui)) return true;
|
|
600
|
+
const visibleLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
601
|
+
if (visibleLines.length === 0) return true;
|
|
602
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
603
|
+
const rootLines = renderFixedScrollableRoot(tui, width);
|
|
604
|
+
if (rootLines.length <= visibleLines.length) return true;
|
|
605
|
+
const tail = rootLines.slice(-visibleLines.length);
|
|
606
|
+
return tail.every(
|
|
607
|
+
(line, index) =>
|
|
608
|
+
stripTerminalSequences(String(line)) ===
|
|
609
|
+
stripTerminalSequences(String(visibleLines[index] ?? "")),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function hideScrollButton(tui: any): void {
|
|
614
|
+
const changed = scrollButtonVisible || pendingScrollMessages > 0;
|
|
615
|
+
scrollButtonVisible = false;
|
|
616
|
+
pendingScrollMessages = 0;
|
|
617
|
+
if (changed) tui.requestRender?.();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
621
|
+
if (!useFixedEditorFeatures(tui) || !isScrollNavigationInput(data) || scrollButtonSyncScheduled)
|
|
622
|
+
return;
|
|
623
|
+
scrollButtonSyncScheduled = true;
|
|
624
|
+
const previousLines = tui.previousLines;
|
|
625
|
+
const check = (attempt: number) => {
|
|
626
|
+
scrollButtonSyncScheduled = false;
|
|
627
|
+
if (toolMouseTui !== tui) return;
|
|
628
|
+
// Pi renders on its own frame timer. Inspect the resulting viewport before
|
|
629
|
+
// showing the button so empty or non-scrollable transcripts never flash it.
|
|
630
|
+
const rendered = tui.previousLines !== previousLines;
|
|
631
|
+
if (!rendered && attempt < 4) {
|
|
632
|
+
scrollButtonSyncScheduled = true;
|
|
633
|
+
const timer = setTimeout(() => check(attempt + 1), 16);
|
|
634
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
635
|
+
(timer as { unref: () => void }).unref();
|
|
636
|
+
}
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const nextVisible = !isFixedEditorAtBottom(tui);
|
|
640
|
+
if (!nextVisible) pendingScrollMessages = 0;
|
|
641
|
+
if (nextVisible !== scrollButtonVisible) {
|
|
642
|
+
scrollButtonVisible = nextVisible;
|
|
643
|
+
tui.requestRender?.();
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
process.nextTick(() => check(0));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function updateScrollButtonFromInput(tui: any, data: string): void {
|
|
650
|
+
if (!useFixedEditorFeatures(tui)) return;
|
|
651
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || isScrollBottomInput(data)) {
|
|
652
|
+
hideScrollButton(tui);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function renderComponentTree(component: any, width: number): string[] {
|
|
657
|
+
if (!component || typeof component !== "object") return [];
|
|
658
|
+
try {
|
|
659
|
+
const lines = component.render?.(width);
|
|
660
|
+
if (Array.isArray(lines) && lines.length > 0) return lines;
|
|
661
|
+
} catch {
|
|
662
|
+
// Fall through to children for hidden container renderers.
|
|
663
|
+
}
|
|
664
|
+
if (!Array.isArray(component.children)) return [];
|
|
665
|
+
return component.children.flatMap((child: any) => renderComponentTree(child, width));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function renderTreeWithTarget(
|
|
669
|
+
component: any,
|
|
670
|
+
target: any,
|
|
671
|
+
width: number,
|
|
672
|
+
seen = new Set<any>(),
|
|
673
|
+
): { lines: string[]; targetStart: number | null } {
|
|
674
|
+
if (!component || typeof component !== "object" || seen.has(component)) {
|
|
675
|
+
return { lines: [], targetStart: null };
|
|
676
|
+
}
|
|
677
|
+
seen.add(component);
|
|
678
|
+
if (component === target) {
|
|
679
|
+
return { lines: renderComponentTree(component, width), targetStart: 0 };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (Array.isArray(component.children)) {
|
|
683
|
+
const lines: string[] = [];
|
|
684
|
+
let targetStart: number | null = null;
|
|
685
|
+
for (const child of component.children) {
|
|
686
|
+
const result = renderTreeWithTarget(child, target, width, seen);
|
|
687
|
+
if (result.targetStart !== null) targetStart = lines.length + result.targetStart;
|
|
688
|
+
lines.push(...result.lines);
|
|
689
|
+
}
|
|
690
|
+
if (targetStart !== null) return { lines, targetStart };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return { lines: renderComponentTree(component, width), targetStart: null };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function normalizedClusterLines(component: any, width: number): string[] {
|
|
697
|
+
if (!component) return [];
|
|
698
|
+
const lines = renderComponentTree(component, width);
|
|
699
|
+
let end = lines.length;
|
|
700
|
+
while (end > 0 && visibleWidth(lines[end - 1] ?? "") === 0) end--;
|
|
701
|
+
return lines.slice(0, Math.max(end, 1));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function rawTerminalRows(tui: any): number {
|
|
705
|
+
const terminal = tui?.terminal;
|
|
706
|
+
if (!terminal) return 0;
|
|
707
|
+
const prototype = Object.getPrototypeOf(terminal);
|
|
708
|
+
const rows = prototype ? Object.getOwnPropertyDescriptor(prototype, "rows") : undefined;
|
|
709
|
+
if (typeof rows?.get === "function") {
|
|
710
|
+
try {
|
|
711
|
+
const value = rows.get.call(terminal);
|
|
712
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
713
|
+
} catch {
|
|
714
|
+
// Fall through to the current terminal value.
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return typeof terminal.rows === "number" && Number.isFinite(terminal.rows) ? terminal.rows : 0;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function containsEditorLike(component: any, focused: any, seen = new Set<any>()): boolean {
|
|
721
|
+
if (!component || typeof component !== "object" || seen.has(component)) return false;
|
|
722
|
+
seen.add(component);
|
|
723
|
+
if (component === focused) return true;
|
|
724
|
+
if (
|
|
725
|
+
typeof component.getText === "function" &&
|
|
726
|
+
typeof component.setText === "function" &&
|
|
727
|
+
typeof component.handleInput === "function"
|
|
728
|
+
)
|
|
729
|
+
return true;
|
|
730
|
+
return (
|
|
731
|
+
Array.isArray(component.children) &&
|
|
732
|
+
component.children.some((child: any) => containsEditorLike(child, focused, seen))
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function scrollButtonScreenRow(tui: any, width: number): number | null {
|
|
737
|
+
if (!scrollButtonVisible || !useFixedEditorFeatures(tui) || !scrollButtonWidget) return null;
|
|
738
|
+
const children = Array.isArray(tui?.children) ? tui.children : [];
|
|
739
|
+
const editorIndex = children.findIndex((child: any) =>
|
|
740
|
+
containsEditorLike(child, tui.focusedComponent),
|
|
741
|
+
);
|
|
742
|
+
if (editorIndex < 2 || editorIndex + 2 >= children.length) return null;
|
|
743
|
+
|
|
744
|
+
const above = children[editorIndex - 1];
|
|
745
|
+
const widthValue = Math.max(1, width || Number(tui?.terminal?.columns) || 80);
|
|
746
|
+
const target = renderTreeWithTarget(above, scrollButtonWidget, widthValue);
|
|
747
|
+
if (target.targetStart === null) return null;
|
|
748
|
+
|
|
749
|
+
const rawRows = rawTerminalRows(tui);
|
|
750
|
+
if (rawRows <= 0) return null;
|
|
751
|
+
const maxRows = Math.max(1, rawRows - 1);
|
|
752
|
+
const status = normalizedClusterLines(children[editorIndex - 2], widthValue);
|
|
753
|
+
const editor = normalizedClusterLines(children[editorIndex], widthValue);
|
|
754
|
+
const below = normalizedClusterLines(children[editorIndex + 1], widthValue);
|
|
755
|
+
const footer = normalizedClusterLines(children[editorIndex + 2], widthValue);
|
|
756
|
+
const aboveLines =
|
|
757
|
+
target.lines.length > 0 ? target.lines : normalizedClusterLines(above, widthValue);
|
|
758
|
+
|
|
759
|
+
const takeLast = (lines: string[], count: number): string[] =>
|
|
760
|
+
count > 0 ? lines.slice(-count) : [];
|
|
761
|
+
const editorVisible = takeLast(editor, Math.min(editor.length, maxRows));
|
|
762
|
+
let remaining = Math.max(0, maxRows - editorVisible.length);
|
|
763
|
+
const footerVisible = takeLast(footer, remaining);
|
|
764
|
+
remaining -= footerVisible.length;
|
|
765
|
+
const belowVisible = takeLast(below, remaining);
|
|
766
|
+
remaining -= belowVisible.length;
|
|
767
|
+
const aboveVisible = takeLast(aboveLines, remaining);
|
|
768
|
+
const statusVisible = takeLast(status, Math.max(0, remaining - aboveVisible.length));
|
|
769
|
+
const aboveStart = aboveLines.length - aboveVisible.length;
|
|
770
|
+
const targetRow = target.targetStart - aboveStart;
|
|
771
|
+
if (targetRow < 0 || targetRow >= aboveVisible.length) return null;
|
|
772
|
+
|
|
773
|
+
const allLines = [
|
|
774
|
+
...statusVisible,
|
|
775
|
+
...aboveVisible,
|
|
776
|
+
...editorVisible,
|
|
777
|
+
...belowVisible,
|
|
778
|
+
...footerVisible,
|
|
779
|
+
];
|
|
780
|
+
let leadingBlank = 0;
|
|
781
|
+
while (leadingBlank < allLines.length - 1 && visibleWidth(allLines[leadingBlank] ?? "") === 0) {
|
|
782
|
+
leadingBlank++;
|
|
783
|
+
}
|
|
784
|
+
const clusterRow = statusVisible.length + targetRow - leadingBlank;
|
|
785
|
+
if (clusterRow < 0 || clusterRow >= allLines.length - leadingBlank) return null;
|
|
786
|
+
return rawRows - allLines.length + clusterRow + 1;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function isScrollButtonAtScreenRow(tui: any, packet: SgrMousePacket): boolean {
|
|
790
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
791
|
+
if (scrollButtonScreenRow(tui, width) !== packet.row || !scrollButtonWidget) return false;
|
|
792
|
+
const rendered = scrollButtonWidget.render?.(width)?.[0];
|
|
793
|
+
if (typeof rendered !== "string") return false;
|
|
794
|
+
const plain = rendered
|
|
795
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
796
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
797
|
+
const leading = plain.length - plain.trimStart().length;
|
|
798
|
+
const end = visibleWidth(plain.trimEnd());
|
|
799
|
+
return packet.col >= leading + 1 && packet.col <= end;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function jumpToBottomWithoutSubmit(tui: any): boolean {
|
|
803
|
+
const originalHandle = toolMouseInputPatchTui === tui ? toolMouseInputPatchOriginalHandle : null;
|
|
804
|
+
if (!originalHandle) return false;
|
|
805
|
+
|
|
806
|
+
// Route Enter through Pi's normal listener chain so pi-zentui can update its
|
|
807
|
+
// private scroll offset, but suppress the focused editor for this synthetic
|
|
808
|
+
// dispatch so clicking the button never submits the current input.
|
|
809
|
+
const focused = tui.focusedComponent;
|
|
810
|
+
try {
|
|
811
|
+
tui.focusedComponent = null;
|
|
812
|
+
Reflect.apply(originalHandle, tui, ["\r"]);
|
|
813
|
+
} finally {
|
|
814
|
+
tui.focusedComponent = focused;
|
|
815
|
+
}
|
|
816
|
+
hideScrollButton(tui);
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function handleScrollButtonClick(tui: any, packet: SgrMousePacket): boolean {
|
|
821
|
+
if (!isScrollButtonAtScreenRow(tui, packet)) return false;
|
|
822
|
+
return jumpToBottomWithoutSubmit(tui);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
826
|
+
const hit = findToolAtScreenRow(tui, packet.row);
|
|
827
|
+
if (!hit) return false;
|
|
828
|
+
|
|
829
|
+
const nextExpanded = !Boolean(hit.component.expanded);
|
|
830
|
+
hit.component.setExpanded(nextExpanded);
|
|
831
|
+
hit.component.invalidate?.();
|
|
832
|
+
tui.requestRender?.();
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function renderScrollButton(width: number, theme: any): string[] {
|
|
837
|
+
if (!scrollButtonVisible || !useFixedEditorFeatures(toolMouseTui)) return [];
|
|
838
|
+
const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
|
|
839
|
+
const messageText =
|
|
840
|
+
pendingScrollMessages > 0
|
|
841
|
+
? `${pendingScrollMessages} new message${pendingScrollMessages === 1 ? "" : "s"}`
|
|
842
|
+
: "Back to bottom";
|
|
843
|
+
const label = theme.fg("accent", `[ ↓ ${messageText} · ${shortcut} · click ]`);
|
|
844
|
+
const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
|
|
845
|
+
return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* pi-zentui consumes left-button presses for text selection. Intercept only a
|
|
850
|
+
* tool-row click at the TUI input boundary, before extension listeners run.
|
|
851
|
+
* Keyboard, wheel, drag, release, and non-tool clicks continue through Pi's
|
|
852
|
+
* original dispatcher, preserving pi-zentui's scroll-to-bottom behavior.
|
|
853
|
+
*/
|
|
854
|
+
function patchToolMouseInputCapture(tui: any): void {
|
|
855
|
+
if (toolMouseInputPatchTui === tui) return;
|
|
856
|
+
|
|
857
|
+
restoreToolMouseInputCapture();
|
|
858
|
+
const originalHandle = tui?.handleInput;
|
|
859
|
+
if (typeof originalHandle !== "function") return;
|
|
860
|
+
|
|
861
|
+
const wrapper = function (this: any, ...args: any[]): any {
|
|
862
|
+
const data = args[0];
|
|
863
|
+
if (typeof data === "string") {
|
|
864
|
+
updateScrollButtonFromInput(this, data);
|
|
865
|
+
// Capture the current viewport before Pi/Zentui applies the scroll input.
|
|
866
|
+
scheduleScrollButtonSync(this, data);
|
|
867
|
+
if (
|
|
868
|
+
useFixedEditorFeatures(this) &&
|
|
869
|
+
isScrollBottomInput(data) &&
|
|
870
|
+
jumpToBottomWithoutSubmit(this)
|
|
871
|
+
)
|
|
872
|
+
return;
|
|
873
|
+
const packets = parseSgrMousePackets(data);
|
|
874
|
+
if (packets) {
|
|
875
|
+
for (const packet of packets) {
|
|
876
|
+
if (!isSgrLeftPress(packet)) continue;
|
|
877
|
+
if (handleScrollButtonClick(this, packet) || toggleToolAtMouseClick(this, packet)) return;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const result = Reflect.apply(originalHandle, this, args);
|
|
882
|
+
if (typeof data === "string") scheduleScrollButtonSync(this, data);
|
|
883
|
+
return result;
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
tui.handleInput = wrapper;
|
|
888
|
+
} catch {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
toolMouseInputPatchTui = tui;
|
|
892
|
+
toolMouseInputPatchOriginalHandle = originalHandle;
|
|
893
|
+
toolMouseInputPatchWrapper = wrapper;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function restoreToolMouseInputCapture(): void {
|
|
897
|
+
if (
|
|
898
|
+
toolMouseInputPatchTui &&
|
|
899
|
+
toolMouseInputPatchOriginalHandle &&
|
|
900
|
+
toolMouseInputPatchTui.handleInput === toolMouseInputPatchWrapper
|
|
901
|
+
) {
|
|
902
|
+
toolMouseInputPatchTui.handleInput = toolMouseInputPatchOriginalHandle;
|
|
903
|
+
}
|
|
904
|
+
toolMouseInputPatchTui = null;
|
|
905
|
+
toolMouseInputPatchOriginalHandle = null;
|
|
906
|
+
toolMouseInputPatchWrapper = null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
910
|
+
if (!toolMouseTui) return undefined;
|
|
911
|
+
updateScrollButtonFromInput(toolMouseTui, data);
|
|
912
|
+
if (isScrollBottomInput(data)) {
|
|
913
|
+
if (useFixedEditorFeatures(toolMouseTui) && jumpToBottomWithoutSubmit(toolMouseTui)) {
|
|
914
|
+
return { consume: true };
|
|
915
|
+
}
|
|
916
|
+
if (!toolMouseFixedFeaturesEnabled) {
|
|
917
|
+
// Native Pi scrolls through terminal history rather than an internal
|
|
918
|
+
// viewport. A harmless terminal write makes Ctrl+End snap that history
|
|
919
|
+
// to the active cursor without enabling mouse reporting.
|
|
920
|
+
toolMouseTui.terminal?.write?.("\x1b[0m");
|
|
921
|
+
toolMouseTui.requestRender?.();
|
|
922
|
+
return { consume: true };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const packets = parseSgrMousePackets(data);
|
|
926
|
+
if (!packets) {
|
|
927
|
+
scheduleScrollButtonSync(toolMouseTui, data);
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
let consumed = false;
|
|
932
|
+
for (const packet of packets) {
|
|
933
|
+
if (!isSgrLeftPress(packet)) continue;
|
|
934
|
+
if (
|
|
935
|
+
handleScrollButtonClick(toolMouseTui, packet) ||
|
|
936
|
+
toggleToolAtMouseClick(toolMouseTui, packet)
|
|
937
|
+
) {
|
|
938
|
+
consumed = true;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Let scrolling, motion, release, and clicks outside tool results reach the
|
|
943
|
+
// normal TUI input chain (including other extensions such as pi-zentui).
|
|
944
|
+
scheduleScrollButtonSync(toolMouseTui, data);
|
|
945
|
+
return consumed ? { consume: true } : undefined;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function teardownToolMouseInteraction(): void {
|
|
949
|
+
if (sessionRenderTimer) {
|
|
950
|
+
clearTimeout(sessionRenderTimer);
|
|
951
|
+
sessionRenderTimer = null;
|
|
952
|
+
}
|
|
953
|
+
toolMouseInputUnsubscribe?.();
|
|
954
|
+
toolMouseInputUnsubscribe = null;
|
|
955
|
+
try {
|
|
956
|
+
toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
957
|
+
} catch {
|
|
958
|
+
// The terminal may already be closed during shutdown.
|
|
959
|
+
}
|
|
960
|
+
try {
|
|
961
|
+
toolMouseUi?.setWidget?.(TOOL_MOUSE_WIDGET_KEY, undefined);
|
|
962
|
+
} catch {
|
|
963
|
+
// The UI context may already have been reset during /reload.
|
|
964
|
+
}
|
|
965
|
+
restoreToolMouseInputCapture();
|
|
966
|
+
scrollButtonVisible = false;
|
|
967
|
+
scrollButtonWidget = null;
|
|
968
|
+
pendingScrollMessages = 0;
|
|
969
|
+
assistantMessageActive = false;
|
|
970
|
+
scrollButtonSyncScheduled = false;
|
|
971
|
+
toolMouseTui = null;
|
|
972
|
+
toolMouseUi = null;
|
|
973
|
+
toolMouseFixedFeaturesEnabled = false;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
export function installToolMouseInteraction(
|
|
977
|
+
ctx: any,
|
|
978
|
+
fixedEditorFeatures = config.fixedEditorFeatures,
|
|
979
|
+
): void {
|
|
980
|
+
teardownToolMouseInteraction();
|
|
981
|
+
if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
|
|
982
|
+
if (typeof ctx.ui?.onTerminalInput !== "function" || typeof ctx.ui?.setWidget !== "function")
|
|
983
|
+
return;
|
|
984
|
+
|
|
985
|
+
toolMouseUi = ctx.ui;
|
|
986
|
+
toolMouseFixedFeaturesEnabled = fixedEditorFeatures;
|
|
987
|
+
ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
|
|
988
|
+
toolMouseTui = tui;
|
|
989
|
+
if (fixedEditorFeatures) {
|
|
990
|
+
patchToolMouseInputCapture(tui);
|
|
991
|
+
tui?.terminal?.write?.(TOOL_MOUSE_ENABLE);
|
|
992
|
+
}
|
|
993
|
+
const widget = {
|
|
994
|
+
render: (width: number) => renderScrollButton(width, theme),
|
|
995
|
+
invalidate() {},
|
|
996
|
+
};
|
|
997
|
+
scrollButtonWidget = widget;
|
|
998
|
+
return widget;
|
|
999
|
+
});
|
|
1000
|
+
toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function scheduleSessionRender(refresh?: () => void): void {
|
|
1004
|
+
const tui = toolMouseTui;
|
|
1005
|
+
if (!tui || typeof tui.requestRender !== "function") return;
|
|
1006
|
+
if (sessionRenderTimer) clearTimeout(sessionRenderTimer);
|
|
1007
|
+
// Restored transcripts are populated at different points for startup, reload,
|
|
1008
|
+
// and session replacement. Repaint after session_start and the surrounding UI
|
|
1009
|
+
// rebuild finish so messages are not left hidden until the next terminal input.
|
|
1010
|
+
sessionRenderTimer = setTimeout(() => {
|
|
1011
|
+
sessionRenderTimer = null;
|
|
1012
|
+
if (toolMouseTui !== tui) return;
|
|
1013
|
+
refresh?.();
|
|
1014
|
+
tui.requestRender(true);
|
|
1015
|
+
}, 0);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Bright green for success icon (truecolor ANSI escape)
|
|
1019
|
+
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
1020
|
+
const ANSI_RESET = "\x1b[0m";
|
|
1021
|
+
|
|
1022
|
+
function refreshCurrentTranscript(compactStyle: CompactStyleHooks, ctx?: any): void {
|
|
1023
|
+
compactStyle.refresh();
|
|
1024
|
+
toolMouseTui?.requestRender?.(true);
|
|
1025
|
+
ctx?.ui?.requestRender?.(true);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function applyStyleMode(mode: CompactStyleMode, ctx: any, compactStyle: CompactStyleHooks): void {
|
|
1029
|
+
config.mode = mode;
|
|
1030
|
+
saveConfig();
|
|
1031
|
+
refreshCurrentTranscript(compactStyle, ctx);
|
|
1032
|
+
ctx.ui.notify(`Claude Code style: ${mode}`, "info");
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function modeSettingDescription(mode: CompactStyleMode): string {
|
|
1036
|
+
if (mode === "compact") {
|
|
1037
|
+
return "Compact transcript summaries are enabled. Fixed editor interaction is configured separately below.";
|
|
1038
|
+
}
|
|
1039
|
+
if (mode === "off") {
|
|
1040
|
+
return "Pi native tool rendering is enabled. Fixed editor interaction is configured separately below.";
|
|
1041
|
+
}
|
|
1042
|
+
return "Claude Code style and rich edit/write diffs are enabled. Fixed editor interaction is configured separately below.";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function fixedEditorSettingDescription(enabled: boolean): string {
|
|
1046
|
+
return enabled
|
|
1047
|
+
? "Enables mouse capture, tool click expansion, fixed viewport mapping, the back-to-bottom button, message count, and Ctrl+End."
|
|
1048
|
+
: "Uses terminal-native wheel scrolling; disables mouse capture, tool clicks, fixed viewport mapping, button, and message count. Ctrl+End remains enabled.";
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Promise<void> {
|
|
1052
|
+
if (ctx?.mode !== "tui" || !ctx?.hasUI || typeof ctx.ui?.custom !== "function") {
|
|
1053
|
+
ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
await ctx.ui.custom((tui: any, theme: any, _keybindings: any, done: () => void) => {
|
|
1058
|
+
const container = new Container();
|
|
1059
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
1060
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Claude Code Style")), 1, 0));
|
|
1061
|
+
const modeSetting = {
|
|
1062
|
+
id: "mode",
|
|
1063
|
+
label: "Mode",
|
|
1064
|
+
description: modeSettingDescription(config.mode),
|
|
1065
|
+
currentValue: config.mode,
|
|
1066
|
+
values: ["on", "off", "compact"],
|
|
1067
|
+
};
|
|
1068
|
+
const fixedEditorSetting = {
|
|
1069
|
+
id: "fixedEditorFeatures",
|
|
1070
|
+
label: "Fixed editor feature",
|
|
1071
|
+
description: fixedEditorSettingDescription(config.fixedEditorFeatures),
|
|
1072
|
+
currentValue: config.fixedEditorFeatures ? "on" : "off",
|
|
1073
|
+
values: ["on", "off"],
|
|
1074
|
+
};
|
|
1075
|
+
const settingsList = new SettingsList(
|
|
1076
|
+
[modeSetting, fixedEditorSetting],
|
|
1077
|
+
4,
|
|
1078
|
+
getSettingsListTheme(),
|
|
1079
|
+
(id: string, value: string) => {
|
|
1080
|
+
if (id === "mode") {
|
|
1081
|
+
modeSetting.description = modeSettingDescription(value as CompactStyleMode);
|
|
1082
|
+
applyStyleMode(value as CompactStyleMode, ctx, compactStyle);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (id === "fixedEditorFeatures") {
|
|
1086
|
+
config.fixedEditorFeatures = value === "on";
|
|
1087
|
+
fixedEditorSetting.description = fixedEditorSettingDescription(
|
|
1088
|
+
config.fixedEditorFeatures,
|
|
1089
|
+
);
|
|
1090
|
+
saveConfig();
|
|
1091
|
+
installToolMouseInteraction(ctx);
|
|
1092
|
+
refreshCurrentTranscript(compactStyle, ctx);
|
|
1093
|
+
ctx.ui.notify(`Fixed editor feature: ${value}`, "info");
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
() => done(),
|
|
1097
|
+
{ enableSearch: false },
|
|
1098
|
+
);
|
|
1099
|
+
container.addChild(settingsList);
|
|
1100
|
+
container.addChild(
|
|
1101
|
+
new Text(theme.fg("dim", "↑↓ navigate · enter/space change · esc close"), 1, 0),
|
|
1102
|
+
);
|
|
1103
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
1104
|
+
return {
|
|
1105
|
+
render: (width: number) => container.render(width),
|
|
1106
|
+
invalidate: () => container.invalidate(),
|
|
1107
|
+
handleInput(data: string) {
|
|
1108
|
+
settingsList.handleInput?.(data);
|
|
1109
|
+
tui.requestRender();
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
|
|
1116
|
+
try {
|
|
1117
|
+
if (typeof tool?.[slot] === "function") return tool[slot](...args);
|
|
1118
|
+
} catch {
|
|
1119
|
+
// Fall through to raw fallback.
|
|
1120
|
+
}
|
|
1121
|
+
return new Text(fallback, 0, 0);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Generate a descriptive label for an unknown tool from its args.
|
|
1126
|
+
* Uses the tool label + first stringable arg value.
|
|
1127
|
+
*/
|
|
1128
|
+
function toolCallLabel(toolName: string, toolLabel: string, args: any): string {
|
|
1129
|
+
if (!args) return toolLabel;
|
|
1130
|
+
const keys = Object.keys(args);
|
|
1131
|
+
if (keys.length === 0) return toolLabel;
|
|
1132
|
+
// Prefer "query" or "question" args, then fall back to the first stringable value
|
|
1133
|
+
const preferred = ["query", "question", "command", "pattern", "name", "path", "url", "message"];
|
|
1134
|
+
for (const key of preferred) {
|
|
1135
|
+
const val = args[key];
|
|
1136
|
+
if (val !== undefined && val !== null && typeof val !== "object") {
|
|
1137
|
+
return `${toolLabel}(${oneLine(val, 60)})`;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const firstKey = keys[0];
|
|
1141
|
+
const firstVal = args[firstKey];
|
|
1142
|
+
if (firstVal !== undefined && firstVal !== null && typeof firstVal !== "object") {
|
|
1143
|
+
return `${toolLabel}(${oneLine(firstVal, 60)})`;
|
|
1144
|
+
}
|
|
1145
|
+
return toolLabel;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
export function shouldRenderRichDiff(
|
|
1149
|
+
mode: CompactStyleMode,
|
|
1150
|
+
toolName: string,
|
|
1151
|
+
isError: boolean,
|
|
1152
|
+
): boolean {
|
|
1153
|
+
return mode === "on" && !isError && (toolName === "edit" || toolName === "write");
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/** Wrap an arbitrary tool definition with ccstyle call/result rendering. */
|
|
1157
|
+
function createCcstyleTool(
|
|
1158
|
+
originalTool: any,
|
|
1159
|
+
writeExecutionMetadata: WriteExecutionMetadataStore,
|
|
1160
|
+
): any {
|
|
1161
|
+
const toolName = originalTool.name;
|
|
1162
|
+
const label = isMcpToolDefinition(originalTool, toolName)
|
|
1163
|
+
? toolName
|
|
1164
|
+
: originalTool.label || toolName;
|
|
1165
|
+
|
|
1166
|
+
return {
|
|
1167
|
+
...originalTool,
|
|
1168
|
+
renderShell: "self",
|
|
1169
|
+
renderCall(args: any, theme: any, context: any) {
|
|
1170
|
+
if (config.mode !== "on") {
|
|
1171
|
+
return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const visualState = resolveToolVisualState(context);
|
|
1175
|
+
const isPending =
|
|
1176
|
+
visualState === "pending" ||
|
|
1177
|
+
(!visualState && (context?.isPartial || context?.executionStarted));
|
|
1178
|
+
if (isPending) scheduleAnimation(context);
|
|
1179
|
+
const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
|
|
1180
|
+
const icon =
|
|
1181
|
+
visualState === "success"
|
|
1182
|
+
? `${BRIGHT_GREEN}${rawIcon}${ANSI_RESET}`
|
|
1183
|
+
: theme.fg(toolIconColor(context), rawIcon);
|
|
1184
|
+
|
|
1185
|
+
const title = `${icon} ${theme.fg("toolTitle", toolCallLabel(toolName, label, args))}`;
|
|
1186
|
+
return new Text(title, 0, 0);
|
|
1187
|
+
},
|
|
1188
|
+
renderResult(result: any, options: any, theme: any, context: any) {
|
|
1189
|
+
if (config.mode !== "on") {
|
|
1190
|
+
return renderDefault(
|
|
1191
|
+
originalTool,
|
|
1192
|
+
"renderResult",
|
|
1193
|
+
[result, options, theme, context],
|
|
1194
|
+
textFromResult(result),
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (options?.isPartial) {
|
|
1199
|
+
return new Text(theme.fg("muted", " ⎿ Pending…"), 0, 0);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const isError = options?.isError || context?.isError;
|
|
1203
|
+
setToolVisualState(context, isError ? "error" : "success");
|
|
1204
|
+
const expanded = isToolExpanded(options, context);
|
|
1205
|
+
if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
|
|
1206
|
+
const richResult = renderRichToolResult(
|
|
1207
|
+
toolName,
|
|
1208
|
+
result,
|
|
1209
|
+
{ ...options, expanded },
|
|
1210
|
+
theme,
|
|
1211
|
+
context,
|
|
1212
|
+
writeExecutionMetadata,
|
|
1213
|
+
);
|
|
1214
|
+
if (richResult) return richResult;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const text = textFromResult(result);
|
|
1218
|
+
const rendered = !expanded ? (text ? oneLine(text, 96) : "Done") : text;
|
|
1219
|
+
|
|
1220
|
+
const hint = !expanded && hasExpandableResult(text) ? expandHint(theme) : "";
|
|
1221
|
+
if (expanded)
|
|
1222
|
+
return renderExpandedToolResult(rendered, theme, isError, context?.lastComponent);
|
|
1223
|
+
return new Text(
|
|
1224
|
+
theme.fg(isError ? "error" : "muted", renderCollapsedToolResult(rendered, hint)),
|
|
1225
|
+
0,
|
|
1226
|
+
0,
|
|
1227
|
+
);
|
|
1228
|
+
},
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Apart from the write override used to capture pre-write content, renderers are
|
|
1234
|
+
* applied through ToolExecutionComponent. Patch its lookup once so tools use the
|
|
1235
|
+
* same compact fallback shell by default. Tools named in excludeRenderers keep
|
|
1236
|
+
* their original renderer; subagent rendering remains protected.
|
|
1237
|
+
*/
|
|
1238
|
+
const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
|
|
1239
|
+
const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
|
|
1240
|
+
const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
|
|
1241
|
+
const SUBAGENT_TOOL_NAMES = new Set(["Agent"]);
|
|
1242
|
+
|
|
1243
|
+
type ToolRenderMethods = {
|
|
1244
|
+
hasRendererDefinition: (...args: any[]) => boolean;
|
|
1245
|
+
getRenderShell: (...args: any[]) => "default" | "self";
|
|
1246
|
+
getCallRenderer: (...args: any[]) => any;
|
|
1247
|
+
getResultRenderer: (...args: any[]) => any;
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
type GlobalToolRenderPatch = {
|
|
1251
|
+
version: 2;
|
|
1252
|
+
prototype: any;
|
|
1253
|
+
owner: object;
|
|
1254
|
+
active: boolean;
|
|
1255
|
+
enabled: () => boolean;
|
|
1256
|
+
mode: () => CompactStyleMode;
|
|
1257
|
+
wrap: (tool: any) => any;
|
|
1258
|
+
byDefinition: WeakMap<object, any>;
|
|
1259
|
+
byName: Map<string, any>;
|
|
1260
|
+
downstream: ToolRenderMethods;
|
|
1261
|
+
installed: ToolRenderMethods;
|
|
1262
|
+
// Keep the legacy aliases so an older extension build can read this Symbol.
|
|
1263
|
+
originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
|
|
1264
|
+
originalGetRenderShell: ToolRenderMethods["getRenderShell"];
|
|
1265
|
+
originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
|
|
1266
|
+
originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
function isMcpToolDefinition(definition: any, toolName: string): boolean {
|
|
1270
|
+
const label = typeof definition?.label === "string" ? definition.label : "";
|
|
1271
|
+
return toolName === "mcp" || label === "MCP" || label.startsWith("MCP: ");
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/** Return true when this tool must keep its original renderer. */
|
|
1275
|
+
export function preservesOriginalRenderer(
|
|
1276
|
+
extensionDefinition: any,
|
|
1277
|
+
toolName: string,
|
|
1278
|
+
builtInToolDefinition?: any,
|
|
1279
|
+
excludeRenderers: readonly string[] = config.excludeRenderers,
|
|
1280
|
+
): boolean {
|
|
1281
|
+
if (SUBAGENT_TOOL_NAMES.has(toolName)) return true;
|
|
1282
|
+
if (!excludeRenderers.includes(toolName)) return false;
|
|
1283
|
+
return [extensionDefinition, builtInToolDefinition].some(
|
|
1284
|
+
(definition) =>
|
|
1285
|
+
definition?.renderShell === "self" ||
|
|
1286
|
+
typeof definition?.renderCall === "function" ||
|
|
1287
|
+
typeof definition?.renderResult === "function",
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function syncToolShell(component: any, shell: "default" | "self"): void {
|
|
1292
|
+
const target = shell === "self" ? component.selfRenderContainer : component.contentBox;
|
|
1293
|
+
if (!target || !Array.isArray(component.children)) return;
|
|
1294
|
+
const candidates = new Set(
|
|
1295
|
+
[component.contentText, component.contentBox, component.selfRenderContainer].filter(Boolean),
|
|
1296
|
+
);
|
|
1297
|
+
const indexes = component.children
|
|
1298
|
+
.map((child: any, index: number) => (candidates.has(child) ? index : -1))
|
|
1299
|
+
.filter((index: number) => index >= 0);
|
|
1300
|
+
const targetIndex = indexes[0];
|
|
1301
|
+
// During construction getRenderShell() runs immediately before Pi mounts the
|
|
1302
|
+
// selected shell. Do not mount it here or the constructor will add it twice.
|
|
1303
|
+
if (targetIndex === undefined) return;
|
|
1304
|
+
component.children[targetIndex] = target;
|
|
1305
|
+
for (const index of indexes.sort((left: number, right: number) => right - left)) {
|
|
1306
|
+
if (index !== targetIndex) component.children.splice(index, 1);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
|
|
1311
|
+
const extensionDefinition = component.toolDefinition;
|
|
1312
|
+
const builtInDefinition = component.builtInToolDefinition;
|
|
1313
|
+
const definition = extensionDefinition ?? builtInDefinition;
|
|
1314
|
+
const toolName = String(component.toolName || definition?.name || "");
|
|
1315
|
+
const useCcstyle =
|
|
1316
|
+
patch.mode() === "on" &&
|
|
1317
|
+
!preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
|
|
1318
|
+
component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
|
|
1319
|
+
return useCcstyle;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function shouldUseSelfShell(component: any, patch: GlobalToolRenderPatch): boolean {
|
|
1323
|
+
const definition = component.toolDefinition ?? component.builtInToolDefinition;
|
|
1324
|
+
const toolName = String(component.toolName || definition?.name || "");
|
|
1325
|
+
const useSelfShell =
|
|
1326
|
+
patch.enabled() &&
|
|
1327
|
+
SUBAGENT_TOOL_NAMES.has(toolName) &&
|
|
1328
|
+
definition != null &&
|
|
1329
|
+
definition.renderShell === undefined;
|
|
1330
|
+
component[COMPONENT_TOOL_SELF_SHELL_MODE] = useSelfShell;
|
|
1331
|
+
return useSelfShell;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): any {
|
|
1335
|
+
const definition = component.toolDefinition ?? component.builtInToolDefinition;
|
|
1336
|
+
if (definition && typeof definition === "object") {
|
|
1337
|
+
let wrapped = patch.byDefinition.get(definition);
|
|
1338
|
+
if (!wrapped) {
|
|
1339
|
+
wrapped = patch.wrap(definition);
|
|
1340
|
+
patch.byDefinition.set(definition, wrapped);
|
|
1341
|
+
}
|
|
1342
|
+
return wrapped;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const name = String(component.toolName || "tool");
|
|
1346
|
+
let wrapped = patch.byName.get(name);
|
|
1347
|
+
if (!wrapped) {
|
|
1348
|
+
wrapped = patch.wrap({ name, label: name });
|
|
1349
|
+
patch.byName.set(name, wrapped);
|
|
1350
|
+
}
|
|
1351
|
+
return wrapped;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function prototypeToolRenderMethods(prototype: any): ToolRenderMethods {
|
|
1355
|
+
return {
|
|
1356
|
+
hasRendererDefinition: prototype.hasRendererDefinition,
|
|
1357
|
+
getRenderShell: prototype.getRenderShell,
|
|
1358
|
+
getCallRenderer: prototype.getCallRenderer,
|
|
1359
|
+
getResultRenderer: prototype.getResultRenderer,
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch {
|
|
1364
|
+
if (!value || value.version !== 2 || !value.installed || !value.downstream) return false;
|
|
1365
|
+
return ["hasRendererDefinition", "getRenderShell", "getCallRenderer", "getResultRenderer"].every(
|
|
1366
|
+
(name) =>
|
|
1367
|
+
typeof value.installed[name] === "function" && typeof value.downstream[name] === "function",
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function isLegacyInstalledWrapper(method: unknown, downstreamField: string): boolean {
|
|
1372
|
+
if (typeof method !== "function") return false;
|
|
1373
|
+
try {
|
|
1374
|
+
const source = Function.prototype.toString.call(method);
|
|
1375
|
+
return (
|
|
1376
|
+
source.includes(downstreamField) &&
|
|
1377
|
+
(source.includes("shouldGloballyStyleTool") ||
|
|
1378
|
+
source.includes("shouldUseSelfShell") ||
|
|
1379
|
+
source.includes("getGloballyStyledTool"))
|
|
1380
|
+
);
|
|
1381
|
+
} catch {
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods {
|
|
1387
|
+
const current = prototypeToolRenderMethods(prototype);
|
|
1388
|
+
if (!previous || previous.prototype !== prototype) return current;
|
|
1389
|
+
if (isOwnershipAwarePatch(previous)) {
|
|
1390
|
+
return {
|
|
1391
|
+
hasRendererDefinition:
|
|
1392
|
+
current.hasRendererDefinition === previous.installed.hasRendererDefinition
|
|
1393
|
+
? previous.downstream.hasRendererDefinition
|
|
1394
|
+
: current.hasRendererDefinition,
|
|
1395
|
+
getRenderShell:
|
|
1396
|
+
current.getRenderShell === previous.installed.getRenderShell
|
|
1397
|
+
? previous.downstream.getRenderShell
|
|
1398
|
+
: current.getRenderShell,
|
|
1399
|
+
getCallRenderer:
|
|
1400
|
+
current.getCallRenderer === previous.installed.getCallRenderer
|
|
1401
|
+
? previous.downstream.getCallRenderer
|
|
1402
|
+
: current.getCallRenderer,
|
|
1403
|
+
getResultRenderer:
|
|
1404
|
+
current.getResultRenderer === previous.installed.getResultRenderer
|
|
1405
|
+
? previous.downstream.getResultRenderer
|
|
1406
|
+
: current.getResultRenderer,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Pre-v2 Symbol state did not retain wrapper references. Recognize its known
|
|
1411
|
+
// wrappers when possible; otherwise preserve the current method as external.
|
|
1412
|
+
const legacyDownstream = (method: Function, field: string): Function => {
|
|
1413
|
+
const saved = previous[field];
|
|
1414
|
+
return typeof saved === "function" && isLegacyInstalledWrapper(method, field) ? saved : method;
|
|
1415
|
+
};
|
|
1416
|
+
return {
|
|
1417
|
+
hasRendererDefinition: legacyDownstream(
|
|
1418
|
+
current.hasRendererDefinition,
|
|
1419
|
+
"originalHasRendererDefinition",
|
|
1420
|
+
) as ToolRenderMethods["hasRendererDefinition"],
|
|
1421
|
+
getRenderShell: legacyDownstream(
|
|
1422
|
+
current.getRenderShell,
|
|
1423
|
+
"originalGetRenderShell",
|
|
1424
|
+
) as ToolRenderMethods["getRenderShell"],
|
|
1425
|
+
getCallRenderer: legacyDownstream(
|
|
1426
|
+
current.getCallRenderer,
|
|
1427
|
+
"originalGetCallRenderer",
|
|
1428
|
+
) as ToolRenderMethods["getCallRenderer"],
|
|
1429
|
+
getResultRenderer: legacyDownstream(
|
|
1430
|
+
current.getResultRenderer,
|
|
1431
|
+
"originalGetResultRenderer",
|
|
1432
|
+
) as ToolRenderMethods["getResultRenderer"],
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function disconnectGlobalToolRenderPatch(patch: any): void {
|
|
1437
|
+
if (!patch || typeof patch !== "object") return;
|
|
1438
|
+
patch.active = false;
|
|
1439
|
+
patch.enabled = () => false;
|
|
1440
|
+
patch.mode = () => "off";
|
|
1441
|
+
patch.wrap = (tool: any) => tool;
|
|
1442
|
+
patch.byDefinition = new WeakMap();
|
|
1443
|
+
if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear();
|
|
1444
|
+
else patch.byName = new Map();
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function installGlobalToolRendering(
|
|
1448
|
+
writeExecutionMetadata: WriteExecutionMetadataStore,
|
|
1449
|
+
): GlobalToolRenderPatch {
|
|
1450
|
+
const prototype = (ToolExecutionComponent as any).prototype;
|
|
1451
|
+
const host = globalThis as any;
|
|
1452
|
+
const previous = host[GLOBAL_TOOL_RENDER_PATCH];
|
|
1453
|
+
const downstream = downstreamForGlobalToolInstall(prototype, previous);
|
|
1454
|
+
// Any wrapper retained by an external owner must become a callback-free
|
|
1455
|
+
// pass-through before the new installation is linked above it.
|
|
1456
|
+
disconnectGlobalToolRenderPatch(previous);
|
|
1457
|
+
|
|
1458
|
+
const patch: GlobalToolRenderPatch = {
|
|
1459
|
+
version: 2,
|
|
1460
|
+
prototype,
|
|
1461
|
+
owner: {},
|
|
1462
|
+
active: true,
|
|
1463
|
+
enabled: () => config.mode === "on",
|
|
1464
|
+
mode: () => config.mode,
|
|
1465
|
+
wrap: (tool: any) => createCcstyleTool(tool, writeExecutionMetadata),
|
|
1466
|
+
byDefinition: new WeakMap(),
|
|
1467
|
+
byName: new Map(),
|
|
1468
|
+
downstream,
|
|
1469
|
+
installed: undefined as any,
|
|
1470
|
+
originalHasRendererDefinition: downstream.hasRendererDefinition,
|
|
1471
|
+
originalGetRenderShell: downstream.getRenderShell,
|
|
1472
|
+
originalGetCallRenderer: downstream.getCallRenderer,
|
|
1473
|
+
originalGetResultRenderer: downstream.getResultRenderer,
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
patch.installed = {
|
|
1477
|
+
hasRendererDefinition: function (this: any, ...args: any[]) {
|
|
1478
|
+
if (patch.active && shouldGloballyStyleTool(this, patch)) return true;
|
|
1479
|
+
return patch.downstream.hasRendererDefinition.apply(this, args);
|
|
1480
|
+
},
|
|
1481
|
+
getRenderShell: function (this: any, ...args: any[]) {
|
|
1482
|
+
if (!patch.active) return patch.downstream.getRenderShell.apply(this, args);
|
|
1483
|
+
const shell =
|
|
1484
|
+
shouldUseSelfShell(this, patch) || shouldGloballyStyleTool(this, patch)
|
|
1485
|
+
? "self"
|
|
1486
|
+
: patch.downstream.getRenderShell.apply(this, args);
|
|
1487
|
+
syncToolShell(this, shell);
|
|
1488
|
+
return shell;
|
|
1489
|
+
},
|
|
1490
|
+
getCallRenderer: function (this: any, ...args: any[]) {
|
|
1491
|
+
if (patch.active && shouldGloballyStyleTool(this, patch)) {
|
|
1492
|
+
return getGloballyStyledTool(this, patch).renderCall;
|
|
1493
|
+
}
|
|
1494
|
+
return patch.downstream.getCallRenderer.apply(this, args);
|
|
1495
|
+
},
|
|
1496
|
+
getResultRenderer: function (this: any, ...args: any[]) {
|
|
1497
|
+
if (patch.active && shouldGloballyStyleTool(this, patch)) {
|
|
1498
|
+
return getGloballyStyledTool(this, patch).renderResult;
|
|
1499
|
+
}
|
|
1500
|
+
return patch.downstream.getResultRenderer.apply(this, args);
|
|
1501
|
+
},
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
prototype.hasRendererDefinition = patch.installed.hasRendererDefinition;
|
|
1505
|
+
prototype.getRenderShell = patch.installed.getRenderShell;
|
|
1506
|
+
prototype.getCallRenderer = patch.installed.getCallRenderer;
|
|
1507
|
+
prototype.getResultRenderer = patch.installed.getResultRenderer;
|
|
1508
|
+
host[GLOBAL_TOOL_RENDER_PATCH] = patch;
|
|
1509
|
+
return patch;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
|
|
1513
|
+
if (!patch.active) return;
|
|
1514
|
+
disconnectGlobalToolRenderPatch(patch);
|
|
1515
|
+
const prototype = patch.prototype;
|
|
1516
|
+
if (prototype.hasRendererDefinition === patch.installed.hasRendererDefinition) {
|
|
1517
|
+
prototype.hasRendererDefinition = patch.downstream.hasRendererDefinition;
|
|
1518
|
+
}
|
|
1519
|
+
if (prototype.getRenderShell === patch.installed.getRenderShell) {
|
|
1520
|
+
prototype.getRenderShell = patch.downstream.getRenderShell;
|
|
1521
|
+
}
|
|
1522
|
+
if (prototype.getCallRenderer === patch.installed.getCallRenderer) {
|
|
1523
|
+
prototype.getCallRenderer = patch.downstream.getCallRenderer;
|
|
1524
|
+
}
|
|
1525
|
+
if (prototype.getResultRenderer === patch.installed.getResultRenderer) {
|
|
1526
|
+
prototype.getResultRenderer = patch.downstream.getResultRenderer;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
|
|
1531
|
+
|
|
1532
|
+
type LegacyCompactionRenderPatch = {
|
|
1533
|
+
enabled?: () => boolean;
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
/** Disable the pre-native compaction monkey patch left alive by /reload. */
|
|
1537
|
+
function deactivateLegacyCompactionRendering() {
|
|
1538
|
+
const patch = (globalThis as any)[GLOBAL_COMPACTION_RENDER_PATCH] as
|
|
1539
|
+
| LegacyCompactionRenderPatch
|
|
1540
|
+
| undefined;
|
|
1541
|
+
if (patch) patch.enabled = () => false;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function notePendingScrollMessage(role: unknown): void {
|
|
1545
|
+
if (!toolMouseTui || !useFixedEditorFeatures(toolMouseTui) || !scrollButtonVisible) return;
|
|
1546
|
+
if (role === "assistant") {
|
|
1547
|
+
if (assistantMessageActive) return;
|
|
1548
|
+
assistantMessageActive = true;
|
|
1549
|
+
} else if (role !== "toolResult") {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
pendingScrollMessages += 1;
|
|
1553
|
+
toolMouseTui.requestRender?.();
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
|
|
1557
|
+
// The optional override keeps integration tests independent from the user's global config.
|
|
1558
|
+
if (configOverride) config = normalizeConfig({ ...config, ...configOverride });
|
|
1559
|
+
const writeExecutionMetadata = installWriteOverride(pi, new WriteExecutionMetadataStore());
|
|
1560
|
+
const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
|
|
1561
|
+
deactivateLegacyCompactionRendering();
|
|
1562
|
+
const compactStyle: CompactStyleHooks = installCompactStyle(pi, {
|
|
1563
|
+
getMode: () => config.mode,
|
|
1564
|
+
getExcludeRenderers: () => config.excludeRenderers,
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
pi.registerCommand("ccstyle", {
|
|
1568
|
+
description: "Configure Claude Code style",
|
|
1569
|
+
getArgumentCompletions: (prefix) => {
|
|
1570
|
+
const topLevel = [
|
|
1571
|
+
{ value: "on", label: "on", description: "Enable Claude Code style" },
|
|
1572
|
+
{ value: "off", label: "off", description: "Use Pi's native renderer" },
|
|
1573
|
+
{ value: "compact", label: "compact", description: "Use compact transcript rendering" },
|
|
1574
|
+
{ value: "status", label: "status", description: "Show current state" },
|
|
1575
|
+
];
|
|
1576
|
+
return topLevel.filter((item) => item.value.startsWith(prefix));
|
|
1577
|
+
},
|
|
1578
|
+
handler: async (args, ctx) => {
|
|
1579
|
+
const arg = args.trim().toLowerCase();
|
|
1580
|
+
if (!arg) {
|
|
1581
|
+
await showCcstylePanel(ctx, compactStyle);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (arg === "on" || arg === "off" || arg === "compact") {
|
|
1585
|
+
applyStyleMode(arg, ctx, compactStyle);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (arg === "status") {
|
|
1589
|
+
ctx.ui.notify(`Claude Code style: ${config.mode}`, "info");
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
ctx.ui.notify("Usage: /ccstyle [on|off|compact|status]", "warning");
|
|
1593
|
+
},
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
pi.on("session_start", async (event, ctx) => {
|
|
1597
|
+
compactStyle?.onSessionStart(event, ctx);
|
|
1598
|
+
pendingScrollMessages = 0;
|
|
1599
|
+
assistantMessageActive = false;
|
|
1600
|
+
ctx.ui.setStatus("ccstyle", undefined);
|
|
1601
|
+
installToolMouseInteraction(ctx);
|
|
1602
|
+
scheduleSessionRender(compactStyle.refresh);
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
pi.on("session_compact", async (event, ctx) => {
|
|
1606
|
+
compactStyle?.onSessionCompact(event, ctx);
|
|
1607
|
+
// Compaction rebuilds the transcript without session_start. Rebind after
|
|
1608
|
+
// other TUI extensions may have replaced the root input dispatcher.
|
|
1609
|
+
installToolMouseInteraction(ctx);
|
|
1610
|
+
scheduleSessionRender(compactStyle.refresh);
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
pi.on("message_start", async (event) => {
|
|
1614
|
+
notePendingScrollMessage(event?.message?.role);
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
pi.on("message_update", async (event, ctx) => {
|
|
1618
|
+
compactStyle?.onMessageUpdate(event, ctx);
|
|
1619
|
+
if (event?.message?.role === "assistant") notePendingScrollMessage("assistant");
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
pi.on("message_end", async (event) => {
|
|
1623
|
+
if (event?.message?.role === "assistant") assistantMessageActive = false;
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
pi.on("agent_start", async (event, ctx) => {
|
|
1627
|
+
compactStyle?.onAgentStart(event, ctx);
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
1631
|
+
compactStyle?.onAgentEnd(event, ctx);
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
pi.on("turn_start", async (event, ctx) => {
|
|
1635
|
+
compactStyle?.onTurnStart(event, ctx);
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
pi.on("tool_execution_start", async (event, ctx) => {
|
|
1639
|
+
compactStyle?.onToolExecutionStart(event, ctx);
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
pi.on("tool_execution_update", async (event, ctx) => {
|
|
1643
|
+
compactStyle?.onToolExecutionUpdate(event, ctx);
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
pi.on("tool_execution_end", async (event, ctx) => {
|
|
1647
|
+
compactStyle?.onToolExecutionEnd(event, ctx);
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
1651
|
+
compactStyle?.onSessionShutdown(event, ctx);
|
|
1652
|
+
writeExecutionMetadata.clear();
|
|
1653
|
+
teardownToolMouseInteraction();
|
|
1654
|
+
deactivateGlobalToolRendering(globalToolRendering);
|
|
1655
|
+
deactivateLegacyCompactionRendering();
|
|
1656
|
+
clearAllAnimations();
|
|
1657
|
+
});
|
|
1658
|
+
}
|