@poncho-ai/cli 0.13.0 → 0.14.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/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,172 @@ 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
+ --warning: #e8a735;
513
+
514
+ --approve: #78e7a6;
515
+ --approve-border: rgba(58,208,122,0.45);
516
+ --deny: #f59b9b;
517
+ --deny-border: rgba(224,95,95,0.45);
518
+
519
+ --scrollbar: rgba(255,255,255,0.1);
520
+ --scrollbar-hover: rgba(255,255,255,0.16);
521
+ }
522
+
523
+ @media (prefers-color-scheme: light) {
524
+ :root {
525
+ --bg: #ffffff;
526
+ --bg-alt: #f5f5f5;
527
+ --bg-elevated: #e8e8e8;
528
+
529
+ --fg: #1a1a1a;
530
+ --fg-strong: #000;
531
+ --fg-2: #666;
532
+ --fg-3: #555;
533
+ --fg-4: #777;
534
+ --fg-5: #888;
535
+ --fg-6: #888;
536
+ --fg-7: #aaa;
537
+ --fg-8: #bbb;
538
+
539
+ --fg-tool: #666;
540
+ --fg-tool-code: #444;
541
+ --fg-tool-item: #333;
542
+ --fg-approval-label: #666;
543
+ --fg-approval-input: #444;
544
+ --fg-approval-btn: #1a1a1a;
545
+
546
+ --accent: #1a1a1a;
547
+ --accent-fg: #fff;
548
+ --accent-hover: #000;
549
+
550
+ --stop-bg: #d4d4d4;
551
+ --stop-fg: #333;
552
+ --stop-hover: #c4c4c4;
553
+
554
+ --border-1: rgba(0,0,0,0.06);
555
+ --border-2: rgba(0,0,0,0.08);
556
+ --border-3: rgba(0,0,0,0.1);
557
+ --border-4: rgba(0,0,0,0.1);
558
+ --border-5: rgba(0,0,0,0.15);
559
+ --border-focus: rgba(0,0,0,0.2);
560
+ --border-hover: rgba(0,0,0,0.2);
561
+ --border-drag: rgba(0,0,0,0.3);
562
+
563
+ --surface-1: rgba(0,0,0,0.02);
564
+ --surface-2: rgba(0,0,0,0.03);
565
+ --surface-3: rgba(0,0,0,0.03);
566
+ --surface-4: rgba(0,0,0,0.04);
567
+ --surface-5: rgba(0,0,0,0.05);
568
+ --surface-6: rgba(0,0,0,0.07);
569
+ --surface-7: rgba(0,0,0,0.08);
570
+ --surface-8: rgba(0,0,0,0.1);
571
+
572
+ --chip-bg: rgba(255,255,255,0.8);
573
+ --chip-bg-hover: rgba(255,255,255,0.9);
574
+ --backdrop: rgba(0,0,0,0.3);
575
+ --lightbox-bg: rgba(0,0,0,0.75);
576
+ --inset-1: rgba(0,0,0,0.04);
577
+ --inset-2: rgba(0,0,0,0.06);
578
+
579
+ --file-badge-bg: rgba(0,0,0,0.05);
580
+ --file-badge-fg: rgba(0,0,0,0.7);
581
+
582
+ --error: #dc2626;
583
+ --error-soft: #ef4444;
584
+ --error-alt: #ef4444;
585
+ --error-bg: rgba(220,38,38,0.06);
586
+ --error-border: rgba(220,38,38,0.2);
587
+
588
+ --tool-done: #16a34a;
589
+ --tool-error: #dc2626;
590
+
591
+ --warning: #ca8a04;
592
+
593
+ --approve: #16a34a;
594
+ --approve-border: rgba(22,163,74,0.35);
595
+ --deny: #dc2626;
596
+ --deny-border: rgba(220,38,38,0.3);
597
+
598
+ --scrollbar: rgba(0,0,0,0.12);
599
+ --scrollbar-hover: rgba(0,0,0,0.2);
600
+ }
601
+ }
602
+
442
603
  * { box-sizing: border-box; margin: 0; padding: 0; }
443
604
  html, body { height: 100vh; overflow: hidden; overscroll-behavior: none; touch-action: pan-y; }
444
605
  body {
445
606
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif;
446
- background: #000;
447
- color: #ededed;
607
+ background: var(--bg);
608
+ color: var(--fg);
448
609
  font-size: 14px;
449
610
  line-height: 1.5;
450
611
  -webkit-font-smoothing: antialiased;
@@ -452,7 +613,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
452
613
  }
453
614
  button, input, textarea { font: inherit; color: inherit; }
454
615
  .hidden { display: none !important; }
