@poncho-ai/cli 0.38.1 → 0.39.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.
@@ -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: 12px;
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;
@@ -680,6 +1132,40 @@ var WEB_UI_STYLES = `
680
1132
  }
681
1133
  .topbar-new-chat:hover { color: var(--fg); }
682
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) {
@@ -4056,11 +4578,70 @@ var getWebUiClientScript = (markedSource2) => `
4056
4578
  );
4057
4579
  renderThreadPanelMessages();
4058
4580
  }
4059
- alert("Failed to send reply: " + (e && e.message ? e.message : "unknown"));
4581
+ alert("Failed to send reply: " + (e && e.message ? e.message : "unknown"));
4582
+ }
4583
+ };
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;
4060
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");
4061
4625
  };
4062
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
  }
@@ -6836,99 +7437,986 @@ var getWebUiClientScript = (markedSource2) => `
6836
7437
  return;
6837
7438
  }
6838
7439
 
6839
- // Individual approve/deny
6840
- const button = target.closest(".approval-action-btn");
6841
- if (!button) {
6842
- return;
6843
- }
6844
- const approvalId = button.getAttribute("data-approval-id") || "";
6845
- const decision = button.getAttribute("data-approval-decision") || "";
6846
- if (!approvalId || (decision !== "approve" && decision !== "deny")) {
7440
+ // Individual approve/deny
7441
+ const button = target.closest(".approval-action-btn");
7442
+ if (!button) {
7443
+ return;
7444
+ }
7445
+ const approvalId = button.getAttribute("data-approval-id") || "";
7446
+ const decision = button.getAttribute("data-approval-decision") || "";
7447
+ if (!approvalId || (decision !== "approve" && decision !== "deny")) {
7448
+ return;
7449
+ }
7450
+ if (state.approvalRequestsInFlight[approvalId]) {
7451
+ return;
7452
+ }
7453
+ const wasStreaming = state.isStreaming;
7454
+ if (!wasStreaming) {
7455
+ setStreaming(true);
7456
+ }
7457
+ submitApproval(approvalId, decision);
7458
+ renderMessages(state.activeMessages, state.isStreaming);
7459
+ loadConversations();
7460
+ if (!wasStreaming && state.activeConversationId) {
7461
+ const cid = state.activeConversationId;
7462
+ await streamConversationEvents(cid, { liveOnly: true });
7463
+ if (state.activeConversationId === cid) {
7464
+ pollUntilRunIdle(cid);
7465
+ }
7466
+ }
7467
+ });
7468
+
7469
+ elements.messages.addEventListener("click", (e) => {
7470
+ const link = e.target instanceof Element && e.target.closest(".subagent-link");
7471
+ if (!link) return;
7472
+ e.preventDefault();
7473
+ const subId = link.getAttribute("data-subagent-id");
7474
+ if (subId) {
7475
+ state.viewingSubagentId = subId;
7476
+ state.activeConversationId = subId;
7477
+ replaceConversationUrl(subId);
7478
+ loadConversation(subId);
7479
+ }
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();
6847
8239
  return;
6848
8240
  }
6849
- if (state.approvalRequestsInFlight[approvalId]) {
6850
- return;
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
+ }
6851
8251
  }
6852
- const wasStreaming = state.isStreaming;
6853
- if (!wasStreaming) {
6854
- setStreaming(true);
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);
6855
8258
  }
6856
- submitApproval(approvalId, decision);
6857
- renderMessages(state.activeMessages, state.isStreaming);
6858
- loadConversations();
6859
- if (!wasStreaming && state.activeConversationId) {
6860
- const cid = state.activeConversationId;
6861
- await streamConversationEvents(cid, { liveOnly: true });
6862
- if (state.activeConversationId === cid) {
6863
- pollUntilRunIdle(cid);
8259
+ try { await fetchDirEntries(parentDir); } catch {}
8260
+ renderFileExplorer();
8261
+ };
8262
+
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());
6864
8278
  }
8279
+ renderFileExplorer();
8280
+ } else {
8281
+ explorer.classList.add("hidden");
8282
+ list.classList.remove("hidden");
6865
8283
  }
6866
- });
8284
+ };
6867
8285
 
6868
- elements.messages.addEventListener("click", (e) => {
6869
- const link = e.target instanceof Element && e.target.closest(".subagent-link");
6870
- if (!link) return;
6871
- e.preventDefault();
6872
- const subId = link.getAttribute("data-subagent-id");
6873
- if (subId) {
6874
- state.viewingSubagentId = subId;
6875
- state.activeConversationId = subId;
6876
- replaceConversationUrl(subId);
6877
- loadConversation(subId);
6878
- }
6879
- });
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
+ };
6880
8304
 
6881
- elements.messages.addEventListener("scroll", () => {
6882
- state.isMessagesPinnedToBottom = isNearBottom(elements.messages);
6883
- }, { passive: true });
8305
+ const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
6884
8306
 
6885
- document.addEventListener("click", (event) => {
6886
- if (!(event.target instanceof Node)) {
6887
- return;
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
- if (!event.target.closest(".conversation-item") && state.confirmDeleteId) {
6890
- state.confirmDeleteId = null;
6891
- renderConversationList();
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 (!event.target.closest(".thread-row") && state.confirmDeleteThreadId) {
6894
- state.confirmDeleteThreadId = null;
6895
- renderMessages(state.activeMessages, state.isStreaming);
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 navigateToConversation = async (conversationId) => {
6904
- if (conversationId) {
6905
- state.activeConversationId = conversationId;
6906
- renderConversationList();
8331
+ const uploadFiles = async (files, targetDir) => {
8332
+ for (const file of files) {
8333
+ state.pendingUploads += 1;
8334
+ renderFileExplorer();
6907
8335
  try {
6908
- await loadConversation(conversationId);
6909
- } catch {
6910
- // Conversation not found \u2013 fall back to empty state
6911
- state.activeConversationId = null;
6912
- state.activeMessages = [];
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 "/";
8371
+ };
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
+ });
6928
8401
  };
6929
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)}
@@ -12691,7 +14205,7 @@ var runInteractive = async (workingDir, params) => {
12691
14205
  await harness.initialize();
12692
14206
  const identity = await ensureAgentIdentity2(workingDir);
12693
14207
  try {
12694
- const { runInteractiveInk } = await import("./run-interactive-ink-UKPUGCDW.js");
14208
+ const { runInteractiveInk } = await import("./run-interactive-ink-W5YJS7UH.js");
12695
14209
  await runInteractiveInk({
12696
14210
  harness,
12697
14211
  params,
@@ -12767,6 +14281,19 @@ var collectToolCallIds = (msgs) => {
12767
14281
  }
12768
14282
  return ids;
12769
14283
  };
14284
+ var MEMORY_VFILE_PATH = "/memory.md";
14285
+ var isSafeVfsPath = (p) => {
14286
+ if (typeof p !== "string" || p.length === 0) return false;
14287
+ if (!p.startsWith("/")) return false;
14288
+ if (p.includes("\0")) return false;
14289
+ const segments = p.split("/").slice(1);
14290
+ if (p !== "/" && segments[segments.length - 1] === "") return false;
14291
+ for (const seg of segments) {
14292
+ if (seg === "" && p !== "/") return false;
14293
+ if (seg === "." || seg === "..") return false;
14294
+ }
14295
+ return true;
14296
+ };
12770
14297
  var serverlessLog = createLogger("serverless");
12771
14298
  var __dirname3 = dirname7(fileURLToPath3(import.meta.url));
12772
14299
  var createRequestHandler = async (options) => {
@@ -12803,6 +14330,22 @@ var createRequestHandler = async (options) => {
12803
14330
  let activeConversationRuns = /* @__PURE__ */ new Map();
12804
14331
  const conversationEventStreams = /* @__PURE__ */ new Map();
12805
14332
  const conversationEventCallbacks = /* @__PURE__ */ new Map();
14333
+ const MAX_BUFFERED_EVENTS_PER_CONVERSATION = 1e3;
14334
+ const STRIP_LARGE_STRING_BYTES = 4096;
14335
+ const stripLargeStringsForBuffer = (value) => {
14336
+ if (typeof value === "string") {
14337
+ return value.length > STRIP_LARGE_STRING_BYTES ? `[stripped-for-replay len=${value.length}]` : value;
14338
+ }
14339
+ if (Array.isArray(value)) return value.map(stripLargeStringsForBuffer);
14340
+ if (value && typeof value === "object") {
14341
+ const out = {};
14342
+ for (const [k, v] of Object.entries(value)) {
14343
+ out[k] = stripLargeStringsForBuffer(v);
14344
+ }
14345
+ return out;
14346
+ }
14347
+ return value;
14348
+ };
12806
14349
  const broadcastEvent = (conversationId, event) => {
12807
14350
  let stream = conversationEventStreams.get(conversationId);
12808
14351
  if (!stream) {
@@ -12810,7 +14353,10 @@ var createRequestHandler = async (options) => {
12810
14353
  conversationEventStreams.set(conversationId, stream);
12811
14354
  }
12812
14355
  if (event.type !== "browser:frame") {
12813
- stream.buffer.push(event);
14356
+ stream.buffer.push(stripLargeStringsForBuffer(event));
14357
+ if (stream.buffer.length > MAX_BUFFERED_EVENTS_PER_CONVERSATION) {
14358
+ stream.buffer.splice(0, stream.buffer.length - MAX_BUFFERED_EVENTS_PER_CONVERSATION);
14359
+ }
12814
14360
  }
12815
14361
  for (const subscriber of stream.subscribers) {
12816
14362
  try {
@@ -13187,6 +14733,7 @@ data: ${JSON.stringify(statusPayload)}
13187
14733
  let runContextWindow = 0;
13188
14734
  let runContinuation2 = false;
13189
14735
  let runContinuationMessages;
14736
+ let runHarnessMessages;
13190
14737
  let runSteps = 0;
13191
14738
  let runMaxSteps;
13192
14739
  const buildMessages = () => {
@@ -13328,6 +14875,7 @@ data: ${JSON.stringify(statusPayload)}
13328
14875
  });
13329
14876
  runContinuation2 = execution.runContinuation;
13330
14877
  runContinuationMessages = execution.runContinuationMessages;
14878
+ runHarnessMessages = execution.runHarnessMessages;
13331
14879
  runSteps = execution.runSteps;
13332
14880
  runMaxSteps = execution.runMaxSteps;
13333
14881
  runContextTokens = execution.runContextTokens;
@@ -13349,7 +14897,11 @@ data: ${JSON.stringify(statusPayload)}
13349
14897
  contextWindow: runContextWindow,
13350
14898
  continuation: runContinuation2,
13351
14899
  continuationMessages: runContinuationMessages,
13352
- harnessMessages: runContinuationMessages,
14900
+ // Prefer the cancellation/end-of-run snapshot from the harness so
14901
+ // _harnessMessages stays in sync with what the model just saw,
14902
+ // even on aborted runs. Falls back to continuationMessages when
14903
+ // the run completed via continuation.
14904
+ harnessMessages: runHarnessMessages ?? runContinuationMessages,
13353
14905
  toolResultArchive: harness.getToolResultArchive(conversationId)
13354
14906
  }, { shouldRebuildCanonical: true });
13355
14907
  });
@@ -13751,7 +15303,7 @@ data: ${JSON.stringify(statusPayload)}
13751
15303
  selfBaseUrl = `${proto}://${request.headers.host}`;
