@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.
@@ -24,6 +24,7 @@ import {
24
24
  import { getTextContent } from "@poncho-ai/sdk";
25
25
  import {
26
26
  AgentBridge,
27
+ ResendAdapter,
27
28
  SlackAdapter
28
29
  } from "@poncho-ai/messaging";
29
30
  import Busboy from "busboy";
@@ -258,7 +259,8 @@ var renderWebUiHtml = (options) => {
258
259
  <head>
259
260
  <meta charset="utf-8">
260
261
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
261
- <meta name="theme-color" content="#000000">
262
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
263
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
262
264
  <meta name="apple-mobile-web-app-capable" content="yes">
263
265
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
264
266
  <meta name="apple-mobile-web-app-title" content="${agentName}">
@@ -268,12 +270,168 @@ var renderWebUiHtml = (options) => {
268
270
  <title>${agentName}</title>
269
271
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata:400,700">
270
272
  <style>
273
+ :root {
274
+ color-scheme: light dark;
275
+
276
+ --bg: #000;
277
+ --bg-alt: #0a0a0a;
278
+ --bg-elevated: #111;
279
+
280
+ --fg: #ededed;
281
+ --fg-strong: #fff;
282
+ --fg-2: #888;
283
+ --fg-3: #999;
284
+ --fg-4: #777;
285
+ --fg-5: #666;
286
+ --fg-6: #555;
287
+ --fg-7: #444;
288
+ --fg-8: #333;
289
+
290
+ --fg-tool: #8a8a8a;
291
+ --fg-tool-code: #bcbcbc;
292
+ --fg-tool-item: #d6d6d6;
293
+ --fg-approval-label: #b0b0b0;
294
+ --fg-approval-input: #cfcfcf;
295
+ --fg-approval-btn: #f0f0f0;
296
+
297
+ --accent: #ededed;
298
+ --accent-fg: #000;
299
+ --accent-hover: #fff;
300
+
301
+ --stop-bg: #4a4a4a;
302
+ --stop-fg: #fff;
303
+ --stop-hover: #565656;
304
+
305
+ --border-1: rgba(255,255,255,0.06);
306
+ --border-2: rgba(255,255,255,0.08);
307
+ --border-3: rgba(255,255,255,0.1);
308
+ --border-4: rgba(255,255,255,0.12);
309
+ --border-5: rgba(255,255,255,0.18);
310
+ --border-focus: rgba(255,255,255,0.2);
311
+ --border-hover: rgba(255,255,255,0.25);
312
+ --border-drag: rgba(255,255,255,0.4);
313
+
314
+ --surface-1: rgba(255,255,255,0.02);
315
+ --surface-2: rgba(255,255,255,0.03);
316
+ --surface-3: rgba(255,255,255,0.04);
317
+ --surface-4: rgba(255,255,255,0.06);
318
+ --surface-5: rgba(255,255,255,0.08);
319
+ --surface-6: rgba(255,255,255,0.1);
320
+ --surface-7: rgba(255,255,255,0.12);
321
+ --surface-8: rgba(255,255,255,0.14);
322
+
323
+ --chip-bg: rgba(0,0,0,0.6);
324
+ --chip-bg-hover: rgba(0,0,0,0.75);
325
+ --backdrop: rgba(0,0,0,0.6);
326
+ --lightbox-bg: rgba(0,0,0,0.85);
327
+ --inset-1: rgba(0,0,0,0.16);
328
+ --inset-2: rgba(0,0,0,0.25);
329
+
330
+ --file-badge-bg: rgba(0,0,0,0.2);
331
+ --file-badge-fg: rgba(255,255,255,0.8);
332
+
333
+ --error: #ff4444;
334
+ --error-soft: #ff6b6b;
335
+ --error-alt: #ff6666;
336
+ --error-bg: rgba(255,68,68,0.08);
337
+ --error-border: rgba(255,68,68,0.25);
338
+
339
+ --tool-done: #6a9955;
340
+ --tool-error: #f48771;
341
+
342
+ --approve: #78e7a6;
343
+ --approve-border: rgba(58,208,122,0.45);
344
+ --deny: #f59b9b;
345
+ --deny-border: rgba(224,95,95,0.45);
346
+
347
+ --scrollbar: rgba(255,255,255,0.1);
348
+ --scrollbar-hover: rgba(255,255,255,0.16);
349
+ }
350
+
351
+ @media (prefers-color-scheme: light) {
352
+ :root {
353
+ --bg: #ffffff;
354
+ --bg-alt: #f5f5f5;
355
+ --bg-elevated: #e8e8e8;
356
+
357
+ --fg: #1a1a1a;
358
+ --fg-strong: #000;
359
+ --fg-2: #666;
360
+ --fg-3: #555;
361
+ --fg-4: #777;
362
+ --fg-5: #888;
363
+ --fg-6: #888;
364
+ --fg-7: #aaa;
365
+ --fg-8: #bbb;
366
+
367
+ --fg-tool: #666;
368
+ --fg-tool-code: #444;
369
+ --fg-tool-item: #333;
370
+ --fg-approval-label: #666;
371
+ --fg-approval-input: #444;
372
+ --fg-approval-btn: #1a1a1a;
373
+
374
+ --accent: #1a1a1a;
375
+ --accent-fg: #fff;
376
+ --accent-hover: #000;
377
+
378
+ --stop-bg: #d4d4d4;
379
+ --stop-fg: #333;
380
+ --stop-hover: #c4c4c4;
381
+
382
+ --border-1: rgba(0,0,0,0.06);
383
+ --border-2: rgba(0,0,0,0.08);
384
+ --border-3: rgba(0,0,0,0.1);
385
+ --border-4: rgba(0,0,0,0.1);
386
+ --border-5: rgba(0,0,0,0.15);
387
+ --border-focus: rgba(0,0,0,0.2);
388
+ --border-hover: rgba(0,0,0,0.2);
389
+ --border-drag: rgba(0,0,0,0.3);
390
+
391
+ --surface-1: rgba(0,0,0,0.02);
392
+ --surface-2: rgba(0,0,0,0.03);
393
+ --surface-3: rgba(0,0,0,0.03);
394
+ --surface-4: rgba(0,0,0,0.04);
395
+ --surface-5: rgba(0,0,0,0.05);
396
+ --surface-6: rgba(0,0,0,0.07);
397
+ --surface-7: rgba(0,0,0,0.08);
398
+ --surface-8: rgba(0,0,0,0.1);
399
+
400
+ --chip-bg: rgba(255,255,255,0.8);
401
+ --chip-bg-hover: rgba(255,255,255,0.9);
402
+ --backdrop: rgba(0,0,0,0.3);
403
+ --lightbox-bg: rgba(0,0,0,0.75);
404
+ --inset-1: rgba(0,0,0,0.04);
405
+ --inset-2: rgba(0,0,0,0.06);
406
+
407
+ --file-badge-bg: rgba(0,0,0,0.05);
408
+ --file-badge-fg: rgba(0,0,0,0.7);
409
+
410
+ --error: #dc2626;
411
+ --error-soft: #ef4444;
412
+ --error-alt: #ef4444;
413
+ --error-bg: rgba(220,38,38,0.06);
414
+ --error-border: rgba(220,38,38,0.2);
415
+
416
+ --tool-done: #16a34a;
417
+ --tool-error: #dc2626;
418
+
419
+ --approve: #16a34a;
420
+ --approve-border: rgba(22,163,74,0.35);
421
+ --deny: #dc2626;
422
+ --deny-border: rgba(220,38,38,0.3);
423
+
424
+ --scrollbar: rgba(0,0,0,0.12);
425
+ --scrollbar-hover: rgba(0,0,0,0.2);
426
+ }
427
+ }
428
+
271
429
  * { box-sizing: border-box; margin: 0; padding: 0; }
272
430
  html, body { height: 100vh; overflow: hidden; overscroll-behavior: none; touch-action: pan-y; }
273
431
  body {
274
432
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif;
275
- background: #000;
276
- color: #ededed;
433
+ background: var(--bg);
434
+ color: var(--fg);
277
435
  font-size: 14px;
278
436
  line-height: 1.5;
279
437
  -webkit-font-smoothing: antialiased;
@@ -281,7 +439,7 @@ var renderWebUiHtml = (options) => {
281
439
  }
282
440
  button, input, textarea { font: inherit; color: inherit; }
283
441
  .hidden { display: none !important; }
284
- a { color: #ededed; }
442
+ a { color: var(--fg); }
285
443
 
286
444
  /* Auth */
287
445
  .auth {
@@ -289,39 +447,39 @@ var renderWebUiHtml = (options) => {
289
447
  display: grid;
290
448
  place-items: center;
291
449
  padding: 20px;
292
- background: #000;
450
+ background: var(--bg);
293
451
  }
294
452
  .auth-card {
295
453
  width: min(400px, 90vw);
296
454
  }
297
455
  .auth-shell {
298
- background: #0a0a0a;
299
- border: 1px solid rgba(255,255,255,0.1);
456
+ background: var(--bg-alt);
457
+ border: 1px solid var(--border-3);
300
458
  border-radius: 9999px;
301
459
  display: flex;
302
460
  align-items: center;
303
461
  padding: 4px 6px 4px 18px;
304
462
  transition: border-color 0.15s;
305
463
  }
306
- .auth-shell:focus-within { border-color: rgba(255,255,255,0.2); }
464
+ .auth-shell:focus-within { border-color: var(--border-focus); }
307
465
  .auth-input {
308
466
  flex: 1;
309
467
  background: transparent;
310
468
  border: 0;
311
469
  outline: none;
312
- color: #ededed;
470
+ color: var(--fg);
313
471
  padding: 10px 0 8px;
314
472
  font-size: 14px;
315
473
  margin-top: -2px;
316
474
  }
317
- .auth-input::placeholder { color: #444; }
475
+ .auth-input::placeholder { color: var(--fg-7); }
318
476
  .auth-submit {
319
477
  width: 32px;
320
478
  height: 32px;
321
- background: #ededed;
479
+ background: var(--accent);
322
480
  border: 0;
323
481
  border-radius: 50%;
324
- color: #000;
482
+ color: var(--accent-fg);
325
483
  cursor: pointer;
326
484
  display: grid;
327
485
  place-items: center;
@@ -329,19 +487,19 @@ var renderWebUiHtml = (options) => {
329
487
  margin-bottom: 2px;
330
488
  transition: background 0.15s;
331
489
  }
332
- .auth-submit:hover { background: #fff; }
333
- .error { color: #ff4444; font-size: 13px; min-height: 16px; }
490
+ .auth-submit:hover { background: var(--accent-hover); }
491
+ .error { color: var(--error); font-size: 13px; min-height: 16px; }
334
492
  .message-error {
335
- background: rgba(255,68,68,0.08);
336
- border: 1px solid rgba(255,68,68,0.25);
493
+ background: var(--error-bg);
494
+ border: 1px solid var(--error-border);
337
495
  border-radius: 10px;
338
- color: #ff6b6b;
496
+ color: var(--error-soft);
339
497
  padding: 12px 16px;
340
498
  font-size: 13px;
341
499
  line-height: 1.5;
342
500
  max-width: 600px;
343
501
  }
344
- .message-error strong { color: #ff4444; }
502
+ .message-error strong { color: var(--error); }
345
503
 
346
504
  /* Layout - use fixed positioning with explicit dimensions */
347
505
  .shell {
@@ -373,8 +531,8 @@ var renderWebUiHtml = (options) => {
373
531
  }
374
532
  .sidebar {
375
533
  width: 260px;
376
- background: #000;
377
- border-right: 1px solid rgba(255,255,255,0.06);
534
+ background: var(--bg);
535
+ border-right: 1px solid var(--border-1);
378
536
  display: flex;
379
537
  flex-direction: column;
380
538
  padding: 12px 8px;
@@ -382,7 +540,7 @@ var renderWebUiHtml = (options) => {
382
540
  .new-chat-btn {
383
541
  background: transparent;
384
542
  border: 0;
385
- color: #888;
543
+ color: var(--fg-2);
386
544
  border-radius: 12px;
387
545
  height: 36px;
388
546
  padding: 0 10px;
@@ -393,7 +551,7 @@ var renderWebUiHtml = (options) => {
393
551
  cursor: pointer;
394
552
  transition: background 0.15s, color 0.15s;
395
553
  }
396
- .new-chat-btn:hover { color: #ededed; }
554
+ .new-chat-btn:hover { color: var(--fg); }
397
555
  .new-chat-btn svg { width: 16px; height: 16px; }
398
556
  .conversation-list {
399
557
  flex: 1;
@@ -413,16 +571,16 @@ var renderWebUiHtml = (options) => {
413
571
  cursor: pointer;
414
572
  font-size: 13px;
415
573
  line-height: 36px;
416
- color: #555;
574
+ color: var(--fg-6);
417
575
  white-space: nowrap;
418
576
  overflow: hidden;
419
577
  text-overflow: ellipsis;
420
578
  position: relative;
421
579
  transition: color 0.15s;
422
580
  }
423
- .conversation-item:hover { color: #999; }
581
+ .conversation-item:hover { color: var(--fg-3); }
424
582
  .conversation-item.active {
425
- color: #ededed;
583
+ color: var(--fg);
426
584
  }
427
585
  .conversation-item .delete-btn {
428
586
  position: absolute;
@@ -430,9 +588,9 @@ var renderWebUiHtml = (options) => {
430
588
  top: 0;
431
589
  bottom: 0;
432
590
  opacity: 0;
433
- background: #000;
591
+ background: var(--bg);
434
592
  border: 0;
435
- color: #444;
593
+ color: var(--fg-7);
436
594
  padding: 0 8px;
437
595
  border-radius: 0 4px 4px 0;
438
596
  cursor: pointer;
@@ -443,7 +601,7 @@ var renderWebUiHtml = (options) => {
443
601
  transition: opacity 0.15s, color 0.15s;
444
602
  }
445
603
  .conversation-item:hover .delete-btn { opacity: 1; }
446
- .conversation-item.active .delete-btn { background: rgba(0,0,0,1); }
604
+ .conversation-item.active .delete-btn { background: var(--bg); }
447
605
  .conversation-item .delete-btn::before {
448
606
  content: "";
449
607
  position: absolute;
@@ -451,23 +609,23 @@ var renderWebUiHtml = (options) => {
451
609
  top: 0;
452
610
  bottom: 0;
453
611
  width: 24px;
454
- background: linear-gradient(to right, transparent, #000);
612
+ background: linear-gradient(to right, transparent, var(--bg));
455
613
  pointer-events: none;
456
614
  }
457
615
  .conversation-item.active .delete-btn::before {
458
- background: linear-gradient(to right, transparent, rgba(0,0,0,1));
616
+ background: linear-gradient(to right, transparent, var(--bg));
459
617
  }
460
- .conversation-item .delete-btn:hover { color: #888; }
618
+ .conversation-item .delete-btn:hover { color: var(--fg-2); }
461
619
  .conversation-item .delete-btn.confirming {
462
620
  opacity: 1;
463
621
  width: auto;
464
622
  padding: 0 8px;
465
623
  font-size: 11px;
466
- color: #ff4444;
624
+ color: var(--error);
467
625
  border-radius: 3px;
468
626
  }
469
627
  .conversation-item .delete-btn.confirming:hover {
470
- color: #ff6666;
628
+ color: var(--error-alt);
471
629
  }
472
630
  .sidebar-footer {
473
631
  margin-top: auto;
@@ -476,7 +634,7 @@ var renderWebUiHtml = (options) => {
476
634
  .logout-btn {
477
635
  background: transparent;
478
636
  border: 0;
479
- color: #555;
637
+ color: var(--fg-6);
480
638
  width: 100%;
481
639
  padding: 8px 10px;
482
640
  text-align: left;
@@ -485,10 +643,10 @@ var renderWebUiHtml = (options) => {
485
643
  font-size: 13px;
486
644
  transition: color 0.15s, background 0.15s;
487
645
  }
488
- .logout-btn:hover { color: #888; }
646
+ .logout-btn:hover { color: var(--fg-2); }
489
647
 
490
648
  /* Main */
491
- .main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: #000; overflow: hidden; }
649
+ .main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: var(--bg); overflow: hidden; }
492
650
  .topbar {
493
651
  height: calc(52px + env(safe-area-inset-top, 0px));
494
652
  padding-top: env(safe-area-inset-top, 0px);
@@ -497,8 +655,8 @@ var renderWebUiHtml = (options) => {
497
655
  justify-content: center;
498
656
  font-size: 13px;
499
657
  font-weight: 500;
500
- color: #888;
501
- border-bottom: 1px solid rgba(255,255,255,0.06);
658
+ color: var(--fg-2);
659
+ border-bottom: 1px solid var(--border-1);
502
660
  position: relative;
503
661
  flex-shrink: 0;
504
662
  }
@@ -517,7 +675,7 @@ var renderWebUiHtml = (options) => {
517
675
  bottom: 4px; /* Position from bottom of topbar content area */
518
676
  background: transparent;
519
677
  border: 0;
520
- color: #666;
678
+ color: var(--fg-5);
521
679
  width: 44px;
522
680
  height: 44px;
523
681
  border-radius: 6px;
@@ -527,7 +685,7 @@ var renderWebUiHtml = (options) => {
527
685
  z-index: 10;
528
686
  -webkit-tap-highlight-color: transparent;
529
687
  }
530
- .sidebar-toggle:hover { color: #ededed; }
688
+ .sidebar-toggle:hover { color: var(--fg); }
531
689
  .topbar-new-chat {
532
690
  display: none;
533
691
  position: absolute;
@@ -535,7 +693,7 @@ var renderWebUiHtml = (options) => {
535
693
  bottom: 4px;
536
694
  background: transparent;
537
695
  border: 0;
538
- color: #666;
696
+ color: var(--fg-5);
539
697
  width: 44px;
540
698
  height: 44px;
541
699
  border-radius: 6px;
@@ -544,7 +702,7 @@ var renderWebUiHtml = (options) => {
544
702
  z-index: 10;
545
703
  -webkit-tap-highlight-color: transparent;
546
704
  }
547
- .topbar-new-chat:hover { color: #ededed; }
705
+ .topbar-new-chat:hover { color: var(--fg); }
548
706
  .topbar-new-chat svg { width: 16px; height: 16px; }
549
707
 
550
708
  /* Messages */
@@ -556,8 +714,8 @@ var renderWebUiHtml = (options) => {
556
714
  .assistant-avatar {
557
715
  width: 24px;
558
716
  height: 24px;
559
- background: #ededed;
560
- color: #000;
717
+ background: var(--accent);
718
+ color: var(--accent-fg);
561
719
  border-radius: 6px;
562
720
  display: grid;
563
721
  place-items: center;
@@ -568,7 +726,7 @@ var renderWebUiHtml = (options) => {
568
726
  }
569
727
  .assistant-content {
570
728
  line-height: 1.65;
571
- color: #ededed;
729
+ color: var(--fg);
572
730
  font-size: 14px;
573
731
  min-width: 0;
574
732
  max-width: 100%;
@@ -580,32 +738,32 @@ var renderWebUiHtml = (options) => {
580
738
  .assistant-content p:last-child { margin-bottom: 0; }
581
739
  .assistant-content ul, .assistant-content ol { margin: 8px 0; padding-left: 20px; }
582
740
  .assistant-content li { margin: 4px 0; }
583
- .assistant-content strong { font-weight: 600; color: #fff; }
741
+ .assistant-content strong { font-weight: 600; color: var(--fg-strong); }
584
742
  .assistant-content h2 {
585
743
  font-size: 16px;
586
744
  font-weight: 600;
587
745
  letter-spacing: -0.02em;
588
746
  margin: 20px 0 8px;
589
- color: #fff;
747
+ color: var(--fg-strong);
590
748
  }
591
749
  .assistant-content h3 {
592
750
  font-size: 14px;
593
751
  font-weight: 600;
594
752
  letter-spacing: -0.01em;
595
753
  margin: 16px 0 6px;
596
- color: #fff;
754
+ color: var(--fg-strong);
597
755
  }
598
756
  .assistant-content code {
599
- background: rgba(255,255,255,0.06);
600
- border: 1px solid rgba(255,255,255,0.06);
757
+ background: var(--surface-4);
758
+ border: 1px solid var(--border-1);
601
759
  padding: 2px 5px;
602
760
  border-radius: 4px;
603
761
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
604
762
  font-size: 0.88em;
605
763
  }
606
764
  .assistant-content pre {
607
- background: #0a0a0a;
608
- border: 1px solid rgba(255,255,255,0.06);
765
+ background: var(--bg-alt);
766
+ border: 1px solid var(--border-1);
609
767
  padding: 14px 16px;
610
768
  border-radius: 8px;
611
769
  overflow-x: auto;
@@ -622,33 +780,33 @@ var renderWebUiHtml = (options) => {
622
780
  margin: 8px 0;
623
781
  font-size: 12px;
624
782
  line-height: 1.45;
625
- color: #8a8a8a;
783
+ color: var(--fg-tool);
626
784
  }
627
785
  .tool-activity-inline code {
628
786
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
629
- background: rgba(255,255,255,0.04);
630
- border: 1px solid rgba(255,255,255,0.08);
787
+ background: var(--surface-3);
788
+ border: 1px solid var(--border-2);
631
789
  padding: 4px 8px;
632
790
  border-radius: 6px;
633
- color: #bcbcbc;
791
+ color: var(--fg-tool-code);
634
792
  font-size: 11px;
635
793
  }
636
794
  .tool-status {
637
- color: #8a8a8a;
795
+ color: var(--fg-tool);
638
796
  font-style: italic;
639
797
  }
640
798
  .tool-done {
641
- color: #6a9955;
799
+ color: var(--tool-done);
642
800
  }
643
801
  .tool-error {
644
- color: #f48771;
802
+ color: var(--tool-error);
645
803
  }
646
804
  .assistant-content table {
647
805
  border-collapse: collapse;
648
806
  width: 100%;
649
807
  margin: 14px 0;
650
808
  font-size: 13px;
651
- border: 1px solid rgba(255,255,255,0.08);
809
+ border: 1px solid var(--border-2);
652
810
  border-radius: 8px;
653
811
  overflow: hidden;
654
812
  display: block;
@@ -657,17 +815,17 @@ var renderWebUiHtml = (options) => {
657
815
  white-space: nowrap;
658
816
  }
659
817
  .assistant-content th {
660
- background: rgba(255,255,255,0.06);
818
+ background: var(--surface-4);
661
819
  padding: 10px 12px;
662
820
  text-align: left;
663
821
  font-weight: 600;
664
- border-bottom: 1px solid rgba(255,255,255,0.12);
665
- color: #fff;
822
+ border-bottom: 1px solid var(--border-4);
823
+ color: var(--fg-strong);
666
824
  min-width: 100px;
667
825
  }
668
826
  .assistant-content td {
669
827
  padding: 10px 12px;
670
- border-bottom: 1px solid rgba(255,255,255,0.06);
828
+ border-bottom: 1px solid var(--border-1);
671
829
  width: 100%;
672
830
  min-width: 100px;
673
831
  }
@@ -675,22 +833,22 @@ var renderWebUiHtml = (options) => {
675
833
  border-bottom: none;
676
834
  }
677
835
  .assistant-content tbody tr:hover {
678
- background: rgba(255,255,255,0.02);
836
+ background: var(--surface-1);
679
837
  }
680
838
  .assistant-content hr {
681
839
  border: 0;
682
- border-top: 1px solid rgba(255,255,255,0.1);
840
+ border-top: 1px solid var(--border-3);
683
841
  margin: 20px 0;
684
842
  }
685
843
  .tool-activity {
686
844
  margin-top: 12px;
687
845
  margin-bottom: 12px;
688
- border: 1px solid rgba(255,255,255,0.08);
689
- background: rgba(255,255,255,0.03);
846
+ border: 1px solid var(--border-2);
847
+ background: var(--surface-2);
690
848
  border-radius: 10px;
691
849
  font-size: 12px;
692
850
  line-height: 1.45;
693
- color: #bcbcbc;
851
+ color: var(--fg-tool-code);
694
852
  width: 300px;
695
853
  }
696
854
  .assistant-content > .tool-activity:first-child {
@@ -715,12 +873,12 @@ var renderWebUiHtml = (options) => {
715
873
  font-size: 11px;
716
874
  text-transform: uppercase;
717
875
  letter-spacing: 0.06em;
718
- color: #8a8a8a;
876
+ color: var(--fg-tool);
719
877
  font-weight: 600;
720
878
  }
721
879
  .tool-activity-caret {
722
880
  margin-left: auto;
723
- color: #8a8a8a;
881
+ color: var(--fg-tool);
724
882
  display: inline-flex;
725
883
  align-items: center;
726
884
  justify-content: center;
@@ -742,28 +900,28 @@ var renderWebUiHtml = (options) => {
742
900
  }
743
901
  .tool-activity-item {
744
902
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
745
- background: rgba(255,255,255,0.04);
903
+ background: var(--surface-3);
746
904
  border-radius: 6px;
747
905
  padding: 4px 7px;
748
- color: #d6d6d6;
906
+ color: var(--fg-tool-item);
749
907
  }
750
908
  .approval-requests {
751
- border-top: 1px solid rgba(255,255,255,0.08);
909
+ border-top: 1px solid var(--border-2);
752
910
  padding: 10px 12px 12px;
753
911
  display: grid;
754
912
  gap: 8px;
755
- background: rgba(0,0,0,0.16);
913
+ background: var(--inset-1);
756
914
  }
757
915
  .approval-requests-label {
758
916
  font-size: 11px;
759
917
  text-transform: uppercase;
760
918
  letter-spacing: 0.06em;
761
- color: #b0b0b0;
919
+ color: var(--fg-approval-label);
762
920
  font-weight: 600;
763
921
  }
764
922
  .approval-request-item {
765
- border: 1px solid rgba(255,255,255,0.1);
766
- background: rgba(255,255,255,0.03);
923
+ border: 1px solid var(--border-3);
924
+ background: var(--surface-2);
767
925
  border-radius: 8px;
768
926
  padding: 8px;
769
927
  display: grid;
@@ -771,15 +929,15 @@ var renderWebUiHtml = (options) => {
771
929
  }
772
930
  .approval-request-tool {
773
931
  font-size: 12px;
774
- color: #fff;
932
+ color: var(--fg-strong);
775
933
  font-weight: 600;
776
934
  overflow-wrap: anywhere;
777
935
  }
778
936
  .approval-request-input {
779
937
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
780
938
  font-size: 11px;
781
- color: #cfcfcf;
782
- background: rgba(0,0,0,0.25);
939
+ color: var(--fg-approval-input);
940
+ background: var(--inset-2);
783
941
  border-radius: 6px;
784
942
  padding: 6px;
785
943
  overflow-wrap: anywhere;
@@ -792,32 +950,32 @@ var renderWebUiHtml = (options) => {
792
950
  }
793
951
  .approval-action-btn {
794
952
  border-radius: 6px;
795
- border: 1px solid rgba(255,255,255,0.18);
796
- background: rgba(255,255,255,0.06);
797
- color: #f0f0f0;
953
+ border: 1px solid var(--border-5);
954
+ background: var(--surface-4);
955
+ color: var(--fg-approval-btn);
798
956
  font-size: 11px;
799
957
  font-weight: 600;
800
958
  padding: 4px 8px;
801
959
  cursor: pointer;
802
960
  }
803
961
  .approval-action-btn:hover {
804
- background: rgba(255,255,255,0.12);
962
+ background: var(--surface-7);
805
963
  }
806
964
  .approval-action-btn.approve {
807
- border-color: rgba(58, 208, 122, 0.45);
808
- color: #78e7a6;
965
+ border-color: var(--approve-border);
966
+ color: var(--approve);
809
967
  }
810
968
  .approval-action-btn.deny {
811
- border-color: rgba(224, 95, 95, 0.45);
812
- color: #f59b9b;
969
+ border-color: var(--deny-border);
970
+ color: var(--deny);
813
971
  }
814
972
  .approval-action-btn[disabled] {
815
973
  opacity: 0.55;
816
974
  cursor: not-allowed;
817
975
  }
818
976
  .user-bubble {
819
- background: #111;
820
- border: 1px solid rgba(255,255,255,0.08);
977
+ background: var(--bg-elevated);
978
+ border: 1px solid var(--border-2);
821
979
  padding: 10px 16px;
822
980
  border-radius: 18px;
823
981
  max-width: 70%;
@@ -833,7 +991,7 @@ var renderWebUiHtml = (options) => {
833
991
  justify-content: center;
834
992
  height: 100%;
835
993
  gap: 16px;
836
- color: #555;
994
+ color: var(--fg-6);
837
995
  }
838
996
  .empty-state .assistant-avatar {
839
997
  width: 36px;
@@ -843,7 +1001,7 @@ var renderWebUiHtml = (options) => {
843
1001
  }
844
1002
  .empty-state-text {
845
1003
  font-size: 14px;
846
- color: #555;
1004
+ color: var(--fg-6);
847
1005
  }
848
1006
  .thinking-indicator {
849
1007
  display: inline-block;
@@ -851,7 +1009,7 @@ var renderWebUiHtml = (options) => {
851
1009
  font-size: 20px;
852
1010
  line-height: 1;
853
1011
  vertical-align: middle;
854
- color: #ededed;
1012
+ color: var(--fg);
855
1013
  opacity: 0.5;
856
1014
  }
857
1015
  .thinking-status {
@@ -859,13 +1017,13 @@ var renderWebUiHtml = (options) => {
859
1017
  align-items: center;
860
1018
  gap: 8px;
861
1019
  margin-top: 2px;
862
- color: #8a8a8a;
1020
+ color: var(--fg-tool);
863
1021
  font-size: 14px;
864
1022
  line-height: 1.65;
865
1023
  font-weight: 400;
866
1024
  }
867
1025
  .thinking-status-label {
868
- color: #8a8a8a;
1026
+ color: var(--fg-tool);
869
1027
  font-size: 14px;
870
1028
  line-height: 1.65;
871
1029
  font-weight: 400;
@@ -896,26 +1054,26 @@ var renderWebUiHtml = (options) => {
896
1054
  right: 0;
897
1055
  bottom: 100%;
898
1056
  height: 48px;
899
- background: linear-gradient(to top, #000 0%, transparent 100%);
1057
+ background: linear-gradient(to top, var(--bg) 0%, transparent 100%);
900
1058
  pointer-events: none;
901
1059
  }
902
1060
  .composer-inner { max-width: 680px; margin: 0 auto; }
903
1061
  .composer-shell {
904
- background: #0a0a0a;
905
- border: 1px solid rgba(255,255,255,0.1);
1062
+ background: var(--bg-alt);
1063
+ border: 1px solid var(--border-3);
906
1064
  border-radius: 24px;
907
1065
  display: flex;
908
1066
  align-items: end;
909
1067
  padding: 4px 6px 4px 6px;
910
1068
  transition: border-color 0.15s;
911
1069
  }
912
- .composer-shell:focus-within { border-color: rgba(255,255,255,0.2); }
1070
+ .composer-shell:focus-within { border-color: var(--border-focus); }
913
1071
  .composer-input {
914
1072
  flex: 1;
915
1073
  background: transparent;
916
1074
  border: 0;
917
1075
  outline: none;
918
- color: #ededed;
1076
+ color: var(--fg);
919
1077
  min-height: 40px;
920
1078
  max-height: 200px;
921
1079
  resize: none;
@@ -924,14 +1082,14 @@ var renderWebUiHtml = (options) => {
924
1082
  line-height: 1.5;
925
1083
  margin-top: -4px;
926
1084
  }
927
- .composer-input::placeholder { color: #444; }
1085
+ .composer-input::placeholder { color: var(--fg-7); }
928
1086
  .send-btn {
929
1087
  width: 32px;
930
1088
  height: 32px;
931
- background: #ededed;
1089
+ background: var(--accent);
932
1090
  border: 0;
933
1091
  border-radius: 50%;
934
- color: #000;
1092
+ color: var(--accent-fg);
935
1093
  cursor: pointer;
936
1094
  display: grid;
937
1095
  place-items: center;
@@ -939,21 +1097,79 @@ var renderWebUiHtml = (options) => {
939
1097
  margin-bottom: 2px;
940
1098
  transition: background 0.15s, opacity 0.15s;
941
1099
  }
942
- .send-btn:hover { background: #fff; }
1100
+ .send-btn:hover { background: var(--accent-hover); }
943
1101
  .send-btn.stop-mode {
944
- background: #4a4a4a;
945
- color: #fff;
1102
+ background: var(--stop-bg);
1103
+ color: var(--stop-fg);
946
1104
  }
947
- .send-btn.stop-mode:hover { background: #565656; }
1105
+ .send-btn.stop-mode:hover { background: var(--stop-hover); }
948
1106
  .send-btn:disabled { opacity: 0.2; cursor: default; }
949
- .send-btn:disabled:hover { background: #ededed; }
1107
+ .send-btn:disabled:hover { background: var(--accent); }
1108
+ .send-btn-wrapper {
1109
+ position: relative;
1110
+ width: 36px;
1111
+ height: 36px;
1112
+ display: grid;
1113
+ place-items: center;
1114
+ flex-shrink: 0;
1115
+ margin-bottom: 0;
1116
+ }
1117
+ .send-btn-wrapper .send-btn {
1118
+ margin-bottom: 0;
1119
+ }
1120
+ .context-ring {
1121
+ position: absolute;
1122
+ inset: 0;
1123
+ width: 36px;
1124
+ height: 36px;
1125
+ pointer-events: none;
1126
+ transform: rotate(-90deg);
1127
+ }
1128
+ .context-ring-fill {
1129
+ fill: none;
1130
+ stroke: var(--bg-alt);
1131
+ stroke-width: 3;
1132
+ stroke-linecap: butt;
1133
+ transition: stroke-dashoffset 0.4s ease, stroke 0.3s ease;
1134
+ }
1135
+ .send-btn-wrapper.stop-mode .context-ring-fill {
1136
+ stroke: var(--fg-3);
1137
+ }
1138
+ .context-ring-fill.warning {
1139
+ stroke: #e5a33d;
1140
+ }
1141
+ .context-ring-fill.critical {
1142
+ stroke: #e55d4a;
1143
+ }
1144
+ .context-tooltip {
1145
+ position: absolute;
1146
+ bottom: calc(100% + 8px);
1147
+ right: 0;
1148
+ background: var(--bg-elevated);
1149
+ border: 1px solid var(--border-3);
1150
+ border-radius: 8px;
1151
+ padding: 6px 10px;
1152
+ font-size: 12px;
1153
+ color: var(--fg-2);
1154
+ white-space: nowrap;
1155
+ pointer-events: none;
1156
+ opacity: 0;
1157
+ transform: translateY(4px);
1158
+ transition: opacity 0.15s, transform 0.15s;
1159
+ z-index: 10;
1160
+ }
1161
+ .send-btn-wrapper:hover .context-tooltip,
1162
+ .send-btn-wrapper:focus-within .context-tooltip {
1163
+ opacity: 1;
1164
+ transform: translateY(0);
1165
+ }
950
1166
  .attach-btn {
951
1167
  width: 32px;
952
1168
  height: 32px;
953
- background: rgba(255,255,255,0.08);
1169
+ background: var(--surface-5);
954
1170
  border: 0;
955
1171
  border-radius: 50%;
956
- color: #999;
1172
+ color: var(--fg-3);
957
1173
  cursor: pointer;
958
1174
  display: grid;
959
1175
  place-items: center;
@@ -962,7 +1178,7 @@ var renderWebUiHtml = (options) => {
962
1178
  margin-right: 8px;
963
1179
  transition: color 0.15s, background 0.15s;
964
1180
  }
965
- .attach-btn:hover { color: #ededed; background: rgba(255,255,255,0.14); }
1181
+ .attach-btn:hover { color: var(--fg); background: var(--surface-8); }
966
1182
  .attachment-preview {
967
1183
  display: flex;
968
1184
  gap: 8px;
@@ -973,12 +1189,12 @@ var renderWebUiHtml = (options) => {
973
1189
  display: inline-flex;
974
1190
  align-items: center;
975
1191
  gap: 6px;
976
- background: rgba(0, 0, 0, 0.6);
977
- border: 1px solid rgba(255, 255, 255, 0.12);
1192
+ background: var(--chip-bg);
1193
+ border: 1px solid var(--border-4);
978
1194
  border-radius: 9999px;
979
1195
  padding: 4px 10px 4px 6px;
980
1196
  font-size: 11px;
981
- color: #777;
1197
+ color: var(--fg-4);
982
1198
  max-width: 200px;
983
1199
  cursor: pointer;
984
1200
  backdrop-filter: blur(6px);
@@ -986,9 +1202,9 @@ var renderWebUiHtml = (options) => {
986
1202
  transition: color 0.15s, border-color 0.15s, background 0.15s;
987
1203
  }
988
1204
  .attachment-chip:hover {
989
- color: #ededed;
990
- border-color: rgba(255, 255, 255, 0.25);
991
- background: rgba(0, 0, 0, 0.75);
1205
+ color: var(--fg);
1206
+ border-color: var(--border-hover);
1207
+ background: var(--chip-bg-hover);
992
1208
  }
993
1209
  .attachment-chip img {
994
1210
  width: 20px;
@@ -1002,7 +1218,7 @@ var renderWebUiHtml = (options) => {
1002
1218
  width: 20px;
1003
1219
  height: 20px;
1004
1220
  border-radius: 50%;
1005
- background: rgba(255,255,255,0.1);
1221
+ background: var(--surface-6);
1006
1222
  display: grid;
1007
1223
  place-items: center;
1008
1224
  font-size: 11px;
@@ -1010,13 +1226,13 @@ var renderWebUiHtml = (options) => {
1010
1226
  }
1011
1227
  .attachment-chip .remove-attachment {
1012
1228
  cursor: pointer;
1013
- color: #555;
1229
+ color: var(--fg-6);
1014
1230
  font-size: 14px;
1015
1231
  margin-left: 2px;
1016
1232
  line-height: 1;
1017
1233
  transition: color 0.15s;
1018
1234
  }
1019
- .attachment-chip .remove-attachment:hover { color: #fff; }
1235
+ .attachment-chip .remove-attachment:hover { color: var(--fg-strong); }
1020
1236
  .attachment-chip .filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
1021
1237
  .user-bubble .user-file-attachments {
1022
1238
  display: flex;
@@ -1046,7 +1262,7 @@ var renderWebUiHtml = (options) => {
1046
1262
  transition: background 0.25s ease, backdrop-filter 0.25s ease;
1047
1263
  }
1048
1264
  .lightbox.active {
1049
- background: rgba(0,0,0,0.85);
1265
+ background: var(--lightbox-bg);
1050
1266
  backdrop-filter: blur(8px);
1051
1267
  }
1052
1268
  .lightbox img {
@@ -1066,16 +1282,16 @@ var renderWebUiHtml = (options) => {
1066
1282
  display: inline-flex;
1067
1283
  align-items: center;
1068
1284
  gap: 4px;
1069
- background: rgba(0,0,0,0.2);
1285
+ background: var(--file-badge-bg);
1070
1286
  border-radius: 6px;
1071
1287
  padding: 4px 8px;
1072
1288
  font-size: 12px;
1073
- color: rgba(255,255,255,0.8);
1289
+ color: var(--file-badge-fg);
1074
1290
  }
1075
1291
  .drag-overlay {
1076
1292
  position: fixed;
1077
1293
  inset: 0;
1078
- background: rgba(0,0,0,0.6);
1294
+ background: var(--backdrop);
1079
1295
  z-index: 9999;
1080
1296
  display: none;
1081
1297
  align-items: center;
@@ -1084,15 +1300,15 @@ var renderWebUiHtml = (options) => {
1084
1300
  }
1085
1301
  .drag-overlay.active { display: flex; }
1086
1302
  .drag-overlay-inner {
1087
- border: 2px dashed rgba(255,255,255,0.4);
1303
+ border: 2px dashed var(--border-drag);
1088
1304
  border-radius: 16px;
1089
1305
  padding: 40px 60px;
1090
- color: #fff;
1306
+ color: var(--fg-strong);
1091
1307
  font-size: 16px;
1092
1308
  }
1093
1309
  .disclaimer {
1094
1310
  text-align: center;
1095
- color: #333;
1311
+ color: var(--fg-8);
1096
1312
  font-size: 12px;
1097
1313
  margin-top: 10px;
1098
1314
  }
@@ -1107,10 +1323,10 @@ var renderWebUiHtml = (options) => {
1107
1323
  align-items: center;
1108
1324
  gap: 6px;
1109
1325
  font-size: 11px;
1110
- color: #777;
1326
+ color: var(--fg-4);
1111
1327
  text-decoration: none;
1112
- background: rgba(0, 0, 0, 0.6);
1113
- border: 1px solid rgba(255, 255, 255, 0.12);
1328
+ background: var(--chip-bg);
1329
+ border: 1px solid var(--border-4);
1114
1330
  border-radius: 9999px;
1115
1331
  padding: 4px 10px 4px 6px;
1116
1332
  backdrop-filter: blur(6px);
@@ -1118,9 +1334,9 @@ var renderWebUiHtml = (options) => {
1118
1334
  transition: color 0.15s, border-color 0.15s, background 0.15s;
1119
1335
  }
1120
1336
  .poncho-badge:hover {
1121
- color: #ededed;
1122
- border-color: rgba(255, 255, 255, 0.25);
1123
- background: rgba(0, 0, 0, 0.75);
1337
+ color: var(--fg);
1338
+ border-color: var(--border-hover);
1339
+ background: var(--chip-bg-hover);
1124
1340
  }
1125
1341
  .poncho-badge-avatar {
1126
1342
  width: 16px;
@@ -1134,8 +1350,8 @@ var renderWebUiHtml = (options) => {
1134
1350
  /* Scrollbar */
1135
1351
  ::-webkit-scrollbar { width: 6px; }
1136
1352
  ::-webkit-scrollbar-track { background: transparent; }
1137
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
1138
- ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
1353
+ ::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 3px; }
1354
+ ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); }
1139
1355
 
1140
1356
  /* Mobile */
1141
1357
  @media (max-width: 768px) {
@@ -1165,7 +1381,7 @@ var renderWebUiHtml = (options) => {
1165
1381
  .sidebar-backdrop {
1166
1382
  position: fixed;
1167
1383
  inset: 0;
1168
- background: rgba(0,0,0,0.6);
1384
+ background: var(--backdrop);
1169
1385
  z-index: 50;
1170
1386
  backdrop-filter: blur(2px);
1171
1387
  -webkit-backdrop-filter: blur(2px);
@@ -1240,9 +1456,15 @@ var renderWebUiHtml = (options) => {
1240
1456
  </button>
1241
1457
  <input id="file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
1242
1458
  <textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
1243
- <button id="send" class="send-btn" type="submit">
1244
- <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>
1245
- </button>
1459
+ <div class="send-btn-wrapper" id="send-btn-wrapper">
1460
+ <svg class="context-ring" viewBox="0 0 36 36">
1461
+ <circle class="context-ring-fill" id="context-ring-fill" cx="18" cy="18" r="14.5" />
1462
+ </svg>
1463
+ <button id="send" class="send-btn" type="submit">
1464
+ <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>
1465
+ </button>
1466
+ <div class="context-tooltip" id="context-tooltip"></div>
1467
+ </div>
1246
1468
  </div>
1247
1469
  </div>
1248
1470
  </form>
@@ -1274,6 +1496,8 @@ var renderWebUiHtml = (options) => {
1274
1496
  confirmDeleteId: null,
1275
1497
  approvalRequestsInFlight: {},
1276
1498
  pendingFiles: [],
1499
+ contextTokens: 0,
1500
+ contextWindow: 0,
1277
1501
  };
1278
1502
 
1279
1503
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -1301,12 +1525,41 @@ var renderWebUiHtml = (options) => {
1301
1525
  attachmentPreview: $("attachment-preview"),
1302
1526
  dragOverlay: $("drag-overlay"),
1303
1527
  lightbox: $("lightbox"),
1528
+ contextRingFill: $("context-ring-fill"),
1529
+ contextTooltip: $("context-tooltip"),
1530
+ sendBtnWrapper: $("send-btn-wrapper"),
1304
1531
  };
1305
1532
  const sendIconMarkup =
1306
1533
  '<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>';
1307
1534
  const stopIconMarkup =
1308
1535
  '<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>';
1309
1536
 
1537
+ const CONTEXT_RING_CIRCUMFERENCE = 2 * Math.PI * 14.5;
1538
+ const formatTokenCount = (n) => {
1539
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\\.0$/, "") + "M";
1540
+ if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\\.0$/, "") + "k";
1541
+ return String(n);
1542
+ };
1543
+ const updateContextRing = () => {
1544
+ const ring = elements.contextRingFill;
1545
+ const tooltip = elements.contextTooltip;
1546
+ if (!ring || !tooltip) return;
1547
+ if (state.contextWindow <= 0) {
1548
+ ring.style.strokeDasharray = String(CONTEXT_RING_CIRCUMFERENCE);
1549
+ ring.style.strokeDashoffset = String(CONTEXT_RING_CIRCUMFERENCE);
1550
+ tooltip.textContent = "";
1551
+ return;
1552
+ }
1553
+ const ratio = Math.min(state.contextTokens / state.contextWindow, 1);
1554
+ const offset = CONTEXT_RING_CIRCUMFERENCE * (1 - ratio);
1555
+ ring.style.strokeDasharray = String(CONTEXT_RING_CIRCUMFERENCE);
1556
+ ring.style.strokeDashoffset = String(offset);
1557
+ ring.classList.toggle("warning", ratio >= 0.7 && ratio < 0.9);
1558
+ ring.classList.toggle("critical", ratio >= 0.9);
1559
+ const pct = (ratio * 100).toFixed(1).replace(/\\.0$/, "");
1560
+ tooltip.textContent = formatTokenCount(state.contextTokens) + " / " + formatTokenCount(state.contextWindow) + " tokens (" + pct + "%)";
1561
+ };
1562
+
1310
1563
  const pushConversationUrl = (conversationId) => {
1311
1564
  const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
1312
1565
  if (window.location.pathname !== target) {
@@ -1626,6 +1879,9 @@ var renderWebUiHtml = (options) => {
1626
1879
  if (state.activeConversationId === c.conversationId) {
1627
1880
  state.activeConversationId = null;
1628
1881
  state.activeMessages = [];
1882
+ state.contextTokens = 0;
1883
+ state.contextWindow = 0;
1884
+ updateContextRing();
1629
1885
  pushConversationUrl(null);
1630
1886
  elements.chatTitle.textContent = "";
1631
1887
  renderMessages([]);
@@ -1722,8 +1978,14 @@ var renderWebUiHtml = (options) => {
1722
1978
  } else if (shouldRenderEmptyStreamingIndicator) {
1723
1979
  content.appendChild(createThinkingIndicator(getThinkingStatusLabel(m)));
1724
1980
  } else {
1725
- // Check for sections in _sections (streaming) or metadata.sections (stored)
1726
- const sections = m._sections || (m.metadata && m.metadata.sections);
1981
+ // Merge stored sections (persisted) with live sections (from
1982
+ // an active stream). For normal messages only one source
1983
+ // exists; for liveOnly reconnects both contribute.
1984
+ const storedSections = (m.metadata && m.metadata.sections) || [];
1985
+ const liveSections = m._sections || [];
1986
+ const sections = liveSections.length > 0 && storedSections.length > 0
1987
+ ? storedSections.concat(liveSections)
1988
+ : liveSections.length > 0 ? liveSections : (storedSections.length > 0 ? storedSections : null);
1727
1989
  const pendingApprovals = Array.isArray(m._pendingApprovals) ? m._pendingApprovals : [];
1728
1990
 
1729
1991
  if (sections && sections.length > 0) {
@@ -1864,11 +2126,24 @@ var renderWebUiHtml = (options) => {
1864
2126
  payload.conversation.messages || [],
1865
2127
  payload.conversation.pendingApprovals || payload.pendingApprovals || [],
1866
2128
  );
2129
+ state.contextTokens = 0;
2130
+ state.contextWindow = 0;
2131
+ updateContextRing();
1867
2132
  renderMessages(state.activeMessages, false, { forceScrollBottom: true });
1868
2133
  elements.prompt.focus();
2134
+ if (payload.hasActiveRun && !state.isStreaming) {
2135
+ setStreaming(true);
2136
+ streamConversationEvents(conversationId, { liveOnly: true }).finally(() => {
2137
+ if (state.activeConversationId === conversationId) {
2138
+ setStreaming(false);
2139
+ renderMessages(state.activeMessages, false);
2140
+ }
2141
+ });
2142
+ }
1869
2143
  };
1870
2144
 
1871
- const streamConversationEvents = (conversationId) => {
2145
+ const streamConversationEvents = (conversationId, options) => {
2146
+ const liveOnly = options && options.liveOnly;
1872
2147
  return new Promise((resolve) => {
1873
2148
  const localMessages = state.activeMessages || [];
1874
2149
  const renderIfActiveConversation = (streaming) => {
@@ -1887,20 +2162,36 @@ var renderWebUiHtml = (options) => {
1887
2162
  _currentText: "",
1888
2163
  _currentTools: [],
1889
2164
  _pendingApprovals: [],
2165
+ _activeActivities: [],
1890
2166
  metadata: { toolActivity: [] },
1891
2167
  };
1892
2168
  localMessages.push(assistantMessage);
1893
2169
  state.activeMessages = localMessages;
1894
2170
  }
1895
- if (!assistantMessage._sections) assistantMessage._sections = [];
1896
- if (!assistantMessage._currentText) assistantMessage._currentText = "";
1897
- if (!assistantMessage._currentTools) assistantMessage._currentTools = [];
1898
- if (!assistantMessage._activeActivities) assistantMessage._activeActivities = [];
1899
- if (!assistantMessage._pendingApprovals) assistantMessage._pendingApprovals = [];
1900
- if (!assistantMessage.metadata) assistantMessage.metadata = {};
1901
- if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
1902
-
1903
- const url = "/api/conversations/" + encodeURIComponent(conversationId) + "/events";
2171
+ if (liveOnly) {
2172
+ // Live-only mode: keep metadata.sections intact (the stored
2173
+ // base content) and start _sections empty so it only collects
2174
+ // NEW sections from live events. The renderer merges both.
2175
+ assistantMessage._sections = [];
2176
+ assistantMessage._currentText = "";
2177
+ assistantMessage._currentTools = [];
2178
+ if (!assistantMessage._activeActivities) assistantMessage._activeActivities = [];
2179
+ if (!assistantMessage._pendingApprovals) assistantMessage._pendingApprovals = [];
2180
+ if (!assistantMessage.metadata) assistantMessage.metadata = {};
2181
+ if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
2182
+ } else {
2183
+ // Full replay mode: reset transient state so replayed events
2184
+ // rebuild from scratch (the buffer has the full event history).
2185
+ assistantMessage.content = "";
2186
+ assistantMessage._sections = [];
2187
+ assistantMessage._currentText = "";
2188
+ assistantMessage._currentTools = [];
2189
+ assistantMessage._activeActivities = [];
2190
+ assistantMessage._pendingApprovals = [];
2191
+ assistantMessage.metadata = { toolActivity: [] };
2192
+ }
2193
+
2194
+ const url = "/api/conversations/" + encodeURIComponent(conversationId) + "/events" + (liveOnly ? "?live_only=true" : "");
1904
2195
  fetch(url, { credentials: "include" }).then((response) => {
1905
2196
  if (!response.ok || !response.body) {
1906
2197
  resolve(undefined);
@@ -1921,6 +2212,11 @@ var renderWebUiHtml = (options) => {
1921
2212
  if (eventName === "stream:end") {
1922
2213
  return;
1923
2214
  }
2215
+ if (eventName === "run:started") {
2216
+ if (typeof payload.contextWindow === "number" && payload.contextWindow > 0) {
2217
+ state.contextWindow = payload.contextWindow;
2218
+ }
2219
+ }
1924
2220
  if (eventName === "model:chunk") {
1925
2221
  const chunk = String(payload.content || "");
1926
2222
  if (assistantMessage._currentTools.length > 0 && chunk.length > 0) {
@@ -1934,6 +2230,12 @@ var renderWebUiHtml = (options) => {
1934
2230
  assistantMessage._currentText += chunk;
1935
2231
  renderIfActiveConversation(true);
1936
2232
  }
2233
+ if (eventName === "model:response") {
2234
+ if (typeof payload.usage?.input === "number") {
2235
+ state.contextTokens = payload.usage.input;
2236
+ updateContextRing();
2237
+ }
2238
+ }
1937
2239
  if (eventName === "tool:started") {
1938
2240
  const toolName = payload.tool || "tool";
1939
2241
  const startedActivity = addActiveActivityFromToolStart(
@@ -2006,6 +2308,75 @@ var renderWebUiHtml = (options) => {
2006
2308
  assistantMessage.metadata.toolActivity.push(toolText);
2007
2309
  renderIfActiveConversation(true);
2008
2310
  }
2311
+ if (eventName === "tool:approval:required") {
2312
+ const toolName = payload.tool || "tool";
2313
+ const activeActivity = removeActiveActivityForTool(
2314
+ assistantMessage,
2315
+ toolName,
2316
+ );
2317
+ const detailFromPayload = describeToolStart(payload);
2318
+ const detail =
2319
+ (activeActivity && typeof activeActivity.detail === "string"
2320
+ ? activeActivity.detail.trim()
2321
+ : "") ||
2322
+ (detailFromPayload && typeof detailFromPayload.detail === "string"
2323
+ ? detailFromPayload.detail.trim()
2324
+ : "");
2325
+ const toolText =
2326
+ "- approval required \\x60" +
2327
+ toolName +
2328
+ "\\x60" +
2329
+ (detail ? " (" + detail + ")" : "");
2330
+ assistantMessage._currentTools.push(toolText);
2331
+ assistantMessage.metadata.toolActivity.push(toolText);
2332
+ const approvalId =
2333
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2334
+ if (approvalId) {
2335
+ const preview = safeJsonPreview(payload.input ?? {});
2336
+ const inputPreview = preview.length > 600 ? preview.slice(0, 600) + "..." : preview;
2337
+ if (!Array.isArray(assistantMessage._pendingApprovals)) {
2338
+ assistantMessage._pendingApprovals = [];
2339
+ }
2340
+ const exists = assistantMessage._pendingApprovals.some(
2341
+ (req) => req.approvalId === approvalId,
2342
+ );
2343
+ if (!exists) {
2344
+ assistantMessage._pendingApprovals.push({
2345
+ approvalId,
2346
+ tool: toolName,
2347
+ inputPreview,
2348
+ state: "pending",
2349
+ });
2350
+ }
2351
+ }
2352
+ renderIfActiveConversation(true);
2353
+ }
2354
+ if (eventName === "tool:approval:granted") {
2355
+ const toolText = "- approval granted";
2356
+ assistantMessage._currentTools.push(toolText);
2357
+ assistantMessage.metadata.toolActivity.push(toolText);
2358
+ const approvalId =
2359
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2360
+ if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2361
+ assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2362
+ (req) => req.approvalId !== approvalId,
2363
+ );
2364
+ }
2365
+ renderIfActiveConversation(true);
2366
+ }
2367
+ if (eventName === "tool:approval:denied") {
2368
+ const toolText = "- approval denied";
2369
+ assistantMessage._currentTools.push(toolText);
2370
+ assistantMessage.metadata.toolActivity.push(toolText);
2371
+ const approvalId =
2372
+ typeof payload.approvalId === "string" ? payload.approvalId : "";
2373
+ if (approvalId && Array.isArray(assistantMessage._pendingApprovals)) {
2374
+ assistantMessage._pendingApprovals = assistantMessage._pendingApprovals.filter(
2375
+ (req) => req.approvalId !== approvalId,
2376
+ );
2377
+ }
2378
+ renderIfActiveConversation(true);
2379
+ }
2009
2380
  if (eventName === "run:completed") {
2010
2381
  assistantMessage._activeActivities = [];
2011
2382
  if (
@@ -2052,9 +2423,22 @@ var renderWebUiHtml = (options) => {
2052
2423
  }
2053
2424
  if (eventName === "run:error") {
2054
2425
  assistantMessage._activeActivities = [];
2426
+ if (assistantMessage._currentTools.length > 0) {
2427
+ assistantMessage._sections.push({
2428
+ type: "tools",
2429
+ content: assistantMessage._currentTools,
2430
+ });
2431
+ assistantMessage._currentTools = [];
2432
+ }
2433
+ if (assistantMessage._currentText.length > 0) {
2434
+ assistantMessage._sections.push({
2435
+ type: "text",
2436
+ content: assistantMessage._currentText,
2437
+ });
2438
+ assistantMessage._currentText = "";
2439
+ }
2055
2440
  const errMsg =
2056
2441
  payload.error?.message || "Something went wrong";
2057
- assistantMessage.content = "";
2058
2442
  assistantMessage._error = errMsg;
2059
2443
  renderIfActiveConversation(false);
2060
2444
  }
@@ -2127,6 +2511,9 @@ var renderWebUiHtml = (options) => {
2127
2511
  elements.send.disabled = value ? !canStop : false;
2128
2512
  elements.send.innerHTML = value ? stopIconMarkup : sendIconMarkup;
2129
2513
  elements.send.classList.toggle("stop-mode", value);
2514
+ if (elements.sendBtnWrapper) {
2515
+ elements.sendBtnWrapper.classList.toggle("stop-mode", value);
2516
+ }
2130
2517
  elements.send.setAttribute("aria-label", value ? "Stop response" : "Send message");
2131
2518
  elements.send.setAttribute(
2132
2519
  "title",
@@ -2499,8 +2886,17 @@ var renderWebUiHtml = (options) => {
2499
2886
  }
2500
2887
  if (eventName === "run:started") {
2501
2888
  state.activeStreamRunId = typeof payload.runId === "string" ? payload.runId : null;
2889
+ if (typeof payload.contextWindow === "number" && payload.contextWindow > 0) {
2890
+ state.contextWindow = payload.contextWindow;
2891
+ }
2502
2892
  setStreaming(state.isStreaming);
2503
2893
  }
2894
+ if (eventName === "model:response") {
2895
+ if (typeof payload.usage?.input === "number") {
2896
+ state.contextTokens = payload.usage.input;
2897
+ updateContextRing();
2898
+ }
2899
+ }
2504
2900
  if (eventName === "tool:started") {
2505
2901
  const toolName = payload.tool || "tool";
2506
2902
  const startedActivity = addActiveActivityFromToolStart(
@@ -2663,9 +3059,8 @@ var renderWebUiHtml = (options) => {
2663
3059
  renderIfActiveConversation(false);
2664
3060
  }
2665
3061
  if (eventName === "run:error") {
2666
- assistantMessage._activeActivities = [];
3062
+ finalizeAssistantMessage();
2667
3063
  const errMsg = payload.error?.message || "Something went wrong";
2668
- assistantMessage.content = "";
2669
3064
  assistantMessage._error = errMsg;
2670
3065
  renderIfActiveConversation(false);
2671
3066
  }
@@ -2768,6 +3163,9 @@ var renderWebUiHtml = (options) => {
2768
3163
  state.activeConversationId = null;
2769
3164
  state.activeMessages = [];
2770
3165
  state.confirmDeleteId = null;
3166
+ state.contextTokens = 0;
3167
+ state.contextWindow = 0;
3168
+ updateContextRing();
2771
3169
  pushConversationUrl(null);
2772
3170
  elements.chatTitle.textContent = "";
2773
3171
  renderMessages([]);
@@ -2805,6 +3203,9 @@ var renderWebUiHtml = (options) => {
2805
3203
  state.confirmDeleteId = null;
2806
3204
  state.conversations = [];
2807
3205
  state.csrfToken = "";
3206
+ state.contextTokens = 0;
3207
+ state.contextWindow = 0;
3208
+ updateContextRing();
2808
3209
  await requireAuth();
2809
3210
  });
2810
3211
 
@@ -3009,6 +3410,9 @@ var renderWebUiHtml = (options) => {
3009
3410
  } else {
3010
3411
  state.activeConversationId = null;
3011
3412
  state.activeMessages = [];
3413
+ state.contextTokens = 0;
3414
+ state.contextWindow = 0;
3415
+ updateContextRing();
3012
3416
  elements.chatTitle.textContent = "";
3013
3417
  renderMessages([]);
3014
3418
  renderConversationList();
@@ -3050,6 +3454,7 @@ var renderWebUiHtml = (options) => {
3050
3454
  await createConversation();
3051
3455
  }
3052
3456
  autoResizePrompt();
3457
+ updateContextRing();
3053
3458
  elements.prompt.focus();
3054
3459
  })();
3055
3460
 
@@ -3266,6 +3671,631 @@ var renderWebUiHtml = (options) => {
3266
3671
  </html>`;
3267
3672
  };
3268
3673
 
3674
+ // src/api-docs.ts
3675
+ var buildOpenApiSpec = (options) => ({
3676
+ openapi: "3.1.0",
3677
+ info: {
3678
+ title: `${options.agentName} API`,
3679
+ description: "HTTP API for interacting with a Poncho agent. Supports conversation management, streaming message responses via Server-Sent Events (SSE), tool approval workflows, file uploads, and cron job triggers.",
3680
+ version: "1.0.0"
3681
+ },
3682
+ servers: [{ url: "/", description: "Current host" }],
3683
+ components: {
3684
+ securitySchemes: {
3685
+ bearerAuth: {
3686
+ type: "http",
3687
+ scheme: "bearer",
3688
+ description: "Pass the PONCHO_AUTH_TOKEN value as a Bearer token. Required only when `auth.required: true` in poncho.config.js."
3689
+ }
3690
+ },
3691
+ schemas: {
3692
+ ConversationSummary: {
3693
+ type: "object",
3694
+ properties: {
3695
+ conversationId: { type: "string" },
3696
+ title: { type: "string" },
3697
+ runtimeRunId: { type: "string" },
3698
+ ownerId: { type: "string" },
3699
+ tenantId: { type: ["string", "null"] },
3700
+ createdAt: { type: "number", description: "Unix epoch ms" },
3701
+ updatedAt: { type: "number", description: "Unix epoch ms" },
3702
+ messageCount: { type: "integer" }
3703
+ }
3704
+ },
3705
+ Message: {
3706
+ type: "object",
3707
+ properties: {
3708
+ role: { type: "string", enum: ["user", "assistant"] },
3709
+ content: {
3710
+ oneOf: [
3711
+ { type: "string" },
3712
+ {
3713
+ type: "array",
3714
+ items: {
3715
+ oneOf: [
3716
+ {
3717
+ type: "object",
3718
+ properties: {
3719
+ type: { type: "string", const: "text" },
3720
+ text: { type: "string" }
3721
+ }
3722
+ },
3723
+ {
3724
+ type: "object",
3725
+ properties: {
3726
+ type: { type: "string", const: "file" },
3727
+ data: { type: "string" },
3728
+ mediaType: { type: "string" },
3729
+ filename: { type: "string" }
3730
+ }
3731
+ }
3732
+ ]
3733
+ }
3734
+ }
3735
+ ]
3736
+ },
3737
+ metadata: {
3738
+ type: "object",
3739
+ properties: {
3740
+ id: { type: "string" },
3741
+ timestamp: { type: "number" },
3742
+ tokenCount: { type: "number" },
3743
+ step: { type: "number" },
3744
+ toolActivity: { type: "array", items: { type: "string" } }
3745
+ }
3746
+ }
3747
+ }
3748
+ },
3749
+ Conversation: {
3750
+ type: "object",
3751
+ properties: {
3752
+ conversationId: { type: "string" },
3753
+ title: { type: "string" },
3754
+ ownerId: { type: "string" },
3755
+ tenantId: { type: ["string", "null"] },
3756
+ createdAt: { type: "number" },
3757
+ updatedAt: { type: "number" },
3758
+ messages: { type: "array", items: { $ref: "#/components/schemas/Message" } },
3759
+ pendingApprovals: {
3760
+ type: "array",
3761
+ items: { $ref: "#/components/schemas/PendingApproval" }
3762
+ }
3763
+ }
3764
+ },
3765
+ PendingApproval: {
3766
+ type: "object",
3767
+ properties: {
3768
+ approvalId: { type: "string" },
3769
+ runId: { type: "string" },
3770
+ tool: { type: "string" },
3771
+ input: {}
3772
+ }
3773
+ },
3774
+ TokenUsage: {
3775
+ type: "object",
3776
+ properties: {
3777
+ input: { type: "integer" },
3778
+ output: { type: "integer" },
3779
+ cached: { type: "integer" }
3780
+ }
3781
+ },
3782
+ RunResult: {
3783
+ type: "object",
3784
+ properties: {
3785
+ status: { type: "string", enum: ["completed", "error", "cancelled"] },
3786
+ response: { type: "string" },
3787
+ steps: { type: "integer" },
3788
+ tokens: { $ref: "#/components/schemas/TokenUsage" },
3789
+ duration: { type: "number", description: "Duration in ms" },
3790
+ continuation: { type: "boolean" },
3791
+ maxSteps: { type: "integer" }
3792
+ }
3793
+ },
3794
+ FileAttachment: {
3795
+ type: "object",
3796
+ properties: {
3797
+ data: { type: "string", description: "base64-encoded file data" },
3798
+ mediaType: { type: "string" },
3799
+ filename: { type: "string" }
3800
+ },
3801
+ required: ["data", "mediaType"]
3802
+ },
3803
+ Error: {
3804
+ type: "object",
3805
+ properties: {
3806
+ code: { type: "string" },
3807
+ message: { type: "string" }
3808
+ }
3809
+ }
3810
+ }
3811
+ },
3812
+ security: [{ bearerAuth: [] }],
3813
+ paths: {
3814
+ "/health": {
3815
+ get: {
3816
+ tags: ["Health"],
3817
+ summary: "Health check",
3818
+ security: [],
3819
+ responses: {
3820
+ "200": {
3821
+ description: "Server is healthy",
3822
+ content: {
3823
+ "application/json": {
3824
+ schema: {
3825
+ type: "object",
3826
+ properties: { status: { type: "string", const: "ok" } }
3827
+ }
3828
+ }
3829
+ }
3830
+ }
3831
+ }
3832
+ }
3833
+ },
3834
+ "/api/auth/session": {
3835
+ get: {
3836
+ tags: ["Auth"],
3837
+ summary: "Check session status",
3838
+ description: "Returns whether the caller is authenticated and provides a CSRF token for subsequent mutating requests.",
3839
+ security: [],
3840
+ responses: {
3841
+ "200": {
3842
+ description: "Session status",
3843
+ content: {
3844
+ "application/json": {
3845
+ schema: {
3846
+ type: "object",
3847
+ properties: {
3848
+ authenticated: { type: "boolean" },
3849
+ sessionId: { type: "string" },
3850
+ ownerId: { type: "string" },
3851
+ csrfToken: { type: "string" }
3852
+ },
3853
+ required: ["authenticated"]
3854
+ }
3855
+ }
3856
+ }
3857
+ }
3858
+ }
3859
+ }
3860
+ },
3861
+ "/api/auth/login": {
3862
+ post: {
3863
+ tags: ["Auth"],
3864
+ summary: "Authenticate with passphrase",
3865
+ description: "Creates a session cookie. Only needed for browser-based auth; API clients should use Bearer tokens instead.",
3866
+ security: [],
3867
+ requestBody: {
3868
+ required: true,
3869
+ content: {
3870
+ "application/json": {
3871
+ schema: {
3872
+ type: "object",
3873
+ properties: { passphrase: { type: "string" } },
3874
+ required: ["passphrase"]
3875
+ }
3876
+ }
3877
+ }
3878
+ },
3879
+ responses: {
3880
+ "200": {
3881
+ description: "Login successful",
3882
+ content: {
3883
+ "application/json": {
3884
+ schema: {
3885
+ type: "object",
3886
+ properties: {
3887
+ ok: { type: "boolean" },
3888
+ sessionId: { type: "string" },
3889
+ csrfToken: { type: "string" }
3890
+ }
3891
+ }
3892
+ }
3893
+ }
3894
+ },
3895
+ "401": {
3896
+ description: "Invalid passphrase",
3897
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
3898
+ },
3899
+ "429": {
3900
+ description: "Too many login attempts",
3901
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
3902
+ }
3903
+ }
3904
+ }
3905
+ },
3906
+ "/api/auth/logout": {
3907
+ post: {
3908
+ tags: ["Auth"],
3909
+ summary: "End session",
3910
+ responses: {
3911
+ "200": {
3912
+ description: "Logged out",
3913
+ content: {
3914
+ "application/json": {
3915
+ schema: { type: "object", properties: { ok: { type: "boolean" } } }
3916
+ }
3917
+ }
3918
+ }
3919
+ }
3920
+ }
3921
+ },
3922
+ "/api/conversations": {
3923
+ get: {
3924
+ tags: ["Conversations"],
3925
+ summary: "List conversations",
3926
+ responses: {
3927
+ "200": {
3928
+ description: "Conversation list",
3929
+ content: {
3930
+ "application/json": {
3931
+ schema: {
3932
+ type: "object",
3933
+ properties: {
3934
+ conversations: {
3935
+ type: "array",
3936
+ items: { $ref: "#/components/schemas/ConversationSummary" }
3937
+ }
3938
+ }
3939
+ }
3940
+ }
3941
+ }
3942
+ }
3943
+ }
3944
+ },
3945
+ post: {
3946
+ tags: ["Conversations"],
3947
+ summary: "Create a conversation",
3948
+ requestBody: {
3949
+ content: {
3950
+ "application/json": {
3951
+ schema: {
3952
+ type: "object",
3953
+ properties: { title: { type: "string" } }
3954
+ }
3955
+ }
3956
+ }
3957
+ },
3958
+ responses: {
3959
+ "201": {
3960
+ description: "Conversation created",
3961
+ content: {
3962
+ "application/json": {
3963
+ schema: {
3964
+ type: "object",
3965
+ properties: { conversation: { $ref: "#/components/schemas/Conversation" } }
3966
+ }
3967
+ }
3968
+ }
3969
+ }
3970
+ }
3971
+ }
3972
+ },
3973
+ "/api/conversations/{conversationId}": {
3974
+ get: {
3975
+ tags: ["Conversations"],
3976
+ summary: "Get conversation",
3977
+ description: "Returns the full conversation including messages and any pending tool approval requests.",
3978
+ parameters: [
3979
+ { name: "conversationId", in: "path", required: true, schema: { type: "string" } }
3980
+ ],
3981
+ responses: {
3982
+ "200": {
3983
+ description: "Conversation with messages",
3984
+ content: {
3985
+ "application/json": {
3986
+ schema: {
3987
+ type: "object",
3988
+ properties: { conversation: { $ref: "#/components/schemas/Conversation" } }
3989
+ }
3990
+ }
3991
+ }
3992
+ },
3993
+ "404": {
3994
+ description: "Conversation not found",
3995
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
3996
+ }
3997
+ }
3998
+ },
3999
+ patch: {
4000
+ tags: ["Conversations"],
4001
+ summary: "Rename conversation",
4002
+ parameters: [
4003
+ { name: "conversationId", in: "path", required: true, schema: { type: "string" } }
4004
+ ],
4005
+ requestBody: {
4006
+ required: true,
4007
+ content: {
4008
+ "application/json": {
4009
+ schema: {
4010
+ type: "object",
4011
+ properties: { title: { type: "string" } },
4012
+ required: ["title"]
4013
+ }
4014
+ }
4015
+ }
4016
+ },
4017
+ responses: {
4018
+ "200": {
4019
+ description: "Conversation renamed",
4020
+ content: {
4021
+ "application/json": {
4022
+ schema: {
4023
+ type: "object",
4024
+ properties: { conversation: { $ref: "#/components/schemas/Conversation" } }
4025
+ }
4026
+ }
4027
+ }
4028
+ }
4029
+ }
4030
+ },
4031
+ delete: {
4032
+ tags: ["Conversations"],
4033
+ summary: "Delete conversation",
4034
+ parameters: [
4035
+ { name: "conversationId", in: "path", required: true, schema: { type: "string" } }
4036
+ ],
4037
+ responses: {
4038
+ "200": {
4039
+ description: "Conversation deleted",
4040
+ content: {
4041
+ "application/json": {
4042
+ schema: { type: "object", properties: { ok: { type: "boolean" } } }
4043
+ }
4044
+ }
4045
+ }
4046
+ }
4047
+ }
4048
+ },
4049
+ "/api/conversations/{conversationId}/messages": {
4050
+ post: {
4051
+ tags: ["Messages"],
4052
+ summary: "Send a message (streaming)",
4053
+ description: "Sends a user message and streams the agent's response via Server-Sent Events.\n\n### SSE protocol\n\nThe response is a stream of SSE frames. Each frame has the format:\n\n```\nevent: <type>\ndata: <json>\n\n```\n\n**Event types:**\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `run:started` | `{ runId, agentId }` | Agent run has begun |\n| `model:chunk` | `{ content }` | Incremental text token from the model |\n| `model:response` | `{ usage: { input, output, cached } }` | Model call finished |\n| `step:started` | `{ step }` | Agent step started |\n| `step:completed` | `{ step, duration }` | Agent step finished |\n| `tool:started` | `{ tool, input }` | Tool invocation started |\n| `tool:completed` | `{ tool, output, duration }` | Tool finished successfully |\n| `tool:error` | `{ tool, error, recoverable }` | Tool returned an error |\n| `tool:approval:required` | `{ tool, input, approvalId }` | Tool needs human approval |\n| `tool:approval:granted` | `{ approvalId }` | Approval was granted |\n| `tool:approval:denied` | `{ approvalId, reason? }` | Approval was denied |\n| `run:completed` | `{ runId, result: RunResult }` | Agent finished |\n| `run:error` | `{ runId, error: { code, message } }` | Agent failed |\n| `run:cancelled` | `{ runId }` | Run was cancelled via stop endpoint |\n\nTo build the assistant's response, concatenate all `model:chunk` content values.\n\n### Reconnection\n\nIf the SSE connection drops mid-stream, reconnect via `GET /api/conversations/{conversationId}/events` to replay buffered events.",
4054
+ parameters: [
4055
+ { name: "conversationId", in: "path", required: true, schema: { type: "string" } }
4056
+ ],
4057
+ requestBody: {
4058
+ required: true,
4059
+ content: {
4060
+ "application/json": {
4061
+ schema: {
4062
+ type: "object",
4063
+ properties: {
4064
+ message: { type: "string", description: "User message text" },
4065
+ parameters: {
4066
+ type: "object",
4067
+ additionalProperties: true,
4068
+ description: "Key-value parameters passed to the agent run"
4069
+ },
4070
+ files: {
4071
+ type: "array",
4072
+ items: { $ref: "#/components/schemas/FileAttachment" },
4073
+ description: "Attached files (base64-encoded)"
4074
+ }
4075
+ },
4076
+ required: ["message"]
4077
+ }
4078
+ },
4079
+ "multipart/form-data": {
4080
+ schema: {
4081
+ type: "object",
4082
+ properties: {
4083
+ message: { type: "string" },
4084
+ parameters: { type: "string", description: "JSON-encoded parameters object" },
4085
+ files: { type: "array", items: { type: "string", format: "binary" } }
4086
+ }
4087
+ }
4088
+ }
4089
+ }
4090
+ },
4091
+ responses: {
4092
+ "200": {
4093
+ description: "SSE stream of agent events",
4094
+ content: { "text/event-stream": { schema: { type: "string" } } }
4095
+ },
4096
+ "404": {
4097
+ description: "Conversation not found",
4098
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
4099
+ }
4100
+ }
4101
+ }
4102
+ },
4103
+ "/api/conversations/{conversationId}/events": {
4104
+ get: {
4105
+ tags: ["Messages"],
4106
+ summary: "Attach to live event stream",
4107
+ description: "Connects to the SSE event stream for an in-progress run. Replays all buffered events from the current run, then streams live events. If no run is active, sends a `stream:end` event and closes.",
4108
+ parameters: [
4109
+ { name: "conversationId", in: "path", required: true, schema: { type: "string" } }
4110
+ ],
4111
+ responses: {
4112
+ "200": {
4113
+ description: "SSE stream (same event format as POST /messages)",
4114
+ content: { "text/event-stream": { schema: { type: "string" } } }
4115
+ },
4116
+ "404": {
4117
+ description: "Conversation not found",
4118
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
4119
+ }
4120
+ }
4121
+ }
4122
+ },
4123
+ "/api/conversations/{conversationId}/stop": {
4124
+ post: {
4125
+ tags: ["Messages"],
4126
+ summary: "Stop an in-flight run",
4127
+ parameters: [
4128
+ { name: "conversationId", in: "path", required: true, schema: { type: "string" } }
4129
+ ],
4130
+ requestBody: {
4131
+ required: true,
4132
+ content: {
4133
+ "application/json": {
4134
+ schema: {
4135
+ type: "object",
4136
+ properties: {
4137
+ runId: { type: "string", description: "The run ID to cancel (from run:started event)" }
4138
+ },
4139
+ required: ["runId"]
4140
+ }
4141
+ }
4142
+ }
4143
+ },
4144
+ responses: {
4145
+ "200": {
4146
+ description: "Stop result",
4147
+ content: {
4148
+ "application/json": {
4149
+ schema: {
4150
+ type: "object",
4151
+ properties: {
4152
+ ok: { type: "boolean" },
4153
+ stopped: { type: "boolean" },
4154
+ runId: { type: "string" }
4155
+ }
4156
+ }
4157
+ }
4158
+ }
4159
+ }
4160
+ }
4161
+ }
4162
+ },
4163
+ "/api/approvals/{approvalId}": {
4164
+ post: {
4165
+ tags: ["Approvals"],
4166
+ summary: "Resolve a tool approval request",
4167
+ description: "When an agent run encounters a gated tool, it emits a `tool:approval:required` SSE event and pauses. Use this endpoint to approve or deny the tool invocation.",
4168
+ parameters: [
4169
+ { name: "approvalId", in: "path", required: true, schema: { type: "string" } }
4170
+ ],
4171
+ requestBody: {
4172
+ required: true,
4173
+ content: {
4174
+ "application/json": {
4175
+ schema: {
4176
+ type: "object",
4177
+ properties: { approved: { type: "boolean" } },
4178
+ required: ["approved"]
4179
+ }
4180
+ }
4181
+ }
4182
+ },
4183
+ responses: {
4184
+ "200": {
4185
+ description: "Approval resolved",
4186
+ content: {
4187
+ "application/json": {
4188
+ schema: {
4189
+ type: "object",
4190
+ properties: {
4191
+ ok: { type: "boolean" },
4192
+ approvalId: { type: "string" },
4193
+ approved: { type: "boolean" }
4194
+ }
4195
+ }
4196
+ }
4197
+ }
4198
+ },
4199
+ "404": {
4200
+ description: "Approval not found or expired",
4201
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
4202
+ }
4203
+ }
4204
+ }
4205
+ },
4206
+ "/api/uploads/{key}": {
4207
+ get: {
4208
+ tags: ["Assets"],
4209
+ summary: "Retrieve an uploaded file",
4210
+ description: "Serves a file previously uploaded during a conversation. The key is returned in file content part references.",
4211
+ parameters: [
4212
+ {
4213
+ name: "key",
4214
+ in: "path",
4215
+ required: true,
4216
+ schema: { type: "string" },
4217
+ description: "Upload key (e.g. filename or storage path)"
4218
+ }
4219
+ ],
4220
+ responses: {
4221
+ "200": {
4222
+ description: "File content with appropriate Content-Type",
4223
+ content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
4224
+ },
4225
+ "404": {
4226
+ description: "Upload not found",
4227
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
4228
+ }
4229
+ }
4230
+ }
4231
+ },
4232
+ "/api/cron/{jobName}": {
4233
+ get: {
4234
+ tags: ["Cron"],
4235
+ summary: "Trigger a cron job",
4236
+ description: "Triggers a named cron job defined in AGENT.md frontmatter. Supports continuation via the `continue` query parameter.",
4237
+ parameters: [
4238
+ { name: "jobName", in: "path", required: true, schema: { type: "string" } },
4239
+ {
4240
+ name: "continue",
4241
+ in: "query",
4242
+ schema: { type: "string" },
4243
+ description: "Conversation ID to continue a previous cron run"
4244
+ }
4245
+ ],
4246
+ responses: {
4247
+ "200": {
4248
+ description: "Cron job result",
4249
+ content: {
4250
+ "application/json": {
4251
+ schema: {
4252
+ type: "object",
4253
+ properties: {
4254
+ conversationId: { type: "string" },
4255
+ response: { type: "string" },
4256
+ steps: { type: "integer" },
4257
+ status: { type: "string" },
4258
+ continuation: { type: "string", description: "URL to continue this run" }
4259
+ }
4260
+ }
4261
+ }
4262
+ }
4263
+ },
4264
+ "404": {
4265
+ description: "Cron job not found",
4266
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
4267
+ }
4268
+ }
4269
+ }
4270
+ }
4271
+ },
4272
+ tags: [
4273
+ { name: "Health", description: "Server health check" },
4274
+ { name: "Auth", description: "Session and authentication management" },
4275
+ { name: "Conversations", description: "Create, list, read, rename, and delete conversations" },
4276
+ {
4277
+ name: "Messages",
4278
+ description: "Send messages and stream agent responses via SSE"
4279
+ },
4280
+ { name: "Approvals", description: "Resolve gated tool approval requests" },
4281
+ { name: "Assets", description: "Retrieve uploaded files" },
4282
+ { name: "Cron", description: "Trigger cron jobs defined in AGENT.md" }
4283
+ ]
4284
+ });
4285
+ var renderApiDocsHtml = (specUrl) => `<!DOCTYPE html>
4286
+ <html lang="en">
4287
+ <head>
4288
+ <meta charset="utf-8" />
4289
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4290
+ <title>API Documentation</title>
4291
+ <style>body { margin: 0; }</style>
4292
+ </head>
4293
+ <body>
4294
+ <script id="api-reference" data-url="${specUrl}"></script>
4295
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
4296
+ </body>
4297
+ </html>`;
4298
+
3269
4299
  // src/index.ts
3270
4300
  import { createInterface } from "readline/promises";
3271
4301
 
@@ -3521,7 +4551,20 @@ var buildConfigFromOnboardingAnswers = (answers) => {
3521
4551
  telemetry
3522
4552
  };
3523
4553
  if (messagingPlatform !== "none") {
3524
- config.messaging = [{ platform: messagingPlatform }];
4554
+ const channelConfig = {
4555
+ platform: messagingPlatform
4556
+ };
4557
+ if (messagingPlatform === "resend") {
4558
+ const mode = String(answers["messaging.resend.mode"] ?? "auto-reply");
4559
+ if (mode === "tool") {
4560
+ channelConfig.mode = "tool";
4561
+ }
4562
+ const recipientsRaw = String(answers["messaging.resend.allowedRecipients"] ?? "");
4563
+ if (recipientsRaw.trim().length > 0) {
4564
+ channelConfig.allowedRecipients = recipientsRaw.split(",").map((s) => s.trim()).filter(Boolean);
4565
+ }
4566
+ }
4567
+ config.messaging = [channelConfig];
3525
4568
  }
3526
4569
  return config;
3527
4570
  };
@@ -3993,7 +5036,7 @@ cp .env.example .env
3993
5036
  poncho dev
3994
5037
  \`\`\`
3995
5038
 
3996
- Open \`http://localhost:3000\` for the web UI.
5039
+ Open \`http://localhost:3000\` for the web UI, or \`http://localhost:3000/api/docs\` for interactive API documentation.
3997
5040
 
3998
5041
  On your first interactive session, the agent introduces its configurable capabilities.
3999
5042
  While a response is streaming, you can stop it:
@@ -4115,7 +5158,7 @@ Core files:
4115
5158
 
4116
5159
  - \`AGENT.md\`: behavior, model selection, runtime guidance
4117
5160
  - \`poncho.config.js\`: runtime config (storage, auth, telemetry, MCP, tools)
4118
- - \`.env\`: secrets and environment variables
5161
+ - \`.env\`: secrets and environment variables (loaded before the harness starts, so \`process.env\` is available in skill scripts)
4119
5162
 
4120
5163
  Example \`poncho.config.js\`:
4121
5164
 
@@ -4141,18 +5184,20 @@ export default {
4141
5184
  auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
4142
5185
  },
4143
5186
  ],
5187
+ // Tool access: true (available), false (disabled), 'approval' (requires human approval)
4144
5188
  tools: {
4145
- defaults: {
4146
- list_directory: true,
4147
- read_file: true,
4148
- write_file: true, // still gated by environment/policy
4149
- },
5189
+ write_file: true, // gated by environment for writes
5190
+ send_email: 'approval', // requires human approval
4150
5191
  byEnvironment: {
4151
5192
  production: {
4152
- read_file: false, // example override
5193
+ write_file: false,
5194
+ },
5195
+ development: {
5196
+ send_email: true, // skip approval in dev
4153
5197
  },
4154
5198
  },
4155
5199
  },
5200
+ // webUi: false, // Disable built-in UI for API-only deployments
4156
5201
  };
4157
5202
  \`\`\`
4158
5203
 
@@ -4211,6 +5256,26 @@ Connect your agent to Slack so it responds to @mentions:
4211
5256
  messaging: [{ platform: 'slack' }]
4212
5257
  \`\`\`
4213
5258
 
5259
+ ## Messaging (Email via Resend)
5260
+
5261
+ Connect your agent to email so users can interact by sending emails:
5262
+
5263
+ 1. Set up a domain and enable Inbound at [resend.com](https://resend.com)
5264
+ 2. Create a webhook for \`email.received\` pointing to \`https://<your-url>/api/messaging/resend\`
5265
+ 3. Install the Resend SDK: \`npm install resend\`
5266
+ 4. Set env vars:
5267
+ \`\`\`
5268
+ RESEND_API_KEY=re_...
5269
+ RESEND_WEBHOOK_SECRET=whsec_...
5270
+ RESEND_FROM=Agent <agent@yourdomain.com>
5271
+ \`\`\`
5272
+ 5. Add to \`poncho.config.js\`:
5273
+ \`\`\`javascript
5274
+ messaging: [{ platform: 'resend' }]
5275
+ \`\`\`
5276
+
5277
+ For full control over outbound emails, use **tool mode** (\`mode: 'tool'\`) \u2014 the agent gets a \`send_email\` tool instead of auto-replying. See the repo README for details.
5278
+
4214
5279
  ## Deployment
4215
5280
 
4216
5281
  \`\`\`bash
@@ -4942,20 +6007,162 @@ var createRequestHandler = async (options) => {
4942
6007
  return { messages: [] };
4943
6008
  },
4944
6009
  async run(conversationId, input2) {
4945
- const output = await harness.runToCompletion({
4946
- task: input2.task,
4947
- messages: input2.messages
6010
+ console.log("[messaging-runner] starting run for", conversationId, "task:", input2.task.slice(0, 80));
6011
+ const historyMessages = [...input2.messages];
6012
+ const userContent = input2.task;
6013
+ const updateConversation = async (patch) => {
6014
+ const fresh = await conversationStore.get(conversationId);
6015
+ if (!fresh) return;
6016
+ patch(fresh);
6017
+ fresh.updatedAt = Date.now();
6018
+ await conversationStore.update(fresh);
6019
+ };
6020
+ await updateConversation((c) => {
6021
+ c.messages = [...historyMessages, { role: "user", content: userContent }];
4948
6022
  });
4949
- const response = output.result.response ?? "";
4950
- const conversation = await conversationStore.get(conversationId);
4951
- if (conversation) {
4952
- conversation.messages = [
4953
- ...input2.messages,
4954
- { role: "user", content: input2.task },
4955
- { role: "assistant", content: response }
6023
+ let latestRunId = "";
6024
+ let assistantResponse = "";
6025
+ const toolTimeline = [];
6026
+ const sections = [];
6027
+ let currentTools = [];
6028
+ let currentText = "";
6029
+ const buildMessages = () => {
6030
+ const draftSections = [
6031
+ ...sections.map((s) => ({
6032
+ type: s.type,
6033
+ content: Array.isArray(s.content) ? [...s.content] : s.content
6034
+ }))
4956
6035
  ];
4957
- await conversationStore.update(conversation);
6036
+ if (currentTools.length > 0) {
6037
+ draftSections.push({ type: "tools", content: [...currentTools] });
6038
+ }
6039
+ if (currentText.length > 0) {
6040
+ draftSections.push({ type: "text", content: currentText });
6041
+ }
6042
+ const hasDraftContent = assistantResponse.length > 0 || toolTimeline.length > 0 || draftSections.length > 0;
6043
+ if (!hasDraftContent) {
6044
+ return [...historyMessages, { role: "user", content: userContent }];
6045
+ }
6046
+ return [
6047
+ ...historyMessages,
6048
+ { role: "user", content: userContent },
6049
+ {
6050
+ role: "assistant",
6051
+ content: assistantResponse,
6052
+ metadata: toolTimeline.length > 0 || draftSections.length > 0 ? {
6053
+ toolActivity: [...toolTimeline],
6054
+ sections: draftSections.length > 0 ? draftSections : void 0
6055
+ } : void 0
6056
+ }
6057
+ ];
6058
+ };
6059
+ const persistDraftAssistantTurn = async () => {
6060
+ if (assistantResponse.length === 0 && toolTimeline.length === 0) return;
6061
+ await updateConversation((c) => {
6062
+ c.messages = buildMessages();
6063
+ });
6064
+ };
6065
+ const runInput = {
6066
+ task: input2.task,
6067
+ conversationId,
6068
+ messages: input2.messages,
6069
+ files: input2.files,
6070
+ parameters: input2.metadata ? {
6071
+ __messaging_platform: input2.metadata.platform,
6072
+ __messaging_sender_id: input2.metadata.sender.id,
6073
+ __messaging_sender_name: input2.metadata.sender.name ?? "",
6074
+ __messaging_thread_id: input2.metadata.threadId
6075
+ } : void 0
6076
+ };
6077
+ try {
6078
+ for await (const event of harness.runWithTelemetry(runInput)) {
6079
+ if (event.type === "run:started") {
6080
+ latestRunId = event.runId;
6081
+ runOwners.set(event.runId, "local-owner");
6082
+ runConversations.set(event.runId, conversationId);
6083
+ }
6084
+ if (event.type === "model:chunk") {
6085
+ if (currentTools.length > 0) {
6086
+ sections.push({ type: "tools", content: currentTools });
6087
+ currentTools = [];
6088
+ }
6089
+ assistantResponse += event.content;
6090
+ currentText += event.content;
6091
+ }
6092
+ if (event.type === "tool:started") {
6093
+ if (currentText.length > 0) {
6094
+ sections.push({ type: "text", content: currentText });
6095
+ currentText = "";
6096
+ }
6097
+ const toolText = `- start \`${event.tool}\``;
6098
+ toolTimeline.push(toolText);
6099
+ currentTools.push(toolText);
6100
+ }
6101
+ if (event.type === "tool:completed") {
6102
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
6103
+ toolTimeline.push(toolText);
6104
+ currentTools.push(toolText);
6105
+ }
6106
+ if (event.type === "tool:error") {
6107
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
6108
+ toolTimeline.push(toolText);
6109
+ currentTools.push(toolText);
6110
+ }
6111
+ if (event.type === "step:completed") {
6112
+ await persistDraftAssistantTurn();
6113
+ }
6114
+ if (event.type === "tool:approval:required") {
6115
+ const toolText = `- approval required \`${event.tool}\``;
6116
+ toolTimeline.push(toolText);
6117
+ currentTools.push(toolText);
6118
+ await persistDraftAssistantTurn();
6119
+ await persistConversationPendingApprovals(conversationId);
6120
+ }
6121
+ if (event.type === "tool:approval:granted") {
6122
+ const toolText = `- approval granted (${event.approvalId})`;
6123
+ toolTimeline.push(toolText);
6124
+ currentTools.push(toolText);
6125
+ await persistDraftAssistantTurn();
6126
+ }
6127
+ if (event.type === "tool:approval:denied") {
6128
+ const toolText = `- approval denied (${event.approvalId})`;
6129
+ toolTimeline.push(toolText);
6130
+ currentTools.push(toolText);
6131
+ await persistDraftAssistantTurn();
6132
+ }
6133
+ if (event.type === "run:completed" && assistantResponse.length === 0 && event.result.response) {
6134
+ assistantResponse = event.result.response;
6135
+ }
6136
+ if (event.type === "run:error") {
6137
+ assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
6138
+ }
6139
+ broadcastEvent(conversationId, event);
6140
+ }
6141
+ } catch (err) {
6142
+ console.error("[messaging-runner] run failed:", err instanceof Error ? err.message : err);
6143
+ assistantResponse = assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
4958
6144
  }
6145
+ if (currentTools.length > 0) {
6146
+ sections.push({ type: "tools", content: currentTools });
6147
+ currentTools = [];
6148
+ }
6149
+ if (currentText.length > 0) {
6150
+ sections.push({ type: "text", content: currentText });
6151
+ currentText = "";
6152
+ }
6153
+ await updateConversation((c) => {
6154
+ c.messages = buildMessages();
6155
+ c.runtimeRunId = latestRunId || c.runtimeRunId;
6156
+ c.pendingApprovals = [];
6157
+ });
6158
+ finishConversationStream(conversationId);
6159
+ await persistConversationPendingApprovals(conversationId);
6160
+ if (latestRunId) {
6161
+ runOwners.delete(latestRunId);
6162
+ runConversations.delete(latestRunId);
6163
+ }
6164
+ console.log("[messaging-runner] run complete, response length:", assistantResponse.length);
6165
+ const response = assistantResponse;
4959
6166
  return { response };
4960
6167
  }
4961
6168
  };
@@ -4982,7 +6189,8 @@ var createRequestHandler = async (options) => {
4982
6189
  const bridge = new AgentBridge({
4983
6190
  adapter,
4984
6191
  runner: messagingRunner,
4985
- waitUntil: waitUntilHook
6192
+ waitUntil: waitUntilHook,
6193
+ ownerId: "local-owner"
4986
6194
  });
4987
6195
  adapter.registerRoutes(messagingRouteRegistrar);
4988
6196
  try {
@@ -4994,6 +6202,37 @@ var createRequestHandler = async (options) => {
4994
6202
  ` Slack messaging disabled: ${err instanceof Error ? err.message : String(err)}`
4995
6203
  );
4996
6204
  }
6205
+ } else if (channelConfig.platform === "resend") {
6206
+ const adapter = new ResendAdapter({
6207
+ apiKeyEnv: channelConfig.apiKeyEnv,
6208
+ webhookSecretEnv: channelConfig.webhookSecretEnv,
6209
+ fromEnv: channelConfig.fromEnv,
6210
+ allowedSenders: channelConfig.allowedSenders,
6211
+ mode: channelConfig.mode,
6212
+ allowedRecipients: channelConfig.allowedRecipients,
6213
+ maxSendsPerRun: channelConfig.maxSendsPerRun
6214
+ });
6215
+ const bridge = new AgentBridge({
6216
+ adapter,
6217
+ runner: messagingRunner,
6218
+ waitUntil: waitUntilHook,
6219
+ ownerId: "local-owner"
6220
+ });
6221
+ adapter.registerRoutes(messagingRouteRegistrar);
6222
+ try {
6223
+ await bridge.start();
6224
+ messagingBridges.push(bridge);
6225
+ const adapterTools = adapter.getToolDefinitions?.() ?? [];
6226
+ if (adapterTools.length > 0) {
6227
+ harness.registerTools(adapterTools);
6228
+ }
6229
+ const modeLabel = channelConfig.mode === "tool" ? "tool" : "auto-reply";
6230
+ console.log(` Resend email messaging enabled at /api/messaging/resend (mode: ${modeLabel})`);
6231
+ } catch (err) {
6232
+ console.warn(
6233
+ ` Resend email messaging disabled: ${err instanceof Error ? err.message : String(err)}`
6234
+ );
6235
+ }
4997
6236
  }
4998
6237
  }
4999
6238
  }
@@ -5002,6 +6241,7 @@ var createRequestHandler = async (options) => {
5002
6241
  const authToken = process.env.PONCHO_AUTH_TOKEN ?? "";
5003
6242
  const authRequired = config?.auth?.required ?? false;
5004
6243
  const requireAuth = authRequired && authToken.length > 0;
6244
+ const webUiEnabled = config?.webUi !== false;
5005
6245
  const isProduction = resolveHarnessEnvironment() === "production";
5006
6246
  const secureCookies = isProduction;
5007
6247
  const validateBearerToken = (authHeader) => {
@@ -5023,35 +6263,45 @@ var createRequestHandler = async (options) => {
5023
6263
  return;
5024
6264
  }
5025
6265
  const [pathname] = request.url.split("?");
5026
- if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
5027
- writeHtml(response, 200, renderWebUiHtml({ agentName }));
5028
- return;
5029
- }
5030
- if (pathname === "/manifest.json" && request.method === "GET") {
5031
- response.writeHead(200, { "Content-Type": "application/manifest+json" });
5032
- response.end(renderManifest({ agentName }));
5033
- return;
5034
- }
5035
- if (pathname === "/sw.js" && request.method === "GET") {
5036
- response.writeHead(200, {
5037
- "Content-Type": "application/javascript",
5038
- "Service-Worker-Allowed": "/"
5039
- });
5040
- response.end(renderServiceWorker());
5041
- return;
6266
+ if (webUiEnabled) {
6267
+ if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
6268
+ writeHtml(response, 200, renderWebUiHtml({ agentName }));
6269
+ return;
6270
+ }
6271
+ if (pathname === "/manifest.json" && request.method === "GET") {
6272
+ response.writeHead(200, { "Content-Type": "application/manifest+json" });
6273
+ response.end(renderManifest({ agentName }));
6274
+ return;
6275
+ }
6276
+ if (pathname === "/sw.js" && request.method === "GET") {
6277
+ response.writeHead(200, {
6278
+ "Content-Type": "application/javascript",
6279
+ "Service-Worker-Allowed": "/"
6280
+ });
6281
+ response.end(renderServiceWorker());
6282
+ return;
6283
+ }
6284
+ if (pathname === "/icon.svg" && request.method === "GET") {
6285
+ response.writeHead(200, { "Content-Type": "image/svg+xml" });
6286
+ response.end(renderIconSvg({ agentName }));
6287
+ return;
6288
+ }
6289
+ if ((pathname === "/icon-192.png" || pathname === "/icon-512.png") && request.method === "GET") {
6290
+ response.writeHead(302, { Location: "/icon.svg" });
6291
+ response.end();
6292
+ return;
6293
+ }
5042
6294
  }
5043
- if (pathname === "/icon.svg" && request.method === "GET") {
5044
- response.writeHead(200, { "Content-Type": "image/svg+xml" });
5045
- response.end(renderIconSvg({ agentName }));
6295
+ if (pathname === "/health" && request.method === "GET") {
6296
+ writeJson(response, 200, { status: "ok" });
5046
6297
  return;
5047
6298
  }
5048
- if ((pathname === "/icon-192.png" || pathname === "/icon-512.png") && request.method === "GET") {
5049
- response.writeHead(302, { Location: "/icon.svg" });
5050
- response.end();
6299
+ if (pathname === "/api/openapi.json" && request.method === "GET") {
6300
+ writeJson(response, 200, buildOpenApiSpec({ agentName }));
5051
6301
  return;
5052
6302
  }
5053
- if (pathname === "/health" && request.method === "GET") {
5054
- writeJson(response, 200, { status: "ok" });
6303
+ if (pathname === "/api/docs" && request.method === "GET") {
6304
+ writeHtml(response, 200, renderApiDocsHtml("/api/openapi.json"));
5055
6305
  return;
5056
6306
  }
5057
6307
  const messagingByMethod = messagingRoutes.get(pathname ?? "");
@@ -5249,12 +6499,15 @@ var createRequestHandler = async (options) => {
5249
6499
  response.end();
5250
6500
  return;
5251
6501
  }
5252
- for (const bufferedEvent of stream.buffer) {
5253
- try {
5254
- response.write(formatSseEvent(bufferedEvent));
5255
- } catch {
5256
- response.end();
5257
- return;
6502
+ const liveOnly = (request.url ?? "").includes("live_only=true");
6503
+ if (!liveOnly) {
6504
+ for (const bufferedEvent of stream.buffer) {
6505
+ try {
6506
+ response.write(formatSseEvent(bufferedEvent));
6507
+ } catch {
6508
+ response.end();
6509
+ return;
6510
+ }
5258
6511
  }
5259
6512
  }
5260
6513
  if (stream.finished) {
@@ -5298,11 +6551,14 @@ var createRequestHandler = async (options) => {
5298
6551
  for (const approval of livePending) {
5299
6552
  mergedPendingById.set(approval.approvalId, approval);
5300
6553
  }
6554
+ const activeStream = conversationEventStreams.get(conversationId);
6555
+ const hasActiveRun = !!activeStream && !activeStream.finished;
5301
6556
  writeJson(response, 200, {
5302
6557
  conversation: {
5303
6558
  ...conversation,
5304
6559
  pendingApprovals: Array.from(mergedPendingById.values())
5305
- }
6560
+ },
6561
+ hasActiveRun
5306
6562
  });
5307
6563
  return;
5308
6564
  }
@@ -5563,6 +6819,7 @@ var createRequestHandler = async (options) => {
5563
6819
  })).filter((item) => item.content.length > 0);
5564
6820
  for await (const event of harness.runWithTelemetry({
5565
6821
  task: messageText,
6822
+ conversationId,
5566
6823
  parameters: {
5567
6824
  ...bodyParameters ?? {},
5568
6825
  __conversationRecallCorpus: recallCorpus,
@@ -5807,6 +7064,7 @@ var createRequestHandler = async (options) => {
5807
7064
  const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
5808
7065
  for await (const event of harness.runWithTelemetry({
5809
7066
  task: cronJob.task,
7067
+ conversationId: conversation.conversationId,
5810
7068
  parameters: { __activeConversationId: conversation.conversationId },
5811
7069
  messages: historyMessages,
5812
7070
  abortSignal: abortController.signal
@@ -5968,6 +7226,7 @@ var startDevServer = async (port, options) => {
5968
7226
  let currentText = "";
5969
7227
  for await (const event of harness.runWithTelemetry({
5970
7228
  task: config.task,
7229
+ conversationId: conversation.conversationId,
5971
7230
  parameters: { __activeConversationId: conversation.conversationId },
5972
7231
  messages: []
5973
7232
  })) {
@@ -6158,7 +7417,7 @@ var runInteractive = async (workingDir, params) => {
6158
7417
  await harness.initialize();
6159
7418
  const identity = await ensureAgentIdentity2(workingDir);
6160
7419
  try {
6161
- const { runInteractiveInk } = await import("./run-interactive-ink-VZBOYJYS.js");
7420
+ const { runInteractiveInk } = await import("./run-interactive-ink-SLWDVTDX.js");
6162
7421
  await runInteractiveInk({
6163
7422
  harness,
6164
7423
  params,