@poncho-ai/cli 0.12.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.
@@ -22,6 +22,11 @@ import {
22
22
  resolveStateConfig
23
23
  } from "@poncho-ai/harness";
24
24
  import { getTextContent } from "@poncho-ai/sdk";
25
+ import {
26
+ AgentBridge,
27
+ ResendAdapter,
28
+ SlackAdapter
29
+ } from "@poncho-ai/messaging";
25
30
  import Busboy from "busboy";
26
31
  import { Command } from "commander";
27
32
  import dotenv from "dotenv";
@@ -254,7 +259,8 @@ var renderWebUiHtml = (options) => {
254
259
  <head>
255
260
  <meta charset="utf-8">
256
261
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
257
- <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)">
258
264
  <meta name="apple-mobile-web-app-capable" content="yes">
259
265
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
260
266
  <meta name="apple-mobile-web-app-title" content="${agentName}">
@@ -264,12 +270,168 @@ var renderWebUiHtml = (options) => {
264
270
  <title>${agentName}</title>
265
271
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata:400,700">
266
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
+
267
429
  * { box-sizing: border-box; margin: 0; padding: 0; }
268
430
  html, body { height: 100vh; overflow: hidden; overscroll-behavior: none; touch-action: pan-y; }
269
431
  body {
270
432
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif;
271
- background: #000;
272
- color: #ededed;
433
+ background: var(--bg);
434
+ color: var(--fg);
273
435
  font-size: 14px;
274
436
  line-height: 1.5;
275
437
  -webkit-font-smoothing: antialiased;
@@ -277,7 +439,7 @@ var renderWebUiHtml = (options) => {
277
439
  }
278
440
  button, input, textarea { font: inherit; color: inherit; }
279
441
  .hidden { display: none !important; }
280
- a { color: #ededed; }
442
+ a { color: var(--fg); }
281
443
 
282
444
  /* Auth */
283
445
  .auth {
@@ -285,39 +447,39 @@ var renderWebUiHtml = (options) => {
285
447
  display: grid;
286
448
  place-items: center;
287
449
  padding: 20px;
288
- background: #000;
450
+ background: var(--bg);
289
451
  }
290
452
  .auth-card {
291
453
  width: min(400px, 90vw);
292
454
  }
293
455
  .auth-shell {
294
- background: #0a0a0a;
295
- border: 1px solid rgba(255,255,255,0.1);
456
+ background: var(--bg-alt);
457
+ border: 1px solid var(--border-3);
296
458
  border-radius: 9999px;
297
459
  display: flex;
298
460
  align-items: center;
299
461
  padding: 4px 6px 4px 18px;
300
462
  transition: border-color 0.15s;
301
463
  }
302
- .auth-shell:focus-within { border-color: rgba(255,255,255,0.2); }
464
+ .auth-shell:focus-within { border-color: var(--border-focus); }
303
465
  .auth-input {
304
466
  flex: 1;
305
467
  background: transparent;
306
468
  border: 0;
307
469
  outline: none;
308
- color: #ededed;
470
+ color: var(--fg);
309
471
  padding: 10px 0 8px;
310
472
  font-size: 14px;
311
473
  margin-top: -2px;
312
474
  }
313
- .auth-input::placeholder { color: #444; }
475
+ .auth-input::placeholder { color: var(--fg-7); }
314
476
  .auth-submit {
315
477
  width: 32px;
316
478
  height: 32px;
317
- background: #ededed;
479
+ background: var(--accent);
318
480
  border: 0;
319
481
  border-radius: 50%;
320
- color: #000;
482
+ color: var(--accent-fg);
321
483
  cursor: pointer;
322
484
  display: grid;
323
485
  place-items: center;
@@ -325,19 +487,19 @@ var renderWebUiHtml = (options) => {
325
487
  margin-bottom: 2px;
326
488
  transition: background 0.15s;
327
489
  }
328
- .auth-submit:hover { background: #fff; }
329
- .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; }
330
492
  .message-error {
331
- background: rgba(255,68,68,0.08);
332
- border: 1px solid rgba(255,68,68,0.25);
493
+ background: var(--error-bg);
494
+ border: 1px solid var(--error-border);
333
495
  border-radius: 10px;
334
- color: #ff6b6b;
496
+ color: var(--error-soft);
335
497
  padding: 12px 16px;
336
498
  font-size: 13px;
337
499
  line-height: 1.5;
338
500
  max-width: 600px;
339
501
  }
340
- .message-error strong { color: #ff4444; }
502
+ .message-error strong { color: var(--error); }
341
503
 
342
504
  /* Layout - use fixed positioning with explicit dimensions */
343
505
  .shell {
@@ -369,8 +531,8 @@ var renderWebUiHtml = (options) => {
369
531
  }
370
532
  .sidebar {
371
533
  width: 260px;
372
- background: #000;
373
- border-right: 1px solid rgba(255,255,255,0.06);
534
+ background: var(--bg);
535
+ border-right: 1px solid var(--border-1);
374
536
  display: flex;
375
537
  flex-direction: column;
376
538
  padding: 12px 8px;
@@ -378,7 +540,7 @@ var renderWebUiHtml = (options) => {
378
540
  .new-chat-btn {
379
541
  background: transparent;
380
542
  border: 0;
381
- color: #888;
543
+ color: var(--fg-2);
382
544
  border-radius: 12px;
383
545
  height: 36px;
384
546
  padding: 0 10px;
@@ -389,7 +551,7 @@ var renderWebUiHtml = (options) => {
389
551
  cursor: pointer;
390
552
  transition: background 0.15s, color 0.15s;
391
553
  }
392
- .new-chat-btn:hover { color: #ededed; }
554
+ .new-chat-btn:hover { color: var(--fg); }
393
555
  .new-chat-btn svg { width: 16px; height: 16px; }
394
556
  .conversation-list {
395
557
  flex: 1;
@@ -409,16 +571,16 @@ var renderWebUiHtml = (options) => {
409
571
  cursor: pointer;
410
572
  font-size: 13px;
411
573
  line-height: 36px;
412
- color: #555;
574
+ color: var(--fg-6);
413
575
  white-space: nowrap;
414
576
  overflow: hidden;
415
577
  text-overflow: ellipsis;
416
578
  position: relative;
417
579
  transition: color 0.15s;
418
580
  }
419
- .conversation-item:hover { color: #999; }
581
+ .conversation-item:hover { color: var(--fg-3); }
420
582
  .conversation-item.active {
421
- color: #ededed;
583
+ color: var(--fg);
422
584
  }
423
585
  .conversation-item .delete-btn {
424
586
  position: absolute;
@@ -426,9 +588,9 @@ var renderWebUiHtml = (options) => {
426
588
  top: 0;
427
589
  bottom: 0;
428
590
  opacity: 0;
429
- background: #000;
591
+ background: var(--bg);
430
592
  border: 0;
431
- color: #444;
593
+ color: var(--fg-7);
432
594
  padding: 0 8px;
433
595
  border-radius: 0 4px 4px 0;
434
596
  cursor: pointer;
@@ -439,7 +601,7 @@ var renderWebUiHtml = (options) => {
439
601
  transition: opacity 0.15s, color 0.15s;
440
602
  }
441
603
  .conversation-item:hover .delete-btn { opacity: 1; }
442
- .conversation-item.active .delete-btn { background: rgba(0,0,0,1); }
604
+ .conversation-item.active .delete-btn { background: var(--bg); }
443
605
  .conversation-item .delete-btn::before {
444
606
  content: "";
445
607
  position: absolute;
@@ -447,23 +609,23 @@ var renderWebUiHtml = (options) => {
447
609
  top: 0;
448
610
  bottom: 0;
449
611
  width: 24px;
450
- background: linear-gradient(to right, transparent, #000);
612
+ background: linear-gradient(to right, transparent, var(--bg));
451
613
  pointer-events: none;
452
614
  }
453
615
  .conversation-item.active .delete-btn::before {
454
- background: linear-gradient(to right, transparent, rgba(0,0,0,1));
616
+ background: linear-gradient(to right, transparent, var(--bg));
455
617
  }
456
- .conversation-item .delete-btn:hover { color: #888; }
618
+ .conversation-item .delete-btn:hover { color: var(--fg-2); }
457
619
  .conversation-item .delete-btn.confirming {
458
620
  opacity: 1;
459
621
  width: auto;
460
622
  padding: 0 8px;
461
623
  font-size: 11px;
462
- color: #ff4444;
624
+ color: var(--error);
463
625
  border-radius: 3px;
464
626
  }
465
627
  .conversation-item .delete-btn.confirming:hover {
466
- color: #ff6666;
628
+ color: var(--error-alt);
467
629
  }
468
630
  .sidebar-footer {
469
631
  margin-top: auto;
@@ -472,7 +634,7 @@ var renderWebUiHtml = (options) => {
472
634
  .logout-btn {
473
635
  background: transparent;
474
636
  border: 0;
475
- color: #555;
637
+ color: var(--fg-6);
476
638
  width: 100%;
477
639
  padding: 8px 10px;
478
640
  text-align: left;
@@ -481,10 +643,10 @@ var renderWebUiHtml = (options) => {
481
643
  font-size: 13px;
482
644
  transition: color 0.15s, background 0.15s;
483
645
  }
484
- .logout-btn:hover { color: #888; }
646
+ .logout-btn:hover { color: var(--fg-2); }
485
647
 
486
648
  /* Main */
487
- .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; }
488
650
  .topbar {
489
651
  height: calc(52px + env(safe-area-inset-top, 0px));
490
652
  padding-top: env(safe-area-inset-top, 0px);
@@ -493,8 +655,8 @@ var renderWebUiHtml = (options) => {
493
655
  justify-content: center;
494
656
  font-size: 13px;
495
657
  font-weight: 500;
496
- color: #888;
497
- border-bottom: 1px solid rgba(255,255,255,0.06);
658
+ color: var(--fg-2);
659
+ border-bottom: 1px solid var(--border-1);
498
660
  position: relative;
499
661
  flex-shrink: 0;
500
662
  }
@@ -513,7 +675,7 @@ var renderWebUiHtml = (options) => {
513
675
  bottom: 4px; /* Position from bottom of topbar content area */
514
676
  background: transparent;
515
677
  border: 0;
516
- color: #666;
678
+ color: var(--fg-5);
517
679
  width: 44px;
518
680
  height: 44px;
519
681
  border-radius: 6px;
@@ -523,7 +685,7 @@ var renderWebUiHtml = (options) => {
523
685
  z-index: 10;
524
686
  -webkit-tap-highlight-color: transparent;
525
687
  }
526
- .sidebar-toggle:hover { color: #ededed; }
688
+ .sidebar-toggle:hover { color: var(--fg); }
527
689
  .topbar-new-chat {
528
690
  display: none;
529
691
  position: absolute;
@@ -531,7 +693,7 @@ var renderWebUiHtml = (options) => {
531
693
  bottom: 4px;
532
694
  background: transparent;
533
695
  border: 0;
534
- color: #666;
696
+ color: var(--fg-5);
535
697
  width: 44px;
536
698
  height: 44px;
537
699
  border-radius: 6px;
@@ -540,7 +702,7 @@ var renderWebUiHtml = (options) => {
540
702
  z-index: 10;
541
703
  -webkit-tap-highlight-color: transparent;
542
704
  }
543
- .topbar-new-chat:hover { color: #ededed; }
705
+ .topbar-new-chat:hover { color: var(--fg); }
544
706
  .topbar-new-chat svg { width: 16px; height: 16px; }
545
707
 
546
708
  /* Messages */
@@ -552,8 +714,8 @@ var renderWebUiHtml = (options) => {
552
714
  .assistant-avatar {
553
715
  width: 24px;
554
716
  height: 24px;
555
- background: #ededed;
556
- color: #000;
717
+ background: var(--accent);
718
+ color: var(--accent-fg);
557
719
  border-radius: 6px;
558
720
  display: grid;
559
721
  place-items: center;
@@ -564,7 +726,7 @@ var renderWebUiHtml = (options) => {
564
726
  }
565
727
  .assistant-content {
566
728
  line-height: 1.65;
567
- color: #ededed;
729
+ color: var(--fg);
568
730
  font-size: 14px;
569
731
  min-width: 0;
570
732
  max-width: 100%;
@@ -576,32 +738,32 @@ var renderWebUiHtml = (options) => {
576
738
  .assistant-content p:last-child { margin-bottom: 0; }
577
739
  .assistant-content ul, .assistant-content ol { margin: 8px 0; padding-left: 20px; }
578
740
  .assistant-content li { margin: 4px 0; }
579
- .assistant-content strong { font-weight: 600; color: #fff; }
741
+ .assistant-content strong { font-weight: 600; color: var(--fg-strong); }
580
742
  .assistant-content h2 {
581
743
  font-size: 16px;
582
744
  font-weight: 600;
583
745
  letter-spacing: -0.02em;
584
746
  margin: 20px 0 8px;
585
- color: #fff;
747
+ color: var(--fg-strong);
586
748
  }
587
749
  .assistant-content h3 {
588
750
  font-size: 14px;
589
751
  font-weight: 600;
590
752
  letter-spacing: -0.01em;
591
753
  margin: 16px 0 6px;
592
- color: #fff;
754
+ color: var(--fg-strong);
593
755
  }
594
756
  .assistant-content code {
595
- background: rgba(255,255,255,0.06);
596
- border: 1px solid rgba(255,255,255,0.06);
757
+ background: var(--surface-4);
758
+ border: 1px solid var(--border-1);
597
759
  padding: 2px 5px;
598
760
  border-radius: 4px;
599
761
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
600
762
  font-size: 0.88em;
601
763
  }
602
764
  .assistant-content pre {
603
- background: #0a0a0a;
604
- border: 1px solid rgba(255,255,255,0.06);
765
+ background: var(--bg-alt);
766
+ border: 1px solid var(--border-1);
605
767
  padding: 14px 16px;
606
768
  border-radius: 8px;
607
769
  overflow-x: auto;
@@ -618,33 +780,33 @@ var renderWebUiHtml = (options) => {
618
780
  margin: 8px 0;
619
781
  font-size: 12px;
620
782
  line-height: 1.45;
621
- color: #8a8a8a;
783
+ color: var(--fg-tool);
622
784
  }
623
785
  .tool-activity-inline code {
624
786
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
625
- background: rgba(255,255,255,0.04);
626
- border: 1px solid rgba(255,255,255,0.08);
787
+ background: var(--surface-3);
788
+ border: 1px solid var(--border-2);
627
789
  padding: 4px 8px;
628
790
  border-radius: 6px;
629
- color: #bcbcbc;
791
+ color: var(--fg-tool-code);
630
792
  font-size: 11px;
631
793
  }
632
794
  .tool-status {
633
- color: #8a8a8a;
795
+ color: var(--fg-tool);
634
796
  font-style: italic;
635
797
  }
636
798
  .tool-done {
637
- color: #6a9955;
799
+ color: var(--tool-done);
638
800
  }
639
801
  .tool-error {
640
- color: #f48771;
802
+ color: var(--tool-error);
641
803
  }
642
804
  .assistant-content table {
643
805
  border-collapse: collapse;
644
806
  width: 100%;
645
807
  margin: 14px 0;
646
808
  font-size: 13px;
647
- border: 1px solid rgba(255,255,255,0.08);
809
+ border: 1px solid var(--border-2);
648
810
  border-radius: 8px;
649
811
  overflow: hidden;
650
812
  display: block;
@@ -653,17 +815,17 @@ var renderWebUiHtml = (options) => {
653
815
  white-space: nowrap;
654
816
  }
655
817
  .assistant-content th {
656
- background: rgba(255,255,255,0.06);
818
+ background: var(--surface-4);
657
819
  padding: 10px 12px;
658
820
  text-align: left;
659
821
  font-weight: 600;
660
- border-bottom: 1px solid rgba(255,255,255,0.12);
661
- color: #fff;
822
+ border-bottom: 1px solid var(--border-4);
823
+ color: var(--fg-strong);
662
824
  min-width: 100px;
663
825
  }
664
826
  .assistant-content td {
665
827
  padding: 10px 12px;
666
- border-bottom: 1px solid rgba(255,255,255,0.06);
828
+ border-bottom: 1px solid var(--border-1);
667
829
  width: 100%;
668
830
  min-width: 100px;
669
831
  }
@@ -671,22 +833,22 @@ var renderWebUiHtml = (options) => {
671
833
  border-bottom: none;
672
834
  }
673
835
  .assistant-content tbody tr:hover {
674
- background: rgba(255,255,255,0.02);
836
+ background: var(--surface-1);
675
837
  }
676
838
  .assistant-content hr {
677
839
  border: 0;
678
- border-top: 1px solid rgba(255,255,255,0.1);
840
+ border-top: 1px solid var(--border-3);
679
841
  margin: 20px 0;
680
842
  }
681
843
  .tool-activity {
682
844
  margin-top: 12px;
683
845
  margin-bottom: 12px;
684
- border: 1px solid rgba(255,255,255,0.08);
685
- background: rgba(255,255,255,0.03);
846
+ border: 1px solid var(--border-2);
847
+ background: var(--surface-2);
686
848
  border-radius: 10px;
687
849
  font-size: 12px;
688
850
  line-height: 1.45;
689
- color: #bcbcbc;
851
+ color: var(--fg-tool-code);
690
852
  width: 300px;
691
853
  }
692
854
  .assistant-content > .tool-activity:first-child {
@@ -711,12 +873,12 @@ var renderWebUiHtml = (options) => {
711
873
  font-size: 11px;
712
874
  text-transform: uppercase;
713
875
  letter-spacing: 0.06em;
714
- color: #8a8a8a;
876
+ color: var(--fg-tool);
715
877
  font-weight: 600;
716
878
  }
717
879
  .tool-activity-caret {
718
880
  margin-left: auto;
719
- color: #8a8a8a;
881
+ color: var(--fg-tool);
720
882
  display: inline-flex;
721
883
  align-items: center;
722
884
  justify-content: center;
@@ -738,28 +900,28 @@ var renderWebUiHtml = (options) => {
738
900
  }
739
901
  .tool-activity-item {
740
902
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
741
- background: rgba(255,255,255,0.04);
903
+ background: var(--surface-3);
742
904
  border-radius: 6px;
743
905
  padding: 4px 7px;
744
- color: #d6d6d6;
906
+ color: var(--fg-tool-item);
745
907
  }
746
908
  .approval-requests {
747
- border-top: 1px solid rgba(255,255,255,0.08);
909
+ border-top: 1px solid var(--border-2);
748
910
  padding: 10px 12px 12px;
749
911
  display: grid;
750
912
  gap: 8px;
751
- background: rgba(0,0,0,0.16);
913
+ background: var(--inset-1);
752
914
  }
753
915
  .approval-requests-label {
754
916
  font-size: 11px;
755
917
  text-transform: uppercase;
756
918
  letter-spacing: 0.06em;
757
- color: #b0b0b0;
919
+ color: var(--fg-approval-label);
758
920
  font-weight: 600;
759
921
  }
760
922
  .approval-request-item {
761
- border: 1px solid rgba(255,255,255,0.1);
762
- background: rgba(255,255,255,0.03);
923
+ border: 1px solid var(--border-3);
924
+ background: var(--surface-2);
763
925
  border-radius: 8px;
764
926
  padding: 8px;
765
927
  display: grid;
@@ -767,15 +929,15 @@ var renderWebUiHtml = (options) => {
767
929
  }
768
930
  .approval-request-tool {
769
931
  font-size: 12px;
770
- color: #fff;
932
+ color: var(--fg-strong);
771
933
  font-weight: 600;
772
934
  overflow-wrap: anywhere;
773
935
  }
774
936
  .approval-request-input {
775
937
  font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
776
938
  font-size: 11px;
777
- color: #cfcfcf;
778
- background: rgba(0,0,0,0.25);
939
+ color: var(--fg-approval-input);
940
+ background: var(--inset-2);
779
941
  border-radius: 6px;
780
942
  padding: 6px;
781
943
  overflow-wrap: anywhere;
@@ -788,32 +950,32 @@ var renderWebUiHtml = (options) => {
788
950
  }
789
951
  .approval-action-btn {
790
952
  border-radius: 6px;
791
- border: 1px solid rgba(255,255,255,0.18);
792
- background: rgba(255,255,255,0.06);
793
- color: #f0f0f0;
953
+ border: 1px solid var(--border-5);
954
+ background: var(--surface-4);
955
+ color: var(--fg-approval-btn);
794
956
  font-size: 11px;
795
957
  font-weight: 600;
796
958
  padding: 4px 8px;
797
959
  cursor: pointer;
798
960
  }
799
961
  .approval-action-btn:hover {
800
- background: rgba(255,255,255,0.12);
962
+ background: var(--surface-7);
801
963
  }
802
964
  .approval-action-btn.approve {
803
- border-color: rgba(58, 208, 122, 0.45);
804
- color: #78e7a6;
965
+ border-color: var(--approve-border);
966
+ color: var(--approve);
805
967
  }
806
968
  .approval-action-btn.deny {
807
- border-color: rgba(224, 95, 95, 0.45);
808
- color: #f59b9b;
969
+ border-color: var(--deny-border);
970
+ color: var(--deny);
809
971
  }
810
972
  .approval-action-btn[disabled] {
811
973
  opacity: 0.55;
812
974
  cursor: not-allowed;
813
975
  }
814
976
  .user-bubble {
815
- background: #111;
816
- border: 1px solid rgba(255,255,255,0.08);
977
+ background: var(--bg-elevated);
978
+ border: 1px solid var(--border-2);
817
979
  padding: 10px 16px;
818
980
  border-radius: 18px;
819
981
  max-width: 70%;
@@ -829,7 +991,7 @@ var renderWebUiHtml = (options) => {
829
991
  justify-content: center;
830
992
  height: 100%;
831
993
  gap: 16px;
832
- color: #555;
994
+ color: var(--fg-6);
833
995
  }
834
996
  .empty-state .assistant-avatar {
835
997
  width: 36px;
@@ -839,7 +1001,7 @@ var renderWebUiHtml = (options) => {
839
1001
  }
840
1002
  .empty-state-text {
841
1003
  font-size: 14px;
842
- color: #555;
1004
+ color: var(--fg-6);
843
1005
  }
844
1006
  .thinking-indicator {
845
1007
  display: inline-block;
@@ -847,7 +1009,7 @@ var renderWebUiHtml = (options) => {
847
1009
  font-size: 20px;
848
1010
  line-height: 1;
849
1011
  vertical-align: middle;
850
- color: #ededed;
1012
+ color: var(--fg);
851
1013
  opacity: 0.5;
852
1014
  }
853
1015
  .thinking-status {
@@ -855,13 +1017,13 @@ var renderWebUiHtml = (options) => {
855
1017
  align-items: center;
856
1018
  gap: 8px;
857
1019
  margin-top: 2px;
858
- color: #8a8a8a;
1020
+ color: var(--fg-tool);
859
1021
  font-size: 14px;
860
1022
  line-height: 1.65;
861
1023
  font-weight: 400;
862
1024
  }
863
1025
  .thinking-status-label {
864
- color: #8a8a8a;
1026
+ color: var(--fg-tool);
865
1027
  font-size: 14px;
866
1028
  line-height: 1.65;
867
1029
  font-weight: 400;
@@ -892,26 +1054,26 @@ var renderWebUiHtml = (options) => {
892
1054
  right: 0;
893
1055
  bottom: 100%;
894
1056
  height: 48px;
895
- background: linear-gradient(to top, #000 0%, transparent 100%);
1057
+ background: linear-gradient(to top, var(--bg) 0%, transparent 100%);
896
1058
  pointer-events: none;
897
1059
  }
898
1060
  .composer-inner { max-width: 680px; margin: 0 auto; }
899
1061
  .composer-shell {
900
- background: #0a0a0a;
901
- border: 1px solid rgba(255,255,255,0.1);
1062
+ background: var(--bg-alt);
1063
+ border: 1px solid var(--border-3);
902
1064
  border-radius: 24px;
903
1065
  display: flex;
904
1066
  align-items: end;
905
1067
  padding: 4px 6px 4px 6px;
906
1068
  transition: border-color 0.15s;
907
1069
  }
908
- .composer-shell:focus-within { border-color: rgba(255,255,255,0.2); }
1070
+ .composer-shell:focus-within { border-color: var(--border-focus); }
909
1071
  .composer-input {
910
1072
  flex: 1;
911
1073
  background: transparent;
912
1074
  border: 0;
913
1075
  outline: none;
914
- color: #ededed;
1076
+ color: var(--fg);
915
1077
  min-height: 40px;
916
1078
  max-height: 200px;
917
1079
  resize: none;
@@ -920,14 +1082,14 @@ var renderWebUiHtml = (options) => {
920
1082
  line-height: 1.5;
921
1083
  margin-top: -4px;
922
1084
  }
923
- .composer-input::placeholder { color: #444; }
1085
+ .composer-input::placeholder { color: var(--fg-7); }
924
1086
  .send-btn {
925
1087
  width: 32px;
926
1088
  height: 32px;
927
- background: #ededed;
1089
+ background: var(--accent);
928
1090
  border: 0;
929
1091
  border-radius: 50%;
930
- color: #000;
1092
+ color: var(--accent-fg);
931
1093
  cursor: pointer;
932
1094
  display: grid;
933
1095
  place-items: center;
@@ -935,21 +1097,79 @@ var renderWebUiHtml = (options) => {
935
1097
  margin-bottom: 2px;
936
1098
  transition: background 0.15s, opacity 0.15s;
937
1099
  }
938
- .send-btn:hover { background: #fff; }
1100
+ .send-btn:hover { background: var(--accent-hover); }
939
1101
  .send-btn.stop-mode {
940
- background: #4a4a4a;
941
- color: #fff;
1102
+ background: var(--stop-bg);
1103
+ color: var(--stop-fg);
942
1104
  }
943
- .send-btn.stop-mode:hover { background: #565656; }
1105
+ .send-btn.stop-mode:hover { background: var(--stop-hover); }
944
1106
  .send-btn:disabled { opacity: 0.2; cursor: default; }
945
- .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
+ }
946
1166
  .attach-btn {
947
1167
  width: 32px;
948
1168
  height: 32px;
949
- background: rgba(255,255,255,0.08);
1169
+ background: var(--surface-5);
950
1170
  border: 0;
951
1171
  border-radius: 50%;
952
- color: #999;
1172
+ color: var(--fg-3);
953
1173
  cursor: pointer;
954
1174
  display: grid;
955
1175
  place-items: center;
@@ -958,7 +1178,7 @@ var renderWebUiHtml = (options) => {
958
1178
  margin-right: 8px;
959
1179
  transition: color 0.15s, background 0.15s;
960
1180
  }
961
- .attach-btn:hover { color: #ededed; background: rgba(255,255,255,0.14); }
1181
+ .attach-btn:hover { color: var(--fg); background: var(--surface-8); }
962
1182
  .attachment-preview {
963
1183
  display: flex;
964
1184
  gap: 8px;
@@ -969,12 +1189,12 @@ var renderWebUiHtml = (options) => {
969
1189
  display: inline-flex;
970
1190
  align-items: center;
971
1191
  gap: 6px;
972
- background: rgba(0, 0, 0, 0.6);
973
- border: 1px solid rgba(255, 255, 255, 0.12);
1192
+ background: var(--chip-bg);
1193
+ border: 1px solid var(--border-4);
974
1194
  border-radius: 9999px;
975
1195
  padding: 4px 10px 4px 6px;
976
1196
  font-size: 11px;
977
- color: #777;
1197
+ color: var(--fg-4);
978
1198
  max-width: 200px;
979
1199
  cursor: pointer;
980
1200
  backdrop-filter: blur(6px);
@@ -982,9 +1202,9 @@ var renderWebUiHtml = (options) => {
982
1202
  transition: color 0.15s, border-color 0.15s, background 0.15s;
983
1203
  }
984
1204
  .attachment-chip:hover {
985
- color: #ededed;
986
- border-color: rgba(255, 255, 255, 0.25);
987
- background: rgba(0, 0, 0, 0.75);
1205
+ color: var(--fg);
1206
+ border-color: var(--border-hover);
1207
+ background: var(--chip-bg-hover);
988
1208
  }
989
1209
  .attachment-chip img {
990
1210
  width: 20px;
@@ -998,7 +1218,7 @@ var renderWebUiHtml = (options) => {
998
1218
  width: 20px;
999
1219
  height: 20px;
1000
1220
  border-radius: 50%;
1001
- background: rgba(255,255,255,0.1);
1221
+ background: var(--surface-6);
1002
1222
  display: grid;
1003
1223
  place-items: center;
1004
1224
  font-size: 11px;
@@ -1006,13 +1226,13 @@ var renderWebUiHtml = (options) => {
1006
1226
  }
1007
1227
  .attachment-chip .remove-attachment {
1008
1228
  cursor: pointer;
1009
- color: #555;
1229
+ color: var(--fg-6);
1010
1230
  font-size: 14px;
1011
1231
  margin-left: 2px;
1012
1232
  line-height: 1;
1013
1233
  transition: color 0.15s;
1014
1234
  }
1015
- .attachment-chip .remove-attachment:hover { color: #fff; }
1235
+ .attachment-chip .remove-attachment:hover { color: var(--fg-strong); }
1016
1236
  .attachment-chip .filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
1017
1237
  .user-bubble .user-file-attachments {
1018
1238
  display: flex;
@@ -1042,7 +1262,7 @@ var renderWebUiHtml = (options) => {
1042
1262
  transition: background 0.25s ease, backdrop-filter 0.25s ease;
1043
1263
  }
1044
1264
  .lightbox.active {
1045
- background: rgba(0,0,0,0.85);
1265
+ background: var(--lightbox-bg);
1046
1266
  backdrop-filter: blur(8px);
1047
1267
  }
1048
1268
  .lightbox img {
@@ -1062,16 +1282,16 @@ var renderWebUiHtml = (options) => {
1062
1282
  display: inline-flex;
1063
1283
  align-items: center;
1064
1284
  gap: 4px;
1065
- background: rgba(0,0,0,0.2);
1285
+ background: var(--file-badge-bg);
1066
1286
  border-radius: 6px;
1067
1287
  padding: 4px 8px;
1068
1288
  font-size: 12px;
1069
- color: rgba(255,255,255,0.8);
1289
+ color: var(--file-badge-fg);
1070
1290
  }
1071
1291
  .drag-overlay {
1072
1292
  position: fixed;
1073
1293
  inset: 0;
1074
- background: rgba(0,0,0,0.6);
1294
+ background: var(--backdrop);
1075
1295
  z-index: 9999;
1076
1296
  display: none;
1077
1297
  align-items: center;
@@ -1080,15 +1300,15 @@ var renderWebUiHtml = (options) => {
1080
1300
  }
1081
1301
  .drag-overlay.active { display: flex; }
1082
1302
  .drag-overlay-inner {
1083
- border: 2px dashed rgba(255,255,255,0.4);
1303
+ border: 2px dashed var(--border-drag);
1084
1304
  border-radius: 16px;
1085
1305
  padding: 40px 60px;
1086
- color: #fff;
1306
+ color: var(--fg-strong);
1087
1307
  font-size: 16px;
1088
1308
  }
1089
1309
  .disclaimer {
1090
1310
  text-align: center;
1091
- color: #333;
1311
+ color: var(--fg-8);
1092
1312
  font-size: 12px;
1093
1313
  margin-top: 10px;
1094
1314
  }
@@ -1103,10 +1323,10 @@ var renderWebUiHtml = (options) => {
1103
1323
  align-items: center;
1104
1324
  gap: 6px;
1105
1325
  font-size: 11px;
1106
- color: #777;
1326
+ color: var(--fg-4);
1107
1327
  text-decoration: none;
1108
- background: rgba(0, 0, 0, 0.6);
1109
- border: 1px solid rgba(255, 255, 255, 0.12);
1328
+ background: var(--chip-bg);
1329
+ border: 1px solid var(--border-4);
1110
1330
  border-radius: 9999px;
1111
1331
  padding: 4px 10px 4px 6px;
1112
1332
  backdrop-filter: blur(6px);
@@ -1114,9 +1334,9 @@ var renderWebUiHtml = (options) => {
1114
1334
  transition: color 0.15s, border-color 0.15s, background 0.15s;
1115
1335
  }
1116
1336
  .poncho-badge:hover {
1117
- color: #ededed;
1118
- border-color: rgba(255, 255, 255, 0.25);
1119
- background: rgba(0, 0, 0, 0.75);
1337
+ color: var(--fg);
1338
+ border-color: var(--border-hover);
1339
+ background: var(--chip-bg-hover);
1120
1340
  }
1121
1341
  .poncho-badge-avatar {
1122
1342
  width: 16px;
@@ -1130,8 +1350,8 @@ var renderWebUiHtml = (options) => {
1130
1350
  /* Scrollbar */
1131
1351
  ::-webkit-scrollbar { width: 6px; }
1132
1352
  ::-webkit-scrollbar-track { background: transparent; }
1133
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
1134
- ::-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); }
1135
1355
 
1136
1356
  /* Mobile */
1137
1357
  @media (max-width: 768px) {
@@ -1161,7 +1381,7 @@ var renderWebUiHtml = (options) => {
1161
1381
  .sidebar-backdrop {
1162
1382
  position: fixed;
1163
1383
  inset: 0;
1164
- background: rgba(0,0,0,0.6);
1384
+ background: var(--backdrop);
1165
1385
  z-index: 50;
1166
1386
  backdrop-filter: blur(2px);
1167
1387
  -webkit-backdrop-filter: blur(2px);
@@ -1236,9 +1456,15 @@ var renderWebUiHtml = (options) => {
1236
1456
  </button>
1237
1457
  <input id="file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
1238
1458
  <textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
1239
- <button id="send" class="send-btn" type="submit">
1240
- <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>
1241
- </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>
1242
1468
  </div>
1243
1469
  </div>
1244
1470
  </form>
@@ -1270,6 +1496,8 @@ var renderWebUiHtml = (options) => {
1270
1496
  confirmDeleteId: null,
1271
1497
  approvalRequestsInFlight: {},
1272
1498
  pendingFiles: [],
1499
+ contextTokens: 0,
1500
+ contextWindow: 0,
1273
1501
  };
1274
1502
 
1275
1503
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -1297,12 +1525,41 @@ var renderWebUiHtml = (options) => {
1297
1525
  attachmentPreview: $("attachment-preview"),
1298
1526
  dragOverlay: $("drag-overlay"),
1299
1527
  lightbox: $("lightbox"),
1528
+ contextRingFill: $("context-ring-fill"),
1529
+ contextTooltip: $("context-tooltip"),
1530
+ sendBtnWrapper: $("send-btn-wrapper"),
1300
1531
  };
1301
1532
  const sendIconMarkup =
1302
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>';
1303
1534
  const stopIconMarkup =
1304
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>';
1305
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
+
1306
1563
  const pushConversationUrl = (conversationId) => {
1307
1564
  const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
1308
1565
  if (window.location.pathname !== target) {
@@ -1622,6 +1879,9 @@ var renderWebUiHtml = (options) => {
1622
1879
  if (state.activeConversationId === c.conversationId) {
1623
1880
  state.activeConversationId = null;
1624
1881
  state.activeMessages = [];
1882
+ state.contextTokens = 0;
1883
+ state.contextWindow = 0;
1884
+ updateContextRing();
1625
1885
  pushConversationUrl(null);
1626
1886
  elements.chatTitle.textContent = "";
1627
1887
  renderMessages([]);
@@ -1718,8 +1978,14 @@ var renderWebUiHtml = (options) => {
1718
1978
  } else if (shouldRenderEmptyStreamingIndicator) {
1719
1979
  content.appendChild(createThinkingIndicator(getThinkingStatusLabel(m)));
1720
1980
  } else {
1721
- // Check for sections in _sections (streaming) or metadata.sections (stored)
1722
- 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);
1723
1989
  const pendingApprovals = Array.isArray(m._pendingApprovals) ? m._pendingApprovals : [];
1724
1990
 
1725
1991
  if (sections && sections.length > 0) {
@@ -1860,11 +2126,24 @@ var renderWebUiHtml = (options) => {
1860
2126
  payload.conversation.messages || [],
1861
2127
  payload.conversation.pendingApprovals || payload.pendingApprovals || [],
1862
2128
  );
2129
+ state.contextTokens = 0;
2130
+ state.contextWindow = 0;
2131
+ updateContextRing();
1863
2132
  renderMessages(state.activeMessages, false, { forceScrollBottom: true });
1864
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
+ }
1865
2143
  };
1866
2144
 
1867
- const streamConversationEvents = (conversationId) => {
2145
+ const streamConversationEvents = (conversationId, options) => {
2146
+ const liveOnly = options && options.liveOnly;
1868
2147
  return new Promise((resolve) => {
1869
2148
  const localMessages = state.activeMessages || [];
1870
2149
  const renderIfActiveConversation = (streaming) => {
@@ -1883,20 +2162,36 @@ var renderWebUiHtml = (options) => {
1883
2162
  _currentText: "",
1884
2163
  _currentTools: [],
1885
2164
  _pendingApprovals: [],
2165
+ _activeActivities: [],
1886
2166
  metadata: { toolActivity: [] },
1887
2167
  };
1888
2168
  localMessages.push(assistantMessage);
1889
2169
  state.activeMessages = localMessages;
1890
2170
  }
1891
- if (!assistantMessage._sections) assistantMessage._sections = [];
1892
- if (!assistantMessage._currentText) assistantMessage._currentText = "";
1893
- if (!assistantMessage._currentTools) assistantMessage._currentTools = [];
1894
- if (!assistantMessage._activeActivities) assistantMessage._activeActivities = [];
1895
- if (!assistantMessage._pendingApprovals) assistantMessage._pendingApprovals = [];
1896
- if (!assistantMessage.metadata) assistantMessage.metadata = {};
1897
- if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
1898
-
1899
- 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" : "");
1900
2195
  fetch(url, { credentials: "include" }).then((response) => {
1901
2196
  if (!response.ok || !response.body) {
1902
2197
  resolve(undefined);
@@ -1917,6 +2212,11 @@ var renderWebUiHtml = (options) => {
1917
2212
  if (eventName === "stream:end") {
1918
2213
  return;
1919
2214
  }
2215
+ if (eventName === "run:started") {
2216
+ if (typeof payload.contextWindow === "number" && payload.contextWindow > 0) {
2217
+ state.contextWindow = payload.contextWindow;
2218
+ }
2219
+ }
1920
2220
  if (eventName === "model:chunk") {
1921
2221
  const chunk = String(payload.content || "");
1922
2222
  if (assistantMessage._currentTools.length > 0 && chunk.length > 0) {
@@ -1930,6 +2230,12 @@ var renderWebUiHtml = (options) => {
1930
2230
  assistantMessage._currentText += chunk;
1931
2231
  renderIfActiveConversation(true);
1932
2232
  }
2233
+ if (eventName === "model:response") {
2234
+ if (typeof payload.usage?.input === "number") {
2235
+ state.contextTokens = payload.usage.input;
2236
+ updateContextRing();
2237
+ }
2238
+ }
1933
2239
  if (eventName === "tool:started") {
1934
2240
  const toolName = payload.tool || "tool";
1935
2241
  const startedActivity = addActiveActivityFromToolStart(
@@ -2002,6 +2308,75 @@ var renderWebUiHtml = (options) => {
2002
2308
  assistantMessage.metadata.toolActivity.push(toolText);
2003
2309
  renderIfActiveConversation(true);
2004
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
+ }
2005
2380
  if (eventName === "run:completed") {
2006
2381
  assistantMessage._activeActivities = [];
2007
2382
  if (
@@ -2048,9 +2423,22 @@ var renderWebUiHtml = (options) => {
2048
2423
  }
2049
2424
  if (eventName === "run:error") {
2050
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
+ }
2051
2440
  const errMsg =
2052
2441
  payload.error?.message || "Something went wrong";
2053
- assistantMessage.content = "";
2054
2442
  assistantMessage._error = errMsg;
2055
2443
  renderIfActiveConversation(false);
2056
2444
  }
@@ -2123,6 +2511,9 @@ var renderWebUiHtml = (options) => {
2123
2511
  elements.send.disabled = value ? !canStop : false;
2124
2512
  elements.send.innerHTML = value ? stopIconMarkup : sendIconMarkup;
2125
2513
  elements.send.classList.toggle("stop-mode", value);
2514
+ if (elements.sendBtnWrapper) {
2515
+ elements.sendBtnWrapper.classList.toggle("stop-mode", value);
2516
+ }
2126
2517
  elements.send.setAttribute("aria-label", value ? "Stop response" : "Send message");
2127
2518
  elements.send.setAttribute(
2128
2519
  "title",
@@ -2495,8 +2886,17 @@ var renderWebUiHtml = (options) => {
2495
2886
  }
2496
2887
  if (eventName === "run:started") {
2497
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
+ }
2498
2892
  setStreaming(state.isStreaming);
2499
2893
  }
2894
+ if (eventName === "model:response") {
2895
+ if (typeof payload.usage?.input === "number") {
2896
+ state.contextTokens = payload.usage.input;
2897
+ updateContextRing();
2898
+ }
2899
+ }
2500
2900
  if (eventName === "tool:started") {
2501
2901
  const toolName = payload.tool || "tool";
2502
2902
  const startedActivity = addActiveActivityFromToolStart(
@@ -2659,9 +3059,8 @@ var renderWebUiHtml = (options) => {
2659
3059
  renderIfActiveConversation(false);
2660
3060
  }
2661
3061
  if (eventName === "run:error") {
2662
- assistantMessage._activeActivities = [];
3062
+ finalizeAssistantMessage();
2663
3063
  const errMsg = payload.error?.message || "Something went wrong";
2664
- assistantMessage.content = "";
2665
3064
  assistantMessage._error = errMsg;
2666
3065
  renderIfActiveConversation(false);
2667
3066
  }
@@ -2764,6 +3163,9 @@ var renderWebUiHtml = (options) => {
2764
3163
  state.activeConversationId = null;
2765
3164
  state.activeMessages = [];
2766
3165
  state.confirmDeleteId = null;
3166
+ state.contextTokens = 0;
3167
+ state.contextWindow = 0;
3168
+ updateContextRing();
2767
3169
  pushConversationUrl(null);
2768
3170
  elements.chatTitle.textContent = "";
2769
3171
  renderMessages([]);
@@ -2801,6 +3203,9 @@ var renderWebUiHtml = (options) => {
2801
3203
  state.confirmDeleteId = null;
2802
3204
  state.conversations = [];
2803
3205
  state.csrfToken = "";
3206
+ state.contextTokens = 0;
3207
+ state.contextWindow = 0;
3208
+ updateContextRing();
2804
3209
  await requireAuth();
2805
3210
  });
2806
3211
 
@@ -3005,6 +3410,9 @@ var renderWebUiHtml = (options) => {
3005
3410
  } else {
3006
3411
  state.activeConversationId = null;
3007
3412
  state.activeMessages = [];
3413
+ state.contextTokens = 0;
3414
+ state.contextWindow = 0;
3415
+ updateContextRing();
3008
3416
  elements.chatTitle.textContent = "";
3009
3417
  renderMessages([]);
3010
3418
  renderConversationList();
@@ -3046,6 +3454,7 @@ var renderWebUiHtml = (options) => {
3046
3454
  await createConversation();
3047
3455
  }
3048
3456
  autoResizePrompt();
3457
+ updateContextRing();
3049
3458
  elements.prompt.focus();
3050
3459
  })();
3051
3460
 
@@ -3262,6 +3671,631 @@ var renderWebUiHtml = (options) => {
3262
3671
  </html>`;
3263
3672
  };
3264
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
+
3265
4299
  // src/index.ts
3266
4300
  import { createInterface } from "readline/promises";
3267
4301
 
@@ -3509,12 +4543,30 @@ var buildConfigFromOnboardingAnswers = (answers) => {
3509
4543
  enabled: telemetryEnabled
3510
4544
  };
3511
4545
  maybeSet(telemetry, "otlp", answers["telemetry.otlp"]);
3512
- return {
4546
+ const messagingPlatform = String(answers["messaging.platform"] ?? "none");
4547
+ const config = {
3513
4548
  mcp: [],
3514
4549
  auth,
3515
4550
  storage,
3516
4551
  telemetry
3517
4552
  };
4553
+ if (messagingPlatform !== "none") {
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];
4568
+ }
4569
+ return config;
3518
4570
  };
3519
4571
  var collectEnvVars = (answers) => {
3520
4572
  const envVars = /* @__PURE__ */ new Set();
@@ -3658,11 +4710,13 @@ var summarizeConfig = (config) => {
3658
4710
  const memoryEnabled = config?.storage?.memory?.enabled ?? config?.memory?.enabled ?? false;
3659
4711
  const authRequired = config?.auth?.required ?? false;
3660
4712
  const telemetryEnabled = config?.telemetry?.enabled ?? true;
4713
+ const messagingPlatforms = (config?.messaging ?? []).map((m) => m.platform);
3661
4714
  return [
3662
4715
  `storage: ${provider}`,
3663
4716
  `memory tools: ${memoryEnabled ? "enabled" : "disabled"}`,
3664
4717
  `auth: ${authRequired ? "required" : "not required"}`,
3665
- `telemetry: ${telemetryEnabled ? "enabled" : "disabled"}`
4718
+ `telemetry: ${telemetryEnabled ? "enabled" : "disabled"}`,
4719
+ ...messagingPlatforms.length > 0 ? [`messaging: ${messagingPlatforms.join(", ")}`] : []
3666
4720
  ];
3667
4721
  };
3668
4722
  var getOnboardingMarkerPath = async (workingDir) => {
@@ -3738,6 +4792,7 @@ var consumeFirstRunIntro = async (workingDir, input2) => {
3738
4792
  "- **Turn on telemetry**: Track usage with OpenTelemetry/OTLP",
3739
4793
  "- **Add MCP servers**: Connect external tool servers",
3740
4794
  "- **Schedule cron jobs**: Set up recurring tasks in AGENT.md frontmatter",
4795
+ "- **Connect to Slack**: Set up messaging so users can @mention this agent in Slack",
3741
4796
  "",
3742
4797
  "Just let me know what you'd like to work on!\n"
3743
4798
  ].join("\n");
@@ -3981,7 +5036,7 @@ cp .env.example .env
3981
5036
  poncho dev
3982
5037
  \`\`\`
3983
5038
 
3984
- 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.
3985
5040
 
3986
5041
  On your first interactive session, the agent introduces its configurable capabilities.
3987
5042
  While a response is streaming, you can stop it:
@@ -4103,7 +5158,7 @@ Core files:
4103
5158
 
4104
5159
  - \`AGENT.md\`: behavior, model selection, runtime guidance
4105
5160
  - \`poncho.config.js\`: runtime config (storage, auth, telemetry, MCP, tools)
4106
- - \`.env\`: secrets and environment variables
5161
+ - \`.env\`: secrets and environment variables (loaded before the harness starts, so \`process.env\` is available in skill scripts)
4107
5162
 
4108
5163
  Example \`poncho.config.js\`:
4109
5164
 
@@ -4129,18 +5184,20 @@ export default {
4129
5184
  auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
4130
5185
  },
4131
5186
  ],
5187
+ // Tool access: true (available), false (disabled), 'approval' (requires human approval)
4132
5188
  tools: {
4133
- defaults: {
4134
- list_directory: true,
4135
- read_file: true,
4136
- write_file: true, // still gated by environment/policy
4137
- },
5189
+ write_file: true, // gated by environment for writes
5190
+ send_email: 'approval', // requires human approval
4138
5191
  byEnvironment: {
4139
5192
  production: {
4140
- read_file: false, // example override
5193
+ write_file: false,
5194
+ },
5195
+ development: {
5196
+ send_email: true, // skip approval in dev
4141
5197
  },
4142
5198
  },
4143
5199
  },
5200
+ // webUi: false, // Disable built-in UI for API-only deployments
4144
5201
  };
4145
5202
  \`\`\`
4146
5203
 
@@ -4181,6 +5238,44 @@ cron:
4181
5238
  - Docker/Fly.io: scheduler runs automatically.
4182
5239
  - Trigger manually: \`curl http://localhost:3000/api/cron/daily-report\`
4183
5240
 
5241
+ ## Messaging (Slack)
5242
+
5243
+ Connect your agent to Slack so it responds to @mentions:
5244
+
5245
+ 1. Create a Slack App at [api.slack.com/apps](https://api.slack.com/apps)
5246
+ 2. Add Bot Token Scopes: \`app_mentions:read\`, \`chat:write\`, \`reactions:write\`
5247
+ 3. Enable Event Subscriptions, set Request URL to \`https://<your-url>/api/messaging/slack\`, subscribe to \`app_mention\`
5248
+ 4. Install to workspace, copy Bot Token and Signing Secret
5249
+ 5. Set env vars:
5250
+ \`\`\`
5251
+ SLACK_BOT_TOKEN=xoxb-...
5252
+ SLACK_SIGNING_SECRET=...
5253
+ \`\`\`
5254
+ 6. Add to \`poncho.config.js\`:
5255
+ \`\`\`javascript
5256
+ messaging: [{ platform: 'slack' }]
5257
+ \`\`\`
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
+
4184
5279
  ## Deployment
4185
5280
 
4186
5281
  \`\`\`bash
@@ -4883,11 +5978,270 @@ var createRequestHandler = async (options) => {
4883
5978
  workingDir,
4884
5979
  agentId: identity.id
4885
5980
  });
5981
+ const messagingRoutes = /* @__PURE__ */ new Map();
5982
+ const messagingRouteRegistrar = (method, path, routeHandler) => {
5983
+ let byMethod = messagingRoutes.get(path);
5984
+ if (!byMethod) {
5985
+ byMethod = /* @__PURE__ */ new Map();
5986
+ messagingRoutes.set(path, byMethod);
5987
+ }
5988
+ byMethod.set(method, routeHandler);
5989
+ };
5990
+ const messagingRunner = {
5991
+ async getOrCreateConversation(conversationId, meta) {
5992
+ const existing = await conversationStore.get(conversationId);
5993
+ if (existing) {
5994
+ return { messages: existing.messages };
5995
+ }
5996
+ const now = Date.now();
5997
+ const conversation = {
5998
+ conversationId,
5999
+ title: meta.title ?? `${meta.platform} thread`,
6000
+ messages: [],
6001
+ ownerId: meta.ownerId,
6002
+ tenantId: null,
6003
+ createdAt: now,
6004
+ updatedAt: now
6005
+ };
6006
+ await conversationStore.update(conversation);
6007
+ return { messages: [] };
6008
+ },
6009
+ async run(conversationId, input2) {
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 }];
6022
+ });
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
+ }))
6035
+ ];
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"}]`;
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;
6166
+ return { response };
6167
+ }
6168
+ };
6169
+ const messagingBridges = [];
6170
+ if (config?.messaging && config.messaging.length > 0) {
6171
+ let waitUntilHook;
6172
+ if (process.env.VERCEL) {
6173
+ try {
6174
+ const modName = "@vercel/functions";
6175
+ const mod = await import(
6176
+ /* webpackIgnore: true */
6177
+ modName
6178
+ );
6179
+ waitUntilHook = mod.waitUntil;
6180
+ } catch {
6181
+ }
6182
+ }
6183
+ for (const channelConfig of config.messaging) {
6184
+ if (channelConfig.platform === "slack") {
6185
+ const adapter = new SlackAdapter({
6186
+ botTokenEnv: channelConfig.botTokenEnv,
6187
+ signingSecretEnv: channelConfig.signingSecretEnv
6188
+ });
6189
+ const bridge = new AgentBridge({
6190
+ adapter,
6191
+ runner: messagingRunner,
6192
+ waitUntil: waitUntilHook,
6193
+ ownerId: "local-owner"
6194
+ });
6195
+ adapter.registerRoutes(messagingRouteRegistrar);
6196
+ try {
6197
+ await bridge.start();
6198
+ messagingBridges.push(bridge);
6199
+ console.log(` Slack messaging enabled at /api/messaging/slack`);
6200
+ } catch (err) {
6201
+ console.warn(
6202
+ ` Slack messaging disabled: ${err instanceof Error ? err.message : String(err)}`
6203
+ );
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
+ }
6236
+ }
6237
+ }
6238
+ }
4886
6239
  const sessionStore = new SessionStore();
4887
6240
  const loginRateLimiter = new LoginRateLimiter();
4888
6241
  const authToken = process.env.PONCHO_AUTH_TOKEN ?? "";
4889
6242
  const authRequired = config?.auth?.required ?? false;
4890
6243
  const requireAuth = authRequired && authToken.length > 0;
6244
+ const webUiEnabled = config?.webUi !== false;
4891
6245
  const isProduction = resolveHarnessEnvironment() === "production";
4892
6246
  const secureCookies = isProduction;
4893
6247
  const validateBearerToken = (authHeader) => {
@@ -4909,36 +6263,54 @@ var createRequestHandler = async (options) => {
4909
6263
  return;
4910
6264
  }
4911
6265
  const [pathname] = request.url.split("?");
4912
- if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
4913
- writeHtml(response, 200, renderWebUiHtml({ agentName }));
4914
- return;
4915
- }
4916
- if (pathname === "/manifest.json" && request.method === "GET") {
4917
- response.writeHead(200, { "Content-Type": "application/manifest+json" });
4918
- response.end(renderManifest({ agentName }));
4919
- 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
+ }
4920
6294
  }
4921
- if (pathname === "/sw.js" && request.method === "GET") {
4922
- response.writeHead(200, {
4923
- "Content-Type": "application/javascript",
4924
- "Service-Worker-Allowed": "/"
4925
- });
4926
- response.end(renderServiceWorker());
6295
+ if (pathname === "/health" && request.method === "GET") {
6296
+ writeJson(response, 200, { status: "ok" });
4927
6297
  return;
4928
6298
  }
4929
- if (pathname === "/icon.svg" && request.method === "GET") {
4930
- response.writeHead(200, { "Content-Type": "image/svg+xml" });
4931
- response.end(renderIconSvg({ agentName }));
6299
+ if (pathname === "/api/openapi.json" && request.method === "GET") {
6300
+ writeJson(response, 200, buildOpenApiSpec({ agentName }));
4932
6301
  return;
4933
6302
  }
4934
- if ((pathname === "/icon-192.png" || pathname === "/icon-512.png") && request.method === "GET") {
4935
- response.writeHead(302, { Location: "/icon.svg" });
4936
- response.end();
6303
+ if (pathname === "/api/docs" && request.method === "GET") {
6304
+ writeHtml(response, 200, renderApiDocsHtml("/api/openapi.json"));
4937
6305
  return;
4938
6306
  }
4939
- if (pathname === "/health" && request.method === "GET") {
4940
- writeJson(response, 200, { status: "ok" });
4941
- return;
6307
+ const messagingByMethod = messagingRoutes.get(pathname ?? "");
6308
+ if (messagingByMethod) {
6309
+ const routeHandler = messagingByMethod.get(request.method ?? "");
6310
+ if (routeHandler) {
6311
+ await routeHandler(request, response);
6312
+ return;
6313
+ }
4942
6314
  }
4943
6315
  const cookies = parseCookies(request);
4944
6316
  const sessionId = cookies.poncho_session;
@@ -5127,12 +6499,15 @@ var createRequestHandler = async (options) => {
5127
6499
  response.end();
5128
6500
  return;
5129
6501
  }
5130
- for (const bufferedEvent of stream.buffer) {
5131
- try {
5132
- response.write(formatSseEvent(bufferedEvent));
5133
- } catch {
5134
- response.end();
5135
- 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
+ }
5136
6511
  }
5137
6512
  }
5138
6513
  if (stream.finished) {
@@ -5176,11 +6551,14 @@ var createRequestHandler = async (options) => {
5176
6551
  for (const approval of livePending) {
5177
6552
  mergedPendingById.set(approval.approvalId, approval);
5178
6553
  }
6554
+ const activeStream = conversationEventStreams.get(conversationId);
6555
+ const hasActiveRun = !!activeStream && !activeStream.finished;
5179
6556
  writeJson(response, 200, {
5180
6557
  conversation: {
5181
6558
  ...conversation,
5182
6559
  pendingApprovals: Array.from(mergedPendingById.values())
5183
- }
6560
+ },
6561
+ hasActiveRun
5184
6562
  });
5185
6563
  return;
5186
6564
  }
@@ -5441,6 +6819,7 @@ var createRequestHandler = async (options) => {
5441
6819
  })).filter((item) => item.content.length > 0);
5442
6820
  for await (const event of harness.runWithTelemetry({
5443
6821
  task: messageText,
6822
+ conversationId,
5444
6823
  parameters: {
5445
6824
  ...bodyParameters ?? {},
5446
6825
  __conversationRecallCorpus: recallCorpus,
@@ -5685,6 +7064,7 @@ var createRequestHandler = async (options) => {
5685
7064
  const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
5686
7065
  for await (const event of harness.runWithTelemetry({
5687
7066
  task: cronJob.task,
7067
+ conversationId: conversation.conversationId,
5688
7068
  parameters: { __activeConversationId: conversation.conversationId },
5689
7069
  messages: historyMessages,
5690
7070
  abortSignal: abortController.signal
@@ -5846,6 +7226,7 @@ var startDevServer = async (port, options) => {
5846
7226
  let currentText = "";
5847
7227
  for await (const event of harness.runWithTelemetry({
5848
7228
  task: config.task,
7229
+ conversationId: conversation.conversationId,
5849
7230
  parameters: { __activeConversationId: conversation.conversationId },
5850
7231
  messages: []
5851
7232
  })) {
@@ -6036,7 +7417,7 @@ var runInteractive = async (workingDir, params) => {
6036
7417
  await harness.initialize();
6037
7418
  const identity = await ensureAgentIdentity2(workingDir);
6038
7419
  try {
6039
- const { runInteractiveInk } = await import("./run-interactive-ink-64QEOUXL.js");
7420
+ const { runInteractiveInk } = await import("./run-interactive-ink-SLWDVTDX.js");
6040
7421
  await runInteractiveInk({
6041
7422
  harness,
6042
7423
  params,