@poncho-ai/cli 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/web-ui.ts CHANGED
@@ -429,7 +429,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
429
429
  <head>
430
430
  <meta charset="utf-8">
431
431
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
432
- <meta name="theme-color" content="#000000">
432
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
433
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
433
434
  <meta name="apple-mobile-web-app-capable" content="yes">
434
435
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
435
436
  <meta name="apple-mobile-web-app-title" content="${agentName}">
@@ -439,12 +440,168 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
439
440
  <title>${agentName}</title>
440
441
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata:400,700">
441
442
  <style>
443
+ :root {
444
+ color-scheme: light dark;
445
+
446
+ --bg: #000;
447
+ --bg-alt: #0a0a0a;
448
+ --bg-elevated: #111;
449
+
450
+ --fg: #ededed;
451
+ --fg-strong: #fff;
452
+ --fg-2: #888;
453
+ --fg-3: #999;
454
+ --fg-4: #777;
455
+ --fg-5: #666;
456
+ --fg-6: #555;
457
+ --fg-7: #444;
458
+ --fg-8: #333;
459
+
460
+ --fg-tool: #8a8a8a;
461
+ --fg-tool-code: #bcbcbc;
462
+ --fg-tool-item: #d6d6d6;
463
+ --fg-approval-label: #b0b0b0;
464
+ --fg-approval-input: #cfcfcf;
465
+ --fg-approval-btn: #f0f0f0;
466
+
467
+ --accent: #ededed;
468
+ --accent-fg: #000;
469
+ --accent-hover: #fff;
470
+
471
+ --stop-bg: #4a4a4a;
472
+ --stop-fg: #fff;
473
+ --stop-hover: #565656;
474
+
475
+ --border-1: rgba(255,255,255,0.06);
476
+ --border-2: rgba(255,255,255,0.08);
477
+ --border-3: rgba(255,255,255,0.1);
478
+ --border-4: rgba(255,255,255,0.12);
479
+ --border-5: rgba(255,255,255,0.18);
480
+ --border-focus: rgba(255,255,255,0.2);
481
+ --border-hover: rgba(255,255,255,0.25);
482
+ --border-drag: rgba(255,255,255,0.4);
483
+
484
+ --surface-1: rgba(255,255,255,0.02);
485
+ --surface-2: rgba(255,255,255,0.03);
486
+ --surface-3: rgba(255,255,255,0.04);
487
+ --surface-4: rgba(255,255,255,0.06);
488
+ --surface-5: rgba(255,255,255,0.08);
489
+ --surface-6: rgba(255,255,255,0.1);
490
+ --surface-7: rgba(255,255,255,0.12);
491
+ --surface-8: rgba(255,255,255,0.14);
492
+
493
+ --chip-bg: rgba(0,0,0,0.6);
494
+ --chip-bg-hover: rgba(0,0,0,0.75);
495
+ --backdrop: rgba(0,0,0,0.6);
496
+ --lightbox-bg: rgba(0,0,0,0.85);
497
+ --inset-1: rgba(0,0,0,0.16);
498
+ --inset-2: rgba(0,0,0,0.25);
499
+
500
+ --file-badge-bg: rgba(0,0,0,0.2);
501
+ --file-badge-fg: rgba(255,255,255,0.8);
502
+
503
+ --error: #ff4444;
504
+ --error-soft: #ff6b6b;
505
+ --error-alt: #ff6666;
506
+ --error-bg: rgba(255,68,68,0.08);
507
+ --error-border: rgba(255,68,68,0.25);
508
+
509
+ --tool-done: #6a9955;
510
+ --tool-error: #f48771;
511
+
512
+ --approve: #78e7a6;
513
+ --approve-border: rgba(58,208,122,0.45);
514
+ --deny: #f59b9b;
515
+ --deny-border: rgba(224,95,95,0.45);
516
+
517
+ --scrollbar: rgba(255,255,255,0.1);
518
+ --scrollbar-hover: rgba(255,255,255,0.16);
519
+ }
520
+
521
+ @media (prefers-color-scheme: light) {
522
+ :root {
523
+ --bg: #ffffff;
524
+ --bg-alt: #f5f5f5;
525
+ --bg-elevated: #e8e8e8;
526
+
527
+ --fg: #1a1a1a;
528
+ --fg-strong: #000;
529
+ --fg-2: #666;
530
+ --fg-3: #555;
531
+ --fg-4: #777;
532
+ --fg-5: #888;
533
+ --fg-6: #888;
534
+ --fg-7: #aaa;
535
+ --fg-8: #bbb;
536
+
537
+ --fg-tool: #666;
538
+ --fg-tool-code: #444;
539
+ --fg-tool-item: #333;
540
+ --fg-approval-label: #666;
541
+ --fg-approval-input: #444;
542
+ --fg-approval-btn: #1a1a1a;
543
+
544
+ --accent: #1a1a1a;
545
+ --accent-fg: #fff;
546
+ --accent-hover: #000;
547
+
548
+ --stop-bg: #d4d4d4;
549
+ --stop-fg: #333;
550
+ --stop-hover: #c4c4c4;
551
+
552
+ --border-1: rgba(0,0,0,0.06);
553
+ --border-2: rgba(0,0,0,0.08);
554
+ --border-3: rgba(0,0,0,0.1);
555
+ --border-4: rgba(0,0,0,0.1);
556
+ --border-5: rgba(0,0,0,0.15);
557
+ --border-focus: rgba(0,0,0,0.2);
558
+ --border-hover: rgba(0,0,0,0.2);
559
+ --border-drag: rgba(0,0,0,0.3);
560
+
561
+ --surface-1: rgba(0,0,0,0.02);
562
+ --surface-2: rgba(0,0,0,0.03);
563
+ --surface-3: rgba(0,0,0,0.03);
564
+ --surface-4: rgba(0,0,0,0.04);
565
+ --surface-5: rgba(0,0,0,0.05);
566
+ --surface-6: rgba(0,0,0,0.07);
567
+ --surface-7: rgba(0,0,0,0.08);
568
+ --surface-8: rgba(0,0,0,0.1);
569
+
570
+ --chip-bg: rgba(255,255,255,0.8);
571
+ --chip-bg-hover: rgba(255,255,255,0.9);
572
+ --backdrop: rgba(0,0,0,0.3);
573
+ --lightbox-bg: rgba(0,0,0,0.75);
574
+ --inset-1: rgba(0,0,0,0.04);
575
+ --inset-2: rgba(0,0,0,0.06);
576
+
577
+ --file-badge-bg: rgba(0,0,0,0.05);
578
+ --file-badge-fg: rgba(0,0,0,0.7);
579
+
580
+ --error: #dc2626;
581
+ --error-soft: #ef4444;
582
+ --error-alt: #ef4444;
583
+ --error-bg: rgba(220,38,38,0.06);
584
+ --error-border: rgba(220,38,38,0.2);
585
+
586
+ --tool-done: #16a34a;
587
+ --tool-error: #dc2626;
588
+
589
+ --approve: #16a34a;
590
+ --approve-border: rgba(22,163,74,0.35);
591
+ --deny: #dc2626;
592
+ --deny-border: rgba(220,38,38,0.3);
593
+
594
+ --scrollbar: rgba(0,0,0,0.12);
595
+ --scrollbar-hover: rgba(0,0,0,0.2);
596
+ }
597
+ }
598
+
442
599
  * { box-sizing: border-box; margin: 0; padding: 0; }
