pi-model-profiles 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -2
- package/README.md +0 -1
- package/package.json +66 -66
- package/src/constants.ts +5 -5
- package/src/debug-logger.ts +383 -351
- package/src/index.ts +1 -1
- package/src/pi-api-utils.ts +1 -1
- package/src/profile-modal.ts +1062 -1175
- package/src/types-shims.d.ts +2 -2
package/src/profile-modal.ts
CHANGED
|
@@ -1,1175 +1,1062 @@
|
|
|
1
|
-
import type { ExtensionCommandContext } from "@
|
|
2
|
-
import { Input, matchesKey, truncateToWidth, visibleWidth } from "@
|
|
3
|
-
|
|
4
|
-
import { MODAL_MIN_HEIGHT, calculateModalHeight, resolveModalOverlayOptions } from "./constants.js";
|
|
5
|
-
import { toErrorMessage } from "./errors.js";
|
|
6
|
-
import { loadModalTheme, BOX, type ResolvedModalTheme } from "./modal-theme.js";
|
|
7
|
-
import { formatProfileFieldValue } from "./profile-fields.js";
|
|
8
|
-
import { getAvailableSortOrders, getCurrentSortOrder,
|
|
9
|
-
import type { AppliedProfileOutcome, ProfileSortOrder, ProfilesFile, SavedProfile, SavedProfileAgent } from "./types.js";
|
|
10
|
-
|
|
11
|
-
interface ThemeLike {
|
|
12
|
-
name?: unknown;
|
|
13
|
-
fg?: unknown;
|
|
14
|
-
bold?: unknown;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ModalMessage {
|
|
18
|
-
text: string;
|
|
19
|
-
level: "info" | "warning" | "error";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface ProfileModalMutationResult {
|
|
23
|
-
data: ProfilesFile;
|
|
24
|
-
message: string;
|
|
25
|
-
selectedProfileId?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface ProfileModalActions {
|
|
29
|
-
renameProfile(profileId: string, nextName: string): Promise<ProfileModalMutationResult>;
|
|
30
|
-
addCurrentProfile(): Promise<ProfileModalMutationResult>;
|
|
31
|
-
applyProfile(profileId: string): Promise<AppliedProfileOutcome>;
|
|
32
|
-
removeProfile(profileId: string): Promise<ProfileModalMutationResult>;
|
|
33
|
-
updateProfile(profileId: string): Promise<ProfileModalMutationResult>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface SortMenuOption {
|
|
37
|
-
order: ProfileSortOrder;
|
|
38
|
-
label: string;
|
|
39
|
-
isSelected: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type ProfileModalResult =
|
|
43
|
-
| { type: "closed" }
|
|
44
|
-
| {
|
|
45
|
-
type: "applied";
|
|
46
|
-
outcome: AppliedProfileOutcome;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
type
|
|
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
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
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
|
-
const
|
|
146
|
-
const preferredLeft = clamp(snapshotNameWidth + 6,
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function
|
|
216
|
-
return theme.
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function formatDisplayedFieldValue(agent: SavedProfileAgent, key: "model" | "temperature" | "reasoningEffort"): string {
|
|
237
|
-
const raw = formatProfileFieldValue(key, agent.fields);
|
|
238
|
-
return raw === "(absent)" ? ABSENT_DISPLAY_VALUE : raw;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
model
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
model,
|
|
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
|
-
private
|
|
390
|
-
private
|
|
391
|
-
private
|
|
392
|
-
private
|
|
393
|
-
private
|
|
394
|
-
private
|
|
395
|
-
private
|
|
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
|
-
const
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
const
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
lines.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
this.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
this.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
this.detailScrollOffset = 0;
|
|
855
|
-
this.
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
this.
|
|
968
|
-
this.
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
this.
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
this.
|
|
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
|
-
this.sortMenuSelectedIndex = 0;
|
|
1064
|
-
this.requestRender();
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
private handleSortMenuInput(data: string): void {
|
|
1068
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
1069
|
-
this.closeSortMenu();
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
1074
|
-
this.sortMenuSelectedIndex = Math.max(0, this.sortMenuSelectedIndex - 1);
|
|
1075
|
-
this.requestRender();
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
1080
|
-
const lastIndex = Math.max(0, this.getSortMenuOptions().length - 1);
|
|
1081
|
-
this.sortMenuSelectedIndex = Math.min(lastIndex, this.sortMenuSelectedIndex + 1);
|
|
1082
|
-
this.requestRender();
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
if (matchesKey(data, "return")) {
|
|
1087
|
-
this.applySortFromMenu();
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
private applySortFromMenu(): void {
|
|
1093
|
-
const options = this.getSortMenuOptions();
|
|
1094
|
-
const selected = options[this.sortMenuSelectedIndex];
|
|
1095
|
-
if (!selected) {
|
|
1096
|
-
this.closeSortMenu();
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
this.currentSortOrder = selected.order;
|
|
1101
|
-
persistSortOrder(selected.order);
|
|
1102
|
-
this.detailScrollOffset = 0;
|
|
1103
|
-
this.ensureSelectedVisible(this.getSnapshotViewportRows());
|
|
1104
|
-
this.message = { text: `Sorted by ${selected.label}`, level: "info" };
|
|
1105
|
-
this.closeSortMenu();
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
private buildSortMenuLines(width: number): string[] {
|
|
1109
|
-
if (!this.sortMenuOpen) {
|
|
1110
|
-
return [];
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
const options = this.getSortMenuOptions();
|
|
1114
|
-
const menuWidth = clamp(width - 2, 32, 54);
|
|
1115
|
-
const innerWidth = Math.max(1, menuWidth - 2);
|
|
1116
|
-
const lines: string[] = [];
|
|
1117
|
-
const title = this.theme.color("accent", " SORT PROFILES ", { bold: true });
|
|
1118
|
-
const titleFill = Math.max(0, innerWidth - visibleWidth(title));
|
|
1119
|
-
lines.push(`${BOX.CORNER_TL}${title}${BOX.H_LINE.repeat(titleFill)}${BOX.CORNER_TR}`);
|
|
1120
|
-
|
|
1121
|
-
for (let i = 0; i < options.length; i++) {
|
|
1122
|
-
const option = options[i];
|
|
1123
|
-
const isSelected = i === this.sortMenuSelectedIndex;
|
|
1124
|
-
const marker = isSelected ? ">" : " ";
|
|
1125
|
-
const checkmark = option.isSelected ? "✓" : " ";
|
|
1126
|
-
const label = `${marker} [${checkmark}] ${option.label}`;
|
|
1127
|
-
const padded = fitText(label, innerWidth);
|
|
1128
|
-
const styled = isSelected
|
|
1129
|
-
? this.theme.color("selectedText", padded, { background: "selectedBg", bold: true })
|
|
1130
|
-
: this.theme.color("text", padded);
|
|
1131
|
-
lines.push(`${BOX.V_LINE}${styled}${BOX.V_LINE}`);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
const hint = "[Enter] Apply [Esc] Cancel";
|
|
1135
|
-
lines.push(`${BOX.V_LINE}${this.theme.color("dim", fitText(hint, innerWidth))}${BOX.V_LINE}`);
|
|
1136
|
-
lines.push(`${BOX.CORNER_BL}${BOX.H_LINE.repeat(innerWidth)}${BOX.CORNER_BR}`);
|
|
1137
|
-
|
|
1138
|
-
return lines.map((line) => centerLineInWidth(line, width));
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
export async function openProfilesModal(
|
|
1143
|
-
ctx: ExtensionCommandContext,
|
|
1144
|
-
data: ProfilesFile,
|
|
1145
|
-
activeAgentName: string | null,
|
|
1146
|
-
actions: ProfileModalActions,
|
|
1147
|
-
): Promise<ProfileModalResult> {
|
|
1148
|
-
const overlayOptions = resolveModalOverlayOptions(getMaximumContentWidth(data));
|
|
1149
|
-
|
|
1150
|
-
return await ctx.ui.custom<ProfileModalResult>(
|
|
1151
|
-
(
|
|
1152
|
-
tui: { requestRender(): void },
|
|
1153
|
-
theme: ThemeLike,
|
|
1154
|
-
_keybindings: unknown,
|
|
1155
|
-
done: (result: ProfileModalResult) => void,
|
|
1156
|
-
) => {
|
|
1157
|
-
const resolvedTheme = loadModalTheme(theme);
|
|
1158
|
-
const contentInstance = new ProfileListModal(data, resolvedTheme, actions, activeAgentName, done, () => tui.requestRender());
|
|
1159
|
-
|
|
1160
|
-
return {
|
|
1161
|
-
render(width: number): string[] {
|
|
1162
|
-
return renderOuterFrame(contentInstance.render(Math.max(1, width - 2)), width, "MODEL PROFILES", resolvedTheme);
|
|
1163
|
-
},
|
|
1164
|
-
invalidate(): void {
|
|
1165
|
-
contentInstance.invalidate();
|
|
1166
|
-
},
|
|
1167
|
-
handleInput(input: string): void {
|
|
1168
|
-
contentInstance.handleInput(input);
|
|
1169
|
-
tui.requestRender();
|
|
1170
|
-
},
|
|
1171
|
-
};
|
|
1172
|
-
},
|
|
1173
|
-
{ overlay: true, overlayOptions },
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Input, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
import { MODAL_MIN_HEIGHT, calculateModalHeight, resolveModalOverlayOptions } from "./constants.js";
|
|
5
|
+
import { toErrorMessage } from "./errors.js";
|
|
6
|
+
import { loadModalTheme, BOX, type ResolvedModalTheme } from "./modal-theme.js";
|
|
7
|
+
import { formatProfileFieldValue } from "./profile-fields.js";
|
|
8
|
+
import { getAvailableSortOrders, getCurrentSortOrder, persistSortOrder, sortProfiles } from "./profile-sort-service.js";
|
|
9
|
+
import type { AppliedProfileOutcome, ProfileSortOrder, ProfilesFile, SavedProfile, SavedProfileAgent } from "./types.js";
|
|
10
|
+
|
|
11
|
+
interface ThemeLike {
|
|
12
|
+
name?: unknown;
|
|
13
|
+
fg?: unknown;
|
|
14
|
+
bold?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ModalMessage {
|
|
18
|
+
text: string;
|
|
19
|
+
level: "info" | "warning" | "error";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ProfileModalMutationResult {
|
|
23
|
+
data: ProfilesFile;
|
|
24
|
+
message: string;
|
|
25
|
+
selectedProfileId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ProfileModalActions {
|
|
29
|
+
renameProfile(profileId: string, nextName: string): Promise<ProfileModalMutationResult>;
|
|
30
|
+
addCurrentProfile(): Promise<ProfileModalMutationResult>;
|
|
31
|
+
applyProfile(profileId: string): Promise<AppliedProfileOutcome>;
|
|
32
|
+
removeProfile(profileId: string): Promise<ProfileModalMutationResult>;
|
|
33
|
+
updateProfile(profileId: string): Promise<ProfileModalMutationResult>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SortMenuOption {
|
|
37
|
+
order: ProfileSortOrder;
|
|
38
|
+
label: string;
|
|
39
|
+
isSelected: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ProfileModalResult =
|
|
43
|
+
| { type: "closed" }
|
|
44
|
+
| {
|
|
45
|
+
type: "applied";
|
|
46
|
+
outcome: AppliedProfileOutcome;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type ConfirmationAction = "remove" | "update";
|
|
50
|
+
|
|
51
|
+
interface ConfirmationState {
|
|
52
|
+
action: ConfirmationAction;
|
|
53
|
+
profileId: string;
|
|
54
|
+
prompt: string;
|
|
55
|
+
input: Input;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ConfirmationRequest {
|
|
59
|
+
action: ConfirmationAction;
|
|
60
|
+
profile: SavedProfile;
|
|
61
|
+
prompt: string;
|
|
62
|
+
busyMessage: string;
|
|
63
|
+
onConfirm(profileId: string): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface TableColumnLayout {
|
|
67
|
+
agent: number;
|
|
68
|
+
model: number;
|
|
69
|
+
temp: number;
|
|
70
|
+
reasoning: number;
|
|
71
|
+
reasoningHeader: string;
|
|
72
|
+
gap: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MODAL_FALLBACK_VIEWPORT = 10;
|
|
76
|
+
const SNAPSHOT_TITLE = "SNAPSHOTS";
|
|
77
|
+
const DETAILS_TITLE = "DETAILS";
|
|
78
|
+
const ACTIVE_PANE_LABEL = "[ACTIVE]";
|
|
79
|
+
const EMPTY_PROFILE_HINT = "No saved snapshots yet.";
|
|
80
|
+
const EMPTY_DETAILS_HINT = "Select a snapshot to inspect its saved agent models.";
|
|
81
|
+
const ABSENT_DISPLAY_VALUE = "absent";
|
|
82
|
+
|
|
83
|
+
function clamp(value: number, min: number, max: number): number {
|
|
84
|
+
return Math.max(min, Math.min(max, value));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatTimestamp(value: string): string {
|
|
88
|
+
const date = new Date(value);
|
|
89
|
+
if (Number.isNaN(date.getTime())) {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
94
|
+
dateStyle: "short",
|
|
95
|
+
timeStyle: "short",
|
|
96
|
+
}).format(date);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function padEndToWidth(text: string, width: number): string {
|
|
100
|
+
const safeWidth = Math.max(0, width);
|
|
101
|
+
const padding = Math.max(0, safeWidth - visibleWidth(text));
|
|
102
|
+
return `${text}${" ".repeat(padding)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fitText(text: string, width: number): string {
|
|
106
|
+
const safeWidth = Math.max(1, width);
|
|
107
|
+
return padEndToWidth(truncateToWidth(text, safeWidth, "…", true), safeWidth);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function centerText(text: string, width: number): string {
|
|
111
|
+
const safeWidth = Math.max(1, width);
|
|
112
|
+
const clipped = truncateToWidth(text, safeWidth, "…", true);
|
|
113
|
+
const remaining = Math.max(0, safeWidth - visibleWidth(clipped));
|
|
114
|
+
const left = Math.floor(remaining / 2);
|
|
115
|
+
const right = remaining - left;
|
|
116
|
+
return `${" ".repeat(left)}${clipped}${" ".repeat(right)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function alignRight(text: string, width: number): string {
|
|
120
|
+
const safeWidth = Math.max(1, width);
|
|
121
|
+
const clipped = truncateToWidth(text, safeWidth, "…", true);
|
|
122
|
+
const padding = Math.max(0, safeWidth - visibleWidth(clipped));
|
|
123
|
+
return `${" ".repeat(padding)}${clipped}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function fitLineToWidth(text: string, width: number): string {
|
|
127
|
+
const safeWidth = Math.max(1, width);
|
|
128
|
+
if (visibleWidth(text) > safeWidth) {
|
|
129
|
+
return truncateToWidth(text, safeWidth, "…", true);
|
|
130
|
+
}
|
|
131
|
+
return padEndToWidth(text, safeWidth);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function centerLineInWidth(text: string, width: number): string {
|
|
135
|
+
const safeWidth = Math.max(1, width);
|
|
136
|
+
const clipped = visibleWidth(text) > safeWidth ? truncateToWidth(text, safeWidth, "…", true) : text;
|
|
137
|
+
const padding = Math.max(0, Math.floor((safeWidth - visibleWidth(clipped)) / 2));
|
|
138
|
+
return fitLineToWidth(`${" ".repeat(padding)}${clipped}`, safeWidth);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function splitGridCellWidths(innerWidth: number, snapshotNameWidth: number): { left: number; right: number } {
|
|
142
|
+
const safeWidth = Math.max(3, innerWidth);
|
|
143
|
+
const minRight = Math.min(24, Math.max(1, safeWidth - 25));
|
|
144
|
+
const maxLeft = Math.max(1, safeWidth - minRight - 1);
|
|
145
|
+
const minLeft = Math.min(maxLeft, Math.max(1, Math.min(28, Math.floor(safeWidth * 0.35))));
|
|
146
|
+
const preferredLeft = clamp(snapshotNameWidth + 6, 28, 32);
|
|
147
|
+
const left = clamp(preferredLeft, minLeft, maxLeft);
|
|
148
|
+
const right = Math.max(1, safeWidth - left - 1);
|
|
149
|
+
return { left, right };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function wrapText(text: string, width: number): string[] {
|
|
153
|
+
const safeWidth = Math.max(1, width);
|
|
154
|
+
const normalized = text.trim();
|
|
155
|
+
if (!normalized) {
|
|
156
|
+
return [""];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const words = normalized.split(/\s+/);
|
|
160
|
+
const lines: string[] = [];
|
|
161
|
+
let current = "";
|
|
162
|
+
|
|
163
|
+
for (const word of words) {
|
|
164
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
165
|
+
if (visibleWidth(candidate) <= safeWidth) {
|
|
166
|
+
current = candidate;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (current) {
|
|
171
|
+
lines.push(current);
|
|
172
|
+
current = "";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (visibleWidth(word) <= safeWidth) {
|
|
176
|
+
current = word;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let remainder = word;
|
|
181
|
+
while (visibleWidth(remainder) > safeWidth) {
|
|
182
|
+
lines.push(truncateToWidth(remainder, safeWidth, "", false));
|
|
183
|
+
remainder = remainder.slice(Math.max(1, safeWidth));
|
|
184
|
+
}
|
|
185
|
+
current = remainder;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (current) {
|
|
189
|
+
lines.push(current);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return lines.length > 0 ? lines : [""];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function colorFrameBorder(theme: ResolvedModalTheme, text: string): string {
|
|
196
|
+
return theme.color("accent", text);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildTopBorder(theme: ResolvedModalTheme, innerWidth: number): string {
|
|
200
|
+
return colorFrameBorder(theme, `${BOX.CORNER_TL}${BOX.H_LINE.repeat(innerWidth)}${BOX.CORNER_TR}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildBottomBorder(theme: ResolvedModalTheme, innerWidth: number): string {
|
|
204
|
+
return colorFrameBorder(theme, `${BOX.CORNER_BL}${BOX.H_LINE.repeat(innerWidth)}${BOX.CORNER_BR}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildGridSeparator(theme: ResolvedModalTheme, leftWidth: number, rightWidth: number, join: "┬" | "┼" | "┴"): string {
|
|
208
|
+
return colorFrameBorder(theme, `${BOX.T_RIGHT}${BOX.H_LINE.repeat(leftWidth)}${join}${BOX.H_LINE.repeat(rightWidth)}${BOX.T_LEFT}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildFullWidthRow(theme: ResolvedModalTheme, innerWidth: number, content: string): string {
|
|
212
|
+
return `${colorFrameBorder(theme, BOX.V_LINE)}${fitText(content, innerWidth)}${colorFrameBorder(theme, BOX.V_LINE)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildGridRow(theme: ResolvedModalTheme, leftWidth: number, rightWidth: number, left: string, right: string): string {
|
|
216
|
+
return `${colorFrameBorder(theme, BOX.V_LINE)}${fitText(left, leftWidth)}${colorFrameBorder(theme, BOX.V_LINE)}${fitText(right, rightWidth)}${colorFrameBorder(theme, BOX.V_LINE)}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildModalTitleLine(theme: ResolvedModalTheme, innerWidth: number): string {
|
|
220
|
+
const title = " MODEL PROFILES";
|
|
221
|
+
const close = "[Esc] Close";
|
|
222
|
+
const gap = " ".repeat(Math.max(1, innerWidth - visibleWidth(title) - visibleWidth(close)));
|
|
223
|
+
return `${theme.color("accent", title, { bold: true })}${gap}${theme.color("dim", close)}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildPaneTitleLine(theme: ResolvedModalTheme, title: string, active: boolean, suffix: string | null, width: number): string {
|
|
227
|
+
const activeLabel = active ? ` ${ACTIVE_PANE_LABEL}` : "";
|
|
228
|
+
const suffixLabel = suffix ? `: ${suffix}` : "";
|
|
229
|
+
return theme.color(active ? "accent" : "text", fitText(` ${title}${activeLabel}${suffixLabel}`, width), { bold: active });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function indentStyledLine(line: string, width: number): string {
|
|
233
|
+
return fitText(` ${line}`, width);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatDisplayedFieldValue(agent: SavedProfileAgent, key: "model" | "temperature" | "reasoningEffort"): string {
|
|
237
|
+
const raw = formatProfileFieldValue(key, agent.fields);
|
|
238
|
+
return raw === "(absent)" ? ABSENT_DISPLAY_VALUE : raw;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatTemperatureValue(agent: SavedProfileAgent): string {
|
|
242
|
+
const value = formatDisplayedFieldValue(agent, "temperature");
|
|
243
|
+
const numeric = Number(value);
|
|
244
|
+
if (Number.isFinite(numeric) && Number.isInteger(numeric)) {
|
|
245
|
+
return String(numeric);
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatReasoningValue(agent: SavedProfileAgent): string {
|
|
251
|
+
const value = formatDisplayedFieldValue(agent, "reasoningEffort");
|
|
252
|
+
const normalized = value.trim().toLowerCase();
|
|
253
|
+
if (["extra-high", "extra high", "x-high", "very-high", "very high"].includes(normalized)) {
|
|
254
|
+
return "xhigh";
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildProfileScrollIndicator(offset: number, totalItems: number, visibleItems: number): string {
|
|
260
|
+
const shownEnd = Math.min(totalItems, offset + visibleItems);
|
|
261
|
+
const remainingAbove = offset;
|
|
262
|
+
const remainingBelow = Math.max(0, totalItems - shownEnd);
|
|
263
|
+
|
|
264
|
+
if (remainingAbove > 0 && remainingBelow > 0) {
|
|
265
|
+
return `[↑ ${remainingAbove} | ↓ ${remainingBelow}]`;
|
|
266
|
+
}
|
|
267
|
+
if (remainingBelow > 0) {
|
|
268
|
+
return `[↓ ${remainingBelow} more]`;
|
|
269
|
+
}
|
|
270
|
+
return `[↑ ${remainingAbove} above]`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildAgentScrollIndicator(offset: number, totalItems: number, visibleItems: number): string {
|
|
274
|
+
const shownEnd = Math.min(totalItems, offset + visibleItems);
|
|
275
|
+
const remainingAbove = offset;
|
|
276
|
+
const remainingBelow = Math.max(0, totalItems - shownEnd);
|
|
277
|
+
|
|
278
|
+
if (remainingAbove > 0 && remainingBelow > 0) {
|
|
279
|
+
return `[ ↑ ${remainingAbove} | ↓ ${remainingBelow} more ]`;
|
|
280
|
+
}
|
|
281
|
+
if (remainingBelow > 0) {
|
|
282
|
+
return `[ ↓ Scroll (${remainingBelow} more) ]`;
|
|
283
|
+
}
|
|
284
|
+
return `[ ↑ Scroll (${remainingAbove} above) ]`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderMetadataLine(theme: ResolvedModalTheme, label: string, value: string, width: number): string {
|
|
288
|
+
const prefix = `${label.padEnd(9, " ")}`;
|
|
289
|
+
const safeValueWidth = Math.max(1, width - visibleWidth(prefix));
|
|
290
|
+
const valueText = truncateToWidth(value, safeValueWidth, "…", true);
|
|
291
|
+
const trailing = " ".repeat(Math.max(0, width - visibleWidth(prefix) - visibleWidth(valueText)));
|
|
292
|
+
return `${theme.color("dim", prefix)}${theme.color("text", valueText)}${trailing}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function computeProfileNameWidth(data: ProfilesFile): number {
|
|
296
|
+
let maxWidth = 0;
|
|
297
|
+
for (const profile of data.profiles) {
|
|
298
|
+
maxWidth = Math.max(maxWidth, visibleWidth(profile.name));
|
|
299
|
+
}
|
|
300
|
+
return maxWidth;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function getMaximumContentWidth(data: ProfilesFile): number {
|
|
304
|
+
let maxWidth = 8;
|
|
305
|
+
for (const profile of data.profiles) {
|
|
306
|
+
maxWidth = Math.max(maxWidth, visibleWidth(profile.name));
|
|
307
|
+
for (const agent of profile.agents) {
|
|
308
|
+
maxWidth = Math.max(maxWidth, visibleWidth(agent.agentName));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return maxWidth;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildTableLayout(profile: SavedProfile, totalWidth: number): TableColumnLayout {
|
|
315
|
+
const gap = " ";
|
|
316
|
+
const gapWidth = visibleWidth(gap) * 3;
|
|
317
|
+
const available = Math.max(24, totalWidth - gapWidth);
|
|
318
|
+
const reasoningHeader = "REASONING";
|
|
319
|
+
|
|
320
|
+
let agent = Math.max("AGENT".length, ...profile.agents.map((entry) => visibleWidth(entry.agentName)));
|
|
321
|
+
agent = clamp(agent, 10, 14);
|
|
322
|
+
|
|
323
|
+
let temp = Math.max("TEMPERATURE".length, ...profile.agents.map((entry) => visibleWidth(formatTemperatureValue(entry))));
|
|
324
|
+
temp = clamp(temp, "TEMPERATURE".length, 14);
|
|
325
|
+
|
|
326
|
+
let reasoning = Math.max(reasoningHeader.length, ...profile.agents.map((entry) => visibleWidth(formatReasoningValue(entry))));
|
|
327
|
+
reasoning = clamp(reasoning, reasoningHeader.length, 12);
|
|
328
|
+
|
|
329
|
+
const minimumModel = 14;
|
|
330
|
+
let model = available - agent - temp - reasoning;
|
|
331
|
+
|
|
332
|
+
while (model < minimumModel && agent > 10) {
|
|
333
|
+
agent -= 1;
|
|
334
|
+
model += 1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
while (model < minimumModel && reasoning > reasoningHeader.length) {
|
|
338
|
+
reasoning -= 1;
|
|
339
|
+
model += 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (model < minimumModel) {
|
|
343
|
+
model = Math.max(10, model);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const totalUsed = agent + model + temp + reasoning;
|
|
347
|
+
const slack = Math.max(0, available - totalUsed);
|
|
348
|
+
model += slack;
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
agent,
|
|
352
|
+
model,
|
|
353
|
+
temp,
|
|
354
|
+
reasoning,
|
|
355
|
+
reasoningHeader,
|
|
356
|
+
gap,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildTableHeaderLine(theme: ResolvedModalTheme, layout: TableColumnLayout): string {
|
|
361
|
+
return [
|
|
362
|
+
theme.color("accent", fitText("AGENT", layout.agent), { bold: true }),
|
|
363
|
+
layout.gap,
|
|
364
|
+
theme.color("accent", fitText("MODEL", layout.model), { bold: true }),
|
|
365
|
+
layout.gap,
|
|
366
|
+
theme.color("accent", alignRight("TEMPERATURE", layout.temp), { bold: true }),
|
|
367
|
+
layout.gap,
|
|
368
|
+
theme.color("accent", fitText(layout.reasoningHeader, layout.reasoning), { bold: true }),
|
|
369
|
+
].join("");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function buildTableSeparatorLine(theme: ResolvedModalTheme, width: number): string {
|
|
373
|
+
return theme.color("borderMuted", BOX.H_LINE.repeat(width));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildTableDataLine(theme: ResolvedModalTheme, layout: TableColumnLayout, agent: SavedProfileAgent): string {
|
|
377
|
+
return [
|
|
378
|
+
theme.color("text", fitText(agent.agentName, layout.agent)),
|
|
379
|
+
layout.gap,
|
|
380
|
+
theme.color("text", fitText(formatDisplayedFieldValue(agent, "model"), layout.model)),
|
|
381
|
+
layout.gap,
|
|
382
|
+
theme.color("text", alignRight(formatTemperatureValue(agent), layout.temp)),
|
|
383
|
+
layout.gap,
|
|
384
|
+
theme.color("text", fitText(formatReasoningValue(agent), layout.reasoning)),
|
|
385
|
+
].join("");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
class ProfileListModal {
|
|
389
|
+
private data: ProfilesFile;
|
|
390
|
+
private selectedProfileId: string | null;
|
|
391
|
+
private listScrollOffset = 0;
|
|
392
|
+
private detailScrollOffset = 0;
|
|
393
|
+
private lastPaneContentRows = MODAL_FALLBACK_VIEWPORT;
|
|
394
|
+
private renameInput: Input | null = null;
|
|
395
|
+
private renameTargetId: string | null = null;
|
|
396
|
+
private confirmation: ConfirmationState | null = null;
|
|
397
|
+
private sortMenuOpen = false;
|
|
398
|
+
private sortMenuSelectedIndex = 0;
|
|
399
|
+
private message: ModalMessage | null = null;
|
|
400
|
+
private busyMessage: string | null = null;
|
|
401
|
+
private finished = false;
|
|
402
|
+
private currentSortOrder: ProfileSortOrder;
|
|
403
|
+
|
|
404
|
+
constructor(
|
|
405
|
+
initialData: ProfilesFile,
|
|
406
|
+
private readonly theme: ResolvedModalTheme,
|
|
407
|
+
private readonly actions: ProfileModalActions,
|
|
408
|
+
private readonly activeAgentName: string | null,
|
|
409
|
+
private readonly done: (result: ProfileModalResult) => void,
|
|
410
|
+
private readonly requestRender: () => void,
|
|
411
|
+
) {
|
|
412
|
+
this.data = initialData;
|
|
413
|
+
this.currentSortOrder = getCurrentSortOrder();
|
|
414
|
+
this.selectedProfileId = this.getSortedProfiles()[0]?.id ?? null;
|
|
415
|
+
if (theme.warnings.length > 0) {
|
|
416
|
+
this.message = { text: theme.warnings.join(" "), level: "warning" };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
invalidate(): void {
|
|
421
|
+
// Rendering is fully state driven.
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
render(width: number): string[] {
|
|
425
|
+
const frameWidth = Math.max(4, Math.floor(width));
|
|
426
|
+
const innerWidth = Math.max(1, frameWidth - 2);
|
|
427
|
+
const paneWidths = splitGridCellWidths(innerWidth, computeProfileNameWidth(this.data));
|
|
428
|
+
const footerLines = this.buildFooterLines(innerWidth);
|
|
429
|
+
const selectedProfile = this.getSelectedProfile();
|
|
430
|
+
const agentCount = selectedProfile?.agents.length ?? 0;
|
|
431
|
+
const paneContentRows = this.resolvePaneContentRows(agentCount, footerLines.length);
|
|
432
|
+
this.lastPaneContentRows = paneContentRows;
|
|
433
|
+
const leftPaneLines = this.buildSnapshotPaneRows(paneWidths.left, paneContentRows);
|
|
434
|
+
const rightPaneLines = this.buildDetailsPaneRows(selectedProfile, paneWidths.right, paneContentRows);
|
|
435
|
+
const detailTitle = selectedProfile?.name ?? "No selection";
|
|
436
|
+
const lines: string[] = [
|
|
437
|
+
buildTopBorder(this.theme, innerWidth),
|
|
438
|
+
buildFullWidthRow(this.theme, innerWidth, buildModalTitleLine(this.theme, innerWidth)),
|
|
439
|
+
buildGridSeparator(this.theme, paneWidths.left, paneWidths.right, "┬"),
|
|
440
|
+
buildGridRow(
|
|
441
|
+
this.theme,
|
|
442
|
+
paneWidths.left,
|
|
443
|
+
paneWidths.right,
|
|
444
|
+
buildPaneTitleLine(this.theme, SNAPSHOT_TITLE, true, null, paneWidths.left),
|
|
445
|
+
buildPaneTitleLine(this.theme, DETAILS_TITLE, false, detailTitle, paneWidths.right),
|
|
446
|
+
),
|
|
447
|
+
buildGridSeparator(this.theme, paneWidths.left, paneWidths.right, "┼"),
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
for (let index = 0; index < paneContentRows; index += 1) {
|
|
451
|
+
lines.push(buildGridRow(this.theme, paneWidths.left, paneWidths.right, leftPaneLines[index] ?? "", rightPaneLines[index] ?? ""));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
lines.push(buildGridSeparator(this.theme, paneWidths.left, paneWidths.right, "┴"));
|
|
455
|
+
for (const footerLine of footerLines) {
|
|
456
|
+
lines.push(buildFullWidthRow(this.theme, innerWidth, footerLine));
|
|
457
|
+
}
|
|
458
|
+
lines.push(buildBottomBorder(this.theme, innerWidth));
|
|
459
|
+
|
|
460
|
+
return lines;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
handleInput(data: string): void {
|
|
464
|
+
if (this.renameInput) {
|
|
465
|
+
this.renameInput.handleInput(data);
|
|
466
|
+
this.requestRender();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (this.confirmation) {
|
|
471
|
+
this.confirmation.input.handleInput(data);
|
|
472
|
+
this.requestRender();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (this.busyMessage) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (this.sortMenuOpen) {
|
|
481
|
+
this.handleSortMenuInput(data);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
486
|
+
this.finish({ type: "closed" });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (matchesKey(data, "r")) {
|
|
491
|
+
this.startRename();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (matchesKey(data, "delete") || matchesKey(data, "ctrl+d")) {
|
|
496
|
+
this.startRemove();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (matchesKey(data, "ctrl+u")) {
|
|
501
|
+
this.startUpdate();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (matchesKey(data, "ctrl+s")) {
|
|
506
|
+
this.toggleSortMenu();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (matchesKey(data, "s") && !this.sortMenuOpen) {
|
|
511
|
+
this.addCurrentProfile();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
this.handleSnapshotPaneInput(data);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private handleSnapshotPaneInput(data: string): void {
|
|
519
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
520
|
+
this.moveSelection(-1);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
525
|
+
this.moveSelection(1);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (matchesKey(data, "pageup")) {
|
|
530
|
+
this.moveSelection(-Math.max(1, this.getSnapshotViewportRows()));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (matchesKey(data, "pagedown")) {
|
|
535
|
+
this.moveSelection(Math.max(1, this.getSnapshotViewportRows()));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (matchesKey(data, "home")) {
|
|
540
|
+
this.moveSelectionToBoundary("start");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (matchesKey(data, "end")) {
|
|
545
|
+
this.moveSelectionToBoundary("end");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (matchesKey(data, "return")) {
|
|
550
|
+
this.applySelectedProfile();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private buildSnapshotPaneRows(width: number, contentRows: number): string[] {
|
|
555
|
+
const lines: string[] = [];
|
|
556
|
+
const profiles = this.getSortedProfiles();
|
|
557
|
+
const needsIndicator = profiles.length > contentRows;
|
|
558
|
+
const viewportRows = Math.max(1, contentRows - (needsIndicator ? 1 : 0));
|
|
559
|
+
this.ensureSelectedVisible(viewportRows);
|
|
560
|
+
|
|
561
|
+
if (profiles.length === 0) {
|
|
562
|
+
lines.push(this.theme.color("dim", fitText(` ${EMPTY_PROFILE_HINT}`, width)));
|
|
563
|
+
while (lines.length < contentRows) {
|
|
564
|
+
lines.push(" ".repeat(width));
|
|
565
|
+
}
|
|
566
|
+
return lines;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
for (let index = 0; index < viewportRows; index += 1) {
|
|
570
|
+
const profile = profiles[this.listScrollOffset + index];
|
|
571
|
+
if (!profile) {
|
|
572
|
+
lines.push(" ".repeat(width));
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const isSelected = profile.id === this.selectedProfileId;
|
|
577
|
+
const label = fitText(` ${isSelected ? ">" : " "} ${profile.name}`, width);
|
|
578
|
+
if (isSelected) {
|
|
579
|
+
lines.push(this.theme.color("selectedText", label, { background: "selectedBg", bold: true }));
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
lines.push(this.theme.color("text", label));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (needsIndicator) {
|
|
587
|
+
const indicator = alignRight(buildProfileScrollIndicator(this.listScrollOffset, profiles.length, viewportRows), width);
|
|
588
|
+
lines.push(this.theme.color("dim", indicator));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
while (lines.length < contentRows) {
|
|
592
|
+
lines.push(" ".repeat(width));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return lines;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private buildDetailsPaneRows(profile: SavedProfile | null, width: number, contentRows: number): string[] {
|
|
599
|
+
const lines: string[] = [];
|
|
600
|
+
|
|
601
|
+
if (!profile) {
|
|
602
|
+
lines.push(indentStyledLine(renderMetadataLine(this.theme, "Updated:", "-", Math.max(1, width - 2)), width));
|
|
603
|
+
lines.push(" ".repeat(width));
|
|
604
|
+
lines.push(this.theme.color("dim", fitText(` ${EMPTY_DETAILS_HINT}`, width)));
|
|
605
|
+
while (lines.length < contentRows) {
|
|
606
|
+
lines.push(" ".repeat(width));
|
|
607
|
+
}
|
|
608
|
+
return lines;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const fixedRowsBeforeData = 4;
|
|
612
|
+
const availableDataArea = Math.max(1, contentRows - fixedRowsBeforeData);
|
|
613
|
+
const needsIndicator = profile.agents.length > availableDataArea;
|
|
614
|
+
const viewportRows = Math.max(1, availableDataArea - (needsIndicator ? 1 : 0));
|
|
615
|
+
const maxOffset = Math.max(0, profile.agents.length - viewportRows);
|
|
616
|
+
this.detailScrollOffset = clamp(this.detailScrollOffset, 0, maxOffset);
|
|
617
|
+
const tableWidth = Math.max(1, width - 4);
|
|
618
|
+
const layout = buildTableLayout(profile, tableWidth);
|
|
619
|
+
|
|
620
|
+
lines.push(indentStyledLine(renderMetadataLine(this.theme, "Updated:", formatTimestamp(profile.updatedAt), tableWidth), width));
|
|
621
|
+
lines.push(" ".repeat(width));
|
|
622
|
+
lines.push(indentStyledLine(buildTableHeaderLine(this.theme, layout), width));
|
|
623
|
+
lines.push(indentStyledLine(buildTableSeparatorLine(this.theme, tableWidth), width));
|
|
624
|
+
|
|
625
|
+
for (let index = 0; index < viewportRows; index += 1) {
|
|
626
|
+
const agent = profile.agents[this.detailScrollOffset + index];
|
|
627
|
+
const content = agent ? indentStyledLine(buildTableDataLine(this.theme, layout, agent), width) : " ".repeat(width);
|
|
628
|
+
lines.push(content);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (needsIndicator) {
|
|
632
|
+
const indicator = centerText(buildAgentScrollIndicator(this.detailScrollOffset, profile.agents.length, viewportRows), width);
|
|
633
|
+
lines.push(this.theme.color("dim", indicator));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
while (lines.length < contentRows) {
|
|
637
|
+
lines.push(" ".repeat(width));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return lines;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private buildFooterLines(width: number): string[] {
|
|
644
|
+
if (this.busyMessage) {
|
|
645
|
+
return [this.theme.color("warning", fitText(`Working: ${this.busyMessage}`, width))];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (this.renameInput) {
|
|
649
|
+
const renameLine = this.renameInput.render(width)[0] ?? "";
|
|
650
|
+
return [
|
|
651
|
+
this.theme.color("warning", fitText("Rename selected snapshot:", width)),
|
|
652
|
+
fitText(renameLine, width),
|
|
653
|
+
this.theme.color("dim", fitText("[Enter] Save [Esc] Cancel Rename", width)),
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (this.confirmation) {
|
|
658
|
+
const confirmLine = this.confirmation.input.render(width)[0] ?? "";
|
|
659
|
+
return [
|
|
660
|
+
this.theme.color("warning", fitText(this.confirmation.prompt, width)),
|
|
661
|
+
fitText(confirmLine, width),
|
|
662
|
+
this.theme.color("dim", fitText("Type 'yes', then [Enter] to confirm. [Esc] Cancel", width)),
|
|
663
|
+
];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (this.sortMenuOpen) {
|
|
667
|
+
return [
|
|
668
|
+
this.theme.color("dim", fitText("Use ↑↓ to select sort order, [Enter] to apply", width)),
|
|
669
|
+
...this.buildSortMenuLines(width),
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (this.message) {
|
|
674
|
+
const slot = this.message.level === "error" ? "error" : this.message.level === "warning" ? "warning" : "success";
|
|
675
|
+
return wrapText(this.message.text, width).map((line) => this.theme.color(slot, fitText(line, width)));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return [
|
|
679
|
+
this.theme.color("dim", fitText(" NAVIGATION: [↑↓] Select Item [Esc] Close Modal", width)),
|
|
680
|
+
this.theme.color("dim", fitText(" ACTIONS: [Enter] Apply [s] Save [r] Rename [Del] Delete [Ctrl+U] Update", width)),
|
|
681
|
+
];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private getSortedProfiles(): SavedProfile[] {
|
|
685
|
+
return sortProfiles(this.data, this.currentSortOrder).sortedProfiles;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private getSelectedProfile(): SavedProfile | null {
|
|
689
|
+
const selected = this.getSortedProfiles().find((profile) => profile.id === this.selectedProfileId);
|
|
690
|
+
return selected ?? null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private resolvePaneContentRows(agentCount: number, footerRows: number): number {
|
|
694
|
+
const hasTerminalRows =
|
|
695
|
+
typeof process.stdout.rows === "number" && Number.isFinite(process.stdout.rows) && process.stdout.rows > 0;
|
|
696
|
+
if (!hasTerminalRows) {
|
|
697
|
+
return Math.max(MODAL_MIN_HEIGHT - footerRows - 7, MODAL_FALLBACK_VIEWPORT);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return Math.max(8, calculateModalHeight(agentCount) - footerRows - 7);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private getSnapshotViewportRows(): number {
|
|
704
|
+
return Math.max(1, this.lastPaneContentRows);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private ensureSelectedVisible(viewportSize: number): void {
|
|
708
|
+
const profiles = this.getSortedProfiles();
|
|
709
|
+
if (profiles.length === 0) {
|
|
710
|
+
this.listScrollOffset = 0;
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const selectedIndex = Math.max(0, profiles.findIndex((profile) => profile.id === this.selectedProfileId));
|
|
715
|
+
if (selectedIndex < this.listScrollOffset) {
|
|
716
|
+
this.listScrollOffset = selectedIndex;
|
|
717
|
+
}
|
|
718
|
+
if (selectedIndex >= this.listScrollOffset + viewportSize) {
|
|
719
|
+
this.listScrollOffset = selectedIndex - viewportSize + 1;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private moveSelection(delta: number): void {
|
|
724
|
+
const profiles = this.getSortedProfiles();
|
|
725
|
+
if (profiles.length === 0) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const currentIndex = Math.max(0, profiles.findIndex((profile) => profile.id === this.selectedProfileId));
|
|
730
|
+
const nextIndex = clamp(currentIndex + delta, 0, profiles.length - 1);
|
|
731
|
+
this.selectedProfileId = profiles[nextIndex]?.id ?? null;
|
|
732
|
+
this.detailScrollOffset = 0;
|
|
733
|
+
this.message = null;
|
|
734
|
+
this.ensureSelectedVisible(this.getSnapshotViewportRows());
|
|
735
|
+
this.requestRender();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private moveSelectionToBoundary(boundary: "start" | "end"): void {
|
|
739
|
+
const profiles = this.getSortedProfiles();
|
|
740
|
+
if (profiles.length === 0) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
this.selectedProfileId = boundary === "start" ? profiles[0]?.id ?? null : profiles[profiles.length - 1]?.id ?? null;
|
|
744
|
+
this.detailScrollOffset = 0;
|
|
745
|
+
this.message = null;
|
|
746
|
+
this.ensureSelectedVisible(this.getSnapshotViewportRows());
|
|
747
|
+
this.requestRender();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private startRename(): void {
|
|
751
|
+
const profile = this.getSelectedProfile();
|
|
752
|
+
if (!profile) {
|
|
753
|
+
this.message = { text: "Select a saved snapshot before renaming.", level: "warning" };
|
|
754
|
+
this.requestRender();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const input = new Input();
|
|
759
|
+
input.focused = true;
|
|
760
|
+
input.setValue(profile.name);
|
|
761
|
+
input.onSubmit = (value: string) => {
|
|
762
|
+
const targetId = this.renameTargetId;
|
|
763
|
+
this.renameInput = null;
|
|
764
|
+
this.renameTargetId = null;
|
|
765
|
+
if (!targetId) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
this.runAction(`Renaming '${profile.name}'...`, async () => {
|
|
769
|
+
const result = await this.actions.renameProfile(targetId, value);
|
|
770
|
+
this.data = result.data;
|
|
771
|
+
this.selectedProfileId = result.selectedProfileId ?? targetId;
|
|
772
|
+
this.message = { text: result.message, level: "info" };
|
|
773
|
+
});
|
|
774
|
+
};
|
|
775
|
+
input.onEscape = () => {
|
|
776
|
+
this.renameInput = null;
|
|
777
|
+
this.renameTargetId = null;
|
|
778
|
+
this.message = { text: "Rename cancelled.", level: "info" };
|
|
779
|
+
this.requestRender();
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
this.renameInput = input;
|
|
783
|
+
this.renameTargetId = profile.id;
|
|
784
|
+
this.message = null;
|
|
785
|
+
this.requestRender();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private startConfirmation(request: ConfirmationRequest): void {
|
|
789
|
+
const input = new Input();
|
|
790
|
+
input.focused = true;
|
|
791
|
+
input.setValue("");
|
|
792
|
+
input.onSubmit = (value: string) => {
|
|
793
|
+
const confirmation = this.confirmation;
|
|
794
|
+
this.confirmation = null;
|
|
795
|
+
|
|
796
|
+
if (
|
|
797
|
+
!confirmation ||
|
|
798
|
+
confirmation.action !== request.action ||
|
|
799
|
+
confirmation.profileId !== request.profile.id ||
|
|
800
|
+
value.trim().toLowerCase() !== "yes"
|
|
801
|
+
) {
|
|
802
|
+
this.message = { text: `${request.action === "remove" ? "Remove" : "Update"} cancelled.`, level: "info" };
|
|
803
|
+
this.requestRender();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
this.runAction(request.busyMessage, async () => request.onConfirm(confirmation.profileId));
|
|
808
|
+
};
|
|
809
|
+
input.onEscape = () => {
|
|
810
|
+
this.confirmation = null;
|
|
811
|
+
this.message = { text: `${request.action === "remove" ? "Remove" : "Update"} cancelled.`, level: "info" };
|
|
812
|
+
this.requestRender();
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
this.confirmation = {
|
|
816
|
+
action: request.action,
|
|
817
|
+
profileId: request.profile.id,
|
|
818
|
+
prompt: request.prompt,
|
|
819
|
+
input,
|
|
820
|
+
};
|
|
821
|
+
this.message = null;
|
|
822
|
+
this.requestRender();
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private startRemove(): void {
|
|
826
|
+
const profile = this.getSelectedProfile();
|
|
827
|
+
if (!profile) {
|
|
828
|
+
this.message = { text: "Select a saved snapshot before removing.", level: "warning" };
|
|
829
|
+
this.requestRender();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
this.startConfirmation({
|
|
834
|
+
action: "remove",
|
|
835
|
+
profile,
|
|
836
|
+
prompt: `Remove profile '${profile.name}'? This cannot be undone.`,
|
|
837
|
+
busyMessage: `Removing '${profile.name}'...`,
|
|
838
|
+
onConfirm: async (targetId) => {
|
|
839
|
+
const result = await this.actions.removeProfile(targetId);
|
|
840
|
+
this.data = result.data;
|
|
841
|
+
this.selectedProfileId = result.selectedProfileId ?? this.getSortedProfiles()[0]?.id ?? null;
|
|
842
|
+
this.detailScrollOffset = 0;
|
|
843
|
+
this.message = { text: result.message, level: "info" };
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private addCurrentProfile(): void {
|
|
849
|
+
const activeHint = this.activeAgentName ? ` from ${this.activeAgentName}` : "";
|
|
850
|
+
this.runAction(`Capturing current snapshot${activeHint}...`, async () => {
|
|
851
|
+
const result = await this.actions.addCurrentProfile();
|
|
852
|
+
this.data = result.data;
|
|
853
|
+
this.selectedProfileId = result.selectedProfileId ?? this.selectedProfileId;
|
|
854
|
+
this.detailScrollOffset = 0;
|
|
855
|
+
this.message = { text: result.message, level: "info" };
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private startUpdate(): void {
|
|
860
|
+
const profile = this.getSelectedProfile();
|
|
861
|
+
if (!profile) {
|
|
862
|
+
this.message = { text: "Select a saved snapshot before updating.", level: "warning" };
|
|
863
|
+
this.requestRender();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const currentAgentCount = profile.agents.length;
|
|
868
|
+
this.startConfirmation({
|
|
869
|
+
action: "update",
|
|
870
|
+
profile,
|
|
871
|
+
prompt: `Update '${profile.name}' with current agent state? This will overwrite ${currentAgentCount} agents.`,
|
|
872
|
+
busyMessage: `Updating '${profile.name}' with current agent state...`,
|
|
873
|
+
onConfirm: async (targetId) => {
|
|
874
|
+
const result = await this.actions.updateProfile(targetId);
|
|
875
|
+
this.data = result.data;
|
|
876
|
+
this.selectedProfileId = result.selectedProfileId ?? targetId;
|
|
877
|
+
this.detailScrollOffset = 0;
|
|
878
|
+
this.message = { text: result.message, level: "info" };
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private applySelectedProfile(): void {
|
|
884
|
+
const profile = this.getSelectedProfile();
|
|
885
|
+
if (!profile) {
|
|
886
|
+
this.message = { text: "Select a saved snapshot before applying.", level: "warning" };
|
|
887
|
+
this.requestRender();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
this.runAction(`Applying '${profile.name}' across saved agent files...`, async () => {
|
|
892
|
+
const outcome = await this.actions.applyProfile(profile.id);
|
|
893
|
+
this.finish({
|
|
894
|
+
type: "applied",
|
|
895
|
+
outcome,
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private runAction(label: string, action: () => Promise<void>): void {
|
|
901
|
+
if (this.busyMessage) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
this.busyMessage = label;
|
|
906
|
+
this.requestRender();
|
|
907
|
+
void action()
|
|
908
|
+
.catch((error: unknown) => {
|
|
909
|
+
this.message = { text: toErrorMessage(error), level: "error" };
|
|
910
|
+
})
|
|
911
|
+
.finally(() => {
|
|
912
|
+
this.busyMessage = null;
|
|
913
|
+
this.requestRender();
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private finish(result: ProfileModalResult): void {
|
|
918
|
+
if (this.finished) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
this.finished = true;
|
|
922
|
+
this.done(result);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private getSortMenuOptions(): SortMenuOption[] {
|
|
926
|
+
return getAvailableSortOrders().map((option) => ({
|
|
927
|
+
...option,
|
|
928
|
+
isSelected: this.currentSortOrder === option.order,
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private toggleSortMenu(): void {
|
|
933
|
+
if (this.sortMenuOpen) {
|
|
934
|
+
this.closeSortMenu();
|
|
935
|
+
} else {
|
|
936
|
+
this.openSortMenu();
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private openSortMenu(): void {
|
|
941
|
+
const options = this.getSortMenuOptions();
|
|
942
|
+
const currentIndex = options.findIndex((option) => option.order === this.currentSortOrder);
|
|
943
|
+
this.sortMenuOpen = true;
|
|
944
|
+
this.sortMenuSelectedIndex = Math.max(0, currentIndex);
|
|
945
|
+
this.requestRender();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private closeSortMenu(): void {
|
|
949
|
+
this.sortMenuOpen = false;
|
|
950
|
+
this.sortMenuSelectedIndex = 0;
|
|
951
|
+
this.requestRender();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
private handleSortMenuInput(data: string): void {
|
|
955
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
956
|
+
this.closeSortMenu();
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
961
|
+
this.sortMenuSelectedIndex = Math.max(0, this.sortMenuSelectedIndex - 1);
|
|
962
|
+
this.requestRender();
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
967
|
+
const lastIndex = Math.max(0, this.getSortMenuOptions().length - 1);
|
|
968
|
+
this.sortMenuSelectedIndex = Math.min(lastIndex, this.sortMenuSelectedIndex + 1);
|
|
969
|
+
this.requestRender();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (matchesKey(data, "return")) {
|
|
974
|
+
this.applySortFromMenu();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private applySortFromMenu(): void {
|
|
980
|
+
const options = this.getSortMenuOptions();
|
|
981
|
+
const selected = options[this.sortMenuSelectedIndex];
|
|
982
|
+
if (!selected) {
|
|
983
|
+
this.closeSortMenu();
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
this.currentSortOrder = selected.order;
|
|
988
|
+
persistSortOrder(selected.order);
|
|
989
|
+
this.detailScrollOffset = 0;
|
|
990
|
+
this.ensureSelectedVisible(this.getSnapshotViewportRows());
|
|
991
|
+
this.message = { text: `Sorted by ${selected.label}`, level: "info" };
|
|
992
|
+
this.closeSortMenu();
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
private buildSortMenuLines(width: number): string[] {
|
|
996
|
+
if (!this.sortMenuOpen) {
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const options = this.getSortMenuOptions();
|
|
1001
|
+
const menuWidth = clamp(width - 2, 32, 54);
|
|
1002
|
+
const innerWidth = Math.max(1, menuWidth - 2);
|
|
1003
|
+
const lines: string[] = [];
|
|
1004
|
+
const title = this.theme.color("accent", " SORT PROFILES ", { bold: true });
|
|
1005
|
+
const titleFill = Math.max(0, innerWidth - visibleWidth(title));
|
|
1006
|
+
lines.push(`${BOX.CORNER_TL}${title}${BOX.H_LINE.repeat(titleFill)}${BOX.CORNER_TR}`);
|
|
1007
|
+
|
|
1008
|
+
for (let i = 0; i < options.length; i++) {
|
|
1009
|
+
const option = options[i];
|
|
1010
|
+
const isSelected = i === this.sortMenuSelectedIndex;
|
|
1011
|
+
const marker = isSelected ? ">" : " ";
|
|
1012
|
+
const checkmark = option.isSelected ? "✓" : " ";
|
|
1013
|
+
const label = `${marker} [${checkmark}] ${option.label}`;
|
|
1014
|
+
const padded = fitText(label, innerWidth);
|
|
1015
|
+
const styled = isSelected
|
|
1016
|
+
? this.theme.color("selectedText", padded, { background: "selectedBg", bold: true })
|
|
1017
|
+
: this.theme.color("text", padded);
|
|
1018
|
+
lines.push(`${BOX.V_LINE}${styled}${BOX.V_LINE}`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const hint = "[Enter] Apply [Esc] Cancel";
|
|
1022
|
+
lines.push(`${BOX.V_LINE}${this.theme.color("dim", fitText(hint, innerWidth))}${BOX.V_LINE}`);
|
|
1023
|
+
lines.push(`${BOX.CORNER_BL}${BOX.H_LINE.repeat(innerWidth)}${BOX.CORNER_BR}`);
|
|
1024
|
+
|
|
1025
|
+
return lines.map((line) => centerLineInWidth(line, width));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export async function openProfilesModal(
|
|
1030
|
+
ctx: ExtensionCommandContext,
|
|
1031
|
+
data: ProfilesFile,
|
|
1032
|
+
activeAgentName: string | null,
|
|
1033
|
+
actions: ProfileModalActions,
|
|
1034
|
+
): Promise<ProfileModalResult> {
|
|
1035
|
+
const overlayOptions = resolveModalOverlayOptions(getMaximumContentWidth(data));
|
|
1036
|
+
|
|
1037
|
+
return await ctx.ui.custom<ProfileModalResult>(
|
|
1038
|
+
(
|
|
1039
|
+
tui: { requestRender(): void },
|
|
1040
|
+
theme: ThemeLike,
|
|
1041
|
+
_keybindings: unknown,
|
|
1042
|
+
done: (result: ProfileModalResult) => void,
|
|
1043
|
+
) => {
|
|
1044
|
+
const resolvedTheme = loadModalTheme(theme);
|
|
1045
|
+
const contentInstance = new ProfileListModal(data, resolvedTheme, actions, activeAgentName, done, () => tui.requestRender());
|
|
1046
|
+
|
|
1047
|
+
return {
|
|
1048
|
+
render(width: number): string[] {
|
|
1049
|
+
return contentInstance.render(width);
|
|
1050
|
+
},
|
|
1051
|
+
invalidate(): void {
|
|
1052
|
+
contentInstance.invalidate();
|
|
1053
|
+
},
|
|
1054
|
+
handleInput(input: string): void {
|
|
1055
|
+
contentInstance.handleInput(input);
|
|
1056
|
+
tui.requestRender();
|
|
1057
|
+
},
|
|
1058
|
+
};
|
|
1059
|
+
},
|
|
1060
|
+
{ overlay: true, overlayOptions },
|
|
1061
|
+
);
|
|
1062
|
+
}
|