@poncho-ai/cli 0.38.1 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +170 -0
- package/dist/{chunk-W7SQVUB4.js → chunk-KVGMTYDD.js} +1959 -66
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-UKPUGCDW.js → run-interactive-ink-LJTKUUV4.js} +1 -1
- package/package.json +4 -4
- package/src/index.ts +366 -6
- package/src/init-onboarding.ts +8 -2
- package/src/scaffolding.ts +75 -13
- package/src/templates.ts +5 -1
- package/src/vfs-zip.ts +94 -0
- package/src/web-ui-client.ts +1022 -0
- package/src/web-ui-styles.ts +408 -1
- package/src/web-ui.ts +6 -0
|
@@ -36,6 +36,85 @@ import {
|
|
|
36
36
|
AgentOrchestrator
|
|
37
37
|
} from "@poncho-ai/harness";
|
|
38
38
|
import { getTextContent } from "@poncho-ai/sdk";
|
|
39
|
+
|
|
40
|
+
// src/vfs-zip.ts
|
|
41
|
+
var CRC_TABLE = (() => {
|
|
42
|
+
const table = new Uint32Array(256);
|
|
43
|
+
for (let i = 0; i < 256; i++) {
|
|
44
|
+
let c = i;
|
|
45
|
+
for (let k = 0; k < 8; k++) c = (c & 1) !== 0 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
46
|
+
table[i] = c >>> 0;
|
|
47
|
+
}
|
|
48
|
+
return table;
|
|
49
|
+
})();
|
|
50
|
+
var crc32 = (data) => {
|
|
51
|
+
let crc = 4294967295;
|
|
52
|
+
for (let i = 0; i < data.length; i++) crc = (CRC_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8) >>> 0;
|
|
53
|
+
return (crc ^ 4294967295) >>> 0;
|
|
54
|
+
};
|
|
55
|
+
var dosDateTime = (date) => {
|
|
56
|
+
const time = (date.getHours() & 31) << 11 | (date.getMinutes() & 63) << 5 | Math.floor(date.getSeconds() / 2) & 31;
|
|
57
|
+
const year = Math.max(1980, date.getFullYear());
|
|
58
|
+
const day = (year - 1980 & 127) << 9 | (date.getMonth() + 1 & 15) << 5 | date.getDate() & 31;
|
|
59
|
+
return { time, day };
|
|
60
|
+
};
|
|
61
|
+
var buildZip = (entries) => {
|
|
62
|
+
const local = [];
|
|
63
|
+
const central = [];
|
|
64
|
+
let offset = 0;
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const nameBuf = Buffer.from(entry.name, "utf8");
|
|
67
|
+
const data = Buffer.from(entry.content);
|
|
68
|
+
const crc = crc32(entry.content);
|
|
69
|
+
const { time, day } = dosDateTime(entry.mtime ?? /* @__PURE__ */ new Date());
|
|
70
|
+
const lfh = Buffer.alloc(30);
|
|
71
|
+
lfh.writeUInt32LE(67324752, 0);
|
|
72
|
+
lfh.writeUInt16LE(20, 4);
|
|
73
|
+
lfh.writeUInt16LE(2048, 6);
|
|
74
|
+
lfh.writeUInt16LE(0, 8);
|
|
75
|
+
lfh.writeUInt16LE(time, 10);
|
|
76
|
+
lfh.writeUInt16LE(day, 12);
|
|
77
|
+
lfh.writeUInt32LE(crc, 14);
|
|
78
|
+
lfh.writeUInt32LE(data.length, 18);
|
|
79
|
+
lfh.writeUInt32LE(data.length, 22);
|
|
80
|
+
lfh.writeUInt16LE(nameBuf.length, 26);
|
|
81
|
+
lfh.writeUInt16LE(0, 28);
|
|
82
|
+
local.push(lfh, nameBuf, data);
|
|
83
|
+
const cdh = Buffer.alloc(46);
|
|
84
|
+
cdh.writeUInt32LE(33639248, 0);
|
|
85
|
+
cdh.writeUInt16LE(20, 4);
|
|
86
|
+
cdh.writeUInt16LE(20, 6);
|
|
87
|
+
cdh.writeUInt16LE(2048, 8);
|
|
88
|
+
cdh.writeUInt16LE(0, 10);
|
|
89
|
+
cdh.writeUInt16LE(time, 12);
|
|
90
|
+
cdh.writeUInt16LE(day, 14);
|
|
91
|
+
cdh.writeUInt32LE(crc, 16);
|
|
92
|
+
cdh.writeUInt32LE(data.length, 20);
|
|
93
|
+
cdh.writeUInt32LE(data.length, 24);
|
|
94
|
+
cdh.writeUInt16LE(nameBuf.length, 28);
|
|
95
|
+
cdh.writeUInt16LE(0, 30);
|
|
96
|
+
cdh.writeUInt16LE(0, 32);
|
|
97
|
+
cdh.writeUInt16LE(0, 34);
|
|
98
|
+
cdh.writeUInt16LE(0, 36);
|
|
99
|
+
cdh.writeUInt32LE(0, 38);
|
|
100
|
+
cdh.writeUInt32LE(offset, 42);
|
|
101
|
+
central.push(cdh, nameBuf);
|
|
102
|
+
offset += lfh.length + nameBuf.length + data.length;
|
|
103
|
+
}
|
|
104
|
+
const centralBuf = Buffer.concat(central);
|
|
105
|
+
const eocd = Buffer.alloc(22);
|
|
106
|
+
eocd.writeUInt32LE(101010256, 0);
|
|
107
|
+
eocd.writeUInt16LE(0, 4);
|
|
108
|
+
eocd.writeUInt16LE(0, 6);
|
|
109
|
+
eocd.writeUInt16LE(entries.length, 8);
|
|
110
|
+
eocd.writeUInt16LE(entries.length, 10);
|
|
111
|
+
eocd.writeUInt32LE(centralBuf.length, 12);
|
|
112
|
+
eocd.writeUInt32LE(offset, 16);
|
|
113
|
+
eocd.writeUInt16LE(0, 20);
|
|
114
|
+
return Buffer.concat([...local, centralBuf, eocd]);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/index.ts
|
|
39
118
|
import {
|
|
40
119
|
AgentBridge,
|
|
41
120
|
ResendAdapter,
|
|
@@ -359,7 +438,7 @@ var WEB_UI_STYLES = `
|
|
|
359
438
|
.conversation-list {
|
|
360
439
|
flex: 1;
|
|
361
440
|
overflow-y: auto;
|
|
362
|
-
margin-top:
|
|
441
|
+
margin-top: 8px;
|
|
363
442
|
display: flex;
|
|
364
443
|
flex-direction: column;
|
|
365
444
|
gap: 2px;
|
|
@@ -489,6 +568,379 @@ var WEB_UI_STYLES = `
|
|
|
489
568
|
user-select: none;
|
|
490
569
|
}
|
|
491
570
|
.cron-view-more:hover { color: var(--fg-3); }
|
|
571
|
+
.sidebar-segmented {
|
|
572
|
+
display: inline-flex;
|
|
573
|
+
align-self: stretch;
|
|
574
|
+
margin: 12px 6px 0;
|
|
575
|
+
padding: 3px;
|
|
576
|
+
background: var(--surface-3);
|
|
577
|
+
border-radius: 999px;
|
|
578
|
+
gap: 2px;
|
|
579
|
+
}
|
|
580
|
+
.seg-btn {
|
|
581
|
+
flex: 1;
|
|
582
|
+
background: transparent;
|
|
583
|
+
border: 0;
|
|
584
|
+
color: var(--fg-5);
|
|
585
|
+
font-size: 12px;
|
|
586
|
+
font-weight: 500;
|
|
587
|
+
padding: 5px 10px;
|
|
588
|
+
border-radius: 999px;
|
|
589
|
+
cursor: pointer;
|
|
590
|
+
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
|
591
|
+
}
|
|
592
|
+
.seg-btn:hover:not(.active) { color: var(--fg-2); }
|
|
593
|
+
.seg-btn.active {
|
|
594
|
+
background: #fff;
|
|
595
|
+
color: #000;
|
|
596
|
+
}
|
|
597
|
+
.file-explorer {
|
|
598
|
+
flex: 1;
|
|
599
|
+
overflow-y: auto;
|
|
600
|
+
margin-top: 8px;
|
|
601
|
+
display: flex;
|
|
602
|
+
flex-direction: column;
|
|
603
|
+
gap: 1px;
|
|
604
|
+
}
|
|
605
|
+
.file-children {
|
|
606
|
+
border-radius: 6px;
|
|
607
|
+
transition: background 0.1s, box-shadow 0.1s;
|
|
608
|
+
}
|
|
609
|
+
.file-children.drop-target {
|
|
610
|
+
background: var(--surface-3);
|
|
611
|
+
box-shadow: inset 0 0 0 1px var(--border-drag);
|
|
612
|
+
}
|
|
613
|
+
.file-row {
|
|
614
|
+
display: flex;
|
|
615
|
+
align-items: center;
|
|
616
|
+
height: 28px;
|
|
617
|
+
padding: 0 8px;
|
|
618
|
+
border-radius: 8px;
|
|
619
|
+
cursor: pointer;
|
|
620
|
+
font-size: 13px;
|
|
621
|
+
color: var(--fg-6);
|
|
622
|
+
user-select: none;
|
|
623
|
+
position: relative;
|
|
624
|
+
transition: color 0.15s, background 0.15s;
|
|
625
|
+
}
|
|
626
|
+
.file-row:hover { color: var(--fg-3); }
|
|
627
|
+
.file-row.active { color: var(--fg); }
|
|
628
|
+
.file-row.is-dir.drop-target { background: var(--surface-4); box-shadow: inset 0 0 0 1px var(--border-drag); }
|
|
629
|
+
.file-row .file-caret {
|
|
630
|
+
display: grid;
|
|
631
|
+
place-items: center;
|
|
632
|
+
width: 14px;
|
|
633
|
+
height: 14px;
|
|
634
|
+
flex-shrink: 0;
|
|
635
|
+
transition: transform 0.15s;
|
|
636
|
+
color: var(--fg-7);
|
|
637
|
+
}
|
|
638
|
+
.file-row .file-caret.open { transform: rotate(90deg); }
|
|
639
|
+
.file-row .file-caret.empty { visibility: hidden; }
|
|
640
|
+
.file-row .file-icon {
|
|
641
|
+
display: inline-flex;
|
|
642
|
+
width: 16px;
|
|
643
|
+
margin-right: 6px;
|
|
644
|
+
flex-shrink: 0;
|
|
645
|
+
color: var(--fg-7);
|
|
646
|
+
}
|
|
647
|
+
.file-row .file-name {
|
|
648
|
+
flex: 1;
|
|
649
|
+
min-width: 0;
|
|
650
|
+
overflow: hidden;
|
|
651
|
+
text-overflow: ellipsis;
|
|
652
|
+
white-space: nowrap;
|
|
653
|
+
}
|
|
654
|
+
.file-row.is-dir .file-name { color: var(--fg-3); }
|
|
655
|
+
.file-row-actions {
|
|
656
|
+
position: absolute;
|
|
657
|
+
right: 0;
|
|
658
|
+
top: 0;
|
|
659
|
+
bottom: 0;
|
|
660
|
+
display: flex;
|
|
661
|
+
align-items: center;
|
|
662
|
+
opacity: 0;
|
|
663
|
+
background: var(--bg);
|
|
664
|
+
border-radius: 0 4px 4px 0;
|
|
665
|
+
transition: opacity 0.15s;
|
|
666
|
+
}
|
|
667
|
+
.file-row:hover .file-row-actions,
|
|
668
|
+
.file-row-actions.confirming { opacity: 1; }
|
|
669
|
+
.file-row-actions::before {
|
|
670
|
+
content: "";
|
|
671
|
+
position: absolute;
|
|
672
|
+
right: 100%;
|
|
673
|
+
top: 0;
|
|
674
|
+
bottom: 0;
|
|
675
|
+
width: 24px;
|
|
676
|
+
background: linear-gradient(to right, transparent, var(--bg));
|
|
677
|
+
pointer-events: none;
|
|
678
|
+
}
|
|
679
|
+
.file-row-action {
|
|
680
|
+
background: transparent;
|
|
681
|
+
border: 0;
|
|
682
|
+
color: var(--fg-7);
|
|
683
|
+
padding: 0 8px;
|
|
684
|
+
cursor: pointer;
|
|
685
|
+
display: grid;
|
|
686
|
+
place-items: center;
|
|
687
|
+
text-decoration: none;
|
|
688
|
+
height: 100%;
|
|
689
|
+
transition: color 0.15s;
|
|
690
|
+
}
|
|
691
|
+
.file-row-action:hover { color: var(--fg-2); }
|
|
692
|
+
.file-row-action svg { width: 14px; height: 14px; }
|
|
693
|
+
.file-row .file-delete.confirming {
|
|
694
|
+
width: auto;
|
|
695
|
+
padding: 0 8px;
|
|
696
|
+
font-size: 11px;
|
|
697
|
+
color: var(--error);
|
|
698
|
+
}
|
|
699
|
+
.file-row .file-delete.confirming:hover { color: var(--error-alt); }
|
|
700
|
+
.file-explorer-empty,
|
|
701
|
+
.file-explorer-error {
|
|
702
|
+
padding: 12px 10px;
|
|
703
|
+
font-size: 12px;
|
|
704
|
+
color: var(--fg-7);
|
|
705
|
+
line-height: 1.5;
|
|
706
|
+
}
|
|
707
|
+
.file-explorer-error { color: var(--error-soft); }
|
|
708
|
+
.file-explorer-error button {
|
|
709
|
+
background: transparent;
|
|
710
|
+
border: 1px solid var(--border-3);
|
|
711
|
+
color: var(--fg-3);
|
|
712
|
+
border-radius: 6px;
|
|
713
|
+
font-size: 12px;
|
|
714
|
+
padding: 4px 10px;
|
|
715
|
+
cursor: pointer;
|
|
716
|
+
margin-top: 6px;
|
|
717
|
+
}
|
|
718
|
+
.file-upload-row {
|
|
719
|
+
display: flex;
|
|
720
|
+
align-items: center;
|
|
721
|
+
gap: 6px;
|
|
722
|
+
padding: 4px 10px;
|
|
723
|
+
font-size: 12px;
|
|
724
|
+
color: var(--fg-7);
|
|
725
|
+
}
|
|
726
|
+
.file-upload-spinner {
|
|
727
|
+
width: 12px;
|
|
728
|
+
height: 12px;
|
|
729
|
+
border-radius: 50%;
|
|
730
|
+
border: 1.5px solid var(--fg-7);
|
|
731
|
+
border-top-color: transparent;
|
|
732
|
+
animation: file-spin 0.8s linear infinite;
|
|
733
|
+
flex-shrink: 0;
|
|
734
|
+
}
|
|
735
|
+
@keyframes file-spin { to { transform: rotate(360deg); } }
|
|
736
|
+
.file-explorer-footer {
|
|
737
|
+
display: flex;
|
|
738
|
+
align-items: center;
|
|
739
|
+
gap: 8px;
|
|
740
|
+
padding: 8px 10px;
|
|
741
|
+
font-size: 11px;
|
|
742
|
+
color: var(--fg-7);
|
|
743
|
+
border-top: 1px solid var(--border-1);
|
|
744
|
+
margin-top: 8px;
|
|
745
|
+
}
|
|
746
|
+
.file-explorer-usage {
|
|
747
|
+
flex: 1;
|
|
748
|
+
min-width: 0;
|
|
749
|
+
display: flex;
|
|
750
|
+
flex-direction: column;
|
|
751
|
+
gap: 2px;
|
|
752
|
+
line-height: 1.3;
|
|
753
|
+
}
|
|
754
|
+
.file-explorer-usage span {
|
|
755
|
+
overflow: hidden;
|
|
756
|
+
text-overflow: ellipsis;
|
|
757
|
+
white-space: nowrap;
|
|
758
|
+
}
|
|
759
|
+
.file-explorer-upload {
|
|
760
|
+
display: inline-flex;
|
|
761
|
+
align-items: center;
|
|
762
|
+
gap: 6px;
|
|
763
|
+
background: transparent;
|
|
764
|
+
border: 1px solid var(--border-3);
|
|
765
|
+
color: var(--fg-3);
|
|
766
|
+
font-size: 12px;
|
|
767
|
+
border-radius: 999px;
|
|
768
|
+
padding: 4px 12px;
|
|
769
|
+
cursor: pointer;
|
|
770
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
771
|
+
}
|
|
772
|
+
.file-explorer-upload:hover { color: var(--fg); border-color: var(--border-5); background: var(--surface-3); }
|
|
773
|
+
.file-explorer-upload svg { width: 14px; height: 14px; }
|
|
774
|
+
.file-explorer-icon-btn {
|
|
775
|
+
display: grid;
|
|
776
|
+
place-items: center;
|
|
777
|
+
width: 26px;
|
|
778
|
+
height: 26px;
|
|
779
|
+
background: transparent;
|
|
780
|
+
border: 1px solid var(--border-3);
|
|
781
|
+
border-radius: 999px;
|
|
782
|
+
color: var(--fg-3);
|
|
783
|
+
cursor: pointer;
|
|
784
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
785
|
+
flex-shrink: 0;
|
|
786
|
+
}
|
|
787
|
+
.file-explorer-icon-btn:hover { color: var(--fg); border-color: var(--border-5); background: var(--surface-3); }
|
|
788
|
+
.file-explorer-icon-btn svg { width: 13px; height: 13px; }
|
|
789
|
+
.file-preview {
|
|
790
|
+
display: flex;
|
|
791
|
+
flex-direction: column;
|
|
792
|
+
min-height: 100%;
|
|
793
|
+
height: 100%;
|
|
794
|
+
}
|
|
795
|
+
.file-preview-text {
|
|
796
|
+
flex: 1;
|
|
797
|
+
overflow: auto;
|
|
798
|
+
padding: 16px 20px;
|
|
799
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
800
|
+
font-size: 12.5px;
|
|
801
|
+
line-height: 1.5;
|
|
802
|
+
color: var(--fg-2);
|
|
803
|
+
white-space: pre-wrap;
|
|
804
|
+
overflow-wrap: anywhere;
|
|
805
|
+
margin: 0;
|
|
806
|
+
}
|
|
807
|
+
.file-preview-markdown {
|
|
808
|
+
flex: 1;
|
|
809
|
+
overflow: auto;
|
|
810
|
+
padding: 16px 20px;
|
|
811
|
+
}
|
|
812
|
+
.file-preview-table-wrap {
|
|
813
|
+
flex: 1;
|
|
814
|
+
overflow: auto;
|
|
815
|
+
padding: 0 16px 16px;
|
|
816
|
+
}
|
|
817
|
+
.file-preview-table {
|
|
818
|
+
border-collapse: separate;
|
|
819
|
+
border-spacing: 0;
|
|
820
|
+
font-size: 12.5px;
|
|
821
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
822
|
+
color: var(--fg-2);
|
|
823
|
+
width: max-content;
|
|
824
|
+
min-width: 100%;
|
|
825
|
+
}
|
|
826
|
+
.file-preview-table th,
|
|
827
|
+
.file-preview-table td {
|
|
828
|
+
border-right: 1px solid var(--border-1);
|
|
829
|
+
border-bottom: 1px solid var(--border-1);
|
|
830
|
+
padding: 6px 10px;
|
|
831
|
+
text-align: left;
|
|
832
|
+
vertical-align: top;
|
|
833
|
+
white-space: pre-wrap;
|
|
834
|
+
max-width: 480px;
|
|
835
|
+
overflow-wrap: anywhere;
|
|
836
|
+
}
|
|
837
|
+
.file-preview-table tr th:first-child,
|
|
838
|
+
.file-preview-table tr td:first-child {
|
|
839
|
+
border-left: 1px solid var(--border-1);
|
|
840
|
+
}
|
|
841
|
+
.file-preview-table thead th {
|
|
842
|
+
position: sticky;
|
|
843
|
+
top: 0;
|
|
844
|
+
background: var(--bg);
|
|
845
|
+
color: var(--fg);
|
|
846
|
+
font-weight: 600;
|
|
847
|
+
z-index: 2;
|
|
848
|
+
border-top: 1px solid var(--border-1);
|
|
849
|
+
box-shadow: 0 1px 0 var(--border-1);
|
|
850
|
+
}
|
|
851
|
+
.file-preview-table tbody td { background: var(--bg); }
|
|
852
|
+
.file-preview-table tbody tr:hover td { background: var(--surface-2); }
|
|
853
|
+
.file-preview-table-truncated {
|
|
854
|
+
padding: 6px 4px 0;
|
|
855
|
+
font-size: 11px;
|
|
856
|
+
color: var(--fg-7);
|
|
857
|
+
}
|
|
858
|
+
.file-preview-actions {
|
|
859
|
+
display: flex;
|
|
860
|
+
justify-content: space-between;
|
|
861
|
+
gap: 6px;
|
|
862
|
+
padding: 8px 16px 12px;
|
|
863
|
+
}
|
|
864
|
+
.file-preview-actions-group {
|
|
865
|
+
display: flex;
|
|
866
|
+
gap: 6px;
|
|
867
|
+
}
|
|
868
|
+
.file-preview-action-btn {
|
|
869
|
+
background: transparent;
|
|
870
|
+
border: 1px solid var(--border-3);
|
|
871
|
+
color: var(--fg-3);
|
|
872
|
+
border-radius: 999px;
|
|
873
|
+
font-size: 12px;
|
|
874
|
+
padding: 4px 12px;
|
|
875
|
+
cursor: pointer;
|
|
876
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
877
|
+
}
|
|
878
|
+
.file-preview-action-btn { text-decoration: none; }
|
|
879
|
+
.file-preview-action-btn:hover { color: var(--fg); border-color: var(--border-5); }
|
|
880
|
+
.file-preview-action-btn.primary {
|
|
881
|
+
background: var(--accent);
|
|
882
|
+
border-color: var(--accent);
|
|
883
|
+
color: var(--accent-fg);
|
|
884
|
+
}
|
|
885
|
+
.file-preview-action-btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--accent-fg); }
|
|
886
|
+
.file-preview-action-btn:disabled { opacity: 0.5; cursor: default; }
|
|
887
|
+
.file-edit-textarea {
|
|
888
|
+
flex: 1;
|
|
889
|
+
width: 100%;
|
|
890
|
+
box-sizing: border-box;
|
|
891
|
+
background: transparent;
|
|
892
|
+
border: 0;
|
|
893
|
+
outline: none;
|
|
894
|
+
resize: none;
|
|
895
|
+
padding: 16px 20px;
|
|
896
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
897
|
+
font-size: 12.5px;
|
|
898
|
+
line-height: 1.5;
|
|
899
|
+
color: var(--fg);
|
|
900
|
+
white-space: pre-wrap;
|
|
901
|
+
overflow-wrap: anywhere;
|
|
902
|
+
}
|
|
903
|
+
.file-preview-image,
|
|
904
|
+
.file-preview-pdf,
|
|
905
|
+
.file-preview-media {
|
|
906
|
+
flex: 1;
|
|
907
|
+
display: grid;
|
|
908
|
+
place-items: center;
|
|
909
|
+
padding: 16px;
|
|
910
|
+
overflow: auto;
|
|
911
|
+
min-height: 0;
|
|
912
|
+
}
|
|
913
|
+
.file-preview-image img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
|
914
|
+
.file-preview-pdf iframe { width: 100%; height: 100%; border: 0; min-height: 600px; }
|
|
915
|
+
.file-preview-placeholder {
|
|
916
|
+
flex: 1;
|
|
917
|
+
display: grid;
|
|
918
|
+
place-items: center;
|
|
919
|
+
color: var(--fg-7);
|
|
920
|
+
text-align: center;
|
|
921
|
+
padding: 32px;
|
|
922
|
+
}
|
|
923
|
+
.file-preview-placeholder .file-preview-name {
|
|
924
|
+
font-size: 14px;
|
|
925
|
+
color: var(--fg-3);
|
|
926
|
+
margin-bottom: 8px;
|
|
927
|
+
word-break: break-all;
|
|
928
|
+
}
|
|
929
|
+
.file-preview-placeholder .file-preview-meta {
|
|
930
|
+
font-size: 12px;
|
|
931
|
+
margin-bottom: 16px;
|
|
932
|
+
}
|
|
933
|
+
.file-preview-download {
|
|
934
|
+
display: inline-block;
|
|
935
|
+
background: var(--surface-4);
|
|
936
|
+
color: var(--fg);
|
|
937
|
+
text-decoration: none;
|
|
938
|
+
padding: 8px 16px;
|
|
939
|
+
border-radius: 8px;
|
|
940
|
+
font-size: 13px;
|
|
941
|
+
transition: background 0.15s;
|
|
942
|
+
}
|
|
943
|
+
.file-preview-download:hover { background: var(--surface-6); }
|
|
492
944
|
.sidebar-footer {
|
|
493
945
|
margin-top: auto;
|
|
494
946
|
padding-top: 8px;
|
|
@@ -678,8 +1130,42 @@ var WEB_UI_STYLES = `
|
|
|
678
1130
|
z-index: 10;
|
|
679
1131
|
-webkit-tap-highlight-color: transparent;
|
|
680
1132
|
}
|
|
681
|
-
.topbar-new-chat:hover { color: var(--fg); }
|
|
682
|
-
.topbar-new-chat svg { width: 16px; height: 16px; }
|
|
1133
|
+
.topbar-new-chat:hover { color: var(--fg); }
|
|
1134
|
+
.topbar-new-chat svg { width: 16px; height: 16px; }
|
|
1135
|
+
.dev-view-toggle {
|
|
1136
|
+
position: fixed;
|
|
1137
|
+
left: 12px;
|
|
1138
|
+
bottom: 12px;
|
|
1139
|
+
background: var(--chip-bg);
|
|
1140
|
+
border: 1px solid var(--border-4);
|
|
1141
|
+
color: var(--fg-4);
|
|
1142
|
+
font-size: 11px;
|
|
1143
|
+
letter-spacing: 0.04em;
|
|
1144
|
+
padding: 4px 10px;
|
|
1145
|
+
border-radius: 999px;
|
|
1146
|
+
cursor: pointer;
|
|
1147
|
+
z-index: 1000;
|
|
1148
|
+
backdrop-filter: blur(6px);
|
|
1149
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1150
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
1151
|
+
}
|
|
1152
|
+
.dev-view-toggle:hover { color: var(--fg); border-color: var(--border-hover); background: var(--chip-bg-hover); }
|
|
1153
|
+
.dev-view-toggle.is-harness { color: var(--fg); border-color: var(--fg); }
|
|
1154
|
+
.harness-debug-view {
|
|
1155
|
+
padding: 16px 20px;
|
|
1156
|
+
font-family: var(--mono, ui-monospace, SFMono-Regular, Consolas, monospace);
|
|
1157
|
+
font-size: 11px;
|
|
1158
|
+
color: var(--fg);
|
|
1159
|
+
white-space: pre-wrap;
|
|
1160
|
+
word-break: break-word;
|
|
1161
|
+
}
|
|
1162
|
+
.harness-debug-view .hd-msg { margin-bottom: 16px; padding: 8px 10px; border-left: 3px solid var(--fg-5); background: var(--bg-2, rgba(255,255,255,0.02)); }
|
|
1163
|
+
.harness-debug-view .hd-msg.role-user { border-left-color: #6ea8fe; }
|
|
1164
|
+
.harness-debug-view .hd-msg.role-assistant { border-left-color: #22c55e; }
|
|
1165
|
+
.harness-debug-view .hd-msg.role-tool { border-left-color: #f59e0b; }
|
|
1166
|
+
.harness-debug-view .hd-msg.role-system { border-left-color: #9ca3af; }
|
|
1167
|
+
.harness-debug-view .hd-role { font-weight: 600; opacity: 0.7; margin-bottom: 4px; text-transform: uppercase; font-size: 10px; letter-spacing: 0.06em; }
|
|
1168
|
+
.harness-debug-view .hd-meta { opacity: 0.5; font-size: 10px; margin-bottom: 6px; }
|
|
683
1169
|
|
|
684
1170
|
/* Messages */
|
|
685
1171
|
.messages { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 24px 24px; }
|
|
@@ -2354,6 +2840,11 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
2354
2840
|
conversations: [],
|
|
2355
2841
|
activeConversationId: null,
|
|
2356
2842
|
activeMessages: [],
|
|
2843
|
+
// Verbose dev (-v) only: mirror of conversation._harnessMessages plus
|
|
2844
|
+
// the current view mode the user toggled to.
|
|
2845
|
+
verboseDev: false,
|
|
2846
|
+
viewMode: "user", // "user" | "harness"
|
|
2847
|
+
harnessMessages: null,
|
|
2357
2848
|
isStreaming: false,
|
|
2358
2849
|
activeStreamAbortController: null,
|
|
2359
2850
|
activeStreamConversationId: null,
|
|
@@ -2387,6 +2878,14 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
2387
2878
|
abortController: null,
|
|
2388
2879
|
pendingFiles: [],
|
|
2389
2880
|
},
|
|
2881
|
+
sidebarMode: "conversations",
|
|
2882
|
+
expandedDirs: new Set(["/"]),
|
|
2883
|
+
dirCache: new Map(),
|
|
2884
|
+
activeFilePath: null,
|
|
2885
|
+
pendingUploads: 0,
|
|
2886
|
+
fileExplorerError: null,
|
|
2887
|
+
fileExplorerUsage: null,
|
|
2888
|
+
confirmDeletePath: null,
|
|
2390
2889
|
};
|
|
2391
2890
|
|
|
2392
2891
|
const agentInitial = document.body.dataset.agentInitial || "A";
|
|
@@ -2402,6 +2901,7 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
2402
2901
|
topbarNewChat: $("topbar-new-chat"),
|
|
2403
2902
|
messages: $("messages"),
|
|
2404
2903
|
chatTitle: $("chat-title"),
|
|
2904
|
+
viewToggle: $("view-toggle"),
|
|
2405
2905
|
logout: $("logout"),
|
|
2406
2906
|
composer: $("composer"),
|
|
2407
2907
|
prompt: $("prompt"),
|
|
@@ -2435,6 +2935,7 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
2435
2935
|
threadAttachmentPreview: $("thread-attachment-preview"),
|
|
2436
2936
|
threadPrompt: $("thread-prompt"),
|
|
2437
2937
|
threadSend: $("thread-send"),
|
|
2938
|
+
fileExplorer: $("file-explorer"),
|
|
2438
2939
|
};
|
|
2439
2940
|
const sendIconMarkup =
|
|
2440
2941
|
'<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>';
|
|
@@ -2486,6 +2987,25 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
2486
2987
|
return match ? decodeURIComponent(match[1]) : null;
|
|
2487
2988
|
};
|
|
2488
2989
|
|
|
2990
|
+
const pushFileUrl = (filePath) => {
|
|
2991
|
+
const target = filePath ? "/f/" + encodeURIComponent(filePath) : "/";
|
|
2992
|
+
if (window.location.pathname !== target) {
|
|
2993
|
+
history.pushState({ filePath: filePath || null }, "", target);
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2996
|
+
|
|
2997
|
+
const replaceFileUrl = (filePath) => {
|
|
2998
|
+
const target = filePath ? "/f/" + encodeURIComponent(filePath) : "/";
|
|
2999
|
+
if (window.location.pathname !== target) {
|
|
3000
|
+
history.replaceState({ filePath: filePath || null }, "", target);
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
|
|
3004
|
+
const getFilePathFromUrl = () => {
|
|
3005
|
+
const match = window.location.pathname.match(/^\\/f\\/(.+)/);
|
|
3006
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
3007
|
+
};
|
|
3008
|
+
|
|
2489
3009
|
const mutatingMethods = new Set(["POST", "PATCH", "PUT", "DELETE"]);
|
|
2490
3010
|
|
|
2491
3011
|
const api = async (path, options = {}) => {
|
|
@@ -3154,6 +3674,8 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
3154
3674
|
}
|
|
3155
3675
|
var switchingFamily = state.subagentsParentId !== c.conversationId;
|
|
3156
3676
|
state.activeConversationId = c.conversationId;
|
|
3677
|
+
state.activeFilePath = null;
|
|
3678
|
+
if (elements.composer) elements.composer.classList.remove("hidden");
|
|
3157
3679
|
state.viewingSubagentId = null;
|
|
3158
3680
|
state.parentConversationId = null;
|
|
3159
3681
|
if (switchingFamily) {
|
|
@@ -4060,7 +4582,66 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
4060
4582
|
}
|
|
4061
4583
|
};
|
|
4062
4584
|
|
|
4585
|
+
const renderHarnessView = () => {
|
|
4586
|
+
const msgs = Array.isArray(state.harnessMessages) ? state.harnessMessages : [];
|
|
4587
|
+
if (msgs.length === 0) {
|
|
4588
|
+
elements.messages.innerHTML = '<div class="harness-debug-view"><em>No harness messages yet \u2014 they appear after the first assistant turn.</em></div>';
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
4591
|
+
const rows = msgs.map((m, i) => {
|
|
4592
|
+
const role = String(m.role || "?");
|
|
4593
|
+
const meta = m.metadata && typeof m.metadata === "object" ? m.metadata : null;
|
|
4594
|
+
const metaLine = meta
|
|
4595
|
+
? "step=" + (meta.step != null ? meta.step : "-") +
|
|
4596
|
+
" runId=" + (meta.runId ? String(meta.runId).slice(0, 16) : "-") +
|
|
4597
|
+
" id=" + (meta.id ? String(meta.id).slice(0, 12) : "-")
|
|
4598
|
+
: "";
|
|
4599
|
+
let content = m.content;
|
|
4600
|
+
if (typeof content !== "string") content = JSON.stringify(content, null, 2);
|
|
4601
|
+
// Pretty-print JSON content where possible (assistant tool calls,
|
|
4602
|
+
// tool result arrays, etc.) so it's actually readable.
|
|
4603
|
+
const trimmed = content.trim();
|
|
4604
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
4605
|
+
try { content = JSON.stringify(JSON.parse(trimmed), null, 2); } catch (_e) { /* leave as-is */ }
|
|
4606
|
+
}
|
|
4607
|
+
return '<div class="hd-msg role-' + escapeHtml(role) + '">' +
|
|
4608
|
+
'<div class="hd-role">#' + i + ' \xB7 ' + escapeHtml(role) + '</div>' +
|
|
4609
|
+
(metaLine ? '<div class="hd-meta">' + escapeHtml(metaLine) + '</div>' : '') +
|
|
4610
|
+
'<div>' + escapeHtml(content) + '</div>' +
|
|
4611
|
+
'</div>';
|
|
4612
|
+
}).join("");
|
|
4613
|
+
elements.messages.innerHTML = '<div class="harness-debug-view">' + rows + '</div>';
|
|
4614
|
+
};
|
|
4615
|
+
|
|
4616
|
+
const updateViewToggleVisibility = () => {
|
|
4617
|
+
if (!elements.viewToggle) return;
|
|
4618
|
+
if (!state.verboseDev) {
|
|
4619
|
+
elements.viewToggle.hidden = true;
|
|
4620
|
+
return;
|
|
4621
|
+
}
|
|
4622
|
+
elements.viewToggle.hidden = false;
|
|
4623
|
+
elements.viewToggle.textContent = state.viewMode === "harness" ? "harness view" : "user view";
|
|
4624
|
+
elements.viewToggle.classList.toggle("is-harness", state.viewMode === "harness");
|
|
4625
|
+
};
|
|
4626
|
+
|
|
4627
|
+
if (elements.viewToggle) {
|
|
4628
|
+
elements.viewToggle.addEventListener("click", () => {
|
|
4629
|
+
state.viewMode = state.viewMode === "harness" ? "user" : "harness";
|
|
4630
|
+
updateViewToggleVisibility();
|
|
4631
|
+
if (state.viewMode === "harness") {
|
|
4632
|
+
renderHarnessView();
|
|
4633
|
+
} else {
|
|
4634
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
4635
|
+
}
|
|
4636
|
+
});
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4063
4639
|
const renderMessages = (messages, isStreaming = false, options = {}) => {
|
|
4640
|
+
// In harness debug view, the user-facing renderer is bypassed.
|
|
4641
|
+
if (state.viewMode === "harness") {
|
|
4642
|
+
renderHarnessView();
|
|
4643
|
+
return;
|
|
4644
|
+
}
|
|
4064
4645
|
const previousScrollTop = elements.messages.scrollTop;
|
|
4065
4646
|
const shouldStickToBottom =
|
|
4066
4647
|
options.forceScrollBottom === true || state.isMessagesPinnedToBottom;
|
|
@@ -4324,6 +4905,13 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
4324
4905
|
.catch(() => ({ threads: [] }));
|
|
4325
4906
|
const payload = await conversationPromise;
|
|
4326
4907
|
elements.chatTitle.textContent = payload.conversation.title;
|
|
4908
|
+
// Verbose dev (-v) only \u2014 server includes verboseDev: true and the
|
|
4909
|
+
// raw harness-message stream so we can offer a debug toggle.
|
|
4910
|
+
state.verboseDev = payload.verboseDev === true;
|
|
4911
|
+
state.harnessMessages = state.verboseDev && Array.isArray(payload.conversation._harnessMessages)
|
|
4912
|
+
? payload.conversation._harnessMessages
|
|
4913
|
+
: null;
|
|
4914
|
+
updateViewToggleVisibility();
|
|
4327
4915
|
// Merge own pending approvals + subagent pending approvals
|
|
4328
4916
|
var allPendingApprovals = [].concat(
|
|
4329
4917
|
payload.conversation.pendingApprovals || payload.pendingApprovals || [],
|
|
@@ -4622,6 +5210,12 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
4622
5210
|
if (typeof payload.conversation.contextWindow === "number" && payload.conversation.contextWindow > 0) {
|
|
4623
5211
|
state.contextWindow = payload.conversation.contextWindow;
|
|
4624
5212
|
}
|
|
5213
|
+
// Keep harness debug view fresh on refetches in -v mode.
|
|
5214
|
+
state.verboseDev = payload.verboseDev === true;
|
|
5215
|
+
state.harnessMessages = state.verboseDev && Array.isArray(payload.conversation._harnessMessages)
|
|
5216
|
+
? payload.conversation._harnessMessages
|
|
5217
|
+
: null;
|
|
5218
|
+
updateViewToggleVisibility();
|
|
4625
5219
|
updateContextRing();
|
|
4626
5220
|
renderMessages(state.activeMessages, streaming);
|
|
4627
5221
|
return payload;
|
|
@@ -6286,6 +6880,8 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
6286
6880
|
const startNewChat = () => {
|
|
6287
6881
|
if (window._resetBrowserPanel) window._resetBrowserPanel();
|
|
6288
6882
|
state.activeConversationId = null;
|
|
6883
|
+
state.activeFilePath = null;
|
|
6884
|
+
if (elements.composer) elements.composer.classList.remove("hidden");
|
|
6289
6885
|
state.activeMessages = [];
|
|
6290
6886
|
state.confirmDeleteId = null;
|
|
6291
6887
|
state.contextTokens = 0;
|
|
@@ -6624,13 +7220,17 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
6624
7220
|
});
|
|
6625
7221
|
|
|
6626
7222
|
let dragCounter = 0;
|
|
7223
|
+
const _inFileExplorer = (target) =>
|
|
7224
|
+
elements.fileExplorer && target instanceof Node && elements.fileExplorer.contains(target);
|
|
6627
7225
|
document.addEventListener("dragenter", (e) => {
|
|
6628
7226
|
e.preventDefault();
|
|
7227
|
+
if (_inFileExplorer(e.target)) return;
|
|
6629
7228
|
dragCounter++;
|
|
6630
7229
|
if (dragCounter === 1) elements.dragOverlay.classList.add("active");
|
|
6631
7230
|
});
|
|
6632
7231
|
document.addEventListener("dragleave", (e) => {
|
|
6633
7232
|
e.preventDefault();
|
|
7233
|
+
if (_inFileExplorer(e.target)) return;
|
|
6634
7234
|
dragCounter--;
|
|
6635
7235
|
if (dragCounter <= 0) { dragCounter = 0; elements.dragOverlay.classList.remove("active"); }
|
|
6636
7236
|
});
|
|
@@ -6639,6 +7239,7 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
6639
7239
|
e.preventDefault();
|
|
6640
7240
|
dragCounter = 0;
|
|
6641
7241
|
elements.dragOverlay.classList.remove("active");
|
|
7242
|
+
if (_inFileExplorer(e.target)) return;
|
|
6642
7243
|
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
|
|
6643
7244
|
addFiles(e.dataTransfer.files);
|
|
6644
7245
|
}
|
|
@@ -6876,59 +7477,946 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
6876
7477
|
replaceConversationUrl(subId);
|
|
6877
7478
|
loadConversation(subId);
|
|
6878
7479
|
}
|
|
6879
|
-
});
|
|
7480
|
+
});
|
|
7481
|
+
|
|
7482
|
+
elements.messages.addEventListener("scroll", () => {
|
|
7483
|
+
state.isMessagesPinnedToBottom = isNearBottom(elements.messages);
|
|
7484
|
+
}, { passive: true });
|
|
7485
|
+
|
|
7486
|
+
document.addEventListener("click", (event) => {
|
|
7487
|
+
if (!(event.target instanceof Node)) {
|
|
7488
|
+
return;
|
|
7489
|
+
}
|
|
7490
|
+
if (!event.target.closest(".conversation-item") && state.confirmDeleteId) {
|
|
7491
|
+
state.confirmDeleteId = null;
|
|
7492
|
+
renderConversationList();
|
|
7493
|
+
}
|
|
7494
|
+
if (!event.target.closest(".thread-row") && state.confirmDeleteThreadId) {
|
|
7495
|
+
state.confirmDeleteThreadId = null;
|
|
7496
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
7497
|
+
}
|
|
7498
|
+
});
|
|
7499
|
+
|
|
7500
|
+
window.addEventListener("resize", () => {
|
|
7501
|
+
setSidebarOpen(false);
|
|
7502
|
+
});
|
|
7503
|
+
|
|
7504
|
+
const navigateToConversation = async (conversationId) => {
|
|
7505
|
+
state.activeFilePath = null;
|
|
7506
|
+
if (elements.composer) elements.composer.classList.remove("hidden");
|
|
7507
|
+
if (conversationId) {
|
|
7508
|
+
state.activeConversationId = conversationId;
|
|
7509
|
+
renderConversationList();
|
|
7510
|
+
try {
|
|
7511
|
+
await loadConversation(conversationId);
|
|
7512
|
+
} catch {
|
|
7513
|
+
// Conversation not found \u2013 fall back to empty state
|
|
7514
|
+
state.activeConversationId = null;
|
|
7515
|
+
state.activeMessages = [];
|
|
7516
|
+
replaceConversationUrl(null);
|
|
7517
|
+
elements.chatTitle.textContent = "";
|
|
7518
|
+
renderMessages([]);
|
|
7519
|
+
renderConversationList();
|
|
7520
|
+
}
|
|
7521
|
+
} else {
|
|
7522
|
+
state.activeConversationId = null;
|
|
7523
|
+
state.activeMessages = [];
|
|
7524
|
+
state.contextTokens = 0;
|
|
7525
|
+
state.contextWindow = 0;
|
|
7526
|
+
updateContextRing();
|
|
7527
|
+
elements.chatTitle.textContent = "";
|
|
7528
|
+
renderMessages([]);
|
|
7529
|
+
renderConversationList();
|
|
7530
|
+
}
|
|
7531
|
+
};
|
|
7532
|
+
|
|
7533
|
+
// ----- File explorer (sidebar Files mode) -----
|
|
7534
|
+
|
|
7535
|
+
const formatBytes = (n) => {
|
|
7536
|
+
if (typeof n !== "number" || !isFinite(n)) return "";
|
|
7537
|
+
if (n < 1024) return n + " B";
|
|
7538
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
7539
|
+
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + " MB";
|
|
7540
|
+
return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
|
7541
|
+
};
|
|
7542
|
+
|
|
7543
|
+
const joinPath = (parent, name) => {
|
|
7544
|
+
if (parent === "/") return "/" + name;
|
|
7545
|
+
return parent + "/" + name;
|
|
7546
|
+
};
|
|
7547
|
+
|
|
7548
|
+
const parentPath = (p) => {
|
|
7549
|
+
if (!p || p === "/") return "/";
|
|
7550
|
+
const idx = p.lastIndexOf("/");
|
|
7551
|
+
if (idx <= 0) return "/";
|
|
7552
|
+
return p.slice(0, idx);
|
|
7553
|
+
};
|
|
7554
|
+
|
|
7555
|
+
const TEXT_LIKE_MIME_PREFIXES = ["text/"];
|
|
7556
|
+
const TEXT_LIKE_MIME_EXACT = new Set([
|
|
7557
|
+
"application/json",
|
|
7558
|
+
"application/javascript",
|
|
7559
|
+
"application/xml",
|
|
7560
|
+
"application/x-sh",
|
|
7561
|
+
"application/x-yaml",
|
|
7562
|
+
"application/yaml",
|
|
7563
|
+
"application/toml",
|
|
7564
|
+
"application/x-www-form-urlencoded",
|
|
7565
|
+
]);
|
|
7566
|
+
const TEXT_LIKE_EXTENSIONS = new Set([
|
|
7567
|
+
"md","txt","log","csv","tsv","js","mjs","cjs","jsx","ts","tsx","json","yaml","yml","toml",
|
|
7568
|
+
"xml","html","htm","css","scss","sass","less","sh","bash","zsh","py","rb","go","rs","java",
|
|
7569
|
+
"kt","swift","c","cpp","h","hpp","sql","env","ini","conf","cfg","gitignore","editorconfig",
|
|
7570
|
+
]);
|
|
7571
|
+
|
|
7572
|
+
const categorizePreview = (mime, name) => {
|
|
7573
|
+
const m = (mime || "").toLowerCase();
|
|
7574
|
+
const ext = (name.split(".").pop() || "").toLowerCase();
|
|
7575
|
+
if (m === "text/html" || ext === "html" || ext === "htm") return "html";
|
|
7576
|
+
if (m.startsWith("image/")) return "image";
|
|
7577
|
+
if (m === "application/pdf") return "pdf";
|
|
7578
|
+
if (m.startsWith("audio/")) return "audio";
|
|
7579
|
+
if (m.startsWith("video/")) return "video";
|
|
7580
|
+
for (const p of TEXT_LIKE_MIME_PREFIXES) if (m.startsWith(p)) return "text";
|
|
7581
|
+
if (TEXT_LIKE_MIME_EXACT.has(m)) return "text";
|
|
7582
|
+
if (TEXT_LIKE_EXTENSIONS.has(ext)) return "text";
|
|
7583
|
+
if (!m && (ext === "" || /^[a-z0-9]+$/i.test(ext))) return "text-maybe";
|
|
7584
|
+
return "binary";
|
|
7585
|
+
};
|
|
7586
|
+
|
|
7587
|
+
const TEXT_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
|
|
7588
|
+
|
|
7589
|
+
const folderIconSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1.5 4.5a1 1 0 0 1 1-1h3l1.5 1.5h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-10.5a1 1 0 0 1-1-1v-7.5z"/></svg>';
|
|
7590
|
+
const fileIconSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 1.5H3.5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V6L9 1.5z"/><path d="M9 1.5V6h4.5"/></svg>';
|
|
7591
|
+
const downloadIconSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M5 7l3 3 3-3M2.5 12.5v.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-.5"/></svg>';
|
|
7592
|
+
const refreshIconSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8a6 6 0 0 1 10.5-4M14 8a6 6 0 0 1-10.5 4"/><path d="M12.5 1.5v3h-3M3.5 14.5v-3h3"/></svg>';
|
|
7593
|
+
const closeIconSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
|
7594
|
+
const caretIconSvg = '<svg viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M4.5 2.75L8 6L4.5 9.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
|
|
7595
|
+
const uploadIconSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 11V3M5 6l3-3 3 3M2.5 12.5v.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-.5"/></svg>';
|
|
7596
|
+
const fetchDirEntries = async (dirPath) => {
|
|
7597
|
+
const qs = "?path=" + encodeURIComponent(dirPath);
|
|
7598
|
+
const data = await api("/api/vfs-list" + qs);
|
|
7599
|
+
state.dirCache.set(dirPath, data.entries || []);
|
|
7600
|
+
if (data.usage) state.fileExplorerUsage = data.usage;
|
|
7601
|
+
return data.entries || [];
|
|
7602
|
+
};
|
|
7603
|
+
|
|
7604
|
+
const ensureDirLoaded = async (dirPath) => {
|
|
7605
|
+
if (state.dirCache.has(dirPath)) return;
|
|
7606
|
+
try {
|
|
7607
|
+
await fetchDirEntries(dirPath);
|
|
7608
|
+
state.fileExplorerError = null;
|
|
7609
|
+
} catch (err) {
|
|
7610
|
+
state.fileExplorerError = err.message || "Failed to load directory";
|
|
7611
|
+
}
|
|
7612
|
+
};
|
|
7613
|
+
|
|
7614
|
+
const renderFileExplorer = () => {
|
|
7615
|
+
const root = elements.fileExplorer;
|
|
7616
|
+
if (!root) return;
|
|
7617
|
+
root.innerHTML = "";
|
|
7618
|
+
|
|
7619
|
+
// Pending uploads spinner row
|
|
7620
|
+
if (state.pendingUploads > 0) {
|
|
7621
|
+
const row = document.createElement("div");
|
|
7622
|
+
row.className = "file-upload-row";
|
|
7623
|
+
row.innerHTML = '<span class="file-upload-spinner"></span><span>Uploading ' + state.pendingUploads + (state.pendingUploads === 1 ? ' file\u2026' : ' files\u2026') + '</span>';
|
|
7624
|
+
root.appendChild(row);
|
|
7625
|
+
}
|
|
7626
|
+
|
|
7627
|
+
// Error row
|
|
7628
|
+
if (state.fileExplorerError) {
|
|
7629
|
+
const err = document.createElement("div");
|
|
7630
|
+
err.className = "file-explorer-error";
|
|
7631
|
+
err.textContent = state.fileExplorerError;
|
|
7632
|
+
const retry = document.createElement("button");
|
|
7633
|
+
retry.textContent = "Retry";
|
|
7634
|
+
retry.onclick = async () => {
|
|
7635
|
+
state.fileExplorerError = null;
|
|
7636
|
+
state.dirCache.clear();
|
|
7637
|
+
for (const dir of Array.from(state.expandedDirs)) {
|
|
7638
|
+
try { await fetchDirEntries(dir); } catch {}
|
|
7639
|
+
}
|
|
7640
|
+
renderFileExplorer();
|
|
7641
|
+
};
|
|
7642
|
+
err.appendChild(document.createElement("br"));
|
|
7643
|
+
err.appendChild(retry);
|
|
7644
|
+
root.appendChild(err);
|
|
7645
|
+
}
|
|
7646
|
+
|
|
7647
|
+
// Tree
|
|
7648
|
+
const tree = document.createElement("div");
|
|
7649
|
+
tree.className = "file-children";
|
|
7650
|
+
tree.dataset.dir = "/";
|
|
7651
|
+
renderDirInto(tree, "/", 0);
|
|
7652
|
+
root.appendChild(tree);
|
|
7653
|
+
|
|
7654
|
+
// Footer (status bar with usage + upload)
|
|
7655
|
+
const footer = document.createElement("div");
|
|
7656
|
+
footer.className = "file-explorer-footer";
|
|
7657
|
+
const usageEl = document.createElement("div");
|
|
7658
|
+
usageEl.className = "file-explorer-usage";
|
|
7659
|
+
if (state.fileExplorerUsage) {
|
|
7660
|
+
const u = state.fileExplorerUsage;
|
|
7661
|
+
const countEl = document.createElement("span");
|
|
7662
|
+
countEl.textContent = u.fileCount + " file" + (u.fileCount === 1 ? "" : "s");
|
|
7663
|
+
const sizeEl = document.createElement("span");
|
|
7664
|
+
sizeEl.textContent = formatBytes(u.totalBytes);
|
|
7665
|
+
usageEl.append(countEl, sizeEl);
|
|
7666
|
+
}
|
|
7667
|
+
const refreshBtn = document.createElement("button");
|
|
7668
|
+
refreshBtn.className = "file-explorer-icon-btn";
|
|
7669
|
+
refreshBtn.title = "Refresh";
|
|
7670
|
+
refreshBtn.innerHTML = refreshIconSvg;
|
|
7671
|
+
refreshBtn.onclick = async () => {
|
|
7672
|
+
state.dirCache.clear();
|
|
7673
|
+
for (const dir of Array.from(state.expandedDirs)) {
|
|
7674
|
+
try { await fetchDirEntries(dir); } catch {}
|
|
7675
|
+
}
|
|
7676
|
+
renderFileExplorer();
|
|
7677
|
+
};
|
|
7678
|
+
const uploadBtn = document.createElement("button");
|
|
7679
|
+
uploadBtn.className = "file-explorer-upload";
|
|
7680
|
+
uploadBtn.title = "Upload files";
|
|
7681
|
+
uploadBtn.innerHTML = uploadIconSvg + '<span>Upload</span>';
|
|
7682
|
+
uploadBtn.onclick = () => triggerUploadPicker("/");
|
|
7683
|
+
footer.append(usageEl, refreshBtn, uploadBtn);
|
|
7684
|
+
root.appendChild(footer);
|
|
7685
|
+
};
|
|
7686
|
+
|
|
7687
|
+
const renderDirInto = (container, dirPath, depth) => {
|
|
7688
|
+
const entries = state.dirCache.get(dirPath);
|
|
7689
|
+
if (!entries) {
|
|
7690
|
+
// Lazy fetch for newly expanded dir
|
|
7691
|
+
ensureDirLoaded(dirPath).then(() => renderFileExplorer());
|
|
7692
|
+
if (dirPath !== "/") {
|
|
7693
|
+
const loading = document.createElement("div");
|
|
7694
|
+
loading.className = "file-explorer-empty";
|
|
7695
|
+
loading.style.paddingLeft = (16 + depth * 14) + "px";
|
|
7696
|
+
loading.textContent = "Loading\u2026";
|
|
7697
|
+
container.appendChild(loading);
|
|
7698
|
+
}
|
|
7699
|
+
return;
|
|
7700
|
+
}
|
|
7701
|
+
if (entries.length === 0 && dirPath === "/") {
|
|
7702
|
+
const empty = document.createElement("div");
|
|
7703
|
+
empty.className = "file-explorer-empty";
|
|
7704
|
+
empty.innerHTML = "No files yet. Tools like " + _TK + "write_file" + _TK + " and " + _TK + "bash" + _TK + " will populate this.";
|
|
7705
|
+
container.appendChild(empty);
|
|
7706
|
+
return;
|
|
7707
|
+
}
|
|
7708
|
+
for (const entry of entries) {
|
|
7709
|
+
const childPath = joinPath(dirPath, entry.name);
|
|
7710
|
+
const row = document.createElement("div");
|
|
7711
|
+
row.className = "file-row" + (entry.type === "directory" ? " is-dir" : "") + (state.activeFilePath === childPath ? " active" : "");
|
|
7712
|
+
row.style.paddingLeft = (8 + depth * 14) + "px";
|
|
7713
|
+
row.dataset.path = childPath;
|
|
7714
|
+
row.dataset.type = entry.type;
|
|
7715
|
+
|
|
7716
|
+
const caret = document.createElement("span");
|
|
7717
|
+
caret.className = "file-caret" + (entry.type === "directory" ? "" : " empty") + (state.expandedDirs.has(childPath) ? " open" : "");
|
|
7718
|
+
caret.innerHTML = caretIconSvg;
|
|
7719
|
+
row.appendChild(caret);
|
|
7720
|
+
|
|
7721
|
+
const icon = document.createElement("span");
|
|
7722
|
+
icon.className = "file-icon";
|
|
7723
|
+
icon.innerHTML = entry.type === "directory" ? folderIconSvg : fileIconSvg;
|
|
7724
|
+
row.appendChild(icon);
|
|
7725
|
+
|
|
7726
|
+
const name = document.createElement("span");
|
|
7727
|
+
name.className = "file-name";
|
|
7728
|
+
name.textContent = entry.name;
|
|
7729
|
+
row.appendChild(name);
|
|
7730
|
+
|
|
7731
|
+
if (entry.type === "directory") {
|
|
7732
|
+
row.onclick = (e) => {
|
|
7733
|
+
e.stopPropagation();
|
|
7734
|
+
if (state.confirmDeletePath) { state.confirmDeletePath = null; renderFileExplorer(); return; }
|
|
7735
|
+
if (state.expandedDirs.has(childPath)) {
|
|
7736
|
+
state.expandedDirs.delete(childPath);
|
|
7737
|
+
} else {
|
|
7738
|
+
state.expandedDirs.add(childPath);
|
|
7739
|
+
}
|
|
7740
|
+
renderFileExplorer();
|
|
7741
|
+
};
|
|
7742
|
+
} else {
|
|
7743
|
+
row.onclick = (e) => {
|
|
7744
|
+
e.stopPropagation();
|
|
7745
|
+
if (state.confirmDeletePath) { state.confirmDeletePath = null; renderFileExplorer(); return; }
|
|
7746
|
+
selectFile(childPath);
|
|
7747
|
+
};
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7750
|
+
const isConfirming = state.confirmDeletePath === childPath;
|
|
7751
|
+
const actions = document.createElement("div");
|
|
7752
|
+
actions.className = "file-row-actions" + (isConfirming ? " confirming" : "");
|
|
7753
|
+
|
|
7754
|
+
const dl = document.createElement("a");
|
|
7755
|
+
dl.className = "file-row-action";
|
|
7756
|
+
if (entry.type === "directory") {
|
|
7757
|
+
dl.href = "/api/vfs-archive?path=" + encodeURIComponent(childPath);
|
|
7758
|
+
dl.title = "Download as zip";
|
|
7759
|
+
dl.setAttribute("download", entry.name + ".zip");
|
|
7760
|
+
} else {
|
|
7761
|
+
dl.href = "/api/vfs/" + encodeURI(childPath.replace(/^\\//, ""));
|
|
7762
|
+
dl.title = "Download";
|
|
7763
|
+
dl.setAttribute("download", entry.name);
|
|
7764
|
+
}
|
|
7765
|
+
dl.innerHTML = downloadIconSvg;
|
|
7766
|
+
dl.onclick = (e) => { e.stopPropagation(); };
|
|
7767
|
+
actions.appendChild(dl);
|
|
7768
|
+
|
|
7769
|
+
const del = document.createElement("button");
|
|
7770
|
+
del.className = "file-row-action file-delete" + (isConfirming ? " confirming" : "");
|
|
7771
|
+
if (isConfirming) del.textContent = "sure?";
|
|
7772
|
+
else del.innerHTML = closeIconSvg;
|
|
7773
|
+
del.title = "Delete";
|
|
7774
|
+
del.onclick = async (e) => {
|
|
7775
|
+
e.stopPropagation();
|
|
7776
|
+
if (!isConfirming) {
|
|
7777
|
+
state.confirmDeletePath = childPath;
|
|
7778
|
+
renderFileExplorer();
|
|
7779
|
+
return;
|
|
7780
|
+
}
|
|
7781
|
+
state.confirmDeletePath = null;
|
|
7782
|
+
await deleteEntry(childPath, entry.type, dirPath);
|
|
7783
|
+
};
|
|
7784
|
+
actions.appendChild(del);
|
|
7785
|
+
row.appendChild(actions);
|
|
7786
|
+
|
|
7787
|
+
container.appendChild(row);
|
|
7788
|
+
|
|
7789
|
+
if (entry.type === "directory" && state.expandedDirs.has(childPath)) {
|
|
7790
|
+
const childWrap = document.createElement("div");
|
|
7791
|
+
childWrap.className = "file-children";
|
|
7792
|
+
childWrap.dataset.dir = childPath;
|
|
7793
|
+
renderDirInto(childWrap, childPath, depth + 1);
|
|
7794
|
+
container.appendChild(childWrap);
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
};
|
|
7798
|
+
|
|
7799
|
+
const selectFile = async (filePath) => {
|
|
7800
|
+
state.activeFilePath = filePath;
|
|
7801
|
+
state.activeConversationId = null;
|
|
7802
|
+
pushFileUrl(filePath);
|
|
7803
|
+
updateComposerVisibility();
|
|
7804
|
+
renderFileExplorer();
|
|
7805
|
+
await renderFilePreview(filePath);
|
|
7806
|
+
};
|
|
7807
|
+
|
|
7808
|
+
const renderFilePreview = async (filePath) => {
|
|
7809
|
+
const messages = elements.messages;
|
|
7810
|
+
if (!messages) return;
|
|
7811
|
+
const filename = filePath.split("/").pop() || filePath;
|
|
7812
|
+
elements.chatTitle.textContent = filename;
|
|
7813
|
+
messages.innerHTML = '<div class="file-preview"><div class="file-explorer-empty">Loading\u2026</div></div>';
|
|
7814
|
+
let response;
|
|
7815
|
+
try {
|
|
7816
|
+
response = await fetch("/api/vfs/" + encodeURI(filePath.replace(/^\\//, "")), {
|
|
7817
|
+
credentials: state.tenantToken ? "omit" : "include",
|
|
7818
|
+
headers: buildAuthHeaders(),
|
|
7819
|
+
});
|
|
7820
|
+
} catch (err) {
|
|
7821
|
+
renderPreviewError(filePath, "Network error");
|
|
7822
|
+
return;
|
|
7823
|
+
}
|
|
7824
|
+
if (!response.ok) {
|
|
7825
|
+
renderPreviewError(filePath, "HTTP " + response.status);
|
|
7826
|
+
return;
|
|
7827
|
+
}
|
|
7828
|
+
const mime = (response.headers.get("content-type") || "").split(";")[0].trim();
|
|
7829
|
+
const sizeHeader = response.headers.get("content-length");
|
|
7830
|
+
const size = sizeHeader ? parseInt(sizeHeader, 10) : 0;
|
|
7831
|
+
const category = categorizePreview(mime, filename);
|
|
7832
|
+
|
|
7833
|
+
if (category === "text" || category === "text-maybe") {
|
|
7834
|
+
if (size && size > TEXT_PREVIEW_MAX_BYTES) {
|
|
7835
|
+
renderPreviewPlaceholder(filePath, mime, size, "Text file too large to preview inline.");
|
|
7836
|
+
return;
|
|
7837
|
+
}
|
|
7838
|
+
const buf = await response.arrayBuffer();
|
|
7839
|
+
if (buf.byteLength > TEXT_PREVIEW_MAX_BYTES) {
|
|
7840
|
+
renderPreviewPlaceholder(filePath, mime, buf.byteLength, "Text file too large to preview inline.");
|
|
7841
|
+
return;
|
|
7842
|
+
}
|
|
7843
|
+
let text;
|
|
7844
|
+
try {
|
|
7845
|
+
text = new TextDecoder("utf-8", { fatal: category === "text-maybe" }).decode(buf);
|
|
7846
|
+
} catch {
|
|
7847
|
+
renderPreviewPlaceholder(filePath, mime, buf.byteLength, "Not a text file.");
|
|
7848
|
+
return;
|
|
7849
|
+
}
|
|
7850
|
+
renderTextView(filePath, text, mime || "text/plain");
|
|
7851
|
+
return;
|
|
7852
|
+
}
|
|
7853
|
+
if (category === "image") {
|
|
7854
|
+
const blob = await response.blob();
|
|
7855
|
+
const url = URL.createObjectURL(blob);
|
|
7856
|
+
const wrap = document.createElement("div");
|
|
7857
|
+
wrap.className = "file-preview";
|
|
7858
|
+
const inner = document.createElement("div");
|
|
7859
|
+
inner.className = "file-preview-image";
|
|
7860
|
+
const img = document.createElement("img");
|
|
7861
|
+
img.src = url;
|
|
7862
|
+
img.alt = filename;
|
|
7863
|
+
img.onload = () => URL.revokeObjectURL(url);
|
|
7864
|
+
inner.appendChild(img);
|
|
7865
|
+
wrap.append(buildPreviewActions(filePath), inner);
|
|
7866
|
+
messages.innerHTML = "";
|
|
7867
|
+
messages.appendChild(wrap);
|
|
7868
|
+
return;
|
|
7869
|
+
}
|
|
7870
|
+
if (category === "pdf") {
|
|
7871
|
+
const blob = await response.blob();
|
|
7872
|
+
const url = URL.createObjectURL(blob);
|
|
7873
|
+
const wrap = document.createElement("div");
|
|
7874
|
+
wrap.className = "file-preview";
|
|
7875
|
+
const inner = document.createElement("div");
|
|
7876
|
+
inner.className = "file-preview-pdf";
|
|
7877
|
+
const iframe = document.createElement("iframe");
|
|
7878
|
+
iframe.src = url;
|
|
7879
|
+
iframe.title = filename;
|
|
7880
|
+
inner.appendChild(iframe);
|
|
7881
|
+
wrap.append(buildPreviewActions(filePath), inner);
|
|
7882
|
+
messages.innerHTML = "";
|
|
7883
|
+
messages.appendChild(wrap);
|
|
7884
|
+
return;
|
|
7885
|
+
}
|
|
7886
|
+
if (category === "html") {
|
|
7887
|
+
const blob = await response.blob();
|
|
7888
|
+
const url = URL.createObjectURL(blob);
|
|
7889
|
+
const wrap = document.createElement("div");
|
|
7890
|
+
wrap.className = "file-preview";
|
|
7891
|
+
const inner = document.createElement("div");
|
|
7892
|
+
inner.className = "file-preview-pdf";
|
|
7893
|
+
const iframe = document.createElement("iframe");
|
|
7894
|
+
iframe.src = url;
|
|
7895
|
+
iframe.title = filename;
|
|
7896
|
+
iframe.setAttribute("sandbox", "");
|
|
7897
|
+
inner.appendChild(iframe);
|
|
7898
|
+
wrap.append(buildPreviewActions(filePath), inner);
|
|
7899
|
+
messages.innerHTML = "";
|
|
7900
|
+
messages.appendChild(wrap);
|
|
7901
|
+
return;
|
|
7902
|
+
}
|
|
7903
|
+
if (category === "audio" || category === "video") {
|
|
7904
|
+
const blob = await response.blob();
|
|
7905
|
+
const url = URL.createObjectURL(blob);
|
|
7906
|
+
const wrap = document.createElement("div");
|
|
7907
|
+
wrap.className = "file-preview";
|
|
7908
|
+
const inner = document.createElement("div");
|
|
7909
|
+
inner.className = "file-preview-media";
|
|
7910
|
+
const media = document.createElement(category);
|
|
7911
|
+
media.src = url;
|
|
7912
|
+
media.controls = true;
|
|
7913
|
+
media.style.maxWidth = "100%";
|
|
7914
|
+
media.style.maxHeight = "100%";
|
|
7915
|
+
inner.appendChild(media);
|
|
7916
|
+
wrap.append(buildPreviewActions(filePath), inner);
|
|
7917
|
+
messages.innerHTML = "";
|
|
7918
|
+
messages.appendChild(wrap);
|
|
7919
|
+
return;
|
|
7920
|
+
}
|
|
7921
|
+
renderPreviewPlaceholder(filePath, mime, size, "This file type can't be previewed.");
|
|
7922
|
+
};
|
|
7923
|
+
|
|
7924
|
+
const buildDownloadLink = (filePath) => {
|
|
7925
|
+
const filename = filePath.split("/").pop() || "download";
|
|
7926
|
+
const a = document.createElement("a");
|
|
7927
|
+
a.className = "file-preview-action-btn";
|
|
7928
|
+
a.href = "/api/vfs/" + encodeURI(filePath.replace(/^\\//, ""));
|
|
7929
|
+
a.textContent = "Download";
|
|
7930
|
+
a.setAttribute("download", filename);
|
|
7931
|
+
return a;
|
|
7932
|
+
};
|
|
7933
|
+
|
|
7934
|
+
const buildPreviewActions = (filePath, leftExtras = [], rightExtras = []) => {
|
|
7935
|
+
const actions = document.createElement("div");
|
|
7936
|
+
actions.className = "file-preview-actions";
|
|
7937
|
+
const left = document.createElement("div");
|
|
7938
|
+
left.className = "file-preview-actions-group";
|
|
7939
|
+
left.appendChild(buildDownloadLink(filePath));
|
|
7940
|
+
left.appendChild(buildCopyLinkButton(filePath));
|
|
7941
|
+
for (const el of leftExtras) left.appendChild(el);
|
|
7942
|
+
const right = document.createElement("div");
|
|
7943
|
+
right.className = "file-preview-actions-group";
|
|
7944
|
+
for (const el of rightExtras) right.appendChild(el);
|
|
7945
|
+
actions.append(left, right);
|
|
7946
|
+
return actions;
|
|
7947
|
+
};
|
|
7948
|
+
|
|
7949
|
+
const csvDelimiter = (mime, name) => {
|
|
7950
|
+
const m = (mime || "").toLowerCase();
|
|
7951
|
+
const ext = (name.split(".").pop() || "").toLowerCase();
|
|
7952
|
+
if (m === "text/csv" || ext === "csv") return ",";
|
|
7953
|
+
if (m === "text/tab-separated-values" || ext === "tsv") return "\\t";
|
|
7954
|
+
return null;
|
|
7955
|
+
};
|
|
7956
|
+
|
|
7957
|
+
const parseDelimited = (text, delimiter) => {
|
|
7958
|
+
const rows = [];
|
|
7959
|
+
let row = [];
|
|
7960
|
+
let field = "";
|
|
7961
|
+
let inQuotes = false;
|
|
7962
|
+
for (let i = 0; i < text.length; i++) {
|
|
7963
|
+
const c = text[i];
|
|
7964
|
+
if (inQuotes) {
|
|
7965
|
+
if (c === '"') {
|
|
7966
|
+
if (text[i + 1] === '"') { field += '"'; i++; }
|
|
7967
|
+
else inQuotes = false;
|
|
7968
|
+
} else {
|
|
7969
|
+
field += c;
|
|
7970
|
+
}
|
|
7971
|
+
continue;
|
|
7972
|
+
}
|
|
7973
|
+
if (c === '"') { inQuotes = true; continue; }
|
|
7974
|
+
if (c === delimiter) { row.push(field); field = ""; continue; }
|
|
7975
|
+
if (c === "\\n" || c === "\\r") {
|
|
7976
|
+
if (c === "\\r" && text[i + 1] === "\\n") i++;
|
|
7977
|
+
row.push(field);
|
|
7978
|
+
field = "";
|
|
7979
|
+
rows.push(row);
|
|
7980
|
+
row = [];
|
|
7981
|
+
continue;
|
|
7982
|
+
}
|
|
7983
|
+
field += c;
|
|
7984
|
+
}
|
|
7985
|
+
if (field.length > 0 || row.length > 0) {
|
|
7986
|
+
row.push(field);
|
|
7987
|
+
rows.push(row);
|
|
7988
|
+
}
|
|
7989
|
+
return rows;
|
|
7990
|
+
};
|
|
7991
|
+
|
|
7992
|
+
const TABLE_PREVIEW_MAX_ROWS = 5000;
|
|
7993
|
+
|
|
7994
|
+
const isMarkdownFile = (mime, name) => {
|
|
7995
|
+
const m = (mime || "").toLowerCase();
|
|
7996
|
+
if (m.startsWith("text/markdown") || m === "text/x-markdown") return true;
|
|
7997
|
+
const ext = (name.split(".").pop() || "").toLowerCase();
|
|
7998
|
+
return ext === "md" || ext === "markdown" || ext === "mdx";
|
|
7999
|
+
};
|
|
8000
|
+
|
|
8001
|
+
const flashLabel = (btn, label, durationMs) => {
|
|
8002
|
+
const original = btn.dataset.label || btn.textContent;
|
|
8003
|
+
btn.dataset.label = original;
|
|
8004
|
+
btn.textContent = label;
|
|
8005
|
+
if (btn._flashTimer) clearTimeout(btn._flashTimer);
|
|
8006
|
+
btn._flashTimer = setTimeout(() => { btn.textContent = original; }, durationMs);
|
|
8007
|
+
};
|
|
8008
|
+
|
|
8009
|
+
const buildCopyTextButton = (text) => {
|
|
8010
|
+
const btn = document.createElement("button");
|
|
8011
|
+
btn.className = "file-preview-action-btn";
|
|
8012
|
+
btn.textContent = "Copy";
|
|
8013
|
+
btn.onclick = async () => {
|
|
8014
|
+
try {
|
|
8015
|
+
await navigator.clipboard.writeText(text);
|
|
8016
|
+
flashLabel(btn, "Copied", 1500);
|
|
8017
|
+
} catch (err) {
|
|
8018
|
+
window.alert("Failed to copy: " + (err.message || "clipboard unavailable"));
|
|
8019
|
+
}
|
|
8020
|
+
};
|
|
8021
|
+
return btn;
|
|
8022
|
+
};
|
|
8023
|
+
|
|
8024
|
+
const buildCopyLinkButton = (filePath) => {
|
|
8025
|
+
const btn = document.createElement("button");
|
|
8026
|
+
btn.className = "file-preview-action-btn";
|
|
8027
|
+
btn.textContent = "Copy link";
|
|
8028
|
+
btn.onclick = async () => {
|
|
8029
|
+
const url = window.location.origin + "/api/vfs/" + encodeURI(filePath.replace(/^\\//, ""));
|
|
8030
|
+
try {
|
|
8031
|
+
await navigator.clipboard.writeText(url);
|
|
8032
|
+
flashLabel(btn, "Copied", 1500);
|
|
8033
|
+
} catch (err) {
|
|
8034
|
+
window.alert("Failed to copy: " + (err.message || "clipboard unavailable"));
|
|
8035
|
+
}
|
|
8036
|
+
};
|
|
8037
|
+
return btn;
|
|
8038
|
+
};
|
|
8039
|
+
|
|
8040
|
+
const renderTextView = (filePath, text, mime) => {
|
|
8041
|
+
const messages = elements.messages;
|
|
8042
|
+
const filename = filePath.split("/").pop() || filePath;
|
|
8043
|
+
const wrap = document.createElement("div");
|
|
8044
|
+
wrap.className = "file-preview";
|
|
8045
|
+
const copyBtn = buildCopyTextButton(text);
|
|
8046
|
+
const editBtn = document.createElement("button");
|
|
8047
|
+
editBtn.className = "file-preview-action-btn";
|
|
8048
|
+
editBtn.textContent = "Edit";
|
|
8049
|
+
editBtn.onclick = () => renderTextEditor(filePath, text, mime);
|
|
8050
|
+
|
|
8051
|
+
let body;
|
|
8052
|
+
const delimiter = csvDelimiter(mime, filename);
|
|
8053
|
+
if (delimiter) {
|
|
8054
|
+
body = document.createElement("div");
|
|
8055
|
+
body.className = "file-preview-table-wrap";
|
|
8056
|
+
const rows = parseDelimited(text, delimiter);
|
|
8057
|
+
if (rows.length === 0) {
|
|
8058
|
+
const empty = document.createElement("div");
|
|
8059
|
+
empty.className = "file-explorer-empty";
|
|
8060
|
+
empty.textContent = "Empty file";
|
|
8061
|
+
body.appendChild(empty);
|
|
8062
|
+
} else {
|
|
8063
|
+
const truncated = rows.length > TABLE_PREVIEW_MAX_ROWS;
|
|
8064
|
+
const visibleRows = truncated ? rows.slice(0, TABLE_PREVIEW_MAX_ROWS) : rows;
|
|
8065
|
+
const table = document.createElement("table");
|
|
8066
|
+
table.className = "file-preview-table";
|
|
8067
|
+
const thead = document.createElement("thead");
|
|
8068
|
+
const headTr = document.createElement("tr");
|
|
8069
|
+
for (const cell of visibleRows[0]) {
|
|
8070
|
+
const th = document.createElement("th");
|
|
8071
|
+
th.textContent = cell;
|
|
8072
|
+
headTr.appendChild(th);
|
|
8073
|
+
}
|
|
8074
|
+
thead.appendChild(headTr);
|
|
8075
|
+
table.appendChild(thead);
|
|
8076
|
+
const tbody = document.createElement("tbody");
|
|
8077
|
+
for (let r = 1; r < visibleRows.length; r++) {
|
|
8078
|
+
const tr = document.createElement("tr");
|
|
8079
|
+
for (const cell of visibleRows[r]) {
|
|
8080
|
+
const td = document.createElement("td");
|
|
8081
|
+
td.textContent = cell;
|
|
8082
|
+
tr.appendChild(td);
|
|
8083
|
+
}
|
|
8084
|
+
tbody.appendChild(tr);
|
|
8085
|
+
}
|
|
8086
|
+
table.appendChild(tbody);
|
|
8087
|
+
body.appendChild(table);
|
|
8088
|
+
if (truncated) {
|
|
8089
|
+
const note = document.createElement("div");
|
|
8090
|
+
note.className = "file-preview-table-truncated";
|
|
8091
|
+
note.textContent = "Showing first " + TABLE_PREVIEW_MAX_ROWS + " of " + rows.length + " rows. Click Edit to see the raw file.";
|
|
8092
|
+
body.appendChild(note);
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
8095
|
+
} else if (isMarkdownFile(mime, filename)) {
|
|
8096
|
+
body = document.createElement("div");
|
|
8097
|
+
body.className = "file-preview-markdown";
|
|
8098
|
+
const inner = document.createElement("div");
|
|
8099
|
+
inner.className = "assistant-content";
|
|
8100
|
+
inner.innerHTML = renderAssistantMarkdown(text);
|
|
8101
|
+
body.appendChild(inner);
|
|
8102
|
+
} else {
|
|
8103
|
+
body = document.createElement("pre");
|
|
8104
|
+
body.className = "file-preview-text";
|
|
8105
|
+
body.textContent = text;
|
|
8106
|
+
}
|
|
8107
|
+
wrap.append(buildPreviewActions(filePath, [copyBtn], [editBtn]), body);
|
|
8108
|
+
messages.innerHTML = "";
|
|
8109
|
+
messages.appendChild(wrap);
|
|
8110
|
+
};
|
|
8111
|
+
|
|
8112
|
+
const renderTextEditor = (filePath, originalText, mime) => {
|
|
8113
|
+
const messages = elements.messages;
|
|
8114
|
+
const wrap = document.createElement("div");
|
|
8115
|
+
wrap.className = "file-preview";
|
|
8116
|
+
const actions = document.createElement("div");
|
|
8117
|
+
actions.className = "file-preview-actions";
|
|
8118
|
+
const cancelBtn = document.createElement("button");
|
|
8119
|
+
cancelBtn.className = "file-preview-action-btn";
|
|
8120
|
+
cancelBtn.textContent = "Cancel";
|
|
8121
|
+
const saveBtn = document.createElement("button");
|
|
8122
|
+
saveBtn.className = "file-preview-action-btn primary";
|
|
8123
|
+
saveBtn.textContent = "Save";
|
|
8124
|
+
saveBtn.disabled = true;
|
|
8125
|
+
const leftGroup = document.createElement("div");
|
|
8126
|
+
leftGroup.className = "file-preview-actions-group";
|
|
8127
|
+
const rightGroup = document.createElement("div");
|
|
8128
|
+
rightGroup.className = "file-preview-actions-group";
|
|
8129
|
+
rightGroup.append(cancelBtn, saveBtn);
|
|
8130
|
+
actions.append(leftGroup, rightGroup);
|
|
8131
|
+
const textarea = document.createElement("textarea");
|
|
8132
|
+
textarea.className = "file-edit-textarea";
|
|
8133
|
+
textarea.value = originalText;
|
|
8134
|
+
textarea.spellcheck = false;
|
|
8135
|
+
textarea.oninput = () => { saveBtn.disabled = textarea.value === originalText; };
|
|
8136
|
+
cancelBtn.onclick = () => renderTextView(filePath, originalText, mime);
|
|
8137
|
+
saveBtn.onclick = async () => {
|
|
8138
|
+
const newText = textarea.value;
|
|
8139
|
+
saveBtn.disabled = true;
|
|
8140
|
+
saveBtn.textContent = "Saving\u2026";
|
|
8141
|
+
cancelBtn.disabled = true;
|
|
8142
|
+
try {
|
|
8143
|
+
const url = "/api/vfs/" + encodeURI(filePath.replace(/^\\//, "")) + "?overwrite=1";
|
|
8144
|
+
const response = await fetch(url, {
|
|
8145
|
+
method: "PUT",
|
|
8146
|
+
credentials: state.tenantToken ? "omit" : "include",
|
|
8147
|
+
headers: { ...buildAuthHeaders(), "Content-Type": mime || "text/plain" },
|
|
8148
|
+
body: newText,
|
|
8149
|
+
});
|
|
8150
|
+
if (!response.ok) {
|
|
8151
|
+
let msg = "Save failed (" + response.status + ")";
|
|
8152
|
+
try { const p = await response.json(); if (p && p.message) msg = p.message; } catch {}
|
|
8153
|
+
throw new Error(msg);
|
|
8154
|
+
}
|
|
8155
|
+
} catch (err) {
|
|
8156
|
+
saveBtn.textContent = "Save";
|
|
8157
|
+
saveBtn.disabled = false;
|
|
8158
|
+
cancelBtn.disabled = false;
|
|
8159
|
+
window.alert("Failed to save: " + (err.message || "unknown error"));
|
|
8160
|
+
return;
|
|
8161
|
+
}
|
|
8162
|
+
// Invalidate parent dir cache so file size/mtime refresh on next view
|
|
8163
|
+
state.dirCache.delete(parentPath(filePath));
|
|
8164
|
+
renderTextView(filePath, newText, mime);
|
|
8165
|
+
};
|
|
8166
|
+
wrap.append(actions, textarea);
|
|
8167
|
+
messages.innerHTML = "";
|
|
8168
|
+
messages.appendChild(wrap);
|
|
8169
|
+
textarea.focus();
|
|
8170
|
+
};
|
|
8171
|
+
|
|
8172
|
+
const renderPreviewPlaceholder = (filePath, mime, size, reason) => {
|
|
8173
|
+
const messages = elements.messages;
|
|
8174
|
+
const filename = filePath.split("/").pop() || filePath;
|
|
8175
|
+
const downloadUrl = "/api/vfs/" + encodeURI(filePath.replace(/^\\//, ""));
|
|
8176
|
+
const wrap = document.createElement("div");
|
|
8177
|
+
wrap.className = "file-preview";
|
|
8178
|
+
const inner = document.createElement("div");
|
|
8179
|
+
inner.className = "file-preview-placeholder";
|
|
8180
|
+
const card = document.createElement("div");
|
|
8181
|
+
const nameEl = document.createElement("div");
|
|
8182
|
+
nameEl.className = "file-preview-name";
|
|
8183
|
+
nameEl.textContent = filename;
|
|
8184
|
+
const metaEl = document.createElement("div");
|
|
8185
|
+
metaEl.className = "file-preview-meta";
|
|
8186
|
+
const metaParts = [];
|
|
8187
|
+
if (mime) metaParts.push(mime);
|
|
8188
|
+
if (size) metaParts.push(formatBytes(size));
|
|
8189
|
+
metaEl.textContent = (reason ? reason + " \xB7 " : "") + metaParts.join(" \xB7 ");
|
|
8190
|
+
const a = document.createElement("a");
|
|
8191
|
+
a.className = "file-preview-download";
|
|
8192
|
+
a.href = downloadUrl;
|
|
8193
|
+
a.textContent = "Download";
|
|
8194
|
+
a.setAttribute("download", filename);
|
|
8195
|
+
card.append(nameEl, metaEl, a);
|
|
8196
|
+
inner.appendChild(card);
|
|
8197
|
+
wrap.appendChild(inner);
|
|
8198
|
+
messages.innerHTML = "";
|
|
8199
|
+
messages.appendChild(wrap);
|
|
8200
|
+
};
|
|
8201
|
+
|
|
8202
|
+
const renderPreviewError = (filePath, message) => {
|
|
8203
|
+
const messages = elements.messages;
|
|
8204
|
+
const filename = filePath.split("/").pop() || filePath;
|
|
8205
|
+
const wrap = document.createElement("div");
|
|
8206
|
+
wrap.className = "file-preview";
|
|
8207
|
+
const inner = document.createElement("div");
|
|
8208
|
+
inner.className = "file-preview-placeholder";
|
|
8209
|
+
const card = document.createElement("div");
|
|
8210
|
+
card.innerHTML = '<div class="file-preview-name">' + escapeHtml(filename) + '</div><div class="file-preview-meta">Failed to load file (' + escapeHtml(message) + ')</div>';
|
|
8211
|
+
inner.appendChild(card);
|
|
8212
|
+
wrap.appendChild(inner);
|
|
8213
|
+
messages.innerHTML = "";
|
|
8214
|
+
messages.appendChild(wrap);
|
|
8215
|
+
};
|
|
8216
|
+
|
|
8217
|
+
const updateComposerVisibility = () => {
|
|
8218
|
+
if (!elements.composer) return;
|
|
8219
|
+
if (state.activeFilePath) elements.composer.classList.add("hidden");
|
|
8220
|
+
else elements.composer.classList.remove("hidden");
|
|
8221
|
+
};
|
|
8222
|
+
|
|
8223
|
+
const deleteEntry = async (path, type, parentDir) => {
|
|
8224
|
+
try {
|
|
8225
|
+
const url = "/api/vfs/" + encodeURI(path.replace(/^\\//, ""));
|
|
8226
|
+
const response = await fetch(url, {
|
|
8227
|
+
method: "DELETE",
|
|
8228
|
+
credentials: state.tenantToken ? "omit" : "include",
|
|
8229
|
+
headers: buildAuthHeaders(),
|
|
8230
|
+
});
|
|
8231
|
+
if (!response.ok) {
|
|
8232
|
+
let msg = "Delete failed (" + response.status + ")";
|
|
8233
|
+
try { const p = await response.json(); if (p && p.message) msg = p.message; } catch {}
|
|
8234
|
+
throw new Error(msg);
|
|
8235
|
+
}
|
|
8236
|
+
} catch (err) {
|
|
8237
|
+
window.alert("Failed to delete: " + (err.message || "unknown error"));
|
|
8238
|
+
renderFileExplorer();
|
|
8239
|
+
return;
|
|
8240
|
+
}
|
|
8241
|
+
// Drop cache for parent and any descendants we cached
|
|
8242
|
+
state.dirCache.delete(parentDir);
|
|
8243
|
+
if (type === "directory") {
|
|
8244
|
+
const prefix = path === "/" ? "/" : path + "/";
|
|
8245
|
+
for (const k of Array.from(state.dirCache.keys())) {
|
|
8246
|
+
if (k === path || k.startsWith(prefix)) state.dirCache.delete(k);
|
|
8247
|
+
}
|
|
8248
|
+
for (const d of Array.from(state.expandedDirs)) {
|
|
8249
|
+
if (d === path || d.startsWith(prefix)) state.expandedDirs.delete(d);
|
|
8250
|
+
}
|
|
8251
|
+
}
|
|
8252
|
+
if (state.activeFilePath === path || (type === "directory" && state.activeFilePath && state.activeFilePath.startsWith(path + "/"))) {
|
|
8253
|
+
state.activeFilePath = null;
|
|
8254
|
+
elements.chatTitle.textContent = "";
|
|
8255
|
+
elements.messages.innerHTML = "";
|
|
8256
|
+
updateComposerVisibility();
|
|
8257
|
+
replaceFileUrl(null);
|
|
8258
|
+
}
|
|
8259
|
+
try { await fetchDirEntries(parentDir); } catch {}
|
|
8260
|
+
renderFileExplorer();
|
|
8261
|
+
};
|
|
6880
8262
|
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
8263
|
+
const switchSidebarMode = (mode) => {
|
|
8264
|
+
if (mode !== "conversations" && mode !== "files") return;
|
|
8265
|
+
state.sidebarMode = mode;
|
|
8266
|
+
const buttons = document.querySelectorAll(".sidebar-segmented .seg-btn");
|
|
8267
|
+
buttons.forEach((b) => {
|
|
8268
|
+
if (b.dataset.mode === mode) b.classList.add("active");
|
|
8269
|
+
else b.classList.remove("active");
|
|
8270
|
+
});
|
|
8271
|
+
const list = elements.list;
|
|
8272
|
+
const explorer = elements.fileExplorer;
|
|
8273
|
+
if (mode === "files") {
|
|
8274
|
+
list.classList.add("hidden");
|
|
8275
|
+
explorer.classList.remove("hidden");
|
|
8276
|
+
if (!state.dirCache.has("/")) {
|
|
8277
|
+
ensureDirLoaded("/").then(() => renderFileExplorer());
|
|
8278
|
+
}
|
|
8279
|
+
renderFileExplorer();
|
|
8280
|
+
} else {
|
|
8281
|
+
explorer.classList.add("hidden");
|
|
8282
|
+
list.classList.remove("hidden");
|
|
8283
|
+
}
|
|
8284
|
+
};
|
|
6884
8285
|
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
8286
|
+
// ----- Uploads + mkdir + drag-drop -----
|
|
8287
|
+
|
|
8288
|
+
let _hiddenUploadInput = null;
|
|
8289
|
+
const triggerUploadPicker = (targetDir) => {
|
|
8290
|
+
if (!_hiddenUploadInput) {
|
|
8291
|
+
_hiddenUploadInput = document.createElement("input");
|
|
8292
|
+
_hiddenUploadInput.type = "file";
|
|
8293
|
+
_hiddenUploadInput.multiple = true;
|
|
8294
|
+
_hiddenUploadInput.style.display = "none";
|
|
8295
|
+
document.body.appendChild(_hiddenUploadInput);
|
|
8296
|
+
}
|
|
8297
|
+
_hiddenUploadInput.value = "";
|
|
8298
|
+
_hiddenUploadInput.onchange = () => {
|
|
8299
|
+
const files = Array.from(_hiddenUploadInput.files || []);
|
|
8300
|
+
if (files.length > 0) uploadFiles(files, targetDir);
|
|
8301
|
+
};
|
|
8302
|
+
_hiddenUploadInput.click();
|
|
8303
|
+
};
|
|
8304
|
+
|
|
8305
|
+
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
8306
|
+
|
|
8307
|
+
const uploadOne = async (file, targetDir, overwrite) => {
|
|
8308
|
+
if (file.size > MAX_UPLOAD_BYTES) {
|
|
8309
|
+
throw new Error("File too large (limit " + formatBytes(MAX_UPLOAD_BYTES) + ")");
|
|
6888
8310
|
}
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
8311
|
+
const targetPath = joinPath(targetDir, file.name);
|
|
8312
|
+
const url = "/api/vfs/" + encodeURI(targetPath.replace(/^\\//, "")) + (overwrite ? "?overwrite=1" : "");
|
|
8313
|
+
const response = await fetch(url, {
|
|
8314
|
+
method: "PUT",
|
|
8315
|
+
credentials: state.tenantToken ? "omit" : "include",
|
|
8316
|
+
headers: { ...buildAuthHeaders(), "Content-Type": file.type || "application/octet-stream" },
|
|
8317
|
+
body: file,
|
|
8318
|
+
});
|
|
8319
|
+
if (response.status === 409) {
|
|
8320
|
+
const ok = window.confirm("A file named \\"" + file.name + "\\" already exists in " + targetDir + ". Overwrite?");
|
|
8321
|
+
if (!ok) return;
|
|
8322
|
+
return uploadOne(file, targetDir, true);
|
|
6892
8323
|
}
|
|
6893
|
-
if (!
|
|
6894
|
-
|
|
6895
|
-
|
|
8324
|
+
if (!response.ok) {
|
|
8325
|
+
let msg = "Upload failed (" + response.status + ")";
|
|
8326
|
+
try { const p = await response.json(); if (p && p.message) msg = p.message; } catch {}
|
|
8327
|
+
throw new Error(msg);
|
|
6896
8328
|
}
|
|
6897
|
-
}
|
|
6898
|
-
|
|
6899
|
-
window.addEventListener("resize", () => {
|
|
6900
|
-
setSidebarOpen(false);
|
|
6901
|
-
});
|
|
8329
|
+
};
|
|
6902
8330
|
|
|
6903
|
-
const
|
|
6904
|
-
|
|
6905
|
-
state.
|
|
6906
|
-
|
|
8331
|
+
const uploadFiles = async (files, targetDir) => {
|
|
8332
|
+
for (const file of files) {
|
|
8333
|
+
state.pendingUploads += 1;
|
|
8334
|
+
renderFileExplorer();
|
|
6907
8335
|
try {
|
|
6908
|
-
await
|
|
6909
|
-
} catch {
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
state.
|
|
6913
|
-
replaceConversationUrl(null);
|
|
6914
|
-
elements.chatTitle.textContent = "";
|
|
6915
|
-
renderMessages([]);
|
|
6916
|
-
renderConversationList();
|
|
8336
|
+
await uploadOne(file, targetDir, false);
|
|
8337
|
+
} catch (err) {
|
|
8338
|
+
window.alert("Failed to upload " + file.name + ": " + (err.message || "unknown error"));
|
|
8339
|
+
} finally {
|
|
8340
|
+
state.pendingUploads -= 1;
|
|
6917
8341
|
}
|
|
6918
|
-
} else {
|
|
6919
|
-
state.activeConversationId = null;
|
|
6920
|
-
state.activeMessages = [];
|
|
6921
|
-
state.contextTokens = 0;
|
|
6922
|
-
state.contextWindow = 0;
|
|
6923
|
-
updateContextRing();
|
|
6924
|
-
elements.chatTitle.textContent = "";
|
|
6925
|
-
renderMessages([]);
|
|
6926
|
-
renderConversationList();
|
|
6927
8342
|
}
|
|
8343
|
+
state.dirCache.delete(targetDir);
|
|
8344
|
+
try { await fetchDirEntries(targetDir); } catch {}
|
|
8345
|
+
state.expandedDirs.add(targetDir);
|
|
8346
|
+
renderFileExplorer();
|
|
8347
|
+
};
|
|
8348
|
+
|
|
8349
|
+
let _dropTargetEl = null;
|
|
8350
|
+
const _setDropTarget = (el) => {
|
|
8351
|
+
if (_dropTargetEl === el) return;
|
|
8352
|
+
if (_dropTargetEl) _dropTargetEl.classList.remove("drop-target");
|
|
8353
|
+
_dropTargetEl = el;
|
|
8354
|
+
if (_dropTargetEl) _dropTargetEl.classList.add("drop-target");
|
|
8355
|
+
};
|
|
8356
|
+
const _resolveDropTarget = (eventTarget) => {
|
|
8357
|
+
if (!(eventTarget instanceof Element)) return null;
|
|
8358
|
+
// A folder header row takes precedence \u2014 drop INTO that folder.
|
|
8359
|
+
const folderRow = eventTarget.closest(".file-row.is-dir");
|
|
8360
|
+
if (folderRow && elements.fileExplorer.contains(folderRow)) return folderRow;
|
|
8361
|
+
// Otherwise the deepest .file-children wrap \u2014 drop into its parent dir.
|
|
8362
|
+
const childrenWrap = eventTarget.closest(".file-children");
|
|
8363
|
+
if (childrenWrap && elements.fileExplorer.contains(childrenWrap)) return childrenWrap;
|
|
8364
|
+
return null;
|
|
8365
|
+
};
|
|
8366
|
+
const _resolveDropPath = (el) => {
|
|
8367
|
+
if (!el) return "/";
|
|
8368
|
+
if (el.classList.contains("file-row")) return el.dataset.path || "/";
|
|
8369
|
+
if (el.classList.contains("file-children")) return el.dataset.dir || "/";
|
|
8370
|
+
return "/";
|
|
6928
8371
|
};
|
|
6929
8372
|
|
|
8373
|
+
const attachExplorerDropHandlers = () => {
|
|
8374
|
+
const root = elements.fileExplorer;
|
|
8375
|
+
if (!root) return;
|
|
8376
|
+
root.addEventListener("dragover", (e) => {
|
|
8377
|
+
if (!e.dataTransfer || ![...(e.dataTransfer.types || [])].includes("Files")) return;
|
|
8378
|
+
e.preventDefault();
|
|
8379
|
+
_setDropTarget(_resolveDropTarget(e.target));
|
|
8380
|
+
});
|
|
8381
|
+
root.addEventListener("dragleave", (e) => {
|
|
8382
|
+
if (!root.contains(e.relatedTarget)) _setDropTarget(null);
|
|
8383
|
+
});
|
|
8384
|
+
root.addEventListener("drop", (e) => {
|
|
8385
|
+
if (!e.dataTransfer || !e.dataTransfer.files || e.dataTransfer.files.length === 0) return;
|
|
8386
|
+
e.preventDefault();
|
|
8387
|
+
const target = _dropTargetEl;
|
|
8388
|
+
const dir = _resolveDropPath(target);
|
|
8389
|
+
_setDropTarget(null);
|
|
8390
|
+
const items = Array.from(e.dataTransfer.items || []);
|
|
8391
|
+
const hasDir = items.some((it) => {
|
|
8392
|
+
const fn = it.webkitGetAsEntry && it.webkitGetAsEntry();
|
|
8393
|
+
return fn && fn.isDirectory;
|
|
8394
|
+
});
|
|
8395
|
+
const files = Array.from(e.dataTransfer.files);
|
|
8396
|
+
if (hasDir) {
|
|
8397
|
+
window.alert("Folder uploads aren't supported yet \u2014 drop individual files.");
|
|
8398
|
+
}
|
|
8399
|
+
if (files.length > 0) uploadFiles(files, dir);
|
|
8400
|
+
});
|
|
8401
|
+
};
|
|
8402
|
+
|
|
8403
|
+
// Wire segmented control + drop-handlers once the DOM is ready
|
|
8404
|
+
(function wireFileExplorer() {
|
|
8405
|
+
const buttons = document.querySelectorAll(".sidebar-segmented .seg-btn");
|
|
8406
|
+
buttons.forEach((b) => {
|
|
8407
|
+
b.addEventListener("click", () => switchSidebarMode(b.dataset.mode));
|
|
8408
|
+
});
|
|
8409
|
+
attachExplorerDropHandlers();
|
|
8410
|
+
})();
|
|
8411
|
+
|
|
6930
8412
|
window.addEventListener("popstate", async () => {
|
|
6931
8413
|
if (state.isStreaming) return;
|
|
8414
|
+
const filePath = getFilePathFromUrl();
|
|
8415
|
+
if (filePath) {
|
|
8416
|
+
switchSidebarMode("files");
|
|
8417
|
+
await selectFile(filePath);
|
|
8418
|
+
return;
|
|
8419
|
+
}
|
|
6932
8420
|
const conversationId = getConversationIdFromUrl();
|
|
6933
8421
|
await navigateToConversation(conversationId);
|
|
6934
8422
|
});
|
|
@@ -6939,6 +8427,26 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
6939
8427
|
return;
|
|
6940
8428
|
}
|
|
6941
8429
|
await loadConversations();
|
|
8430
|
+
const urlFilePath = getFilePathFromUrl();
|
|
8431
|
+
if (urlFilePath) {
|
|
8432
|
+
switchSidebarMode("files");
|
|
8433
|
+
// Expand ancestors so the file is visible in the tree
|
|
8434
|
+
let p = parentPath(urlFilePath);
|
|
8435
|
+
const ancestors = [];
|
|
8436
|
+
while (p && p !== "/") { ancestors.push(p); p = parentPath(p); }
|
|
8437
|
+
ancestors.push("/");
|
|
8438
|
+
for (const a of ancestors.reverse()) {
|
|
8439
|
+
state.expandedDirs.add(a);
|
|
8440
|
+
try { await fetchDirEntries(a); } catch {}
|
|
8441
|
+
}
|
|
8442
|
+
state.activeFilePath = urlFilePath;
|
|
8443
|
+
updateComposerVisibility();
|
|
8444
|
+
renderFileExplorer();
|
|
8445
|
+
await renderFilePreview(urlFilePath);
|
|
8446
|
+
autoResizePrompt();
|
|
8447
|
+
updateContextRing();
|
|
8448
|
+
return;
|
|
8449
|
+
}
|
|
6942
8450
|
const urlConversationId = getConversationIdFromUrl();
|
|
6943
8451
|
if (urlConversationId) {
|
|
6944
8452
|
state.activeConversationId = urlConversationId;
|
|
@@ -7739,7 +9247,12 @@ ${WEB_UI_STYLES}
|
|
|
7739
9247
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
7740
9248
|
</button>
|
|
7741
9249
|
</div>
|
|
9250
|
+
<div class="sidebar-segmented" role="tablist">
|
|
9251
|
+
<button class="seg-btn active" data-mode="conversations" role="tab">Chats</button>
|
|
9252
|
+
<button class="seg-btn" data-mode="files" role="tab">Files</button>
|
|
9253
|
+
</div>
|
|
7742
9254
|
<div id="conversation-list" class="conversation-list"></div>
|
|
9255
|
+
<div id="file-explorer" class="file-explorer hidden"></div>
|
|
7743
9256
|
<div class="sidebar-footer">
|
|
7744
9257
|
<div class="sidebar-footer-row">
|
|
7745
9258
|
<button id="logout" class="logout-btn">Log out</button>
|
|
@@ -7835,6 +9348,7 @@ ${WEB_UI_STYLES}
|
|
|
7835
9348
|
</div>
|
|
7836
9349
|
<div id="drag-overlay" class="drag-overlay"><div class="drag-overlay-inner">Drop files to attach</div></div>
|
|
7837
9350
|
<div id="lightbox" class="lightbox" style="display:none"><img /></div>
|
|
9351
|
+
<button id="view-toggle" class="dev-view-toggle" hidden title="Toggle between user-facing messages and raw harness messages (dev -v only)">user view</button>
|
|
7838
9352
|
|
|
7839
9353
|
<script>
|
|
7840
9354
|
${getWebUiClientScript(markedSource)}
|
|
@@ -10858,7 +12372,7 @@ cron:
|
|
|
10858
12372
|
|
|
10859
12373
|
- \`poncho dev\`: jobs run via an in-process scheduler.
|
|
10860
12374
|
- \`poncho build vercel\`: generates \`vercel.json\` cron entries. Set \`CRON_SECRET\` to the same value as \`PONCHO_AUTH_TOKEN\` so Vercel can authenticate.
|
|
10861
|
-
- Docker/Fly.io: scheduler runs automatically.
|
|
12375
|
+
- Docker/Fly.io/Railway: scheduler runs automatically.
|
|
10862
12376
|
- Lambda: use AWS EventBridge to trigger \`GET /api/cron/<jobName>\` with \`Authorization: Bearer <token>\`.
|
|
10863
12377
|
- Trigger manually: \`curl http://localhost:3000/api/cron/daily-report\`
|
|
10864
12378
|
|
|
@@ -10982,6 +12496,10 @@ poncho build lambda
|
|
|
10982
12496
|
# Fly.io
|
|
10983
12497
|
poncho build fly
|
|
10984
12498
|
fly deploy
|
|
12499
|
+
|
|
12500
|
+
# Railway
|
|
12501
|
+
poncho build railway
|
|
12502
|
+
railway up
|
|
10985
12503
|
\`\`\`
|
|
10986
12504
|
|
|
10987
12505
|
Set environment variables on your deployment platform:
|
|
@@ -11063,7 +12581,7 @@ var ensureFile = async (path, content) => {
|
|
|
11063
12581
|
};
|
|
11064
12582
|
var normalizeDeployTarget = (target) => {
|
|
11065
12583
|
const normalized = target.toLowerCase();
|
|
11066
|
-
if (normalized === "vercel" || normalized === "docker" || normalized === "lambda" || normalized === "fly") {
|
|
12584
|
+
if (normalized === "vercel" || normalized === "docker" || normalized === "lambda" || normalized === "fly" || normalized === "railway") {
|
|
11067
12585
|
return normalized;
|
|
11068
12586
|
}
|
|
11069
12587
|
throw new Error(`Unsupported build target: ${target}`);
|
|
@@ -11224,14 +12742,21 @@ var scaffoldDeployTarget = async (projectDir, target, options) => {
|
|
|
11224
12742
|
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
11225
12743
|
await startDevServer(Number.isNaN(port) ? 3000 : port, { workingDir: process.cwd() });
|
|
11226
12744
|
`;
|
|
12745
|
+
let browserEnabled = false;
|
|
12746
|
+
try {
|
|
12747
|
+
const cfg = await loadPonchoConfig(projectDir);
|
|
12748
|
+
browserEnabled = !!cfg?.browser;
|
|
12749
|
+
} catch {
|
|
12750
|
+
}
|
|
12751
|
+
const chromiumLibsLayer = browserEnabled ? `RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
12752
|
+
ca-certificates fonts-liberation libnss3 libatk1.0-0 libatk-bridge2.0-0 \\
|
|
12753
|
+
libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \\
|
|
12754
|
+
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 \\
|
|
12755
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
12756
|
+
|
|
12757
|
+
` : "";
|
|
11227
12758
|
if (target === "vercel") {
|
|
11228
12759
|
const traceHints = [];
|
|
11229
|
-
let browserEnabled = false;
|
|
11230
|
-
try {
|
|
11231
|
-
const cfg = await loadPonchoConfig(projectDir);
|
|
11232
|
-
browserEnabled = !!cfg?.browser;
|
|
11233
|
-
} catch {
|
|
11234
|
-
}
|
|
11235
12760
|
if (browserEnabled) {
|
|
11236
12761
|
traceHints.push(`import("@poncho-ai/browser").catch(() => {});`);
|
|
11237
12762
|
const projectPkgPath = resolve4(projectDir, "package.json");
|
|
@@ -11336,16 +12861,19 @@ export default async function handler(req, res) {
|
|
|
11336
12861
|
const dockerfilePath = resolve4(projectDir, "Dockerfile");
|
|
11337
12862
|
await writeScaffoldFile(
|
|
11338
12863
|
dockerfilePath,
|
|
11339
|
-
`FROM node:
|
|
12864
|
+
`FROM node:22-slim
|
|
11340
12865
|
WORKDIR /app
|
|
11341
|
-
|
|
12866
|
+
|
|
12867
|
+
${chromiumLibsLayer}COPY package.json package.json
|
|
12868
|
+
RUN npm install --omit=dev
|
|
12869
|
+
|
|
11342
12870
|
COPY AGENT.md AGENT.md
|
|
11343
12871
|
COPY poncho.config.js poncho.config.js
|
|
11344
12872
|
COPY skills skills
|
|
11345
12873
|
COPY tests tests
|
|
11346
12874
|
COPY .env.example .env.example
|
|
11347
|
-
RUN corepack enable && npm install -g @poncho-ai/cli@^${cliVersion}
|
|
11348
12875
|
COPY server.js server.js
|
|
12876
|
+
|
|
11349
12877
|
EXPOSE 3000
|
|
11350
12878
|
CMD ["node","server.js"]
|
|
11351
12879
|
`,
|
|
@@ -11379,6 +12907,45 @@ export const handler = async (event = {}) => {
|
|
|
11379
12907
|
//
|
|
11380
12908
|
// Reminders: Create a CloudWatch Events rule that triggers GET /api/reminders/check
|
|
11381
12909
|
// every 10 minutes (or your preferred interval) with Authorization: Bearer <PONCHO_AUTH_TOKEN>.
|
|
12910
|
+
`,
|
|
12911
|
+
{ force: options?.force, writtenPaths, baseDir: projectDir }
|
|
12912
|
+
);
|
|
12913
|
+
} else if (target === "railway") {
|
|
12914
|
+
await writeScaffoldFile(
|
|
12915
|
+
resolve4(projectDir, "Dockerfile"),
|
|
12916
|
+
`FROM node:22-slim
|
|
12917
|
+
WORKDIR /app
|
|
12918
|
+
|
|
12919
|
+
${chromiumLibsLayer}COPY package.json package.json
|
|
12920
|
+
RUN npm install --omit=dev
|
|
12921
|
+
|
|
12922
|
+
COPY AGENT.md AGENT.md
|
|
12923
|
+
COPY poncho.config.js poncho.config.js
|
|
12924
|
+
COPY skills skills
|
|
12925
|
+
COPY tests tests
|
|
12926
|
+
COPY .env.example .env.example
|
|
12927
|
+
COPY server.js server.js
|
|
12928
|
+
|
|
12929
|
+
EXPOSE 3000
|
|
12930
|
+
CMD ["node","server.js"]
|
|
12931
|
+
`,
|
|
12932
|
+
{ force: options?.force, writtenPaths, baseDir: projectDir }
|
|
12933
|
+
);
|
|
12934
|
+
await writeScaffoldFile(resolve4(projectDir, "server.js"), sharedServerEntrypoint, {
|
|
12935
|
+
force: options?.force,
|
|
12936
|
+
writtenPaths,
|
|
12937
|
+
baseDir: projectDir
|
|
12938
|
+
});
|
|
12939
|
+
await writeScaffoldFile(
|
|
12940
|
+
resolve4(projectDir, "railway.toml"),
|
|
12941
|
+
`[build]
|
|
12942
|
+
builder = "dockerfile"
|
|
12943
|
+
dockerfilePath = "Dockerfile"
|
|
12944
|
+
|
|
12945
|
+
[deploy]
|
|
12946
|
+
startCommand = "node server.js"
|
|
12947
|
+
restartPolicyType = "on_failure"
|
|
12948
|
+
restartPolicyMaxRetries = 3
|
|
11382
12949
|
`,
|
|
11383
12950
|
{ force: options?.force, writtenPaths, baseDir: projectDir }
|
|
11384
12951
|
);
|
|
@@ -11399,15 +12966,18 @@ export const handler = async (event = {}) => {
|
|
|
11399
12966
|
);
|
|
11400
12967
|
await writeScaffoldFile(
|
|
11401
12968
|
resolve4(projectDir, "Dockerfile"),
|
|
11402
|
-
`FROM node:
|
|
12969
|
+
`FROM node:22-slim
|
|
11403
12970
|
WORKDIR /app
|
|
11404
|
-
|
|
12971
|
+
|
|
12972
|
+
${chromiumLibsLayer}COPY package.json package.json
|
|
12973
|
+
RUN npm install --omit=dev
|
|
12974
|
+
|
|
11405
12975
|
COPY AGENT.md AGENT.md
|
|
11406
12976
|
COPY poncho.config.js poncho.config.js
|
|
11407
12977
|
COPY skills skills
|
|
11408
12978
|
COPY tests tests
|
|
11409
|
-
RUN npm install -g @poncho-ai/cli@^${cliVersion}
|
|
11410
12979
|
COPY server.js server.js
|
|
12980
|
+
|
|
11411
12981
|
EXPOSE 3000
|
|
11412
12982
|
CMD ["node","server.js"]
|
|
11413
12983
|
`,
|
|
@@ -11987,7 +13557,7 @@ var askOnboardingQuestions = async (options) => {
|
|
|
11987
13557
|
var getProviderModelName = (provider) => provider === "openai" || provider === "openai-codex" ? "gpt-5.3-codex" : "claude-opus-4-5";
|
|
11988
13558
|
var normalizeDeployTarget2 = (value) => {
|
|
11989
13559
|
const target = typeof value === "string" ? value.toLowerCase() : "";
|
|
11990
|
-
if (target === "vercel" || target === "docker" || target === "fly" || target === "lambda") {
|
|
13560
|
+
if (target === "vercel" || target === "docker" || target === "fly" || target === "lambda" || target === "railway") {
|
|
11991
13561
|
return target;
|
|
11992
13562
|
}
|
|
11993
13563
|
return "none";
|
|
@@ -12691,7 +14261,7 @@ var runInteractive = async (workingDir, params) => {
|
|
|
12691
14261
|
await harness.initialize();
|
|
12692
14262
|
const identity = await ensureAgentIdentity2(workingDir);
|
|
12693
14263
|
try {
|
|
12694
|
-
const { runInteractiveInk } = await import("./run-interactive-ink-
|
|
14264
|
+
const { runInteractiveInk } = await import("./run-interactive-ink-LJTKUUV4.js");
|
|
12695
14265
|
await runInteractiveInk({
|
|
12696
14266
|
harness,
|
|
12697
14267
|
params,
|
|
@@ -12767,6 +14337,19 @@ var collectToolCallIds = (msgs) => {
|
|
|
12767
14337
|
}
|
|
12768
14338
|
return ids;
|
|
12769
14339
|
};
|
|
14340
|
+
var MEMORY_VFILE_PATH = "/memory.md";
|
|
14341
|
+
var isSafeVfsPath = (p) => {
|
|
14342
|
+
if (typeof p !== "string" || p.length === 0) return false;
|
|
14343
|
+
if (!p.startsWith("/")) return false;
|
|
14344
|
+
if (p.includes("\0")) return false;
|
|
14345
|
+
const segments = p.split("/").slice(1);
|
|
14346
|
+
if (p !== "/" && segments[segments.length - 1] === "") return false;
|
|
14347
|
+
for (const seg of segments) {
|
|
14348
|
+
if (seg === "" && p !== "/") return false;
|
|
14349
|
+
if (seg === "." || seg === "..") return false;
|
|
14350
|
+
}
|
|
14351
|
+
return true;
|
|
14352
|
+
};
|
|
12770
14353
|
var serverlessLog = createLogger("serverless");
|
|
12771
14354
|
var __dirname3 = dirname7(fileURLToPath3(import.meta.url));
|
|
12772
14355
|
var createRequestHandler = async (options) => {
|
|
@@ -12803,6 +14386,22 @@ var createRequestHandler = async (options) => {
|
|
|
12803
14386
|
let activeConversationRuns = /* @__PURE__ */ new Map();
|
|
12804
14387
|
const conversationEventStreams = /* @__PURE__ */ new Map();
|
|
12805
14388
|
const conversationEventCallbacks = /* @__PURE__ */ new Map();
|
|
14389
|
+
const MAX_BUFFERED_EVENTS_PER_CONVERSATION = 1e3;
|
|
14390
|
+
const STRIP_LARGE_STRING_BYTES = 4096;
|
|
14391
|
+
const stripLargeStringsForBuffer = (value) => {
|
|
14392
|
+
if (typeof value === "string") {
|
|
14393
|
+
return value.length > STRIP_LARGE_STRING_BYTES ? `[stripped-for-replay len=${value.length}]` : value;
|
|
14394
|
+
}
|
|
14395
|
+
if (Array.isArray(value)) return value.map(stripLargeStringsForBuffer);
|
|
14396
|
+
if (value && typeof value === "object") {
|
|
14397
|
+
const out = {};
|
|
14398
|
+
for (const [k, v] of Object.entries(value)) {
|
|
14399
|
+
out[k] = stripLargeStringsForBuffer(v);
|
|
14400
|
+
}
|
|
14401
|
+
return out;
|
|
14402
|
+
}
|
|
14403
|
+
return value;
|
|
14404
|
+
};
|
|
12806
14405
|
const broadcastEvent = (conversationId, event) => {
|
|
12807
14406
|
let stream = conversationEventStreams.get(conversationId);
|
|
12808
14407
|
if (!stream) {
|
|
@@ -12810,7 +14409,10 @@ var createRequestHandler = async (options) => {
|
|
|
12810
14409
|
conversationEventStreams.set(conversationId, stream);
|
|
12811
14410
|
}
|
|
12812
14411
|
if (event.type !== "browser:frame") {
|
|
12813
|
-
stream.buffer.push(event);
|
|
14412
|
+
stream.buffer.push(stripLargeStringsForBuffer(event));
|
|
14413
|
+
if (stream.buffer.length > MAX_BUFFERED_EVENTS_PER_CONVERSATION) {
|
|
14414
|
+
stream.buffer.splice(0, stream.buffer.length - MAX_BUFFERED_EVENTS_PER_CONVERSATION);
|
|
14415
|
+
}
|
|
12814
14416
|
}
|
|
12815
14417
|
for (const subscriber of stream.subscribers) {
|
|
12816
14418
|
try {
|
|
@@ -13187,6 +14789,7 @@ data: ${JSON.stringify(statusPayload)}
|
|
|
13187
14789
|
let runContextWindow = 0;
|
|
13188
14790
|
let runContinuation2 = false;
|
|
13189
14791
|
let runContinuationMessages;
|
|
14792
|
+
let runHarnessMessages;
|
|
13190
14793
|
let runSteps = 0;
|
|
13191
14794
|
let runMaxSteps;
|
|
13192
14795
|
const buildMessages = () => {
|
|
@@ -13328,6 +14931,7 @@ data: ${JSON.stringify(statusPayload)}
|
|
|
13328
14931
|
});
|
|
13329
14932
|
runContinuation2 = execution.runContinuation;
|
|
13330
14933
|
runContinuationMessages = execution.runContinuationMessages;
|
|
14934
|
+
runHarnessMessages = execution.runHarnessMessages;
|
|
13331
14935
|
runSteps = execution.runSteps;
|
|
13332
14936
|
runMaxSteps = execution.runMaxSteps;
|
|
13333
14937
|
runContextTokens = execution.runContextTokens;
|
|
@@ -13349,7 +14953,11 @@ data: ${JSON.stringify(statusPayload)}
|
|
|
13349
14953
|
contextWindow: runContextWindow,
|
|
13350
14954
|
continuation: runContinuation2,
|
|
13351
14955
|
continuationMessages: runContinuationMessages,
|
|
13352
|
-
|
|
14956
|
+
// Prefer the cancellation/end-of-run snapshot from the harness so
|
|
14957
|
+
// _harnessMessages stays in sync with what the model just saw,
|
|
14958
|
+
// even on aborted runs. Falls back to continuationMessages when
|
|
14959
|
+
// the run completed via continuation.
|
|
14960
|
+
harnessMessages: runHarnessMessages ?? runContinuationMessages,
|
|
13353
14961
|
toolResultArchive: harness.getToolResultArchive(conversationId)
|
|
13354
14962
|
}, { shouldRebuildCanonical: true });
|
|
13355
14963
|
});
|
|
@@ -13751,7 +15359,7 @@ data: ${JSON.stringify(statusPayload)}
|
|
|
13751
15359
|
selfBaseUrl = `${proto}://${request.headers.host}`;
|
|
13752
15360
|
}
|
|
13753
15361
|
if (webUiEnabled) {
|
|
13754
|
-
if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
|
|
15362
|
+
if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/") || pathname.startsWith("/f/"))) {
|
|
13755
15363
|
writeHtml(response, 200, renderWebUiHtml({ agentName, isDev: !isProduction }));
|
|
13756
15364
|
return;
|
|
13757
15365
|
}
|
|
@@ -14754,13 +16362,16 @@ data: ${JSON.stringify(frame)}
|
|
|
14754
16362
|
const hasPendingCallbackResults = Array.isArray(conversation.pendingSubagentResults) && conversation.pendingSubagentResults.length > 0;
|
|
14755
16363
|
const hasPendingApprovals = Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0;
|
|
14756
16364
|
const needsContinuation = !hasActiveRun && Array.isArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0 && !hasPendingApprovals;
|
|
16365
|
+
const verboseDev = process.env.PONCHO_DEV_VERBOSE === "1";
|
|
14757
16366
|
writeJson(response, 200, {
|
|
14758
16367
|
conversation: {
|
|
14759
16368
|
...conversation,
|
|
14760
16369
|
messages: conversation.messages.map(normalizeMessageForClient).filter((m) => m !== null),
|
|
14761
16370
|
pendingApprovals: storedPending,
|
|
14762
16371
|
_continuationMessages: void 0,
|
|
14763
|
-
|
|
16372
|
+
// In verbose dev mode the web UI exposes a toggle to inspect the
|
|
16373
|
+
// raw harness messages sent to the model API. Strip it otherwise.
|
|
16374
|
+
_harnessMessages: verboseDev ? conversation._harnessMessages : void 0,
|
|
14764
16375
|
// The browser has no use for the archive; make sure we never ship
|
|
14765
16376
|
// it back even if the conversation was loaded via getWithArchive.
|
|
14766
16377
|
_toolResultArchive: void 0
|
|
@@ -14768,7 +16379,8 @@ data: ${JSON.stringify(frame)}
|
|
|
14768
16379
|
subagentPendingApprovals: subagentPending,
|
|
14769
16380
|
hasActiveRun: hasActiveRun || hasPendingCallbackResults,
|
|
14770
16381
|
hasRunningSubagents,
|
|
14771
|
-
needsContinuation
|
|
16382
|
+
needsContinuation,
|
|
16383
|
+
verboseDev
|
|
14772
16384
|
});
|
|
14773
16385
|
return;
|
|
14774
16386
|
}
|
|
@@ -14897,6 +16509,22 @@ data: ${JSON.stringify(frame)}
|
|
|
14897
16509
|
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
14898
16510
|
return;
|
|
14899
16511
|
}
|
|
16512
|
+
if (vfsPath === MEMORY_VFILE_PATH) {
|
|
16513
|
+
try {
|
|
16514
|
+
const memory = await engine.memory.get(tenantId);
|
|
16515
|
+
const data = Buffer.from(memory.content, "utf-8");
|
|
16516
|
+
response.writeHead(200, {
|
|
16517
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
16518
|
+
"Content-Length": data.length,
|
|
16519
|
+
"Content-Disposition": `inline; filename="memory.md"`,
|
|
16520
|
+
"Cache-Control": "no-cache"
|
|
16521
|
+
});
|
|
16522
|
+
response.end(data);
|
|
16523
|
+
} catch (err) {
|
|
16524
|
+
writeJson(response, 500, { code: "READ_FAILED", message: err?.message ?? "Failed to read memory" });
|
|
16525
|
+
}
|
|
16526
|
+
return;
|
|
16527
|
+
}
|
|
14900
16528
|
try {
|
|
14901
16529
|
const stat2 = await engine.vfs.stat(tenantId, vfsPath);
|
|
14902
16530
|
if (!stat2 || stat2.type !== "file") {
|
|
@@ -14943,6 +16571,260 @@ data: ${JSON.stringify(frame)}
|
|
|
14943
16571
|
}
|
|
14944
16572
|
return;
|
|
14945
16573
|
}
|
|
16574
|
+
if (vfsMatch && request.method === "PUT") {
|
|
16575
|
+
const rawPath = "/" + decodeURIComponent(vfsMatch[1] ?? "");
|
|
16576
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
16577
|
+
const engine = harness.storageEngine;
|
|
16578
|
+
if (!engine) {
|
|
16579
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
16580
|
+
return;
|
|
16581
|
+
}
|
|
16582
|
+
if (!isSafeVfsPath(rawPath) || rawPath === "/") {
|
|
16583
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
16584
|
+
return;
|
|
16585
|
+
}
|
|
16586
|
+
if (rawPath === MEMORY_VFILE_PATH) {
|
|
16587
|
+
try {
|
|
16588
|
+
const chunks = [];
|
|
16589
|
+
for await (const chunk of request) chunks.push(chunk);
|
|
16590
|
+
const body = Buffer.concat(chunks);
|
|
16591
|
+
const content = body.toString("utf-8").trim();
|
|
16592
|
+
const memory = await engine.memory.update(content, tenantId);
|
|
16593
|
+
writeJson(response, 200, {
|
|
16594
|
+
path: MEMORY_VFILE_PATH,
|
|
16595
|
+
size: Buffer.byteLength(memory.content, "utf-8"),
|
|
16596
|
+
mimeType: "text/markdown",
|
|
16597
|
+
updatedAt: memory.updatedAt
|
|
16598
|
+
});
|
|
16599
|
+
} catch (err) {
|
|
16600
|
+
writeJson(response, 500, { code: "WRITE_FAILED", message: err?.message ?? "Failed to write memory" });
|
|
16601
|
+
}
|
|
16602
|
+
return;
|
|
16603
|
+
}
|
|
16604
|
+
const allowOverwrite = requestUrl.searchParams.get("overwrite") === "1";
|
|
16605
|
+
try {
|
|
16606
|
+
const existing = await engine.vfs.stat(tenantId, rawPath);
|
|
16607
|
+
if (existing && !allowOverwrite) {
|
|
16608
|
+
writeJson(response, 409, { code: "EXISTS", message: "File already exists" });
|
|
16609
|
+
return;
|
|
16610
|
+
}
|
|
16611
|
+
if (existing && existing.type !== "file") {
|
|
16612
|
+
writeJson(response, 409, { code: "NOT_A_FILE", message: "Path exists and is not a file" });
|
|
16613
|
+
return;
|
|
16614
|
+
}
|
|
16615
|
+
const chunks = [];
|
|
16616
|
+
for await (const chunk of request) chunks.push(chunk);
|
|
16617
|
+
const body = Buffer.concat(chunks);
|
|
16618
|
+
const mimeType = request.headers["content-type"]?.split(";")[0]?.trim() || void 0;
|
|
16619
|
+
await engine.vfs.writeFile(tenantId, rawPath, new Uint8Array(body), mimeType);
|
|
16620
|
+
if (rawPath === "/skills" || rawPath.startsWith("/skills/")) {
|
|
16621
|
+
harness.invalidateSkillsForTenant(tenantId);
|
|
16622
|
+
}
|
|
16623
|
+
const stat2 = await engine.vfs.stat(tenantId, rawPath);
|
|
16624
|
+
writeJson(response, 200, {
|
|
16625
|
+
path: rawPath,
|
|
16626
|
+
size: stat2?.size ?? body.length,
|
|
16627
|
+
mimeType: stat2?.mimeType ?? mimeType ?? null,
|
|
16628
|
+
updatedAt: stat2?.updatedAt ?? Date.now()
|
|
16629
|
+
});
|
|
16630
|
+
} catch (err) {
|
|
16631
|
+
const message = err?.message ?? "Upload failed";
|
|
16632
|
+
const code = /quota|too large|exceed/i.test(message) ? 413 : 500;
|
|
16633
|
+
writeJson(response, code, { code: code === 413 ? "QUOTA" : "WRITE_FAILED", message });
|
|
16634
|
+
}
|
|
16635
|
+
return;
|
|
16636
|
+
}
|
|
16637
|
+
if (vfsMatch && request.method === "DELETE") {
|
|
16638
|
+
const rawPath = "/" + decodeURIComponent(vfsMatch[1] ?? "");
|
|
16639
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
16640
|
+
const engine = harness.storageEngine;
|
|
16641
|
+
if (!engine) {
|
|
16642
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
16643
|
+
return;
|
|
16644
|
+
}
|
|
16645
|
+
if (!isSafeVfsPath(rawPath) || rawPath === "/") {
|
|
16646
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
16647
|
+
return;
|
|
16648
|
+
}
|
|
16649
|
+
if (rawPath === MEMORY_VFILE_PATH) {
|
|
16650
|
+
writeJson(response, 400, {
|
|
16651
|
+
code: "RESERVED",
|
|
16652
|
+
message: "memory.md cannot be deleted; clear its contents instead."
|
|
16653
|
+
});
|
|
16654
|
+
return;
|
|
16655
|
+
}
|
|
16656
|
+
try {
|
|
16657
|
+
const stat2 = await engine.vfs.stat(tenantId, rawPath);
|
|
16658
|
+
if (!stat2) {
|
|
16659
|
+
writeJson(response, 404, { code: "NOT_FOUND", message: "Path not found" });
|
|
16660
|
+
return;
|
|
16661
|
+
}
|
|
16662
|
+
if (stat2.type === "directory") {
|
|
16663
|
+
await engine.vfs.deleteDir(tenantId, rawPath, true);
|
|
16664
|
+
} else {
|
|
16665
|
+
await engine.vfs.deleteFile(tenantId, rawPath);
|
|
16666
|
+
}
|
|
16667
|
+
if (rawPath === "/skills" || rawPath.startsWith("/skills/")) {
|
|
16668
|
+
harness.invalidateSkillsForTenant(tenantId);
|
|
16669
|
+
}
|
|
16670
|
+
writeJson(response, 200, { ok: true, path: rawPath });
|
|
16671
|
+
} catch (err) {
|
|
16672
|
+
writeJson(response, 500, { code: "DELETE_FAILED", message: err?.message ?? "Failed to delete" });
|
|
16673
|
+
}
|
|
16674
|
+
return;
|
|
16675
|
+
}
|
|
16676
|
+
if (pathname === "/api/vfs-archive" && request.method === "GET") {
|
|
16677
|
+
const dirPath = requestUrl.searchParams.get("path") ?? "/";
|
|
16678
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
16679
|
+
const engine = harness.storageEngine;
|
|
16680
|
+
if (!engine) {
|
|
16681
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
16682
|
+
return;
|
|
16683
|
+
}
|
|
16684
|
+
if (!isSafeVfsPath(dirPath)) {
|
|
16685
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
16686
|
+
return;
|
|
16687
|
+
}
|
|
16688
|
+
try {
|
|
16689
|
+
if (dirPath !== "/") {
|
|
16690
|
+
const stat2 = await engine.vfs.stat(tenantId, dirPath);
|
|
16691
|
+
if (!stat2 || stat2.type !== "directory") {
|
|
16692
|
+
writeJson(response, 404, { code: "NOT_FOUND", message: "Directory not found" });
|
|
16693
|
+
return;
|
|
16694
|
+
}
|
|
16695
|
+
}
|
|
16696
|
+
const entries = [];
|
|
16697
|
+
const walk = async (dir, prefix) => {
|
|
16698
|
+
const children = await engine.vfs.readdir(tenantId, dir);
|
|
16699
|
+
for (const child of children) {
|
|
16700
|
+
if (dir === "/" && child.name === "memory.md") continue;
|
|
16701
|
+
const childPath = dir === "/" ? "/" + child.name : dir + "/" + child.name;
|
|
16702
|
+
const relName = prefix === "" ? child.name : prefix + "/" + child.name;
|
|
16703
|
+
if (child.type === "directory") {
|
|
16704
|
+
await walk(childPath, relName);
|
|
16705
|
+
} else if (child.type === "file") {
|
|
16706
|
+
const content = await engine.vfs.readFile(tenantId, childPath);
|
|
16707
|
+
const stat2 = await engine.vfs.stat(tenantId, childPath);
|
|
16708
|
+
entries.push({
|
|
16709
|
+
name: relName,
|
|
16710
|
+
content,
|
|
16711
|
+
mtime: stat2?.updatedAt ? new Date(stat2.updatedAt) : void 0
|
|
16712
|
+
});
|
|
16713
|
+
}
|
|
16714
|
+
}
|
|
16715
|
+
};
|
|
16716
|
+
await walk(dirPath, "");
|
|
16717
|
+
if (dirPath === "/") {
|
|
16718
|
+
const memory = await engine.memory.get(tenantId);
|
|
16719
|
+
entries.push({
|
|
16720
|
+
name: "memory.md",
|
|
16721
|
+
content: new Uint8Array(Buffer.from(memory.content, "utf-8")),
|
|
16722
|
+
mtime: memory.updatedAt > 0 ? new Date(memory.updatedAt) : void 0
|
|
16723
|
+
});
|
|
16724
|
+
}
|
|
16725
|
+
const archiveName = (dirPath === "/" ? "vfs" : dirPath.split("/").pop() || "archive") + ".zip";
|
|
16726
|
+
const zip = buildZip(entries);
|
|
16727
|
+
response.writeHead(200, {
|
|
16728
|
+
"Content-Type": "application/zip",
|
|
16729
|
+
"Content-Length": zip.length,
|
|
16730
|
+
"Content-Disposition": `attachment; filename="${archiveName}"`,
|
|
16731
|
+
"Cache-Control": "no-cache"
|
|
16732
|
+
});
|
|
16733
|
+
response.end(zip);
|
|
16734
|
+
} catch (err) {
|
|
16735
|
+
writeJson(response, 500, { code: "ARCHIVE_FAILED", message: err?.message ?? "Failed to build archive" });
|
|
16736
|
+
}
|
|
16737
|
+
return;
|
|
16738
|
+
}
|
|
16739
|
+
if (pathname === "/api/vfs-list" && request.method === "GET") {
|
|
16740
|
+
const dirPath = requestUrl.searchParams.get("path") ?? "/";
|
|
16741
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
16742
|
+
const engine = harness.storageEngine;
|
|
16743
|
+
if (!engine) {
|
|
16744
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
16745
|
+
return;
|
|
16746
|
+
}
|
|
16747
|
+
if (!isSafeVfsPath(dirPath)) {
|
|
16748
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
16749
|
+
return;
|
|
16750
|
+
}
|
|
16751
|
+
try {
|
|
16752
|
+
if (dirPath !== "/") {
|
|
16753
|
+
const stat2 = await engine.vfs.stat(tenantId, dirPath);
|
|
16754
|
+
if (!stat2 || stat2.type !== "directory") {
|
|
16755
|
+
writeJson(response, 404, { code: "NOT_FOUND", message: "Directory not found" });
|
|
16756
|
+
return;
|
|
16757
|
+
}
|
|
16758
|
+
}
|
|
16759
|
+
const rawDirEntries = await engine.vfs.readdir(tenantId, dirPath);
|
|
16760
|
+
const dirEntries = dirPath === "/" ? rawDirEntries.filter((e) => e.name !== "memory.md") : rawDirEntries;
|
|
16761
|
+
const entries = await Promise.all(dirEntries.map(async (entry) => {
|
|
16762
|
+
const childPath = dirPath === "/" ? "/" + entry.name : dirPath + "/" + entry.name;
|
|
16763
|
+
const stat2 = await engine.vfs.stat(tenantId, childPath);
|
|
16764
|
+
return {
|
|
16765
|
+
name: entry.name,
|
|
16766
|
+
type: entry.type,
|
|
16767
|
+
size: stat2?.size ?? 0,
|
|
16768
|
+
mimeType: stat2?.mimeType ?? null,
|
|
16769
|
+
updatedAt: stat2?.updatedAt ?? null
|
|
16770
|
+
};
|
|
16771
|
+
}));
|
|
16772
|
+
if (dirPath === "/") {
|
|
16773
|
+
const memory = await engine.memory.get(tenantId);
|
|
16774
|
+
entries.push({
|
|
16775
|
+
name: "memory.md",
|
|
16776
|
+
type: "file",
|
|
16777
|
+
size: Buffer.byteLength(memory.content, "utf-8"),
|
|
16778
|
+
mimeType: "text/markdown",
|
|
16779
|
+
updatedAt: memory.updatedAt > 0 ? memory.updatedAt : null
|
|
16780
|
+
});
|
|
16781
|
+
}
|
|
16782
|
+
entries.sort((a, b) => {
|
|
16783
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
16784
|
+
return a.name.localeCompare(b.name);
|
|
16785
|
+
});
|
|
16786
|
+
const usage = await engine.vfs.getUsage(tenantId);
|
|
16787
|
+
writeJson(response, 200, { path: dirPath, entries, usage });
|
|
16788
|
+
} catch (err) {
|
|
16789
|
+
writeJson(response, 500, { code: "READDIR_FAILED", message: err?.message ?? "Failed to list directory" });
|
|
16790
|
+
}
|
|
16791
|
+
return;
|
|
16792
|
+
}
|
|
16793
|
+
if (pathname === "/api/vfs-mkdir" && request.method === "POST") {
|
|
16794
|
+
const tenantId = ctx.tenantId ?? "__default__";
|
|
16795
|
+
const engine = harness.storageEngine;
|
|
16796
|
+
if (!engine) {
|
|
16797
|
+
writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
|
|
16798
|
+
return;
|
|
16799
|
+
}
|
|
16800
|
+
try {
|
|
16801
|
+
const chunks = [];
|
|
16802
|
+
for await (const chunk of request) chunks.push(chunk);
|
|
16803
|
+
const body = JSON.parse(Buffer.concat(chunks).toString() || "{}");
|
|
16804
|
+
const dirPath = body.path ?? "";
|
|
16805
|
+
if (!isSafeVfsPath(dirPath) || dirPath === "/") {
|
|
16806
|
+
writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
|
|
16807
|
+
return;
|
|
16808
|
+
}
|
|
16809
|
+
if (dirPath === MEMORY_VFILE_PATH) {
|
|
16810
|
+
writeJson(response, 400, { code: "RESERVED", message: "memory.md is reserved" });
|
|
16811
|
+
return;
|
|
16812
|
+
}
|
|
16813
|
+
const existing = await engine.vfs.stat(tenantId, dirPath);
|
|
16814
|
+
if (existing) {
|
|
16815
|
+
writeJson(response, 409, { code: "EXISTS", message: "Path already exists" });
|
|
16816
|
+
return;
|
|
16817
|
+
}
|
|
16818
|
+
await engine.vfs.mkdir(tenantId, dirPath, true);
|
|
16819
|
+
if (dirPath === "/skills" || dirPath.startsWith("/skills/")) {
|
|
16820
|
+
harness.invalidateSkillsForTenant(tenantId);
|
|
16821
|
+
}
|
|
16822
|
+
writeJson(response, 200, { path: dirPath });
|
|
16823
|
+
} catch (err) {
|
|
16824
|
+
writeJson(response, 500, { code: "MKDIR_FAILED", message: err?.message ?? "Failed to create directory" });
|
|
16825
|
+
}
|
|
16826
|
+
return;
|
|
16827
|
+
}
|
|
14946
16828
|
if (pathname === "/api/slash-commands" && request.method === "GET") {
|
|
14947
16829
|
const skills = harness.listSkills().map((s) => ({
|
|
14948
16830
|
command: "/" + s.name,
|
|
@@ -15209,6 +17091,7 @@ data: ${JSON.stringify(frame)}
|
|
|
15209
17091
|
let checkpointedRun = false;
|
|
15210
17092
|
let runCancelled = false;
|
|
15211
17093
|
let runContinuationMessages;
|
|
17094
|
+
let cancelHarnessMessages;
|
|
15212
17095
|
const turnTimestamp = Date.now();
|
|
15213
17096
|
const userMessage = userContent != null ? {
|
|
15214
17097
|
role: "user",
|
|
@@ -15288,6 +17171,7 @@ data: ${JSON.stringify(frame)}
|
|
|
15288
17171
|
}
|
|
15289
17172
|
if (event.type === "run:cancelled") {
|
|
15290
17173
|
runCancelled = true;
|
|
17174
|
+
if (event.messages) cancelHarnessMessages = event.messages;
|
|
15291
17175
|
}
|
|
15292
17176
|
if (event.type === "compaction:completed") {
|
|
15293
17177
|
if (event.compactedMessages) {
|
|
@@ -15404,7 +17288,13 @@ data: ${JSON.stringify(frame)}
|
|
|
15404
17288
|
if (abortController.signal.aborted || runCancelled) {
|
|
15405
17289
|
if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
|
|
15406
17290
|
conversation.messages = buildMessages();
|
|
15407
|
-
conversation
|
|
17291
|
+
applyTurnMetadata(conversation, {
|
|
17292
|
+
latestRunId,
|
|
17293
|
+
contextTokens: 0,
|
|
17294
|
+
contextWindow: 0,
|
|
17295
|
+
harnessMessages: cancelHarnessMessages,
|
|
17296
|
+
toolResultArchive: harness.getToolResultArchive(conversationId)
|
|
17297
|
+
}, { shouldRebuildCanonical: true });
|
|
15408
17298
|
await conversationStore.update(conversation);
|
|
15409
17299
|
}
|
|
15410
17300
|
if (!checkpointedRun) {
|
|
@@ -16065,6 +17955,9 @@ var buildCli = () => {
|
|
|
16065
17955
|
}
|
|
16066
17956
|
setLogLevel(level);
|
|
16067
17957
|
}
|
|
17958
|
+
if (options.verbose) {
|
|
17959
|
+
process.env.PONCHO_DEV_VERBOSE = "1";
|
|
17960
|
+
}
|
|
16068
17961
|
if (process.stdout.isTTY && !process.env.NO_COLOR) {
|
|
16069
17962
|
process.stdout.write("\n");
|
|
16070
17963
|
for (const line of getMascotLines()) process.stdout.write(`${line}
|
|
@@ -16231,7 +18124,7 @@ var buildCli = () => {
|
|
|
16231
18124
|
process.exitCode = 1;
|
|
16232
18125
|
}
|
|
16233
18126
|
});
|
|
16234
|
-
program.command("build").argument("[target]", "vercel|docker|lambda|fly").option("--force", "overwrite existing deployment files").description("Scaffold deployment files for a target").action(async (target, options) => {
|
|
18127
|
+
program.command("build").argument("[target]", "vercel|docker|lambda|fly|railway").option("--force", "overwrite existing deployment files").description("Scaffold deployment files for a target").action(async (target, options) => {
|
|
16235
18128
|
if (!target) {
|
|
16236
18129
|
return;
|
|
16237
18130
|
}
|