443
600
  html, body { height: 100vh; overflow: hidden; overscroll-behavior: none; touch-action: pan-y; }
444
601
  body {
445
602
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif;
446
- background: #000;
447
- color: #ededed;
603
+ background: var(--bg);
604
+ color: var(--fg);
448
605
  font-size: 14px;
449
606
  line-height: 1.5;
450
607
  -webkit-font-smoothing: antialiased;
@@ -452,7 +609,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
452
609
  }
453
610
  button, input, textarea { font: inherit; color: inherit; }
454
611
  .hidden { display: none !important; }
455
- a { color: #ededed; }
612
+ a { color: var(--fg); }
456
613
 
457
614
  /* Auth */
458
615
  .auth {
@@ -460,39 +617,39 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
460
617
  display: grid;
461
618
  place-items: center;
462
619
  padding: 20px;
463
- background: #000;
620
+ background: var(--bg);
464
621
  }
465
622
  .auth-card {
466
623
  width: min(400px, 90vw);
467
624
  }
468
625
  .auth-shell {
469
- background: #0a0a0a;
470
- border: 1px solid rgba(255,255,255,0.1);
626
+ background: var(--bg-alt);
627
+ border: 1px solid var(--border-3);
471
628
  border-radius: 9999px;
472
629
  display: flex;
473
630
  align-items: center;
474
631
  padding: 4px 6px 4px 18px;
475
632
  transition: border-color 0.15s;
476
633
  }
477
- .auth-shell:focus-within { border-color: rgba(255,255,255,0.2); }
634
+ .auth-shell:focus-within { border-color: var(--border-focus); }
478
635
  .auth-input {
479
636
  flex: 1;
480
637
  background: transparent;
481
638
  border: 0;
482
639
  outline: none;
483
- color: #ededed;
640
+ color: var(--fg);
484
641
  padding: 10px 0 8px;
485
642
  font-size: 14px;
486
643
  margin-top: -2px;
487
644
  }
488
- .auth-input::placeholder { color: #444; }
645
+ .auth-input::placeholder { color: var(--fg-7); }
489
646
  .auth-submit {
490
647
  width: 32px;
491
648
  height: 32px;
492
- background: #ededed;
649
+ background: var(--accent);
493
650
  border: 0;
494
651
  border-radius: 50%;
495
- color: #000;
652
+ color: var(--accent-fg);
496
653
  cursor: pointer;
497
654
  display: grid;
498
655
  place-items: center;
@@ -500,19 +657,19 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
500
657
  margin-bottom: 2px;
501
658
  transition: background 0.15s;
502
659
  }
503
- .auth-submit:hover { background: #fff; }
504
- .error { color: #ff4444; font-size: 13px; min-height: 16px; }
660
+ .auth-submit:hover { background: var(--accent-hover); }
661
+ .error { color: var(--error); font-size: 13px; min-height: 16px; }
505
662
  .message-error {
506
- background: rgba(255,68,68,0.08);
507
- border: 1px solid rgba(255,68,68,0.25);
663
+ background: var(--error-bg);
664
+ border: 1px solid var(--error-border);
508
665
  border-radius: 10px;
509
- color: #ff6b6b;
666
+ color: var(--error-soft);
510
667
  padding: 12px 16px;
511
668
  font-size: 13px;
512
669
  line-height: 1.5;
513
670
  max-width: 600px;
514
671
  }
515
- .message-error strong { color: #ff4444; }
672
+ .message-error strong { color: var(--error); }
516
673
 
517
674
  /* Layout - use fixed positioning with explicit dimensions */
518
675
  .shell {
@@ -544,8 +701,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
544
701
  }
545
702
  .sidebar {
546
703
  width: 260px;
547
- background: #000;
548
- border-right: 1px solid rgba(255,255,255,0.06);
704
+ background: var(--bg);
705
+ border-right: 1px solid var(--border-1);
549
706
  display: flex;
550
707
  flex-direction: column;
551
708
  padding: 12px 8px;
@@ -553,7 +710,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
553
710
  .new-chat-btn {
554
711
  background: transparent;
555
712
  border: 0;
556
- color: #888;
713
+ color: var(--fg-2);
557
714
  border-radius: 12px;
558
715
  height: 36px;
559
716
  padding: 0 10px;
@@ -564,7 +721,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
564
721
  cursor: pointer;
565
722
  transition: background 0.15s, color 0.15s;
566
723
  }
567
- .new-chat-btn:hover { color: #ededed; }
724
+ .new-chat-btn:hover { color: var(--fg); }
568
725
  .new-chat-btn svg { width: 16px; height: 16px; }
569
726
  .conversation-list {
570
727
  flex: 1;
@@ -584,16 +741,16 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
584
741
  cursor: pointer;
585
742
  font-size: 13px;
586
743
  line-height: 36px;
587
- color: #555;
744
+ color: var(--fg-6);
588
745
  white-space: nowrap;
589
746
  overflow: hidden;
590
747
  text-overflow: ellipsis;
591
748
  position: relative;
592
749
  transition: color 0.15s;
593
750
  }
594
- .conversation-item:hover { color: #999; }
751
+ .conversation-item:hover { color: var(--fg-3); }
595
752
  .conversation-item.active {
596
- color: #ededed;
753
+ color: var(--fg);
597
754
  }
598
755
  .conversation-item .delete-btn {
599
756
  position: absolute;
@@ -601,9 +758,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
601
758
  top: 0;
602
759
  bottom: 0;
603
760
  opacity: 0;
604
- background: #000;
761
+ background: var(--bg);
605
762
  border: 0;
606
- color: #444;
763
+ color: var(--fg-7);
607
764
  padding: 0 8px;
608
765
  border-radius: 0 4px 4px 0;
609
766
  cursor: pointer;
@@ -614,7 +771,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
614
771
  transition: opacity 0.15s, color 0.15s;
615
772
  }
616
773
  .conversation-item:hover .delete-btn { opacity: 1; }
617
- .conversation-item.active .delete-btn { background: rgba(0,0,0,1); }
774
+ .conversation-item.active .delete-btn { background: var(--bg); }
618
775
  .conversation-item .delete-btn::before {
619
776
  content: "";
620
777
  position: absolute;
@@ -622,23 +779,23 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
622
779
  top: 0;
623
780
  bottom: 0;
624
781
  width: 24px;
625
- background: linear-gradient(to right, transparent, #000);
782
+ background: linear-gradient(to right, transparent, var(--bg));
626
783
  pointer-events: none;
627
784
  }
628
785
  .conversation-item.active .delete-btn::before {
629
- background: linear-gradient(to right, transparent, rgba(0,0,0,1));
786
+ background: linear-gradient(to right, transparent, var(--bg));
630
787
  }
631
- .conversation-item .delete-btn:hover { color: #888; }
788
+ .conversation-item .delete-btn:hover { color: var(--fg-2); }
632
789
  .conversation-item .delete-btn.confirming {
633
790
  opacity: 1;
634
791
  width: auto;
635
792
  padding: 0 8px;
636
793
  font-size: 11px;
637
- color: #ff4444;
794
+ color: var(--error);
638
795
  border-radius: 3px;
639
796
  }
640
797
  .conversation-item .delete-btn.confirming:hover {
641
- color: #ff6666;
798
+ color: var(--error-alt);
642
799
  }
643
800
  .sidebar-footer {
644
801
  margin-top: auto;
@@ -647,7 +804,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
647
804
  .logout-btn {
648
805
  background: transparent;
649
806
  border: 0;
650
- color: #555;
807
+ color: var(--fg-6);
651
808
  width: 100%;
652
809
  padding: 8px 10px;
653
810
  text-align: left;
@@ -656,10 +813,10 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
656
813
  font-size: 13px;
657
814
  transition: color 0.15s, background 0.15s;
658
815
  }
659
- .logout-btn:hover { color: #888; }
816
+ .logout-btn:hover { color: var(--fg-2); }
660
817
 
661
818
  /* Main */
662
- .main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: #000; overflow: hidden; }
819
+ .main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: var(--bg); overflow: hidden; }
663
820
  .topbar {
664
821
  height: calc(52px + env(safe-area-inset-top, 0px));
665
822
  padding-top: env(safe-area-inset-top, 0px);
@@ -668,8 +825,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
668
825
  justify-content: center;
669
826
  font-size: 13px;
670
827
  font-weight: 500;
671
- color: #888;
672
- border-bottom: 1px solid rgba(255,255,255,0.06);
828
+ color: var(--fg-2);
829
+ border-bottom: 1px solid var(--border-1);
673
830
  position: relative;
674
831
  flex-shrink: 0;
675
832
  }
@@ -688,7 +845,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
688
845
  bottom: 4px; /* Position from bottom of topbar content area */
689
846
  background: transparent;
690
847
  border: 0;
691
- color: #666;
848
+ color: var(--fg-5);
692
849
  width: 44px;
693
850
  height: 44px;
694
851
  border-radius: 6px;
@@ -698,7 +855,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
698
855
  z-index: 10;
699
856
  -webkit-tap-highlight-color: transparent;
700
857
  }
701
- .sidebar-toggle:hover { color: #ededed; }
858
+ .sidebar-toggle:hover { color: var(--fg); }
702
859
  .topbar-new-chat {
703
860
  display: none;
704
861
  position: absolute;
@@ -706,7 +863,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
706
863
  bottom: 4px;
707
864
  background: transparent;
708
865
  border: 0;
709
- color: #666;
866
+ color: var(--fg-5);
710
867
  width: 44px;
711
868
  height: 44px;
712
869
  border-radius: 6px;
@@ -715,7 +872,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
715
872
  z-index: 10;
716
873
  -webkit-tap-highlight-color: transparent;
717
874
  }
718
- .topbar-new-chat:hover { color: #ededed; }
875
+ .topbar-new-chat:hover { color: var(--fg); }
719
876
  .topbar-new-chat svg { width: 16px; height: 16px; }
720
877
 
721
878
  /* Messages */
@@ -727,8 +884,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
727
884
  .assistant-avatar {
728
885
  width: 24px;
729
886
  height: 24px;
730
- background: #ededed;
731
- color: #000;
887
+ background: var(--accent);
888
+ color: var(--accent-fg);
732
889
  border-radius: 6px;
733
890
  display: grid;
734
891
  place-items: center;
@@ -739,7 +896,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
739
896
  }
740
897
  .assistant-content {
741
898
  line-height: 1.65;
742
- color: #ededed;
899
+ color: var(--fg);
743
900
  font-size: 14px;
744
901
  min-width: 0;
745
902
  max-width: 100%;
@@ -751,32 +908,32 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
751
908
  .assistant-content p:last-child { margin-bottom: 0; }
752
909
  .assistant-content ul, .assistant-content ol { margin: 8px 0; padding-left: 20px; }
753
910
  .assistant-content li { margin: 4px 0; }
754
- .assistant-content strong { font-weight: 600; color: #fff; }
911
+ .assistant-content strong { font-weight: 600; color: var(--fg-strong); }
755
912
  .assistant-content h2 {
756
913
  font-size: 16px;
757
914
  font-weight: 600;
758
915
  letter-spacing: -0.02em;
759
916
  margin: 20px 0 8px;
760
- color: #fff;
917
+ color: var(--fg-strong);
761
918
  }
762
919
  .assistant-content h3 {
763
920
  font-size: 14px;
764
921
  font-weight: 600;
765
922
  letter-spacing: -0.01em;
766
923
  margin: 16px 0 6px;
767
- color: #fff;
924
+ color: var(--fg-strong);
768
925
  }
769
926
  .assistant-content code {
770
- background: rgba(255,255,255,0.06);
771
- border: 1px solid rgba(255,255,255,0.06);
927
+ background: var(--surface-4);
928
+ border: 1px solid var(--border-1);
772
929
  padding: 2px 5px;
773
930
  border-radius: 4px;
774
931
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
775
932
  font-size: 0.88em;
776
933
  }
777
934
  .assistant-content pre {
778
- background: #0a0a0a;
779
- border: 1px solid rgba(255,255,255,0.06);
935
+ background: var(--bg-alt);
936
+ border: 1px solid var(--border-1);
780
937
  padding: 14px 16px;
781
938
  border-radius: 8px;
782
939
  overflow-x: auto;
@@ -793,33 +950,33 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
793
950
  margin: 8px 0;
794
951
  font-size: 12px;
795
952
  line-height: 1.45;
796
- color: #8a8a8a;
953
+ color: var(--fg-tool);
797
954
  }
798
955
  .tool-activity-inline code {
799
956
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
800
- background: rgba(255,255,255,0.04);
801
- border: 1px solid rgba(255,255,255,0.08);
957
+ background: var(--surface-3);
958
+ border: 1px solid var(--border-2);
802
959
  padding: 4px 8px;
803
960
  border-radius: 6px;
804
- color: #bcbcbc;
961
+ color: var(--fg-tool-code);
805
962
  font-size: 11px;
806
963
  }
807
964
  .tool-status {
808
- color: #8a8a8a;
965
+ color: var(--fg-tool);
809
966
  font-style: italic;
810
967
  }
811
968
  .tool-done {
812
- color: #6a9955;
969
+ color: var(--tool-done);
813
970
  }
814
971
  .tool-error {
815
- color: #f48771;
972
+ color: var(--tool-error);
816
973
  }
817
974
  .assistant-content table {
818
975
  border-collapse: collapse;
819
976
  width: 100%;
820
977
  margin: 14px 0;
821
978
  font-size: 13px;
822
- border: 1px solid rgba(255,255,255,0.08);
979
+ border: 1px solid var(--border-2);
823
980
  border-radius: 8px;
824
981
  overflow: hidden;
825
982
  display: block;
@@ -828,17 +985,17 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
828
985
  white-space: nowrap;
829
986
  }
830
987
  .assistant-content th {
831
- background: rgba(255,255,255,0.06);
988
+ background: var(--surface-4);
832
989
  padding: 10px 12px;
833
990
  text-align: left;
834
991
  font-weight: 600;
835
- border-bottom: 1px solid rgba(255,255,255,0.12);
836
- color: #fff;
992
+ border-bottom: 1px solid var(--border-4);
993
+ color: var(--fg-strong);
837
994
  min-width: 100px;
838
995
  }
839
996
  .assistant-content td {
840
997
  padding: 10px 12px;
841
- border-bottom: 1px solid rgba(255,255,255,0.06);
998
+ border-bottom: 1px solid var(--border-1);
842
999
  width: 100%;
843
1000
  min-width: 100px;
844
1001
  }
@@ -846,22 +1003,22 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
846
1003
  border-bottom: none;
847
1004
  }
848
1005
  .assistant-content tbody tr:hover {
849
- background: rgba(255,255,255,0.02);
1006
+ background: var(--surface-1);
850
1007
  }
851
1008
  .assistant-content hr {
852
1009
  border: 0;
853
- border-top: 1px solid rgba(255,255,255,0.1);
1010
+ border-top: 1px solid var(--border-3);
854
1011
  margin: 20px 0;
855
1012
  }
856
1013
  .tool-activity {
857
1014
  margin-top: 12px;
858
1015
  margin-bottom: 12px;
859
- border: 1px solid rgba(255,255,255,0.08);
860
- background: rgba(255,255,255,0.03);
1016
+ border: 1px solid var(--border-2);
1017
+ background: var(--surface-2);
861
1018
  border-radius: 10px;
862
1019
  font-size: 12px;
863
1020
  line-height: 1.45;
864
- color: #bcbcbc;
1021
+ color: var(--fg-tool-code);
865
1022
  width: 300px;
866
1023
  }
867
1024
  .assistant-content > .tool-activity:first-child {
@@ -886,12 +1043,12 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
886
1043
  font-size: 11px;
887
1044
  text-transform: uppercase;
888
1045
  letter-spacing: 0.06em;
889
- color: #8a8a8a;
1046
+ color: var(--fg-tool);
890
1047
  font-weight: 600;
891
1048
  }
892
1049
  .tool-activity-caret {
893
1050
  margin-left: auto;
894
- color: #8a8a8a;
1051
+ color: var(--fg-tool);
895
1052
  display: inline-flex;
896
1053
  align-items: center;
897
1054
  justify-content: center;
@@ -913,28 +1070,28 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
913
1070
  }
914
1071
  .tool-activity-item {
915
1072
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
916
- background: rgba(255,255,255,0.04);
1073
+ background: var(--surface-3);
917
1074
  border-radius: 6px;
918
1075
  padding: 4px 7px;
919
- color: #d6d6d6;
1076
+ color: var(--fg-tool-item);
920
1077
  }
921
1078
  .approval-requests {
922
- border-top: 1px solid rgba(255,255,255,0.08);
1079
+ border-top: 1px solid var(--border-2);
923
1080
  padding: 10px 12px 12px;
924
1081
  display: grid;
925
1082
  gap: 8px;
926
- background: rgba(0,0,0,0.16);
1083
+ background: var(--inset-1);
927
1084
  }
928
1085
  .approval-requests-label {
929
1086
  font-size: 11px;
930
1087
  text-transform: uppercase;
931
1088
  letter-spacing: 0.06em;
932
- color: #b0b0b0;
1089
+ color: var(--fg-approval-label);
933
1090
  font-weight: 600;
934
1091
  }
935
1092
  .approval-request-item {
936
- border: 1px solid rgba(255,255,255,0.1);
937
- background: rgba(255,255,255,0.03);
1093
+ border: 1px solid var(--border-3);
1094
+ background: var(--surface-2);
938
1095
  border-radius: 8px;
939
1096
  padding: 8px;
940
1097
  display: grid;
@@ -942,15 +1099,15 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
942
1099
  }
943
1100
  .approval-request-tool {
944
1101
  font-size: 12px;
945
- color: #fff;
1102
+ color: var(--fg-strong);
946
1103
  font-weight: 600;
947
1104
  overflow-wrap: anywhere;
948
1105
  }
949
1106
  .approval-request-input {
950
1107
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
951
1108
  font-size: 11px;
952
- color: #cfcfcf;
953
- background: rgba(0,0,0,0.25);
1109
+ color: var(--fg-approval-input);
1110
+ background: var(--inset-2);
954
1111
  border-radius: 6px;
955
1112
  padding: 6px;
956
1113
  overflow-wrap: anywhere;
@@ -963,32 +1120,32 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
963
1120
  }
964
1121
  .approval-action-btn {
965
1122
  border-radius: 6px;
966
- border: 1px solid rgba(255,255,255,0.18);
967
- background: rgba(255,255,255,0.06);
968
- color: #f0f0f0;
1123
+ border: 1px solid var(--border-5);
1124
+ background: var(--surface-4);
1125
+ color: var(--fg-approval-btn);
969
1126
  font-size: 11px;
970
1127
  font-weight: 600;
971
1128
  padding: 4px 8px;
972
1129
  cursor: pointer;
973
1130
  }
974
1131
  .approval-action-btn:hover {
975
- background: rgba(255,255,255,0.12);
1132
+ background: var(--surface-7);
976
1133
  }
977
1134
  .approval-action-btn.approve {
978
- border-color: rgba(58, 208, 122, 0.45);
979
- color: #78e7a6;
1135
+ border-color: var(--approve-border);
1136
+ color: var(--approve);
980
1137
  }
981
1138
  .approval-action-btn.deny {
982
- border-color: rgba(224, 95, 95, 0.45);
983
- color: #f59b9b;
1139
+ border-color: var(--deny-border);
1140
+ color: var(--deny);
984
1141
  }
985
1142
  .approval-action-btn[disabled] {
986
1143
  opacity: 0.55;
987
1144
  cursor: not-allowed;
988
1145
  }
989
1146
  .user-bubble {
990
- background: #111;
991
- border: 1px solid rgba(255,255,255,0.08);
1147
+ background: var(--bg-elevated);
1148
+ border: 1px solid var(--border-2);
992
1149
  padding: 10px 16px;
993
1150
  border-radius: 18px;
994
1151
  max-width: 70%;
@@ -1004,7 +1161,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1004
1161
  justify-content: center;
1005
1162
  height: 100%;
1006
1163
  gap: 16px;
1007
- color: #555;
1164
+ color: var(--fg-6);
1008
1165
  }
1009
1166
  .empty-state .assistant-avatar {
1010
1167
  width: 36px;
@@ -1014,7 +1171,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1014
1171
  }
1015
1172
  .empty-state-text {
1016
1173
  font-size: 14px;
1017
- color: #555;
1174
+ color: var(--fg-6);
1018
1175
  }
1019
1176
  .thinking-indicator {
1020
1177
  display: inline-block;
@@ -1022,7 +1179,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1022
1179
  font-size: 20px;
1023
1180
  line-height: 1;
1024
1181
  vertical-align: middle;
1025
- color: #ededed;
1182
+ color: var(--fg);
1026
1183
  opacity: 0.5;
1027
1184
  }
1028
1185
  .thinking-status {
@@ -1030,13 +1187,13 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1030
1187
  align-items: center;
1031
1188
  gap: 8px;
1032
1189
  margin-top: 2px;
1033
- color: #8a8a8a;
1190
+ color: var(--fg-tool);
1034
1191
  font-size: 14px;
1035
1192
  line-height: 1.65;
1036
1193
  font-weight: 400;
1037
1194
  }
1038
1195
  .thinking-status-label {
1039
- color: #8a8a8a;
1196
+ color: var(--fg-tool);
1040
1197
  font-size: 14px;
1041
1198
  line-height: 1.65;
1042
1199
  font-weight: 400;
@@ -1067,26 +1224,26 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1067
1224
  right: 0;
1068
1225
  bottom: 100%;
1069
1226
  height: 48px;
1070
- background: linear-gradient(to top, #000 0%, transparent 100%);
1227
+ background: linear-gradient(to top, var(--bg) 0%, transparent 100%);
1071
1228
  pointer-events: none;
1072
1229
  }
1073
1230
  .composer-inner { max-width: 680px; margin: 0 auto; }
1074
1231
  .composer-shell {
1075
- background: #0a0a0a;
1076
- border: 1px solid rgba(255,255,255,0.1);
1232
+ background: var(--bg-alt);
1233
+ border: 1px solid var(--border-3);
1077
1234
  border-radius: 24px;
1078
1235
  display: flex;
1079
1236
  align-items: end;
1080
1237
  padding: 4px 6px 4px 6px;
1081
1238
  transition: border-color 0.15s;
1082
1239
  }
1083
- .composer-shell:focus-within { border-color: rgba(255,255,255,0.2); }
1240
+ .composer-shell:focus-within { border-color: var(--border-focus); }
1084
1241
  .composer-input {
1085
1242
  flex: 1;
1086
1243
  background: transparent;
1087
1244
  border: 0;
1088
1245
  outline: none;
1089
- color: #ededed;
1246
+ color: var(--fg);
1090
1247
  min-height: 40px;
1091
1248
  max-height: 200px;
1092
1249
  resize: none;
@@ -1095,14 +1252,14 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1095
1252
  line-height: 1.5;
1096
1253
  margin-top: -4px;
1097
1254
  }
1098
- .composer-input::placeholder { color: #444; }
1255
+ .composer-input::placeholder { color: var(--fg-7); }
1099
1256
  .send-btn {
1100
1257
  width: 32px;
1101
1258
  height: 32px;
1102
- background: #ededed;
1259
+ background: var(--accent);
1103
1260
  border: 0;
1104
1261
  border-radius: 50%;
1105
- color: #000;
1262
+ color: var(--accent-fg);
1106
1263
  cursor: pointer;
1107
1264
  display: grid;
1108
1265
  place-items: center;
@@ -1110,21 +1267,79 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1110
1267
  margin-bottom: 2px;
1111
1268
  transition: background 0.15s, opacity 0.15s;
1112
1269
  }
1113
- .send-btn:hover { background: #fff; }
1270
+ .send-btn:hover { background: var(--accent-hover); }
1114
1271
  .send-btn.stop-mode {
1115
- background: #4a4a4a;
1116
- color: #fff;
1272
+ background: var(--stop-bg);
1273
+ color: var(--stop-fg);
1117
1274
  }
1118
- .send-btn.stop-mode:hover { background: #565656; }
1275
+ .send-btn.stop-mode:hover { background: var(--stop-hover); }
1119
1276
  .send-btn:disabled { opacity: 0.2; cursor: default; }
1120
- .send-btn:disabled:hover { background: #ededed; }
1277
+ .send-btn:disabled:hover { background: var(--accent); }
1278
+ .send-btn-wrapper {
1279
+ position: relative;
1280
+ width: 36px;
1281
+ height: 36px;
1282
+ display: grid;
1283
+ place-items: center;
1284
+ flex-shrink: 0;
1285
+ margin-bottom: 0;
1286
+ }
1287
+ .send-btn-wrapper .send-btn {
1288
+ margin-bottom: 0;
1289
+ }
1290
+ .context-ring {
1291
+ position: absolute;
1292
+ inset: 0;
1293
+ width: 36px;
1294
+ height: 36px;
1295
+ pointer-events: none;
1296
+ transform: rotate(-90deg);
1297
+ }
1298
+ .context-ring-fill {
1299
+ fill: none;
1300
+ stroke: var(--bg-alt);
1301
+ stroke-width: 3;
1302
+ stroke-linecap: butt;
1303
+ transition: stroke-dashoffset 0.4s ease, stroke 0.3s ease;
1304
+ }
1305
+ .send-btn-wrapper.stop-mode .context-ring-fill {
1306
+ stroke: var(--fg-3);
1307
+ }
1308
+ .context-ring-fill.warning {
1309
+ stroke: #e5a33d;
1310
+ }
1311
+ .context-ring-fill.critical {
1312
+ stroke: #e55d4a;
1313
+ }
1314
+ .context-tooltip {
1315
+ position: absolute;
1316
+ bottom: calc(100% + 8px);
1317
+ right: 0;
1318
+ background: var(--bg-elevated);
1319
+ border: 1px solid var(--border-3);
1320
+ border-radius: 8px;
1321
+ padding: 6px 10px;
1322
+ font-size: 12px;
1323
+ color: var(--fg-2);
1324
+ white-space: nowrap;
1325
+ pointer-events: none;
1326
+ opacity: 0;
1327
+ transform: translateY(4px);
1328
+ transition: opacity 0.15s, transform 0.15s;
1329
+ z-index: 10;
1330
+ }
1331
+ .send-btn-wrapper:hover .context-tooltip,
1332
+ .send-btn-wrapper:focus-within .context-tooltip {
1333
+ opacity: 1;
1334
+ transform: translateY(0);
1335
+ }
1121
1336
  .attach-btn {
1122
1337
  width: 32px;
1123
1338
  height: 32px;
1124
- background: rgba(255,255,255,0.08);
1339
+ background: var(--surface-5);
1125
1340
  border: 0;
1126
1341
  border-radius: 50%;
1127
- color: #999;
1342
+ color: var(--fg-3);
1128
1343
  cursor: pointer;
1129
1344
  display: grid;
1130
1345
  place-items: center;
@@ -1133,7 +1348,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1133
1348
  margin-right: 8px;
1134
1349
  transition: color 0.15s, background 0.15s;
1135
1350
  }
1136
- .attach-btn:hover { color: #ededed; background: rgba(255,255,255,0.14); }
1351
+ .attach-btn:hover { color: var(--fg); background: var(--surface-8); }
1137
1352
  .attachment-preview {
1138
1353
  display: flex;
1139
1354
  gap: 8px;
@@ -1144,12 +1359,12 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1144
1359
  display: inline-flex;
1145
1360
  align-items: center;
1146
1361
  gap: 6px;
1147
- background: rgba(0, 0, 0, 0.6);
1148
- border: 1px solid rgba(255, 255, 255, 0.12);
1362
+ background: var(--chip-bg);
1363
+ border: 1px solid var(--border-4);
1149
1364
  border-radius: 9999px;
1150
1365
  padding: 4px 10px 4px 6px;
1151
1366
  font-size: 11px;
1152
- color: #777;
1367
+ color: var(--fg-4);
1153
1368
  max-width: 200px;
1154
1369
  cursor: pointer;
1155
1370
  backdrop-filter: blur(6px);
@@ -1157,9 +1372,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1157
1372
  transition: color 0.15s, border-color 0.15s, background 0.15s;
1158
1373
  }
1159
1374
  .attachment-chip:hover {
1160
- color: #ededed;
1161
- border-color: rgba(255, 255, 255, 0.25);
1162
- background: rgba(0, 0, 0, 0.75);
1375
+ color: var(--fg);
1376
+ border-color: var(--border-hover);
1377
+ background: var(--chip-bg-hover);
1163
1378
  }
1164
1379
  .attachment-chip img {
1165
1380
  width: 20px;
@@ -1173,7 +1388,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1173
1388
  width: 20px;
1174
1389
  height: 20px;
1175
1390
  border-radius: 50%;
1176
- background: rgba(255,255,255,0.1);
1391
+ background: var(--surface-6);
1177
1392
  display: grid;
1178
1393
  place-items: center;
1179
1394
  font-size: 11px;
@@ -1181,13 +1396,13 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1181
1396
  }
1182
1397
  .attachment-chip .remove-attachment {
1183
1398
  cursor: pointer;
1184
- color: #555;
1399
+ color: var(--fg-6);
1185
1400
  font-size: 14px;
1186
1401
  margin-left: 2px;
1187
1402
  line-height: 1;
1188
1403
  transition: color 0.15s;
1189
1404
  }
1190
- .attachment-chip .remove-attachment:hover { color: #fff; }
1405
+ .attachment-chip .remove-attachment:hover { color: var(--fg-strong); }
1191
1406
  .attachment-chip .filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
1192
1407
  .user-bubble .user-file-attachments {
1193
1408
  display: flex;
@@ -1217,7 +1432,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1217
1432
  transition: background 0.25s ease, backdrop-filter 0.25s ease;
1218
1433
  }
1219
1434
  .lightbox.active {
1220
- background: rgba(0,0,0,0.85);
1435
+ background: var(--lightbox-bg);
1221
1436
  backdrop-filter: blur(8px);
1222
1437
  }
1223
1438
  .lightbox img {
@@ -1237,16 +1452,16 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1237
1452
  display: inline-flex;
1238
1453
  align-items: center;
1239
1454
  gap: 4px;
1240
- background: rgba(0,0,0,0.2);
1455
+ background: var(--file-badge-bg);
1241
1456
  border-radius: 6px;
1242
1457
  padding: 4px 8px;
1243
1458
  font-size: 12px;
1244
- color: rgba(255,255,255,0.8);
1459
+ color: var(--file-badge-fg);
1245
1460
  }
1246
1461
  .drag-overlay {
1247
1462
  position: fixed;
1248
1463
  inset: 0;
1249
- background: rgba(0,0,0,0.6);
1464
+ background: var(--backdrop);
1250
1465
  z-index: 9999;
1251
1466
  display: none;
1252
1467
  align-items: center;
@@ -1255,15 +1470,15 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1255
1470
  }
1256
1471
  .drag-overlay.active { display: flex; }
1257
1472
  .drag-overlay-inner {
1258
- border: 2px dashed rgba(255,255,255,0.4);
1473
+ border: 2px dashed var(--border-drag);
1259
1474
  border-radius: 16px;
1260
1475
  padding: 40px 60px;
1261
- color: #fff;
1476
+ color: var(--fg-strong);
1262
1477
  font-size: 16px;
1263
1478
  }
1264
1479
  .disclaimer {
1265
1480
  text-align: center;
1266
- color: #333;
1481
+ color: var(--fg-8);
1267
1482
  font-size: 12px;
1268
1483
  margin-top: 10px;
1269
1484
  }
@@ -1278,10 +1493,10 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1278
1493
  align-items: center;
1279
1494
  gap: 6px;
1280
1495
  font-size: 11px;
1281
- color: #777;
1496
+ color: var(--fg-4);
1282
1497
  text-decoration: none;
1283
- background: rgba(0, 0, 0, 0.6);
1284
- border: 1px solid rgba(255, 255, 255, 0.12);
1498
+ background: var(--chip-bg);
1499
+ border: 1px solid var(--border-4);
1285
1500
  border-radius: 9999px;
1286
1501
  padding: 4px 10px 4px 6px;
1287
1502
  backdrop-filter: blur(6px);
@@ -1289,9 +1504,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1289
1504
  transition: color 0.15s, border-color 0.15s, background 0.15s;
1290
1505
  }
1291
1506
  .poncho-badge:hover {
1292
- color: #ededed;
1293
- border-color: rgba(255, 255, 255, 0.25);
1294
- background: rgba(0, 0, 0, 0.75);
1507
+ color: var(--fg);
1508
+ border-color: var(--border-hover);
1509
+ background: var(--chip-bg-hover);
1295
1510
  }
1296
1511
  .poncho-badge-avatar {
1297
1512
  width: 16px;
@@ -1305,8 +1520,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1305
1520
  /* Scrollbar */
1306
1521
  ::-webkit-scrollbar { width: 6px; }
1307
1522
  ::-webkit-scrollbar-track { background: transparent; }
1308
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
1309
- ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
1523
+ ::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 3px; }
1524
+ ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); }
1310
1525
 
1311
1526
  /* Mobile */
1312
1527
  @media (max-width: 768px) {
@@ -1336,7 +1551,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1336
1551
  .sidebar-backdrop {
1337
1552
  position: fixed;
1338
1553
  inset: 0;
1339
- background: rgba(0,0,0,0.6);
1554
+ background: var(--backdrop);
1340
1555
  z-index: 50;
1341
1556
  backdrop-filter: blur(2px);
1342
1557
  -webkit-backdrop-filter: blur(2px);
@@ -1411,9 +1626,15 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1411
1626
  </button>
1412
1627
  <input id="file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
1413
1628
  <textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
1414
- <button id="send" class="send-btn" type="submit">
1415
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
1416
- </button>
1629
+ <div class="send-btn-wrapper" id="send-btn-wrapper">
1630
+ <svg class="context-ring" viewBox="0 0 36 36">
1631
+ <circle class="context-ring-fill" id="context-ring-fill" cx="18" cy="18" r="14.5" />
1632
+ </svg>
1633
+ <button id="send" class="send-btn" type="submit">
1634
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
1635
+ </button>
1636
+ <div class="context-tooltip" id="context-tooltip"></div>
1637
+ </div>
1417
1638
  </div>
1418
1639
  </div>
1419
1640
  </form>
@@ -1445,6 +1666,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1445
1666
  confirmDeleteId: null,
1446
1667
  approvalRequestsInFlight: {},
1447
1668
  pendingFiles: [],
1669
+ contextTokens: 0,
1670
+ contextWindow: 0,
1448
1671
  };
1449
1672
 
1450
1673
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -1472,12 +1695,41 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1472
1695
  attachmentPreview: $("attachment-preview"),
1473
1696
  dragOverlay: $("drag-overlay"),
1474
1697
  lightbox: $("lightbox"),
1698
+ contextRingFill: $("context-ring-fill"),
1699
+ contextTooltip: $("context-tooltip"),
1700
+ sendBtnWrapper: $("send-btn-wrapper"),
1475
1701
  };
1476
1702
  const sendIconMarkup =
1477
1703
  '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
1478
1704
  const stopIconMarkup =
1479
1705
  '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="4" y="4" width="8" height="8" rx="2" fill="currentColor"/></svg>';
1480
1706
 
1707
+ const CONTEXT_RING_CIRCUMFERENCE = 2 * Math.PI * 14.5;
1708
+ const formatTokenCount = (n) => {
1709
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\\.0$/, "") + "M";
1710
+ if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\\.0$/, "") + "k";
1711
+ return String(n);
1712
+ };
1713
+ const updateContextRing = () => {
1714
+ const ring = elements.contextRingFill;
1715
+ const tooltip = elements.contextTooltip;
1716
+ if (!ring || !tooltip) return;
1717
+ if (state.contextWindow <= 0) {
1718
+ ring.style.strokeDasharray = String(CONTEXT_RING_CIRCUMFERENCE);
1719
+ ring.style.strokeDashoffset = String(CONTEXT_RING_CIRCUMFERENCE);
1720
+ tooltip.textContent = "";
1721
+ return;
1722
+ }
1723
+ const ratio = Math.min(state.contextTokens / state.contextWindow, 1);
1724
+ const offset = CONTEXT_RING_CIRCUMFERENCE * (1 - ratio);
1725
+ ring.style.strokeDasharray = String(CONTEXT_RING_CIRCUMFERENCE);
1726
+ ring.style.strokeDashoffset = String(offset);
1727
+ ring.classList.toggle("warning", ratio >= 0.7 && ratio < 0.9);
1728
+ ring.classList.toggle("critical", ratio >= 0.9);
1729
+ const pct = (ratio * 100).toFixed(1).replace(/\\.0$/, "");
1730
+ tooltip.textContent = formatTokenCount(state.contextTokens) + " / " + formatTokenCount(state.contextWindow) + " tokens (" + pct + "%)";
1731
+ };
1732
+
1481
1733
  const pushConversationUrl = (conversationId) => {
1482
1734
  const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
1483
1735
  if (window.location.pathname !== target) {
@@ -1797,6 +2049,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1797
2049
  if (state.activeConversationId === c.conversationId) {
1798
2050
  state.activeConversationId = null;
1799
2051
  state.activeMessages = [];
2052
+ state.contextTokens = 0;
2053
+ state.contextWindow = 0;
2054
+ updateContextRing();
1800
2055
  pushConversationUrl(null);
1801
2056
  elements.chatTitle.textContent = "";
1802
2057
  renderMessages([]);
@@ -1893,8 +2148,14 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1893
2148
  } else if (shouldRenderEmptyStreamingIndicator) {
1894
2149
  content.appendChild(createThinkingIndicator(getThinkingStatusLabel(m)));
1895
2150
  } else {
1896
- // Check for sections in _sections (streaming) or metadata.sections (stored)
1897
- const sections = m._sections || (m.metadata && m.metadata.sections);
2151
+ // Merge stored sections (persisted) with live sections (from
2152
+ // an active stream). For normal messages only one source
2153
+ // exists; for liveOnly reconnects both contribute.
2154
+ const storedSections = (m.metadata && m.metadata.sections) || [];
2155
+ const liveSections = m._sections || [];
2156
+ const sections = liveSections.length > 0 && storedSections.length > 0
2157
+ ? storedSections.concat(liveSections)
2158
+ : liveSections.length > 0 ? liveSections : (storedSections.length > 0 ? storedSections : null);
1898
2159
  const pendingApprovals = Array.isArray(m._pendingApprovals) ? m._pendingApprovals : [];
1899
2160
 
1900
2161
  if (sections && sections.length > 0) {
@@ -2035,11 +2296,24 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2035
2296
  payload.conversation.messages || [],
2036
2297
  payload.conversation.pendingApprovals || payload.pendingApprovals || [],
2037
2298
  );
2299
+ state.contextTokens = 0;
2300
+ state.contextWindow = 0;
2301
+ updateContextRing();
2038
2302
  renderMessages(state.activeMessages, false, { forceScrollBottom: true });
2039
2303
  elements.prompt.focus();
2304
+ if (payload.hasActiveRun && !state.isStreaming) {
2305
+ setStreaming(true);
2306
+ streamConversationEvents(conversationId, { liveOnly: true }).finally(() => {
2307
+ if (state.activeConversationId === conversationId) {
2308
+ setStreaming(false);
2309
+ renderMessages(state.activeMessages, false);
2310
+ }
2311
+ });
2312
+ }
2040
2313
  };
2041
2314
 
2042
- const streamConversationEvents = (conversationId) => {
2315
+ const streamConversationEvents = (conversationId, options) => {
2316
+ const liveOnly = options && options.liveOnly;
2043
2317
  return new Promise((resolve) => {
2044
2318
  const localMessages = state.activeMessages || [];
2045
2319
  const renderIfActiveConversation = (streaming) => {
@@ -2058,20 +2332,36 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2058
2332
  _currentText: "",
2059
2333
  _currentTools: [],
2060
2334
  _pendingApprovals: [],
2335
+ _activeActivities: [],
2061
2336
  metadata: { toolActivity: [] },
2062
2337
  };
2063
2338
  localMessages.push(assistantMessage);
2064
2339
  state.activeMessages = localMessages;
2065
2340
  }
2066
- if (!assistantMessage._sections) assistantMessage._sections = [];
2067
- if (!assistantMessage._currentText) assistantMessage._currentText = "";
2068
- if (!assistantMessage._currentTools) assistantMessage._currentTools = [];
2069
- if (!assistantMessage._activeActivities) assistantMessage._activeActivities = [];
2070
- if (!assistantMessage._pendingApprovals) assistantMessage._pendingApprovals = [];
2071
- if (!assistantMessage.metadata) assistantMessage.metadata = {};
2072
- if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
2073
-
2074
- const url = "/api/conversations/" + encodeURIComponent(conversationId) + "/events";
2341
+ if (liveOnly) {
2342
+ // Live-only mode: keep metadata.sections intact (the stored
2343
+ // base content) and start _sections empty so it only collects
2344
+ // NEW sections from live events. The renderer merges both.
2345
+ assistantMessage._sections = [];
2346
+ assistantMessage._currentText = "";
2347
+ assistantMessage._currentTools = [];
2348
+ if (!assistantMessage._activeActivities) assistantMessage._activeActivities = [];
2349
+ if (!assistantMessage._pendingApprovals) assistantMessage._pendingApprovals = [];
2350
+ if (!assistantMessage.metadata) assistantMessage.metadata = {};
2351
+ if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
2352
+ } else {
2353
+ // Full replay mode: reset transient state so replayed events
2354
+ // rebuild from scratch (the buffer has the full event history).
2355
+ assistantMessage.content = "";
2356
+ assistantMessage._sections = [];
2357
+ assistantMessage._currentText = "";
2358
+ assistantMessage._currentTools = [];
2359
+ assistantMessage._activeActivities = [];
2360
+ assistantMessage._pendingApprovals = [];
2361
+ assistantMessage.metadata = { toolActivity: [] };
2362
+ }
2363
+
2364
+ const url = "/api/conversations/" + encodeURIComponent(conversationId) + "/events" + (liveOnly ? "?live_only=true" : "");
2075
2365
  fetch(url, { credentials: "include" }).then((response) => {
2076
2366
  if (!response.ok || !response.body) {
2077
2367
  resolve(undefined);
@@ -2092,6 +2382,11 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2092
2382
  if (eventName === "stream:end") {
2093
2383
  return;
2094
2384
  }
2385
+ if (eventName === "run:started") {
2386
+ if (typeof payload.contextWindow === "number" && payload.contextWindow > 0) {
2387
+ state.contextWindow = payload.contextWindow;
2388
+ }
2389
+ }
2095
2390
  if (eventName === "model:chunk") {
2096
2391
  const chunk = String(payload.content || "");
2097
2392
  if (assistantMessage._currentTools.length > 0 && chunk.length > 0) {
@@ -2105,6 +2400,12 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2105
2400
  assistantMessage._currentText += chunk;
2106
2401
  renderIfActiveConversation(true);
2107
2402
  }
2403
+ if (eventName === "model:response") {
2404
+ if (typeof payload.usage?.input === "number") {
2405
+ state.contextTokens = payload.usage.input;
2406
+ updateContextRing();
2407
+ }
2408
+ }
2108
2409
  if (eventName === "tool:started") {
2109
2410
  const toolName = payload.tool || "tool";
2110
2411
  const startedActivity = addActiveActivityFromToolStart(
@@ -2177,6 +2478,75 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2177
2478
  assistantMessage.metadata.toolActivity.push(toolText);
2178
2479
  renderIfActiveConversation(true);
2179
2480
  }
2481
+ if (eventName === "tool:approval:required") {
2482
+ const toolName = payload.tool || "tool";
2483
+ const activeActivity = removeActiveActivityForTool(
2484
+ assistantMessage,
2485
+ toolName,
2486
+ );
2487
+ const detailFromPayload = describeToolStart(payload);
2488
+ const detail =
2489
+ (activeActivity && typeof activeActivity.detail === "string"
2490
+ ? activeActivity.detail.trim()
2491
+ : "") ||
2492
+ (detailFromPayload && typeof detailFromPayload.detail === "string"
2493
+ ? detailFromPayload.detail.trim()
2494
+ : "");
2495
+ const toolText =
2496
+ "- approval required \\x60" +
2497
+ toolName +
2498
+ "\\x60" +
2499
+ (detail ? " (" + detail + ")" : "");
2500
+ assistantMessage._currentTools.push(toolText);
2501
+ assistantMessage.metadata.toolActivity.push(toolText);
2502
+ const approvalId =
2503
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2504
+ if (approvalId) {
2505
+ const preview = safeJsonPreview(payload.input ?? {});
2506
+ const inputPreview = preview.length > 600 ? preview.slice(0, 600) + "..." : preview;
2507
+ if (!Array.isArray(assistantMessage._pendingApprovals)) {
2508
+ assistantMessage._pendingApprovals = [];
2509
+ }
2510
+ const exists = assistantMessage._pendingApprovals.some(
2511
+ (req) => req.approvalId === approvalId,
2512
+ );
2513
+ if (!exists) {
2514
+ assistantMessage._pendingApprovals.push({
2515
+ approvalId,
2516
+ tool: toolName,
2517
+ inputPreview,
2518
+ state: "pending",
2519
+ });
2520
+ }
2521
+ }
2522
+ renderIfActiveConversation(true);
2523
+ }
2524
+ if (eventName === "tool:approval:granted") {
2525
+ const toolText = "- approval granted";
2526
+ assistantMessage._currentTools.push(toolText);
2527
+ assistantMessage.metadata.toolActivity.push(toolText);
2528
+ const approvalId =
2529
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2530
+ if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2531
+ assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2532
+ (req) => req.approvalId !== approvalId,
2533
+ );
2534
+ }
2535
+ renderIfActiveConversation(true);
2536
+ }
2537
+ if (eventName === "tool:approval:denied") {
2538
+ const toolText = "- approval denied";
2539
+ assistantMessage._currentTools.push(toolText);
2540
+ assistantMessage.metadata.toolActivity.push(toolText);
2541
+ const approvalId =
2542
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2543
+ if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2544
+ assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2545
+ (req) => req.approvalId !== approvalId,
2546
+ );
2547
+ }
2548
+ renderIfActiveConversation(true);
2549
+ }
2180
2550
  if (eventName === "run:completed") {
2181
2551
  assistantMessage._activeActivities = [];
2182
2552
  if (
@@ -2223,9 +2593,22 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2223
2593
  }
2224
2594
  if (eventName === "run:error") {
2225
2595
  assistantMessage._activeActivities = [];
2596
+ if (assistantMessage._currentTools.length > 0) {
2597
+ assistantMessage._sections.push({
2598
+ type: "tools",
2599
+ content: assistantMessage._currentTools,
2600
+ });
2601
+ assistantMessage._currentTools = [];
2602
+ }
2603
+ if (assistantMessage._currentText.length > 0) {
2604
+ assistantMessage._sections.push({
2605
+ type: "text",
2606
+ content: assistantMessage._currentText,
2607
+ });
2608
+ assistantMessage._currentText = "";
2609
+ }
2226
2610
  const errMsg =
2227
2611
  payload.error?.message || "Something went wrong";
2228
- assistantMessage.content = "";
2229
2612
  assistantMessage._error = errMsg;
2230
2613
  renderIfActiveConversation(false);
2231
2614
  }
@@ -2298,6 +2681,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2298
2681
  elements.send.disabled = value ? !canStop : false;
2299
2682
  elements.send.innerHTML = value ? stopIconMarkup : sendIconMarkup;
2300
2683
  elements.send.classList.toggle("stop-mode", value);
2684
+ if (elements.sendBtnWrapper) {
2685
+ elements.sendBtnWrapper.classList.toggle("stop-mode", value);
2686
+ }
2301
2687
  elements.send.setAttribute("aria-label", value ? "Stop response" : "Send message");
2302
2688
  elements.send.setAttribute(
2303
2689
  "title",
@@ -2670,8 +3056,17 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2670
3056
  }
2671
3057
  if (eventName === "run:started") {
2672
3058
  state.activeStreamRunId = typeof payload.runId === "string" ? payload.runId : null;
3059
+ if (typeof payload.contextWindow === "number" && payload.contextWindow > 0) {
3060
+ state.contextWindow = payload.contextWindow;
3061
+ }
2673
3062
  setStreaming(state.isStreaming);
2674
3063
  }
3064
+ if (eventName === "model:response") {
3065
+ if (typeof payload.usage?.input === "number") {
3066
+ state.contextTokens = payload.usage.input;
3067
+ updateContextRing();
3068
+ }
3069
+ }
2675
3070
  if (eventName === "tool:started") {
2676
3071
  const toolName = payload.tool || "tool";
2677
3072
  const startedActivity = addActiveActivityFromToolStart(
@@ -2834,9 +3229,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2834
3229
  renderIfActiveConversation(false);
2835
3230
  }
2836
3231
  if (eventName === "run:error") {
2837
- assistantMessage._activeActivities = [];
3232
+ finalizeAssistantMessage();
2838
3233
  const errMsg = payload.error?.message || "Something went wrong";
2839
- assistantMessage.content = "";
2840
3234
  assistantMessage._error = errMsg;
2841
3235
  renderIfActiveConversation(false);
2842
3236
  }
@@ -2939,6 +3333,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2939
3333
  state.activeConversationId = null;
2940
3334
  state.activeMessages = [];
2941
3335
  state.confirmDeleteId = null;
3336
+ state.contextTokens = 0;
3337
+ state.contextWindow = 0;
3338
+ updateContextRing();
2942
3339
  pushConversationUrl(null);
2943
3340
  elements.chatTitle.textContent = "";
2944
3341
  renderMessages([]);
@@ -2976,6 +3373,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2976
3373
  state.confirmDeleteId = null;
2977
3374
  state.conversations = [];
2978
3375
  state.csrfToken = "";
3376
+ state.contextTokens = 0;
3377
+ state.contextWindow = 0;
3378
+ updateContextRing();
2979
3379
  await requireAuth();
2980
3380
  });
2981
3381
 
@@ -3180,6 +3580,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
3180
3580
  } else {
3181
3581
  state.activeConversationId = null;
3182
3582
  state.activeMessages = [];
3583
+ state.contextTokens = 0;
3584
+ state.contextWindow = 0;
3585
+ updateContextRing();
3183
3586
  elements.chatTitle.textContent = "";
3184
3587
  renderMessages([]);
3185
3588
  renderConversationList();
@@ -3221,6 +3624,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
3221
3624
  await createConversation();
3222
3625
  }
3223
3626
  autoResizePrompt();
3627
+ updateContextRing();
3224
3628
  elements.prompt.focus();
3225
3629
  })();
3226
3630