13752
15304
  }
13753
15305
  if (webUiEnabled) {
13754
- if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
15306
+ if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/") || pathname.startsWith("/f/"))) {
13755
15307
  writeHtml(response, 200, renderWebUiHtml({ agentName, isDev: !isProduction }));
13756
15308
  return;
13757
15309
  }
@@ -14754,13 +16306,16 @@ data: ${JSON.stringify(frame)}
14754
16306
  const hasPendingCallbackResults = Array.isArray(conversation.pendingSubagentResults) && conversation.pendingSubagentResults.length > 0;
14755
16307
  const hasPendingApprovals = Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0;
14756
16308
  const needsContinuation = !hasActiveRun && Array.isArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0 && !hasPendingApprovals;
16309
+ const verboseDev = process.env.PONCHO_DEV_VERBOSE === "1";
14757
16310
  writeJson(response, 200, {
14758
16311
  conversation: {
14759
16312
  ...conversation,
14760
16313
  messages: conversation.messages.map(normalizeMessageForClient).filter((m) => m !== null),
14761
16314
  pendingApprovals: storedPending,
14762
16315
  _continuationMessages: void 0,
14763
- _harnessMessages: void 0,
16316
+ // In verbose dev mode the web UI exposes a toggle to inspect the
16317
+ // raw harness messages sent to the model API. Strip it otherwise.
16318
+ _harnessMessages: verboseDev ? conversation._harnessMessages : void 0,
14764
16319
  // The browser has no use for the archive; make sure we never ship
14765
16320
  // it back even if the conversation was loaded via getWithArchive.
14766
16321
  _toolResultArchive: void 0
@@ -14768,7 +16323,8 @@ data: ${JSON.stringify(frame)}
14768
16323
  subagentPendingApprovals: subagentPending,
14769
16324
  hasActiveRun: hasActiveRun || hasPendingCallbackResults,
14770
16325
  hasRunningSubagents,
14771
- needsContinuation
16326
+ needsContinuation,
16327
+ verboseDev
14772
16328
  });
14773
16329
  return;
14774
16330
  }