455
- a { color: #ededed; }
616
+ a { color: var(--fg); }
456
617
 
457
618
  /* Auth */
458
619
  .auth {
@@ -460,39 +621,39 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
460
621
  display: grid;
461
622
  place-items: center;
462
623
  padding: 20px;
463
- background: #000;
624
+ background: var(--bg);
464
625
  }
465
626
  .auth-card {
466
627
  width: min(400px, 90vw);
467
628
  }
468
629
  .auth-shell {
469
- background: #0a0a0a;
470
- border: 1px solid rgba(255,255,255,0.1);
630
+ background: var(--bg-alt);
631
+ border: 1px solid var(--border-3);
471
632
  border-radius: 9999px;
472
633
  display: flex;
473
634
  align-items: center;
474
635
  padding: 4px 6px 4px 18px;
475
636
  transition: border-color 0.15s;
476
637
  }
477
- .auth-shell:focus-within { border-color: rgba(255,255,255,0.2); }
638
+ .auth-shell:focus-within { border-color: var(--border-focus); }
478
639
  .auth-input {
479
640
  flex: 1;
480
641
  background: transparent;
481
642
  border: 0;
482
643
  outline: none;
483
- color: #ededed;
644
+ color: var(--fg);
484
645
  padding: 10px 0 8px;
485
646
  font-size: 14px;
486
647
  margin-top: -2px;
487
648
  }
488
- .auth-input::placeholder { color: #444; }
649
+ .auth-input::placeholder { color: var(--fg-7); }
489
650
  .auth-submit {
490
651
  width: 32px;
491
652
  height: 32px;
492
- background: #ededed;
653
+ background: var(--accent);
493
654
  border: 0;
494
655
  border-radius: 50%;
495
- color: #000;
656
+ color: var(--accent-fg);
496
657
  cursor: pointer;
497
658
  display: grid;
498
659
  place-items: center;
@@ -500,19 +661,19 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
500
661
  margin-bottom: 2px;
501
662
  transition: background 0.15s;
502
663
  }
503
- .auth-submit:hover { background: #fff; }
504
- .error { color: #ff4444; font-size: 13px; min-height: 16px; }
664
+ .auth-submit:hover { background: var(--accent-hover); }
665
+ .error { color: var(--error); font-size: 13px; min-height: 16px; }
505
666
  .message-error {
506
- background: rgba(255,68,68,0.08);
507
- border: 1px solid rgba(255,68,68,0.25);
667
+ background: var(--error-bg);
668
+ border: 1px solid var(--error-border);
508
669
  border-radius: 10px;
509
- color: #ff6b6b;
670
+ color: var(--error-soft);
510
671
  padding: 12px 16px;
511
672
  font-size: 13px;
512
673
  line-height: 1.5;
513
674
  max-width: 600px;
514
675
  }
515
- .message-error strong { color: #ff4444; }
676
+ .message-error strong { color: var(--error); }
516
677
 
517
678
  /* Layout - use fixed positioning with explicit dimensions */
518
679
  .shell {
@@ -544,8 +705,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
544
705
  }
545
706
  .sidebar {
546
707
  width: 260px;
547
- background: #000;
548
- border-right: 1px solid rgba(255,255,255,0.06);
708
+ background: var(--bg);
709
+ border-right: 1px solid var(--border-1);
549
710
  display: flex;
550
711
  flex-direction: column;
551
712
  padding: 12px 8px;
@@ -553,7 +714,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
553
714
  .new-chat-btn {
554
715
  background: transparent;
555
716
  border: 0;
556
- color: #888;
717
+ color: var(--fg-2);
557
718
  border-radius: 12px;
558
719
  height: 36px;
559
720
  padding: 0 10px;
@@ -564,7 +725,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
564
725
  cursor: pointer;
565
726
  transition: background 0.15s, color 0.15s;
566
727
  }
567
- .new-chat-btn:hover { color: #ededed; }
728
+ .new-chat-btn:hover { color: var(--fg); }
568
729
  .new-chat-btn svg { width: 16px; height: 16px; }
569
730
  .conversation-list {
570
731
  flex: 1;
@@ -574,6 +735,20 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
574
735
  flex-direction: column;
575
736
  gap: 2px;
576
737
  }
738
+ .sidebar-section-label {
739
+ font-size: 11px;
740
+ font-weight: 600;
741
+ color: var(--fg-7);
742
+ text-transform: uppercase;
743
+ letter-spacing: 0.04em;
744
+ padding: 10px 10px 4px;
745
+ }
746
+ .sidebar-section-label:first-child { padding-top: 4px; }
747
+ .sidebar-section-divider {
748
+ height: 1px;
749
+ background: var(--border);
750
+ margin: 6px 10px;
751
+ }
577
752
  .conversation-item {
578
753
  height: 36px;
579
754
  min-height: 36px;
@@ -584,16 +759,26 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
584
759
  cursor: pointer;
585
760
  font-size: 13px;
586
761
  line-height: 36px;
587
- color: #555;
762
+ color: var(--fg-6);
588
763
  white-space: nowrap;
589
764
  overflow: hidden;
590
765
  text-overflow: ellipsis;
591
766
  position: relative;
592
767
  transition: color 0.15s;
593
768
  }
594
- .conversation-item:hover { color: #999; }
769
+ .conversation-item .approval-dot {
770
+ display: inline-block;
771
+ width: 6px;
772
+ height: 6px;
773
+ border-radius: 50%;
774
+ background: var(--warning, #e8a735);
775
+ margin-right: 6px;
776
+ flex-shrink: 0;
777
+ vertical-align: middle;
778
+ }
779
+ .conversation-item:hover { color: var(--fg-3); }
595
780
  .conversation-item.active {
596
- color: #ededed;
781
+ color: var(--fg);
597
782
  }
598
783
  .conversation-item .delete-btn {
599
784
  position: absolute;
@@ -601,9 +786,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
601
786
  top: 0;
602
787
  bottom: 0;
603
788
  opacity: 0;
604
- background: #000;
789
+ background: var(--bg);
605
790
  border: 0;
606
- color: #444;
791
+ color: var(--fg-7);
607
792
  padding: 0 8px;
608
793
  border-radius: 0 4px 4px 0;
609
794
  cursor: pointer;
@@ -614,7 +799,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
614
799
  transition: opacity 0.15s, color 0.15s;
615
800
  }
616
801
  .conversation-item:hover .delete-btn { opacity: 1; }
617
- .conversation-item.active .delete-btn { background: rgba(0,0,0,1); }
802
+ .conversation-item.active .delete-btn { background: var(--bg); }
618
803
  .conversation-item .delete-btn::before {
619
804
  content: "";
620
805
  position: absolute;
@@ -622,23 +807,23 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
622
807
  top: 0;
623
808
  bottom: 0;
624
809
  width: 24px;
625
- background: linear-gradient(to right, transparent, #000);
810
+ background: linear-gradient(to right, transparent, var(--bg));
626
811
  pointer-events: none;
627
812
  }
628
813
  .conversation-item.active .delete-btn::before {
629
- background: linear-gradient(to right, transparent, rgba(0,0,0,1));
814
+ background: linear-gradient(to right, transparent, var(--bg));
630
815
  }
631
- .conversation-item .delete-btn:hover { color: #888; }
816
+ .conversation-item .delete-btn:hover { color: var(--fg-2); }
632
817
  .conversation-item .delete-btn.confirming {
633
818
  opacity: 1;
634
819
  width: auto;
635
820
  padding: 0 8px;
636
821
  font-size: 11px;
637
- color: #ff4444;
822
+ color: var(--error);
638
823
  border-radius: 3px;
639
824
  }
640
825
  .conversation-item .delete-btn.confirming:hover {
641
- color: #ff6666;
826
+ color: var(--error-alt);
642
827
  }
643
828
  .sidebar-footer {
644
829
  margin-top: auto;
@@ -647,7 +832,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
647
832
  .logout-btn {
648
833
  background: transparent;
649
834
  border: 0;
650
- color: #555;
835
+ color: var(--fg-6);
651
836
  width: 100%;
652
837
  padding: 8px 10px;
653
838
  text-align: left;
@@ -656,10 +841,10 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
656
841
  font-size: 13px;
657
842
  transition: color 0.15s, background 0.15s;
658
843
  }
659
- .logout-btn:hover { color: #888; }
844
+ .logout-btn:hover { color: var(--fg-2); }
660
845
 
661
846
  /* Main */
662
- .main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: #000; overflow: hidden; }
847
+ .main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: var(--bg); overflow: hidden; }
663
848
  .topbar {
664
849
  height: calc(52px + env(safe-area-inset-top, 0px));
665
850
  padding-top: env(safe-area-inset-top, 0px);
@@ -668,8 +853,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
668
853
  justify-content: center;
669
854
  font-size: 13px;
670
855
  font-weight: 500;
671
- color: #888;
672
- border-bottom: 1px solid rgba(255,255,255,0.06);
856
+ color: var(--fg-2);
857
+ border-bottom: 1px solid var(--border-1);
673
858
  position: relative;
674
859
  flex-shrink: 0;
675
860
  }
@@ -688,7 +873,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
688
873
  bottom: 4px; /* Position from bottom of topbar content area */
689
874
  background: transparent;
690
875
  border: 0;
691
- color: #666;
876
+ color: var(--fg-5);
692
877
  width: 44px;
693
878
  height: 44px;
694
879
  border-radius: 6px;
@@ -698,7 +883,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
698
883
  z-index: 10;
699
884
  -webkit-tap-highlight-color: transparent;
700
885
  }
701
- .sidebar-toggle:hover { color: #ededed; }
886
+ .sidebar-toggle:hover { color: var(--fg); }
702
887
  .topbar-new-chat {
703
888
  display: none;
704
889
  position: absolute;
@@ -706,7 +891,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
706
891
  bottom: 4px;
707
892
  background: transparent;
708
893
  border: 0;
709
- color: #666;
894
+ color: var(--fg-5);
710
895
  width: 44px;
711
896
  height: 44px;
712
897
  border-radius: 6px;
@@ -715,7 +900,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
715
900
  z-index: 10;
716
901
  -webkit-tap-highlight-color: transparent;
717
902
  }
718
- .topbar-new-chat:hover { color: #ededed; }
903
+ .topbar-new-chat:hover { color: var(--fg); }
719
904
  .topbar-new-chat svg { width: 16px; height: 16px; }
720
905
 
721
906
  /* Messages */
@@ -727,8 +912,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
727
912
  .assistant-avatar {
728
913
  width: 24px;
729
914
  height: 24px;
730
- background: #ededed;
731
- color: #000;
915
+ background: var(--accent);
916
+ color: var(--accent-fg);
732
917
  border-radius: 6px;
733
918
  display: grid;
734
919
  place-items: center;
@@ -739,7 +924,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
739
924
  }
740
925
  .assistant-content {
741
926
  line-height: 1.65;
742
- color: #ededed;
927
+ color: var(--fg);
743
928
  font-size: 14px;
744
929
  min-width: 0;
745
930
  max-width: 100%;
@@ -751,32 +936,32 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
751
936
  .assistant-content p:last-child { margin-bottom: 0; }
752
937
  .assistant-content ul, .assistant-content ol { margin: 8px 0; padding-left: 20px; }
753
938
  .assistant-content li { margin: 4px 0; }
754
- .assistant-content strong { font-weight: 600; color: #fff; }
939
+ .assistant-content strong { font-weight: 600; color: var(--fg-strong); }
755
940
  .assistant-content h2 {
756
941
  font-size: 16px;
757
942
  font-weight: 600;
758
943
  letter-spacing: -0.02em;
759
944
  margin: 20px 0 8px;
760
- color: #fff;
945
+ color: var(--fg-strong);
761
946
  }
762
947
  .assistant-content h3 {
763
948
  font-size: 14px;
764
949
  font-weight: 600;
765
950
  letter-spacing: -0.01em;
766
951
  margin: 16px 0 6px;
767
- color: #fff;
952
+ color: var(--fg-strong);
768
953
  }
769
954
  .assistant-content code {
770
- background: rgba(255,255,255,0.06);
771
- border: 1px solid rgba(255,255,255,0.06);
955
+ background: var(--surface-4);
956
+ border: 1px solid var(--border-1);
772
957
  padding: 2px 5px;
773
958
  border-radius: 4px;
774
959
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
775
960
  font-size: 0.88em;
776
961
  }
777
962
  .assistant-content pre {
778
- background: #0a0a0a;
779
- border: 1px solid rgba(255,255,255,0.06);
963
+ background: var(--bg-alt);
964
+ border: 1px solid var(--border-1);
780
965
  padding: 14px 16px;
781
966
  border-radius: 8px;
782
967
  overflow-x: auto;
@@ -793,33 +978,33 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
793
978
  margin: 8px 0;
794
979
  font-size: 12px;
795
980
  line-height: 1.45;
796
- color: #8a8a8a;
981
+ color: var(--fg-tool);
797
982
  }
798
983
  .tool-activity-inline code {
799
984
  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);
985
+ background: var(--surface-3);
986
+ border: 1px solid var(--border-2);
802
987
  padding: 4px 8px;
803
988
  border-radius: 6px;
804
- color: #bcbcbc;
989
+ color: var(--fg-tool-code);
805
990
  font-size: 11px;
806
991
  }
807
992
  .tool-status {
808
- color: #8a8a8a;
993
+ color: var(--fg-tool);
809
994
  font-style: italic;
810
995
  }
811
996
  .tool-done {
812
- color: #6a9955;
997
+ color: var(--tool-done);
813
998
  }
814
999
  .tool-error {
815
- color: #f48771;
1000
+ color: var(--tool-error);
816
1001
  }
817
- .assistant-content table {
1002
+ .assistant-content table:not(.approval-request-table) {
818
1003
  border-collapse: collapse;
819
1004
  width: 100%;
820
1005
  margin: 14px 0;
821
1006
  font-size: 13px;
822
- border: 1px solid rgba(255,255,255,0.08);
1007
+ border: 1px solid var(--border-2);
823
1008
  border-radius: 8px;
824
1009
  overflow: hidden;
825
1010
  display: block;
@@ -827,42 +1012,46 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
827
1012
  overflow-x: auto;
828
1013
  white-space: nowrap;
829
1014
  }
830
- .assistant-content th {
831
- background: rgba(255,255,255,0.06);
1015
+ .assistant-content table:not(.approval-request-table) th {
1016
+ background: var(--surface-4);
832
1017
  padding: 10px 12px;
833
1018
  text-align: left;
834
1019
  font-weight: 600;
835
- border-bottom: 1px solid rgba(255,255,255,0.12);
836
- color: #fff;
1020
+ border-bottom: 1px solid var(--border-4);
1021
+ color: var(--fg-strong);
837
1022
  min-width: 100px;
838
1023
  }
839
- .assistant-content td {
1024
+ .assistant-content table:not(.approval-request-table) td {
840
1025
  padding: 10px 12px;
841
- border-bottom: 1px solid rgba(255,255,255,0.06);
1026
+ border-bottom: 1px solid var(--border-1);
842
1027
  width: 100%;
843
1028
  min-width: 100px;
844
1029
  }
845
- .assistant-content tr:last-child td {
1030
+ .assistant-content table:not(.approval-request-table) tr:last-child td {
846
1031
  border-bottom: none;
847
1032
  }
848
- .assistant-content tbody tr:hover {
849
- background: rgba(255,255,255,0.02);
1033
+ .assistant-content table:not(.approval-request-table) tbody tr:hover {
1034
+ background: var(--surface-1);
850
1035
  }
851
1036
  .assistant-content hr {
852
1037
  border: 0;
853
- border-top: 1px solid rgba(255,255,255,0.1);
1038
+ border-top: 1px solid var(--border-3);
854
1039
  margin: 20px 0;
855
1040
  }
856
1041
  .tool-activity {
857
1042
  margin-top: 12px;
858
1043
  margin-bottom: 12px;
859
- border: 1px solid rgba(255,255,255,0.08);
860
- background: rgba(255,255,255,0.03);
1044
+ border: 1px solid var(--border-2);
1045
+ background: var(--surface-2);
861
1046
  border-radius: 10px;
862
1047
  font-size: 12px;
863
1048
  line-height: 1.45;
864
- color: #bcbcbc;
1049
+ color: var(--fg-tool-code);
865
1050
  width: 300px;
1051
+ transition: width 0.2s ease;
1052
+ }
1053
+ .tool-activity.has-approvals {
1054
+ width: 100%;
866
1055
  }
867
1056
  .assistant-content > .tool-activity:first-child {
868
1057
  margin-top: 0;
@@ -886,12 +1075,12 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
886
1075
  font-size: 11px;
887
1076
  text-transform: uppercase;
888
1077
  letter-spacing: 0.06em;
889
- color: #8a8a8a;
1078
+ color: var(--fg-tool);
890
1079
  font-weight: 600;
891
1080
  }
892
1081
  .tool-activity-caret {
893
1082
  margin-left: auto;
894
- color: #8a8a8a;
1083
+ color: var(--fg-tool);
895
1084
  display: inline-flex;
896
1085
  align-items: center;
897
1086
  justify-content: center;
@@ -913,49 +1102,69 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
913
1102
  }
914
1103
  .tool-activity-item {
915
1104
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
916
- background: rgba(255,255,255,0.04);
1105
+ background: var(--surface-3);
917
1106
  border-radius: 6px;
918
1107
  padding: 4px 7px;
919
- color: #d6d6d6;
1108
+ color: var(--fg-tool-item);
920
1109
  }
921
1110
  .approval-requests {
922
- border-top: 1px solid rgba(255,255,255,0.08);
1111
+ border-top: 1px solid var(--border-2);
923
1112
  padding: 10px 12px 12px;
924
1113
  display: grid;
925
- gap: 8px;
926
- background: rgba(0,0,0,0.16);
1114
+ gap: 10px;
927
1115
  }
928
1116
  .approval-requests-label {
929
- font-size: 11px;
1117
+ font-size: 12px;
930
1118
  text-transform: uppercase;
931
1119
  letter-spacing: 0.06em;
932
- color: #b0b0b0;
1120
+ color: var(--fg-approval-label);
933
1121
  font-weight: 600;
934
1122
  }
1123
+ .approval-requests-label code {
1124
+ font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
1125
+ text-transform: none;
1126
+ letter-spacing: 0;
1127
+ color: var(--fg-strong);
1128
+ }
935
1129
  .approval-request-item {
936
- border: 1px solid rgba(255,255,255,0.1);
937
- background: rgba(255,255,255,0.03);
938
- border-radius: 8px;
939
- padding: 8px;
940
1130
  display: grid;
941
- gap: 6px;
1131
+ gap: 8px;
942
1132
  }
943
- .approval-request-tool {
944
- font-size: 12px;
945
- color: #fff;
1133
+ .approval-request-table {
1134
+ width: 100%;
1135
+ border-collapse: collapse;
1136
+ border: none;
1137
+ font-size: 14px;
1138
+ line-height: 1.5;
1139
+ }
1140
+ .approval-request-table tr,
1141
+ .approval-request-table td {
1142
+ border: none;
1143
+ background: none;
1144
+ }
1145
+ .approval-request-table td {
1146
+ padding: 4px 0;
1147
+ vertical-align: top;
1148
+ }
1149
+ .approval-request-table .ak {
946
1150
  font-weight: 600;
947
- overflow-wrap: anywhere;
1151
+ color: var(--fg-approval-label);
1152
+ white-space: nowrap;
1153
+ width: 1%;
1154
+ padding-right: 20px;
948
1155
  }
949
- .approval-request-input {
950
- font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
951
- font-size: 11px;
952
- color: #cfcfcf;
953
- background: rgba(0,0,0,0.25);
954
- border-radius: 6px;
955
- padding: 6px;
1156
+ .approval-request-table .av,
1157
+ .approval-request-table .av-complex {
1158
+ color: var(--fg);
956
1159
  overflow-wrap: anywhere;
957
- max-height: 80px;
1160
+ white-space: pre-wrap;
1161
+ max-height: 200px;
958
1162
  overflow-y: auto;
1163
+ display: block;
1164
+ }
1165
+ .approval-request-table .av-complex {
1166
+ font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
1167
+ font-size: 12px;
959
1168
  }
960
1169
  .approval-request-actions {
961
1170
  display: flex;
@@ -963,32 +1172,32 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
963
1172
  }
964
1173
  .approval-action-btn {
965
1174
  border-radius: 6px;
966
- border: 1px solid rgba(255,255,255,0.18);
967
- background: rgba(255,255,255,0.06);
968
- color: #f0f0f0;
1175
+ border: 1px solid var(--border-5);
1176
+ background: var(--surface-4);
1177
+ color: var(--fg-approval-btn);
969
1178
  font-size: 11px;
970
1179
  font-weight: 600;
971
1180
  padding: 4px 8px;
972
1181
  cursor: pointer;
973
1182
  }
974
1183
  .approval-action-btn:hover {
975
- background: rgba(255,255,255,0.12);
1184
+ background: var(--surface-7);
976
1185
  }
977
1186
  .approval-action-btn.approve {
978
- border-color: rgba(58, 208, 122, 0.45);
979
- color: #78e7a6;
1187
+ border-color: var(--approve-border);
1188
+ color: var(--approve);
980
1189
  }
981
1190
  .approval-action-btn.deny {
982
- border-color: rgba(224, 95, 95, 0.45);
983
- color: #f59b9b;
1191
+ border-color: var(--deny-border);
1192
+ color: var(--deny);
984
1193
  }
985
1194
  .approval-action-btn[disabled] {
986
1195
  opacity: 0.55;
987
1196
  cursor: not-allowed;
988
1197
  }
989
1198
  .user-bubble {
990
- background: #111;
991
- border: 1px solid rgba(255,255,255,0.08);
1199
+ background: var(--bg-elevated);
1200
+ border: 1px solid var(--border-2);
992
1201
  padding: 10px 16px;
993
1202
  border-radius: 18px;
994
1203
  max-width: 70%;
@@ -996,6 +1205,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
996
1205
  line-height: 1.5;
997
1206
  overflow-wrap: break-word;
998
1207
  word-break: break-word;
1208
+ white-space: pre-wrap;
999
1209
  }
1000
1210
  .empty-state {
1001
1211
  display: flex;
@@ -1004,7 +1214,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1004
1214
  justify-content: center;
1005
1215
  height: 100%;
1006
1216
  gap: 16px;
1007
- color: #555;
1217
+ color: var(--fg-6);
1008
1218
  }
1009
1219
  .empty-state .assistant-avatar {
1010
1220
  width: 36px;
@@ -1014,7 +1224,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1014
1224
  }
1015
1225
  .empty-state-text {
1016
1226
  font-size: 14px;
1017
- color: #555;
1227
+ color: var(--fg-6);
1018
1228
  }
1019
1229
  .thinking-indicator {
1020
1230
  display: inline-block;
@@ -1022,7 +1232,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1022
1232
  font-size: 20px;
1023
1233
  line-height: 1;
1024
1234
  vertical-align: middle;
1025
- color: #ededed;
1235
+ color: var(--fg);
1026
1236
  opacity: 0.5;
1027
1237
  }
1028
1238
  .thinking-status {
@@ -1030,13 +1240,13 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1030
1240
  align-items: center;
1031
1241
  gap: 8px;
1032
1242
  margin-top: 2px;
1033
- color: #8a8a8a;
1243
+ color: var(--fg-tool);
1034
1244
  font-size: 14px;
1035
1245
  line-height: 1.65;
1036
1246
  font-weight: 400;
1037
1247
  }
1038
1248
  .thinking-status-label {
1039
- color: #8a8a8a;
1249
+ color: var(--fg-tool);
1040
1250
  font-size: 14px;
1041
1251
  line-height: 1.65;
1042
1252
  font-weight: 400;
@@ -1067,26 +1277,26 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1067
1277
  right: 0;
1068
1278
  bottom: 100%;
1069
1279
  height: 48px;
1070
- background: linear-gradient(to top, #000 0%, transparent 100%);
1280
+ background: linear-gradient(to top, var(--bg) 0%, transparent 100%);
1071
1281
  pointer-events: none;
1072
1282
  }
1073
1283
  .composer-inner { max-width: 680px; margin: 0 auto; }
1074
1284
  .composer-shell {
1075
- background: #0a0a0a;
1076
- border: 1px solid rgba(255,255,255,0.1);
1285
+ background: var(--bg-alt);
1286
+ border: 1px solid var(--border-3);
1077
1287
  border-radius: 24px;
1078
1288
  display: flex;
1079
1289
  align-items: end;
1080
1290
  padding: 4px 6px 4px 6px;
1081
1291
  transition: border-color 0.15s;
1082
1292
  }
1083
- .composer-shell:focus-within { border-color: rgba(255,255,255,0.2); }
1293
+ .composer-shell:focus-within { border-color: var(--border-focus); }
1084
1294
  .composer-input {
1085
1295
  flex: 1;
1086
1296
  background: transparent;
1087
1297
  border: 0;
1088
1298
  outline: none;
1089
- color: #ededed;
1299
+ color: var(--fg);
1090
1300
  min-height: 40px;
1091
1301
  max-height: 200px;
1092
1302
  resize: none;
@@ -1095,14 +1305,14 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1095
1305
  line-height: 1.5;
1096
1306
  margin-top: -4px;
1097
1307
  }
1098
- .composer-input::placeholder { color: #444; }
1308
+ .composer-input::placeholder { color: var(--fg-7); }
1099
1309
  .send-btn {
1100
1310
  width: 32px;
1101
1311
  height: 32px;
1102
- background: #ededed;
1312
+ background: var(--accent);
1103
1313
  border: 0;
1104
1314
  border-radius: 50%;
1105
- color: #000;
1315
+ color: var(--accent-fg);
1106
1316
  cursor: pointer;
1107
1317
  display: grid;
1108
1318
  place-items: center;
@@ -1110,21 +1320,79 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1110
1320
  margin-bottom: 2px;
1111
1321
  transition: background 0.15s, opacity 0.15s;
1112
1322
  }
1113
- .send-btn:hover { background: #fff; }
1323
+ .send-btn:hover { background: var(--accent-hover); }
1114
1324
  .send-btn.stop-mode {
1115
- background: #4a4a4a;
1116
- color: #fff;
1325
+ background: var(--stop-bg);
1326
+ color: var(--stop-fg);
1117
1327
  }
1118
- .send-btn.stop-mode:hover { background: #565656; }
1328
+ .send-btn.stop-mode:hover { background: var(--stop-hover); }
1119
1329
  .send-btn:disabled { opacity: 0.2; cursor: default; }
1120
- .send-btn:disabled:hover { background: #ededed; }
1330
+ .send-btn:disabled:hover { background: var(--accent); }
1331
+ .send-btn-wrapper {
1332
+ position: relative;
1333
+ width: 36px;
1334
+ height: 36px;
1335
+ display: grid;
1336
+ place-items: center;
1337
+ flex-shrink: 0;
1338
+ margin-bottom: 0;
1339
+ }
1340
+ .send-btn-wrapper .send-btn {
1341
+ margin-bottom: 0;
1342
+ }
1343
+ .context-ring {
1344
+ position: absolute;
1345
+ inset: 0;
1346
+ width: 36px;
1347
+ height: 36px;
1348
+ pointer-events: none;
1349
+ transform: rotate(-90deg);
1350
+ }
1351
+ .context-ring-fill {
1352
+ fill: none;
1353
+ stroke: var(--bg-alt);
1354
+ stroke-width: 3;
1355
+ stroke-linecap: butt;
1356
+ transition: stroke-dashoffset 0.4s ease, stroke 0.3s ease;
1357
+ }
1358
+ .send-btn-wrapper.stop-mode .context-ring-fill {
1359
+ stroke: var(--fg-3);
1360
+ }
1361
+ .context-ring-fill.warning {
1362
+ stroke: #e5a33d;
1363
+ }
1364
+ .context-ring-fill.critical {
1365
+ stroke: #e55d4a;
1366
+ }
1367
+ .context-tooltip {
1368
+ position: absolute;
1369
+ bottom: calc(100% + 8px);
1370
+ right: 0;
1371
+ background: var(--bg-elevated);
1372
+ border: 1px solid var(--border-3);
1373
+ border-radius: 8px;
1374
+ padding: 6px 10px;
1375
+ font-size: 12px;
1376
+ color: var(--fg-2);
1377
+ white-space: nowrap;
1378
+ pointer-events: none;
1379
+ opacity: 0;
1380
+ transform: translateY(4px);
1381
+ transition: opacity 0.15s, transform 0.15s;
1382
+ z-index: 10;
1383
+ }
1384
+ .send-btn-wrapper:hover .context-tooltip,
1385
+ .send-btn-wrapper:focus-within .context-tooltip {
1386
+ opacity: 1;
1387
+ transform: translateY(0);
1388
+ }
1121
1389
  .attach-btn {
1122
1390
  width: 32px;
1123
1391
  height: 32px;
1124
- background: rgba(255,255,255,0.08);
1392
+ background: var(--surface-5);
1125
1393
  border: 0;
1126
1394
  border-radius: 50%;
1127
- color: #999;
1395
+ color: var(--fg-3);
1128
1396
  cursor: pointer;
1129
1397
  display: grid;
1130
1398
  place-items: center;
@@ -1133,7 +1401,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1133
1401
  margin-right: 8px;
1134
1402
  transition: color 0.15s, background 0.15s;
1135
1403
  }
1136
- .attach-btn:hover { color: #ededed; background: rgba(255,255,255,0.14); }
1404
+ .attach-btn:hover { color: var(--fg); background: var(--surface-8); }
1137
1405
  .attachment-preview {
1138
1406
  display: flex;
1139
1407
  gap: 8px;
@@ -1144,12 +1412,12 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1144
1412
  display: inline-flex;
1145
1413
  align-items: center;
1146
1414
  gap: 6px;
1147
- background: rgba(0, 0, 0, 0.6);
1148
- border: 1px solid rgba(255, 255, 255, 0.12);
1415
+ background: var(--chip-bg);
1416
+ border: 1px solid var(--border-4);
1149
1417
  border-radius: 9999px;
1150
1418
  padding: 4px 10px 4px 6px;
1151
1419
  font-size: 11px;
1152
- color: #777;
1420
+ color: var(--fg-4);
1153
1421
  max-width: 200px;
1154
1422
  cursor: pointer;
1155
1423
  backdrop-filter: blur(6px);
@@ -1157,9 +1425,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1157
1425
  transition: color 0.15s, border-color 0.15s, background 0.15s;
1158
1426
  }
1159
1427
  .attachment-chip:hover {
1160
- color: #ededed;
1161
- border-color: rgba(255, 255, 255, 0.25);
1162
- background: rgba(0, 0, 0, 0.75);
1428
+ color: var(--fg);
1429
+ border-color: var(--border-hover);
1430
+ background: var(--chip-bg-hover);
1163
1431
  }
1164
1432
  .attachment-chip img {
1165
1433
  width: 20px;
@@ -1173,7 +1441,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1173
1441
  width: 20px;
1174
1442
  height: 20px;
1175
1443
  border-radius: 50%;
1176
- background: rgba(255,255,255,0.1);
1444
+ background: var(--surface-6);
1177
1445
  display: grid;
1178
1446
  place-items: center;
1179
1447
  font-size: 11px;
@@ -1181,13 +1449,13 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1181
1449
  }
1182
1450
  .attachment-chip .remove-attachment {
1183
1451
  cursor: pointer;
1184
- color: #555;
1452
+ color: var(--fg-6);
1185
1453
  font-size: 14px;
1186
1454
  margin-left: 2px;
1187
1455
  line-height: 1;
1188
1456
  transition: color 0.15s;
1189
1457
  }
1190
- .attachment-chip .remove-attachment:hover { color: #fff; }
1458
+ .attachment-chip .remove-attachment:hover { color: var(--fg-strong); }
1191
1459
  .attachment-chip .filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
1192
1460
  .user-bubble .user-file-attachments {
1193
1461
  display: flex;
@@ -1217,7 +1485,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1217
1485
  transition: background 0.25s ease, backdrop-filter 0.25s ease;
1218
1486
  }
1219
1487
  .lightbox.active {
1220
- background: rgba(0,0,0,0.85);
1488
+ background: var(--lightbox-bg);
1221
1489
  backdrop-filter: blur(8px);
1222
1490
  }
1223
1491
  .lightbox img {
@@ -1237,16 +1505,16 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1237
1505
  display: inline-flex;
1238
1506
  align-items: center;
1239
1507
  gap: 4px;
1240
- background: rgba(0,0,0,0.2);
1508
+ background: var(--file-badge-bg);
1241
1509
  border-radius: 6px;
1242
1510
  padding: 4px 8px;
1243
1511
  font-size: 12px;
1244
- color: rgba(255,255,255,0.8);
1512
+ color: var(--file-badge-fg);
1245
1513
  }
1246
1514
  .drag-overlay {
1247
1515
  position: fixed;
1248
1516
  inset: 0;
1249
- background: rgba(0,0,0,0.6);
1517
+ background: var(--backdrop);
1250
1518
  z-index: 9999;
1251
1519
  display: none;
1252
1520
  align-items: center;
@@ -1255,15 +1523,15 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1255
1523
  }
1256
1524
  .drag-overlay.active { display: flex; }
1257
1525
  .drag-overlay-inner {
1258
- border: 2px dashed rgba(255,255,255,0.4);
1526
+ border: 2px dashed var(--border-drag);
1259
1527
  border-radius: 16px;
1260
1528
  padding: 40px 60px;
1261
- color: #fff;
1529
+ color: var(--fg-strong);
1262
1530
  font-size: 16px;
1263
1531
  }
1264
1532
  .disclaimer {
1265
1533
  text-align: center;
1266
- color: #333;
1534
+ color: var(--fg-8);
1267
1535
  font-size: 12px;
1268
1536
  margin-top: 10px;
1269
1537
  }
@@ -1278,10 +1546,10 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1278
1546
  align-items: center;
1279
1547
  gap: 6px;
1280
1548
  font-size: 11px;
1281
- color: #777;
1549
+ color: var(--fg-4);
1282
1550
  text-decoration: none;
1283
- background: rgba(0, 0, 0, 0.6);
1284
- border: 1px solid rgba(255, 255, 255, 0.12);
1551
+ background: var(--chip-bg);
1552
+ border: 1px solid var(--border-4);
1285
1553
  border-radius: 9999px;
1286
1554
  padding: 4px 10px 4px 6px;
1287
1555
  backdrop-filter: blur(6px);
@@ -1289,9 +1557,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1289
1557
  transition: color 0.15s, border-color 0.15s, background 0.15s;
1290
1558
  }
1291
1559
  .poncho-badge:hover {
1292
- color: #ededed;
1293
- border-color: rgba(255, 255, 255, 0.25);
1294
- background: rgba(0, 0, 0, 0.75);
1560
+ color: var(--fg);
1561
+ border-color: var(--border-hover);
1562
+ background: var(--chip-bg-hover);
1295
1563
  }
1296
1564
  .poncho-badge-avatar {
1297
1565
  width: 16px;
@@ -1305,8 +1573,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1305
1573
  /* Scrollbar */
1306
1574
  ::-webkit-scrollbar { width: 6px; }
1307
1575
  ::-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); }
1576
+ ::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 3px; }
1577
+ ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); }
1310
1578
 
1311
1579
  /* Mobile */
1312
1580
  @media (max-width: 768px) {
@@ -1336,7 +1604,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1336
1604
  .sidebar-backdrop {
1337
1605
  position: fixed;
1338
1606
  inset: 0;
1339
- background: rgba(0,0,0,0.6);
1607
+ background: var(--backdrop);
1340
1608
  z-index: 50;
1341
1609
  backdrop-filter: blur(2px);
1342
1610
  -webkit-backdrop-filter: blur(2px);
@@ -1411,9 +1679,15 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1411
1679
  </button>
1412
1680
  <input id="file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
1413
1681
  <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>
1682
+ <div class="send-btn-wrapper" id="send-btn-wrapper">
1683
+ <svg class="context-ring" viewBox="0 0 36 36">
1684
+ <circle class="context-ring-fill" id="context-ring-fill" cx="18" cy="18" r="14.5" />
1685
+ </svg>
1686
+ <button id="send" class="send-btn" type="submit">
1687
+ <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>
1688
+ </button>
1689
+ <div class="context-tooltip" id="context-tooltip"></div>
1690
+ </div>
1417
1691
  </div>
1418
1692
  </div>
1419
1693
  </form>
@@ -1445,6 +1719,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1445
1719
  confirmDeleteId: null,
1446
1720
  approvalRequestsInFlight: {},
1447
1721
  pendingFiles: [],
1722
+ contextTokens: 0,
1723
+ contextWindow: 0,
1448
1724
  };
1449
1725
 
1450
1726
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -1472,12 +1748,41 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1472
1748
  attachmentPreview: $("attachment-preview"),
1473
1749
  dragOverlay: $("drag-overlay"),
1474
1750
  lightbox: $("lightbox"),
1751
+ contextRingFill: $("context-ring-fill"),
1752
+ contextTooltip: $("context-tooltip"),
1753
+ sendBtnWrapper: $("send-btn-wrapper"),
1475
1754
  };
1476
1755
  const sendIconMarkup =
1477
1756
  '<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
1757
  const stopIconMarkup =
1479
1758
  '<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
1759
 
1760
+ const CONTEXT_RING_CIRCUMFERENCE = 2 * Math.PI * 14.5;
1761
+ const formatTokenCount = (n) => {
1762
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\\.0$/, "") + "M";
1763
+ if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\\.0$/, "") + "k";
1764
+ return String(n);
1765
+ };
1766
+ const updateContextRing = () => {
1767
+ const ring = elements.contextRingFill;
1768
+ const tooltip = elements.contextTooltip;
1769
+ if (!ring || !tooltip) return;
1770
+ if (state.contextWindow <= 0) {
1771
+ ring.style.strokeDasharray = String(CONTEXT_RING_CIRCUMFERENCE);
1772
+ ring.style.strokeDashoffset = String(CONTEXT_RING_CIRCUMFERENCE);
1773
+ tooltip.textContent = "";
1774
+ return;
1775
+ }
1776
+ const ratio = Math.min(state.contextTokens / state.contextWindow, 1);
1777
+ const offset = CONTEXT_RING_CIRCUMFERENCE * (1 - ratio);
1778
+ ring.style.strokeDasharray = String(CONTEXT_RING_CIRCUMFERENCE);
1779
+ ring.style.strokeDashoffset = String(offset);
1780
+ ring.classList.toggle("warning", ratio >= 0.7 && ratio < 0.9);
1781
+ ring.classList.toggle("critical", ratio >= 0.9);
1782
+ const pct = (ratio * 100).toFixed(1).replace(/\\.0$/, "");
1783
+ tooltip.textContent = formatTokenCount(state.contextTokens) + " / " + formatTokenCount(state.contextWindow) + " tokens (" + pct + "%)";
1784
+ };
1785
+
1481
1786
  const pushConversationUrl = (conversationId) => {
1482
1787
  const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
1483
1788
  if (window.location.pathname !== target) {
@@ -1574,18 +1879,20 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1574
1879
  .map((req) => {
1575
1880
  const approvalId = typeof req.approvalId === "string" ? req.approvalId : "";
1576
1881
  const tool = typeof req.tool === "string" ? req.tool : "tool";
1577
- const inputPreview = typeof req.inputPreview === "string" ? req.inputPreview : "{}";
1882
+ const input = req.input != null ? req.input : {};
1578
1883
  const submitting = req.state === "submitting";
1579
1884
  const approveLabel = submitting && req.pendingDecision === "approve" ? "Approving..." : "Approve";
1580
1885
  const denyLabel = submitting && req.pendingDecision === "deny" ? "Denying..." : "Deny";
1886
+ const errorHtml = req._error
1887
+ ? '<div style="color: var(--deny); font-size: 11px; margin-top: 4px;">Submit failed: ' + escapeHtml(req._error) + "</div>"
1888
+ : "";
1581
1889
  return (
1582
1890
  '<div class="approval-request-item">' +
1583
- '<div class="approval-request-tool">' +
1891
+ '<div class="approval-requests-label">Approval required: <code>' +
1584
1892
  escapeHtml(tool) +
1585
- "</div>" +
1586
- '<div class="approval-request-input">' +
1587
- escapeHtml(inputPreview) +
1588
- "</div>" +
1893
+ "</code></div>" +
1894
+ renderInputTable(input) +
1895
+ errorHtml +
1589
1896
  '<div class="approval-request-actions">' +
1590
1897
  '<button class="approval-action-btn approve" data-approval-id="' +
1591
1898
  escapeHtml(approvalId) +
@@ -1608,7 +1915,6 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1608
1915
  .join("");
1609
1916
  return (
1610
1917
  '<div class="approval-requests">' +
1611
- '<div class="approval-requests-label">Approval required</div>' +
1612
1918
  rows +
1613
1919
  "</div>"
1614
1920
  );
@@ -1638,22 +1944,46 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1638
1944
  "</details>"
1639
1945
  )
1640
1946
  : "";
1947
+ const cls = "tool-activity" + (hasApprovals ? " has-approvals" : "");
1641
1948
  return (
1642
- '<div class="tool-activity">' +
1949
+ '<div class="' + cls + '">' +
1643
1950
  disclosure +
1644
1951
  renderApprovalRequests(approvalRequests) +
1645
1952
  "</div>"
1646
1953
  );
1647
1954
  };
1648
1955
 
1649
- const safeJsonPreview = (value) => {
1650
- try {
1651
- return JSON.stringify(value, (_, nestedValue) =>
1652
- typeof nestedValue === "bigint" ? String(nestedValue) : nestedValue,
1653
- );
1654
- } catch {
1655
- return "[unserializable input]";
1956
+ const renderInputTable = (input) => {
1957
+ if (!input || typeof input !== "object") {
1958
+ return '<div class="av-complex">' + escapeHtml(String(input ?? "{}")) + "</div>";
1959
+ }
1960
+ const keys = Object.keys(input);
1961
+ if (keys.length === 0) {
1962
+ return '<div class="av-complex">{}</div>';
1656
1963
  }
1964
+ const formatValue = (val) => {
1965
+ if (val === null || val === undefined) return escapeHtml("null");
1966
+ if (typeof val === "boolean" || typeof val === "number") return escapeHtml(String(val));
1967
+ if (typeof val === "string") return escapeHtml(val);
1968
+ try {
1969
+ const replacer = (_, v) => typeof v === "bigint" ? String(v) : v;
1970
+ return escapeHtml(JSON.stringify(val, replacer, 2));
1971
+ } catch {
1972
+ return escapeHtml("[unserializable]");
1973
+ }
1974
+ };
1975
+ const rows = keys.map((key) => {
1976
+ const val = input[key];
1977
+ const isComplex = val !== null && typeof val === "object";
1978
+ const cls = isComplex ? "av-complex" : "av";
1979
+ return (
1980
+ "<tr>" +
1981
+ '<td class="ak">' + escapeHtml(key) + "</td>" +
1982
+ '<td><div class="' + cls + '">' + formatValue(val) + "</div></td>" +
1983
+ "</tr>"
1984
+ );
1985
+ }).join("");
1986
+ return '<table class="approval-request-table">' + rows + "</table>";
1657
1987
  };
1658
1988
 
1659
1989
  const updatePendingApproval = (approvalId, updater) => {
@@ -1692,12 +2022,10 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1692
2022
  return null;
1693
2023
  }
1694
2024
  const toolName = item && typeof item.tool === "string" ? item.tool : "tool";
1695
- const preview = safeJsonPreview(item?.input ?? {});
1696
- const inputPreview = preview.length > 600 ? preview.slice(0, 600) + "..." : preview;
1697
2025
  return {
1698
2026
  approvalId,
1699
2027
  tool: toolName,
1700
- inputPreview,
2028
+ input: item?.input ?? {},
1701
2029
  state: "pending",
1702
2030
  };
1703
2031
  })
@@ -1775,50 +2103,87 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1775
2103
  elements.shell.classList.toggle("sidebar-open", open);
1776
2104
  };
1777
2105
 
1778
- const renderConversationList = () => {
1779
- elements.list.innerHTML = "";
1780
- for (const c of state.conversations) {
1781
- const item = document.createElement("div");
1782
- item.className = "conversation-item" + (c.conversationId === state.activeConversationId ? " active" : "");
1783
- item.textContent = c.title;
1784
-
1785
- const isConfirming = state.confirmDeleteId === c.conversationId;
1786
- const deleteBtn = document.createElement("button");
1787
- deleteBtn.className = "delete-btn" + (isConfirming ? " confirming" : "");
1788
- deleteBtn.textContent = isConfirming ? "sure?" : "\\u00d7";
1789
- deleteBtn.onclick = async (e) => {
1790
- e.stopPropagation();
1791
- if (!isConfirming) {
1792
- state.confirmDeleteId = c.conversationId;
1793
- renderConversationList();
1794
- return;
1795
- }
1796
- await api("/api/conversations/" + c.conversationId, { method: "DELETE" });
1797
- if (state.activeConversationId === c.conversationId) {
1798
- state.activeConversationId = null;
1799
- state.activeMessages = [];
1800
- pushConversationUrl(null);
1801
- elements.chatTitle.textContent = "";
1802
- renderMessages([]);
1803
- }
1804
- state.confirmDeleteId = null;
1805
- await loadConversations();
1806
- };
1807
- item.appendChild(deleteBtn);
2106
+ const buildConversationItem = (c) => {
2107
+ const item = document.createElement("div");
2108
+ item.className = "conversation-item" + (c.conversationId === state.activeConversationId ? " active" : "");
1808
2109
 
1809
- item.onclick = async () => {
1810
- // Clear any delete confirmation, but still navigate
1811
- if (state.confirmDeleteId) {
1812
- state.confirmDeleteId = null;
1813
- }
1814
- state.activeConversationId = c.conversationId;
1815
- pushConversationUrl(c.conversationId);
2110
+ if (c.hasPendingApprovals) {
2111
+ const dot = document.createElement("span");
2112
+ dot.className = "approval-dot";
2113
+ item.appendChild(dot);
2114
+ }
2115
+
2116
+ const titleSpan = document.createElement("span");
2117
+ titleSpan.textContent = c.title;
2118
+ item.appendChild(titleSpan);
2119
+
2120
+ const isConfirming = state.confirmDeleteId === c.conversationId;
2121
+ const deleteBtn = document.createElement("button");
2122
+ deleteBtn.className = "delete-btn" + (isConfirming ? " confirming" : "");
2123
+ deleteBtn.textContent = isConfirming ? "sure?" : "\\u00d7";
2124
+ deleteBtn.onclick = async (e) => {
2125
+ e.stopPropagation();
2126
+ if (!isConfirming) {
2127
+ state.confirmDeleteId = c.conversationId;
1816
2128
  renderConversationList();
1817
- await loadConversation(c.conversationId);
1818
- if (isMobile()) setSidebarOpen(false);
1819
- };
2129
+ return;
2130
+ }
2131
+ await api("/api/conversations/" + c.conversationId, { method: "DELETE" });
2132
+ if (state.activeConversationId === c.conversationId) {
2133
+ state.activeConversationId = null;
2134
+ state.activeMessages = [];
2135
+ state.contextTokens = 0;
2136
+ state.contextWindow = 0;
2137
+ updateContextRing();
2138
+ pushConversationUrl(null);
2139
+ elements.chatTitle.textContent = "";
2140
+ renderMessages([]);
2141
+ }
2142
+ state.confirmDeleteId = null;
2143
+ await loadConversations();
2144
+ };
2145
+ item.appendChild(deleteBtn);
2146
+
2147
+ item.onclick = async () => {
2148
+ if (state.confirmDeleteId) {
2149
+ state.confirmDeleteId = null;
2150
+ }
2151
+ state.activeConversationId = c.conversationId;
2152
+ pushConversationUrl(c.conversationId);
2153
+ renderConversationList();
2154
+ await loadConversation(c.conversationId);
2155
+ if (isMobile()) setSidebarOpen(false);
2156
+ };
2157
+
2158
+ return item;
2159
+ };
2160
+
2161
+ const renderConversationList = () => {
2162
+ elements.list.innerHTML = "";
2163
+ const pending = state.conversations.filter(c => c.hasPendingApprovals);
2164
+ const rest = state.conversations.filter(c => !c.hasPendingApprovals);
2165
+
2166
+ if (pending.length > 0) {
2167
+ const label = document.createElement("div");
2168
+ label.className = "sidebar-section-label";
2169
+ label.textContent = "Awaiting approval";
2170
+ elements.list.appendChild(label);
2171
+ for (const c of pending) {
2172
+ elements.list.appendChild(buildConversationItem(c));
2173
+ }
2174
+ if (rest.length > 0) {
2175
+ const divider = document.createElement("div");
2176
+ divider.className = "sidebar-section-divider";
2177
+ elements.list.appendChild(divider);
2178
+ const recentLabel = document.createElement("div");
2179
+ recentLabel.className = "sidebar-section-label";
2180
+ recentLabel.textContent = "Recent";
2181
+ elements.list.appendChild(recentLabel);
2182
+ }
2183
+ }
1820
2184
 
1821
- elements.list.appendChild(item);
2185
+ for (const c of rest) {
2186
+ elements.list.appendChild(buildConversationItem(c));
1822
2187
  }
1823
2188
  };
1824
2189
 
@@ -1893,8 +2258,14 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
1893
2258
  } else if (shouldRenderEmptyStreamingIndicator) {
1894
2259
  content.appendChild(createThinkingIndicator(getThinkingStatusLabel(m)));
1895
2260
  } else {
1896
- // Check for sections in _sections (streaming) or metadata.sections (stored)
1897
- const sections = m._sections || (m.metadata && m.metadata.sections);
2261
+ // Merge stored sections (persisted) with live sections (from
2262
+ // an active stream). For normal messages only one source
2263
+ // exists; for liveOnly reconnects both contribute.
2264
+ const storedSections = (m.metadata && m.metadata.sections) || [];
2265
+ const liveSections = m._sections || [];
2266
+ const sections = liveSections.length > 0 && storedSections.length > 0
2267
+ ? storedSections.concat(liveSections)
2268
+ : liveSections.length > 0 ? liveSections : (storedSections.length > 0 ? storedSections : null);
1898
2269
  const pendingApprovals = Array.isArray(m._pendingApprovals) ? m._pendingApprovals : [];
1899
2270
 
1900
2271
  if (sections && sections.length > 0) {
@@ -2035,11 +2406,24 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2035
2406
  payload.conversation.messages || [],
2036
2407
  payload.conversation.pendingApprovals || payload.pendingApprovals || [],
2037
2408
  );
2409
+ state.contextTokens = 0;
2410
+ state.contextWindow = 0;
2411
+ updateContextRing();
2038
2412
  renderMessages(state.activeMessages, false, { forceScrollBottom: true });
2039
2413
  elements.prompt.focus();
2414
+ if (payload.hasActiveRun && !state.isStreaming) {
2415
+ setStreaming(true);
2416
+ streamConversationEvents(conversationId, { liveOnly: true }).finally(() => {
2417
+ if (state.activeConversationId === conversationId) {
2418
+ setStreaming(false);
2419
+ renderMessages(state.activeMessages, false);
2420
+ }
2421
+ });
2422
+ }
2040
2423
  };
2041
2424
 
2042
- const streamConversationEvents = (conversationId) => {
2425
+ const streamConversationEvents = (conversationId, options) => {
2426
+ const liveOnly = options && options.liveOnly;
2043
2427
  return new Promise((resolve) => {
2044
2428
  const localMessages = state.activeMessages || [];
2045
2429
  const renderIfActiveConversation = (streaming) => {
@@ -2058,20 +2442,36 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2058
2442
  _currentText: "",
2059
2443
  _currentTools: [],
2060
2444
  _pendingApprovals: [],
2445
+ _activeActivities: [],
2061
2446
  metadata: { toolActivity: [] },
2062
2447
  };
2063
2448
  localMessages.push(assistantMessage);
2064
2449
  state.activeMessages = localMessages;
2065
2450
  }
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";
2451
+ if (liveOnly) {
2452
+ // Live-only mode: keep metadata.sections intact (the stored
2453
+ // base content) and start _sections empty so it only collects
2454
+ // NEW sections from live events. The renderer merges both.
2455
+ assistantMessage._sections = [];
2456
+ assistantMessage._currentText = "";
2457
+ assistantMessage._currentTools = [];
2458
+ if (!assistantMessage._activeActivities) assistantMessage._activeActivities = [];
2459
+ if (!assistantMessage._pendingApprovals) assistantMessage._pendingApprovals = [];
2460
+ if (!assistantMessage.metadata) assistantMessage.metadata = {};
2461
+ if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
2462
+ } else {
2463
+ // Full replay mode: reset transient state so replayed events
2464
+ // rebuild from scratch (the buffer has the full event history).
2465
+ assistantMessage.content = "";
2466
+ assistantMessage._sections = [];
2467
+ assistantMessage._currentText = "";
2468
+ assistantMessage._currentTools = [];
2469
+ assistantMessage._activeActivities = [];
2470
+ assistantMessage._pendingApprovals = [];
2471
+ assistantMessage.metadata = { toolActivity: [] };
2472
+ }
2473
+
2474
+ const url = "/api/conversations/" + encodeURIComponent(conversationId) + "/events" + (liveOnly ? "?live_only=true" : "");
2075
2475
  fetch(url, { credentials: "include" }).then((response) => {
2076
2476
  if (!response.ok || !response.body) {
2077
2477
  resolve(undefined);
@@ -2092,6 +2492,11 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2092
2492
  if (eventName === "stream:end") {
2093
2493
  return;
2094
2494
  }
2495
+ if (eventName === "run:started") {
2496
+ if (typeof payload.contextWindow === "number" && payload.contextWindow > 0) {
2497
+ state.contextWindow = payload.contextWindow;
2498
+ }
2499
+ }
2095
2500
  if (eventName === "model:chunk") {
2096
2501
  const chunk = String(payload.content || "");
2097
2502
  if (assistantMessage._currentTools.length > 0 && chunk.length > 0) {
@@ -2105,6 +2510,12 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2105
2510
  assistantMessage._currentText += chunk;
2106
2511
  renderIfActiveConversation(true);
2107
2512
  }
2513
+ if (eventName === "model:response") {
2514
+ if (typeof payload.usage?.input === "number") {
2515
+ state.contextTokens = payload.usage.input;
2516
+ updateContextRing();
2517
+ }
2518
+ }
2108
2519
  if (eventName === "tool:started") {
2109
2520
  const toolName = payload.tool || "tool";
2110
2521
  const startedActivity = addActiveActivityFromToolStart(
@@ -2177,6 +2588,73 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2177
2588
  assistantMessage.metadata.toolActivity.push(toolText);
2178
2589
  renderIfActiveConversation(true);
2179
2590
  }
2591
+ if (eventName === "tool:approval:required") {
2592
+ const toolName = payload.tool || "tool";
2593
+ const activeActivity = removeActiveActivityForTool(
2594
+ assistantMessage,
2595
+ toolName,
2596
+ );
2597
+ const detailFromPayload = describeToolStart(payload);
2598
+ const detail =
2599
+ (activeActivity && typeof activeActivity.detail === "string"
2600
+ ? activeActivity.detail.trim()
2601
+ : "") ||
2602
+ (detailFromPayload && typeof detailFromPayload.detail === "string"
2603
+ ? detailFromPayload.detail.trim()
2604
+ : "");
2605
+ const toolText =
2606
+ "- approval required \\x60" +
2607
+ toolName +
2608
+ "\\x60" +
2609
+ (detail ? " (" + detail + ")" : "");
2610
+ assistantMessage._currentTools.push(toolText);
2611
+ assistantMessage.metadata.toolActivity.push(toolText);
2612
+ const approvalId =
2613
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2614
+ if (approvalId) {
2615
+ if (!Array.isArray(assistantMessage._pendingApprovals)) {
2616
+ assistantMessage._pendingApprovals = [];
2617
+ }
2618
+ const exists = assistantMessage._pendingApprovals.some(
2619
+ (req) => req.approvalId === approvalId,
2620
+ );
2621
+ if (!exists) {
2622
+ assistantMessage._pendingApprovals.push({
2623
+ approvalId,
2624
+ tool: toolName,
2625
+ input: payload.input ?? {},
2626
+ state: "pending",
2627
+ });
2628
+ }
2629
+ }
2630
+ renderIfActiveConversation(true);
2631
+ }
2632
+ if (eventName === "tool:approval:granted") {
2633
+ const toolText = "- approval granted";
2634
+ assistantMessage._currentTools.push(toolText);
2635
+ assistantMessage.metadata.toolActivity.push(toolText);
2636
+ const approvalId =
2637
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2638
+ if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2639
+ assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2640
+ (req) => req.approvalId !== approvalId,
2641
+ );
2642
+ }
2643
+ renderIfActiveConversation(true);
2644
+ }
2645
+ if (eventName === "tool:approval:denied") {
2646
+ const toolText = "- approval denied";
2647
+ assistantMessage._currentTools.push(toolText);
2648
+ assistantMessage.metadata.toolActivity.push(toolText);
2649
+ const approvalId =
2650
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2651
+ if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2652
+ assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2653
+ (req) => req.approvalId !== approvalId,
2654
+ );
2655
+ }
2656
+ renderIfActiveConversation(true);
2657
+ }
2180
2658
  if (eventName === "run:completed") {
2181
2659
  assistantMessage._activeActivities = [];
2182
2660
  if (
@@ -2223,9 +2701,22 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2223
2701
  }
2224
2702
  if (eventName === "run:error") {
2225
2703
  assistantMessage._activeActivities = [];
2704
+ if (assistantMessage._currentTools.length > 0) {
2705
+ assistantMessage._sections.push({
2706
+ type: "tools",
2707
+ content: assistantMessage._currentTools,
2708
+ });
2709
+ assistantMessage._currentTools = [];
2710
+ }
2711
+ if (assistantMessage._currentText.length > 0) {
2712
+ assistantMessage._sections.push({
2713
+ type: "text",
2714
+ content: assistantMessage._currentText,
2715
+ });
2716
+ assistantMessage._currentText = "";
2717
+ }
2226
2718
  const errMsg =
2227
2719
  payload.error?.message || "Something went wrong";
2228
- assistantMessage.content = "";
2229
2720
  assistantMessage._error = errMsg;
2230
2721
  renderIfActiveConversation(false);
2231
2722
  }
@@ -2298,6 +2789,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2298
2789
  elements.send.disabled = value ? !canStop : false;
2299
2790
  elements.send.innerHTML = value ? stopIconMarkup : sendIconMarkup;
2300
2791
  elements.send.classList.toggle("stop-mode", value);
2792
+ if (elements.sendBtnWrapper) {
2793
+ elements.sendBtnWrapper.classList.toggle("stop-mode", value);
2794
+ }
2301
2795
  elements.send.setAttribute("aria-label", value ? "Stop response" : "Send message");
2302
2796
  elements.send.setAttribute(
2303
2797
  "title",
@@ -2670,8 +3164,17 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2670
3164
  }
2671
3165
  if (eventName === "run:started") {
2672
3166
  state.activeStreamRunId = typeof payload.runId === "string" ? payload.runId : null;
3167
+ if (typeof payload.contextWindow === "number" && payload.contextWindow > 0) {
3168
+ state.contextWindow = payload.contextWindow;
3169
+ }
2673
3170
  setStreaming(state.isStreaming);
2674
3171
  }
3172
+ if (eventName === "model:response") {
3173
+ if (typeof payload.usage?.input === "number") {
3174
+ state.contextTokens = payload.usage.input;
3175
+ updateContextRing();
3176
+ }
3177
+ }
2675
3178
  if (eventName === "tool:started") {
2676
3179
  const toolName = payload.tool || "tool";
2677
3180
  const startedActivity = addActiveActivityFromToolStart(
@@ -2767,8 +3270,6 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2767
3270
  const approvalId =
2768
3271
  typeof payload.approvalId === "string" ? payload.approvalId : "";
2769
3272
  if (approvalId) {
2770
- const preview = safeJsonPreview(payload.input ?? {});
2771
- const inputPreview = preview.length > 600 ? preview.slice(0, 600) + "..." : preview;
2772
3273
  if (!Array.isArray(assistantMessage._pendingApprovals)) {
2773
3274
  assistantMessage._pendingApprovals = [];
2774
3275
  }
@@ -2779,7 +3280,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2779
3280
  assistantMessage._pendingApprovals.push({
2780
3281
  approvalId,
2781
3282
  tool: toolName,
2782
- inputPreview,
3283
+ input: payload.input ?? {},
2783
3284
  state: "pending",
2784
3285
  });
2785
3286
  }
@@ -2834,9 +3335,8 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2834
3335
  renderIfActiveConversation(false);
2835
3336
  }
2836
3337
  if (eventName === "run:error") {
2837
- assistantMessage._activeActivities = [];
3338
+ finalizeAssistantMessage();
2838
3339
  const errMsg = payload.error?.message || "Something went wrong";
2839
- assistantMessage.content = "";
2840
3340
  assistantMessage._error = errMsg;
2841
3341
  renderIfActiveConversation(false);
2842
3342
  }
@@ -2939,6 +3439,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2939
3439
  state.activeConversationId = null;
2940
3440
  state.activeMessages = [];
2941
3441
  state.confirmDeleteId = null;
3442
+ state.contextTokens = 0;
3443
+ state.contextWindow = 0;
3444
+ updateContextRing();
2942
3445
  pushConversationUrl(null);
2943
3446
  elements.chatTitle.textContent = "";
2944
3447
  renderMessages([]);
@@ -2976,6 +3479,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
2976
3479
  state.confirmDeleteId = null;
2977
3480
  state.conversations = [];
2978
3481
  state.csrfToken = "";
3482
+ state.contextTokens = 0;
3483
+ state.contextWindow = 0;
3484
+ updateContextRing();
2979
3485
  await requireAuth();
2980
3486
  });
2981
3487
 
@@ -3123,17 +3629,23 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
3123
3629
  });
3124
3630
  updatePendingApproval(approvalId, () => null);
3125
3631
  renderMessages(state.activeMessages, state.isStreaming);
3632
+ loadConversations();
3126
3633
  if (!wasStreaming && state.activeConversationId) {
3127
- await streamConversationEvents(state.activeConversationId);
3634
+ await streamConversationEvents(state.activeConversationId, { liveOnly: true });
3128
3635
  }
3129
3636
  } catch (error) {
3130
- const errMsg = error instanceof Error ? error.message : String(error);
3131
- updatePendingApproval(approvalId, (request) => ({
3132
- ...request,
3133
- state: "pending",
3134
- pendingDecision: null,
3135
- inputPreview: String(request.inputPreview || "") + " (submit failed: " + errMsg + ")",
3136
- }));
3637
+ const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
3638
+ if (isStale) {
3639
+ updatePendingApproval(approvalId, () => null);
3640
+ } else {
3641
+ const errMsg = error instanceof Error ? error.message : String(error);
3642
+ updatePendingApproval(approvalId, (request) => ({
3643
+ ...request,
3644
+ state: "pending",
3645
+ pendingDecision: null,
3646
+ _error: errMsg,
3647
+ }));
3648
+ }
3137
3649
  renderMessages(state.activeMessages, state.isStreaming);
3138
3650
  } finally {
3139
3651
  if (!wasStreaming) {
@@ -3180,6 +3692,9 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
3180
3692
  } else {
3181
3693
  state.activeConversationId = null;
3182
3694
  state.activeMessages = [];
3695
+ state.contextTokens = 0;
3696
+ state.contextWindow = 0;
3697
+ updateContextRing();
3183
3698
  elements.chatTitle.textContent = "";
3184
3699
  renderMessages([]);
3185
3700
  renderConversationList();
@@ -3221,6 +3736,7 @@ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
3221
3736
  await createConversation();
3222
3737
  }
3223
3738
  autoResizePrompt();
3739
+ updateContextRing();
3224
3740
  elements.prompt.focus();
3225
3741
  })();
3226
3742