@@ -14897,6 +16453,22 @@ data: ${JSON.stringify(frame)}
14897
16453
  writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
14898
16454
  return;
14899
16455
  }
16456
+ if (vfsPath === MEMORY_VFILE_PATH) {
16457
+ try {
16458
+ const memory = await engine.memory.get(tenantId);
16459
+ const data = Buffer.from(memory.content, "utf-8");
16460
+ response.writeHead(200, {
16461
+ "Content-Type": "text/markdown; charset=utf-8",
16462
+ "Content-Length": data.length,
16463
+ "Content-Disposition": `inline; filename="memory.md"`,
16464
+ "Cache-Control": "no-cache"
16465
+ });
16466
+ response.end(data);
16467
+ } catch (err) {
16468
+ writeJson(response, 500, { code: "READ_FAILED", message: err?.message ?? "Failed to read memory" });
16469
+ }
16470
+ return;
16471
+ }
14900
16472
  try {
14901
16473
  const stat2 = await engine.vfs.stat(tenantId, vfsPath);
14902
16474
  if (!stat2 || stat2.type !== "file") {
@@ -14943,6 +16515,260 @@ data: ${JSON.stringify(frame)}
14943
16515
  }
14944
16516
  return;
14945
16517
  }
16518
+ if (vfsMatch && request.method === "PUT") {
16519
+ const rawPath = "/" + decodeURIComponent(vfsMatch[1] ?? "");
16520
+ const tenantId = ctx.tenantId ?? "__default__";
16521
+ const engine = harness.storageEngine;
16522
+ if (!engine) {
16523
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
16524
+ return;
16525
+ }
16526
+ if (!isSafeVfsPath(rawPath) || rawPath === "/") {
16527
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
16528
+ return;
16529
+ }
16530
+ if (rawPath === MEMORY_VFILE_PATH) {
16531
+ try {
16532
+ const chunks = [];
16533
+ for await (const chunk of request) chunks.push(chunk);
16534
+ const body = Buffer.concat(chunks);
16535
+ const content = body.toString("utf-8").trim();
16536
+ const memory = await engine.memory.update(content, tenantId);
16537
+ writeJson(response, 200, {
16538
+ path: MEMORY_VFILE_PATH,
16539
+ size: Buffer.byteLength(memory.content, "utf-8"),
16540
+ mimeType: "text/markdown",
16541
+ updatedAt: memory.updatedAt
16542
+ });
16543
+ } catch (err) {
16544
+ writeJson(response, 500, { code: "WRITE_FAILED", message: err?.message ?? "Failed to write memory" });
16545
+ }
16546
+ return;
16547
+ }
16548
+ const allowOverwrite = requestUrl.searchParams.get("overwrite") === "1";
16549
+ try {
16550
+ const existing = await engine.vfs.stat(tenantId, rawPath);
16551
+ if (existing && !allowOverwrite) {
16552
+ writeJson(response, 409, { code: "EXISTS", message: "File already exists" });
16553
+ return;
16554
+ }
16555
+ if (existing && existing.type !== "file") {
16556
+ writeJson(response, 409, { code: "NOT_A_FILE", message: "Path exists and is not a file" });
16557
+ return;
16558
+ }
16559
+ const chunks = [];
16560
+ for await (const chunk of request) chunks.push(chunk);
16561
+ const body = Buffer.concat(chunks);
16562
+ const mimeType = request.headers["content-type"]?.split(";")[0]?.trim() || void 0;
16563
+ await engine.vfs.writeFile(tenantId, rawPath, new Uint8Array(body), mimeType);
16564
+ if (rawPath === "/skills" || rawPath.startsWith("/skills/")) {
16565
+ harness.invalidateSkillsForTenant(tenantId);
16566
+ }
16567
+ const stat2 = await engine.vfs.stat(tenantId, rawPath);
16568
+ writeJson(response, 200, {
16569
+ path: rawPath,
16570
+ size: stat2?.size ?? body.length,
16571
+ mimeType: stat2?.mimeType ?? mimeType ?? null,
16572
+ updatedAt: stat2?.updatedAt ?? Date.now()
16573
+ });
16574
+ } catch (err) {
16575
+ const message = err?.message ?? "Upload failed";
16576
+ const code = /quota|too large|exceed/i.test(message) ? 413 : 500;
16577
+ writeJson(response, code, { code: code === 413 ? "QUOTA" : "WRITE_FAILED", message });
16578
+ }
16579
+ return;
16580
+ }
16581
+ if (vfsMatch && request.method === "DELETE") {
16582
+ const rawPath = "/" + decodeURIComponent(vfsMatch[1] ?? "");
16583
+ const tenantId = ctx.tenantId ?? "__default__";
16584
+ const engine = harness.storageEngine;
16585
+ if (!engine) {
16586
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
16587
+ return;
16588
+ }
16589
+ if (!isSafeVfsPath(rawPath) || rawPath === "/") {
16590
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
16591
+ return;
16592
+ }
16593
+ if (rawPath === MEMORY_VFILE_PATH) {
16594
+ writeJson(response, 400, {
16595
+ code: "RESERVED",
16596
+ message: "memory.md cannot be deleted; clear its contents instead."
16597
+ });
16598
+ return;
16599
+ }
16600
+ try {
16601
+ const stat2 = await engine.vfs.stat(tenantId, rawPath);
16602
+ if (!stat2) {
16603
+ writeJson(response, 404, { code: "NOT_FOUND", message: "Path not found" });
16604
+ return;
16605
+ }
16606
+ if (stat2.type === "directory") {
16607
+ await engine.vfs.deleteDir(tenantId, rawPath, true);
16608
+ } else {
16609
+ await engine.vfs.deleteFile(tenantId, rawPath);
16610
+ }
16611
+ if (rawPath === "/skills" || rawPath.startsWith("/skills/")) {
16612
+ harness.invalidateSkillsForTenant(tenantId);
16613
+ }
16614
+ writeJson(response, 200, { ok: true, path: rawPath });
16615
+ } catch (err) {
16616
+ writeJson(response, 500, { code: "DELETE_FAILED", message: err?.message ?? "Failed to delete" });
16617
+ }
16618
+ return;
16619
+ }
16620
+ if (pathname === "/api/vfs-archive" && request.method === "GET") {
16621
+ const dirPath = requestUrl.searchParams.get("path") ?? "/";
16622
+ const tenantId = ctx.tenantId ?? "__default__";
16623
+ const engine = harness.storageEngine;
16624
+ if (!engine) {
16625
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
16626
+ return;
16627
+ }
16628
+ if (!isSafeVfsPath(dirPath)) {
16629
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
16630
+ return;
16631
+ }
16632
+ try {
16633
+ if (dirPath !== "/") {
16634
+ const stat2 = await engine.vfs.stat(tenantId, dirPath);
16635
+ if (!stat2 || stat2.type !== "directory") {
16636
+ writeJson(response, 404, { code: "NOT_FOUND", message: "Directory not found" });
16637
+ return;
16638
+ }
16639
+ }
16640
+ const entries = [];
16641
+ const walk = async (dir, prefix) => {
16642
+ const children = await engine.vfs.readdir(tenantId, dir);
16643
+ for (const child of children) {
16644
+ if (dir === "/" && child.name === "memory.md") continue;
16645
+ const childPath = dir === "/" ? "/" + child.name : dir + "/" + child.name;
16646
+ const relName = prefix === "" ? child.name : prefix + "/" + child.name;
16647
+ if (child.type === "directory") {
16648
+ await walk(childPath, relName);
16649
+ } else if (child.type === "file") {
16650
+ const content = await engine.vfs.readFile(tenantId, childPath);
16651
+ const stat2 = await engine.vfs.stat(tenantId, childPath);
16652
+ entries.push({
16653
+ name: relName,
16654
+ content,
16655
+ mtime: stat2?.updatedAt ? new Date(stat2.updatedAt) : void 0
16656
+ });
16657
+ }
16658
+ }
16659
+ };
16660
+ await walk(dirPath, "");
16661
+ if (dirPath === "/") {
16662
+ const memory = await engine.memory.get(tenantId);
16663
+ entries.push({
16664
+ name: "memory.md",
16665
+ content: new Uint8Array(Buffer.from(memory.content, "utf-8")),
16666
+ mtime: memory.updatedAt > 0 ? new Date(memory.updatedAt) : void 0
16667
+ });
16668
+ }
16669
+ const archiveName = (dirPath === "/" ? "vfs" : dirPath.split("/").pop() || "archive") + ".zip";
16670
+ const zip = buildZip(entries);
16671
+ response.writeHead(200, {
16672
+ "Content-Type": "application/zip",
16673
+ "Content-Length": zip.length,
16674
+ "Content-Disposition": `attachment; filename="${archiveName}"`,
16675
+ "Cache-Control": "no-cache"
16676
+ });
16677
+ response.end(zip);
16678
+ } catch (err) {
16679
+ writeJson(response, 500, { code: "ARCHIVE_FAILED", message: err?.message ?? "Failed to build archive" });
16680
+ }
16681
+ return;
16682
+ }
16683
+ if (pathname === "/api/vfs-list" && request.method === "GET") {
16684
+ const dirPath = requestUrl.searchParams.get("path") ?? "/";
16685
+ const tenantId = ctx.tenantId ?? "__default__";
16686
+ const engine = harness.storageEngine;
16687
+ if (!engine) {
16688
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
16689
+ return;
16690
+ }
16691
+ if (!isSafeVfsPath(dirPath)) {
16692
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
16693
+ return;
16694
+ }
16695
+ try {
16696
+ if (dirPath !== "/") {
16697
+ const stat2 = await engine.vfs.stat(tenantId, dirPath);
16698
+ if (!stat2 || stat2.type !== "directory") {
16699
+ writeJson(response, 404, { code: "NOT_FOUND", message: "Directory not found" });
16700
+ return;
16701
+ }
16702
+ }
16703
+ const rawDirEntries = await engine.vfs.readdir(tenantId, dirPath);
16704
+ const dirEntries = dirPath === "/" ? rawDirEntries.filter((e) => e.name !== "memory.md") : rawDirEntries;
16705
+ const entries = await Promise.all(dirEntries.map(async (entry) => {
16706
+ const childPath = dirPath === "/" ? "/" + entry.name : dirPath + "/" + entry.name;
16707
+ const stat2 = await engine.vfs.stat(tenantId, childPath);
16708
+ return {
16709
+ name: entry.name,
16710
+ type: entry.type,
16711
+ size: stat2?.size ?? 0,
16712
+ mimeType: stat2?.mimeType ?? null,
16713
+ updatedAt: stat2?.updatedAt ?? null
16714
+ };
16715
+ }));
16716
+ if (dirPath === "/") {
16717
+ const memory = await engine.memory.get(tenantId);
16718
+ entries.push({
16719
+ name: "memory.md",
16720
+ type: "file",
16721
+ size: Buffer.byteLength(memory.content, "utf-8"),
16722
+ mimeType: "text/markdown",
16723
+ updatedAt: memory.updatedAt > 0 ? memory.updatedAt : null
16724
+ });
16725
+ }
16726
+ entries.sort((a, b) => {
16727
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
16728
+ return a.name.localeCompare(b.name);
16729
+ });
16730
+ const usage = await engine.vfs.getUsage(tenantId);
16731
+ writeJson(response, 200, { path: dirPath, entries, usage });
16732
+ } catch (err) {
16733
+ writeJson(response, 500, { code: "READDIR_FAILED", message: err?.message ?? "Failed to list directory" });
16734
+ }
16735
+ return;
16736
+ }
16737
+ if (pathname === "/api/vfs-mkdir" && request.method === "POST") {
16738
+ const tenantId = ctx.tenantId ?? "__default__";
16739
+ const engine = harness.storageEngine;
16740
+ if (!engine) {
16741
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
16742
+ return;
16743
+ }
16744
+ try {
16745
+ const chunks = [];
16746
+ for await (const chunk of request) chunks.push(chunk);
16747
+ const body = JSON.parse(Buffer.concat(chunks).toString() || "{}");
16748
+ const dirPath = body.path ?? "";
16749
+ if (!isSafeVfsPath(dirPath) || dirPath === "/") {
16750
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
16751
+ return;
16752
+ }
16753
+ if (dirPath === MEMORY_VFILE_PATH) {
16754
+ writeJson(response, 400, { code: "RESERVED", message: "memory.md is reserved" });
16755
+ return;
16756
+ }
16757
+ const existing = await engine.vfs.stat(tenantId, dirPath);
16758
+ if (existing) {
16759
+ writeJson(response, 409, { code: "EXISTS", message: "Path already exists" });
16760
+ return;
16761
+ }
16762
+ await engine.vfs.mkdir(tenantId, dirPath, true);
16763
+ if (dirPath === "/skills" || dirPath.startsWith("/skills/")) {
16764
+ harness.invalidateSkillsForTenant(tenantId);
16765
+ }
16766
+ writeJson(response, 200, { path: dirPath });
16767
+ } catch (err) {
16768
+ writeJson(response, 500, { code: "MKDIR_FAILED", message: err?.message ?? "Failed to create directory" });
16769
+ }
16770
+ return;
16771
+ }
14946
16772
  if (pathname === "/api/slash-commands" && request.method === "GET") {
14947
16773
  const skills = harness.listSkills().map((s) => ({
14948
16774
  command: "/" + s.name,
@@ -15209,6 +17035,7 @@ data: ${JSON.stringify(frame)}
15209
17035
  let checkpointedRun = false;
15210
17036
  let runCancelled = false;
15211
17037
  let runContinuationMessages;
17038
+ let cancelHarnessMessages;
15212
17039
  const turnTimestamp = Date.now();
15213
17040
  const userMessage = userContent != null ? {
15214
17041
  role: "user",
@@ -15288,6 +17115,7 @@ data: ${JSON.stringify(frame)}
15288
17115
  }
15289
17116
  if (event.type === "run:cancelled") {
15290
17117
  runCancelled = true;
17118
+ if (event.messages) cancelHarnessMessages = event.messages;
15291
17119
  }
15292
17120
  if (event.type === "compaction:completed") {
15293
17121
  if (event.compactedMessages) {
@@ -15404,7 +17232,13 @@ data: ${JSON.stringify(frame)}
15404
17232
  if (abortController.signal.aborted || runCancelled) {
15405
17233
  if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
15406
17234
  conversation.messages = buildMessages();
15407
- conversation.updatedAt = Date.now();
17235
+ applyTurnMetadata(conversation, {
17236
+ latestRunId,
17237
+ contextTokens: 0,
17238
+ contextWindow: 0,
17239
+ harnessMessages: cancelHarnessMessages,
17240
+ toolResultArchive: harness.getToolResultArchive(conversationId)
17241
+ }, { shouldRebuildCanonical: true });
15408
17242
  await conversationStore.update(conversation);
15409
17243
  }
15410
17244
  if (!checkpointedRun) {
@@ -16065,6 +17899,9 @@ var buildCli = () => {
16065
17899
  }
16066
17900
  setLogLevel(level);
16067
17901
  }
17902
+ if (options.verbose) {
17903
+ process.env.PONCHO_DEV_VERBOSE = "1";
17904
+ }
16068
17905
  if (process.stdout.isTTY && !process.env.NO_COLOR) {
16069
17906
  process.stdout.write("\n");
16070
17907
  for (const line of getMascotLines()) process.stdout.write(`${line}