instar 0.18.4 → 0.18.6

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.
Files changed (55) hide show
  1. package/dashboard/index.html +1448 -1
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +44 -0
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  6. package/dist/core/PostUpdateMigrator.js +50 -0
  7. package/dist/core/PostUpdateMigrator.js.map +1 -1
  8. package/dist/core/types.d.ts +22 -0
  9. package/dist/core/types.d.ts.map +1 -1
  10. package/dist/index.d.ts +9 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/knowledge/ProbeRegistry.d.ts +54 -0
  15. package/dist/knowledge/ProbeRegistry.d.ts.map +1 -0
  16. package/dist/knowledge/ProbeRegistry.js +119 -0
  17. package/dist/knowledge/ProbeRegistry.js.map +1 -0
  18. package/dist/knowledge/SelfKnowledgeTree.d.ts +100 -0
  19. package/dist/knowledge/SelfKnowledgeTree.d.ts.map +1 -0
  20. package/dist/knowledge/SelfKnowledgeTree.js +476 -0
  21. package/dist/knowledge/SelfKnowledgeTree.js.map +1 -0
  22. package/dist/knowledge/TreeGenerator.d.ts +57 -0
  23. package/dist/knowledge/TreeGenerator.d.ts.map +1 -0
  24. package/dist/knowledge/TreeGenerator.js +486 -0
  25. package/dist/knowledge/TreeGenerator.js.map +1 -0
  26. package/dist/knowledge/TreeSynthesis.d.ts +24 -0
  27. package/dist/knowledge/TreeSynthesis.d.ts.map +1 -0
  28. package/dist/knowledge/TreeSynthesis.js +61 -0
  29. package/dist/knowledge/TreeSynthesis.js.map +1 -0
  30. package/dist/knowledge/TreeTraversal.d.ts +91 -0
  31. package/dist/knowledge/TreeTraversal.d.ts.map +1 -0
  32. package/dist/knowledge/TreeTraversal.js +347 -0
  33. package/dist/knowledge/TreeTraversal.js.map +1 -0
  34. package/dist/knowledge/TreeTriage.d.ts +32 -0
  35. package/dist/knowledge/TreeTriage.d.ts.map +1 -0
  36. package/dist/knowledge/TreeTriage.js +127 -0
  37. package/dist/knowledge/TreeTriage.js.map +1 -0
  38. package/dist/knowledge/types.d.ts +180 -0
  39. package/dist/knowledge/types.d.ts.map +1 -0
  40. package/dist/knowledge/types.js +28 -0
  41. package/dist/knowledge/types.js.map +1 -0
  42. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  43. package/dist/messaging/TelegramAdapter.js +18 -2
  44. package/dist/messaging/TelegramAdapter.js.map +1 -1
  45. package/dist/server/AgentServer.d.ts.map +1 -1
  46. package/dist/server/AgentServer.js +4 -0
  47. package/dist/server/AgentServer.js.map +1 -1
  48. package/dist/server/fileRoutes.d.ts +16 -0
  49. package/dist/server/fileRoutes.d.ts.map +1 -0
  50. package/dist/server/fileRoutes.js +488 -0
  51. package/dist/server/fileRoutes.js.map +1 -0
  52. package/package.json +1 -1
  53. package/src/data/builtin-manifest.json +17 -17
  54. package/upgrades/0.18.5.md +25 -0
  55. package/upgrades/0.18.6.md +25 -0
@@ -6,6 +6,8 @@
6
6
  <title>Instar Dashboard</title>
7
7
  <link rel="icon" type="image/png" href="/dashboard/favicon.png">
8
8
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github-dark.min.css"
10
+ integrity="sha384-wH75j6z1lH97ZOpMOInqhgKzFkAInZPPSPlZpYKYTOqsaizPvhQZmAtLcPKXpLyH" crossorigin="anonymous">
9
11
  <style>
10
12
  :root {
11
13
  --bg: #0a0a0a;
@@ -522,6 +524,569 @@
522
524
  ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
523
525
  ::-webkit-scrollbar-thumb:hover { background: #444; }
524
526
 
527
+ /* ── Tab bar ──────────────────────────────────────────── */
528
+ .tab-bar {
529
+ display: flex;
530
+ gap: 0;
531
+ margin-left: 24px;
532
+ }
533
+
534
+ .tab-bar .tab {
535
+ padding: 6px 16px;
536
+ border: 1px solid var(--border);
537
+ border-radius: 6px 6px 0 0;
538
+ background: var(--bg);
539
+ color: var(--text-dim);
540
+ font-size: 13px;
541
+ font-weight: 500;
542
+ cursor: pointer;
543
+ border-bottom: none;
544
+ margin-bottom: -1px;
545
+ transition: color 0.15s, background 0.15s;
546
+ }
547
+
548
+ .tab-bar .tab:hover {
549
+ color: var(--text);
550
+ background: var(--bg-hover);
551
+ }
552
+
553
+ .tab-bar .tab.active {
554
+ background: var(--bg-panel);
555
+ color: var(--accent);
556
+ border-color: var(--border);
557
+ }
558
+
559
+ .tab-bar .tab-count {
560
+ font-size: 11px;
561
+ padding: 1px 6px;
562
+ border-radius: 8px;
563
+ background: var(--border);
564
+ color: var(--text-dim);
565
+ margin-left: 4px;
566
+ }
567
+
568
+ .tab-content {
569
+ display: contents;
570
+ }
571
+
572
+ .tab-content.hidden {
573
+ display: none !important;
574
+ }
575
+
576
+ /* ── File viewer ─────────────────────────────────────── */
577
+ .files-container {
578
+ display: flex;
579
+ grid-column: 1 / -1;
580
+ overflow: hidden;
581
+ background: var(--bg);
582
+ }
583
+
584
+ .file-tree {
585
+ width: 280px;
586
+ min-width: 280px;
587
+ border-right: 1px solid var(--border);
588
+ background: var(--bg-panel);
589
+ overflow-y: auto;
590
+ display: flex;
591
+ flex-direction: column;
592
+ }
593
+
594
+ .file-tree-header {
595
+ padding: 16px;
596
+ border-bottom: 1px solid var(--border);
597
+ display: flex;
598
+ align-items: center;
599
+ justify-content: space-between;
600
+ }
601
+
602
+ .file-tree-header h2 {
603
+ font-size: 13px;
604
+ font-weight: 600;
605
+ color: var(--text-dim);
606
+ text-transform: uppercase;
607
+ letter-spacing: 0.05em;
608
+ }
609
+
610
+ .file-tree-list {
611
+ flex: 1;
612
+ overflow-y: auto;
613
+ padding: 8px;
614
+ }
615
+
616
+ .tree-item {
617
+ display: flex;
618
+ align-items: center;
619
+ gap: 8px;
620
+ padding: 8px 12px;
621
+ border-radius: 6px;
622
+ cursor: pointer;
623
+ font-size: 13px;
624
+ color: var(--text);
625
+ min-height: 44px;
626
+ transition: background 0.15s;
627
+ user-select: none;
628
+ }
629
+
630
+ .tree-item:hover {
631
+ background: var(--bg-hover);
632
+ }
633
+
634
+ .tree-item.active {
635
+ background: var(--bg-active);
636
+ color: var(--text-bright);
637
+ }
638
+
639
+ .tree-item .icon {
640
+ font-size: 14px;
641
+ flex-shrink: 0;
642
+ width: 20px;
643
+ text-align: center;
644
+ }
645
+
646
+ .tree-item .name {
647
+ flex: 1;
648
+ white-space: nowrap;
649
+ overflow: hidden;
650
+ text-overflow: ellipsis;
651
+ }
652
+
653
+ .tree-item .chevron {
654
+ font-size: 10px;
655
+ color: var(--text-dim);
656
+ transition: transform 0.15s;
657
+ flex-shrink: 0;
658
+ }
659
+
660
+ .tree-item .chevron.open {
661
+ transform: rotate(90deg);
662
+ }
663
+
664
+ .tree-children {
665
+ padding-left: 16px;
666
+ }
667
+
668
+ .tree-children.collapsed {
669
+ display: none;
670
+ }
671
+
672
+ .file-content-panel {
673
+ flex: 1;
674
+ display: flex;
675
+ flex-direction: column;
676
+ overflow: hidden;
677
+ background: var(--bg);
678
+ }
679
+
680
+ .file-content-header {
681
+ display: flex;
682
+ align-items: center;
683
+ justify-content: space-between;
684
+ padding: 10px 16px;
685
+ border-bottom: 1px solid var(--border);
686
+ background: var(--bg-panel);
687
+ min-height: 44px;
688
+ gap: 8px;
689
+ }
690
+
691
+ .breadcrumbs {
692
+ display: flex;
693
+ align-items: center;
694
+ gap: 4px;
695
+ font-size: 13px;
696
+ color: var(--text-dim);
697
+ flex: 1;
698
+ min-width: 0;
699
+ overflow: hidden;
700
+ }
701
+
702
+ .breadcrumbs .crumb {
703
+ cursor: pointer;
704
+ color: var(--accent-dim);
705
+ white-space: nowrap;
706
+ }
707
+
708
+ .breadcrumbs .crumb:hover {
709
+ color: var(--accent);
710
+ text-decoration: underline;
711
+ }
712
+
713
+ .breadcrumbs .crumb.current {
714
+ color: var(--text-bright);
715
+ cursor: default;
716
+ }
717
+
718
+ .breadcrumbs .crumb.current:hover {
719
+ text-decoration: none;
720
+ }
721
+
722
+ .breadcrumbs .sep {
723
+ color: var(--text-dim);
724
+ flex-shrink: 0;
725
+ }
726
+
727
+ .file-badges {
728
+ display: flex;
729
+ gap: 6px;
730
+ flex-shrink: 0;
731
+ }
732
+
733
+ .file-badge {
734
+ font-size: 11px;
735
+ padding: 2px 8px;
736
+ border-radius: 4px;
737
+ font-weight: 500;
738
+ }
739
+
740
+ .file-badge.readonly {
741
+ background: #1b2e4e;
742
+ color: #60a5fa;
743
+ }
744
+
745
+ .file-badge.editable {
746
+ background: #1b3e1b;
747
+ color: #86efac;
748
+ }
749
+
750
+ .file-badge.copy-btn {
751
+ background: var(--bg);
752
+ color: var(--text);
753
+ border: 1px solid var(--border);
754
+ cursor: pointer;
755
+ }
756
+
757
+ .file-badge.copy-btn:hover {
758
+ border-color: #333;
759
+ background: var(--bg-hover);
760
+ }
761
+
762
+ .file-content-body {
763
+ flex: 1;
764
+ overflow-y: auto;
765
+ padding: 16px;
766
+ }
767
+
768
+ .file-content-body .markdown-content {
769
+ line-height: 1.6;
770
+ color: var(--text);
771
+ }
772
+
773
+ .file-content-body .markdown-content h1,
774
+ .file-content-body .markdown-content h2,
775
+ .file-content-body .markdown-content h3,
776
+ .file-content-body .markdown-content h4 {
777
+ color: var(--text-bright);
778
+ margin-top: 1.5em;
779
+ margin-bottom: 0.5em;
780
+ }
781
+
782
+ .file-content-body .markdown-content h1 { font-size: 1.5em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
783
+ .file-content-body .markdown-content h2 { font-size: 1.3em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
784
+ .file-content-body .markdown-content h3 { font-size: 1.1em; }
785
+
786
+ .file-content-body .markdown-content p { margin: 0.8em 0; }
787
+
788
+ .file-content-body .markdown-content a {
789
+ color: var(--accent);
790
+ text-decoration: none;
791
+ }
792
+
793
+ .file-content-body .markdown-content a:hover {
794
+ text-decoration: underline;
795
+ }
796
+
797
+ .file-content-body .markdown-content code {
798
+ background: #1a1a1a;
799
+ padding: 2px 6px;
800
+ border-radius: 4px;
801
+ font-family: 'SF Mono', 'Fira Code', monospace;
802
+ font-size: 0.9em;
803
+ }
804
+
805
+ .file-content-body .markdown-content pre {
806
+ background: #1a1a1a;
807
+ border: 1px solid var(--border);
808
+ border-radius: 6px;
809
+ padding: 12px;
810
+ overflow-x: auto;
811
+ margin: 1em 0;
812
+ }
813
+
814
+ .file-content-body .markdown-content pre code {
815
+ background: none;
816
+ padding: 0;
817
+ font-size: 13px;
818
+ }
819
+
820
+ .file-content-body .markdown-content blockquote {
821
+ border-left: 3px solid var(--accent-dim);
822
+ padding-left: 12px;
823
+ color: var(--text-dim);
824
+ margin: 1em 0;
825
+ }
826
+
827
+ .file-content-body .markdown-content table {
828
+ border-collapse: collapse;
829
+ width: 100%;
830
+ margin: 1em 0;
831
+ }
832
+
833
+ .file-content-body .markdown-content th,
834
+ .file-content-body .markdown-content td {
835
+ border: 1px solid var(--border);
836
+ padding: 6px 12px;
837
+ text-align: left;
838
+ }
839
+
840
+ .file-content-body .markdown-content th {
841
+ background: var(--bg-panel);
842
+ color: var(--text-bright);
843
+ }
844
+
845
+ .file-content-body .markdown-content ul,
846
+ .file-content-body .markdown-content ol {
847
+ margin: 0.8em 0;
848
+ padding-left: 1.5em;
849
+ }
850
+
851
+ .file-content-body .markdown-content li {
852
+ margin: 0.3em 0;
853
+ }
854
+
855
+ .file-content-body .markdown-content img {
856
+ max-width: 100%;
857
+ }
858
+
859
+ .file-content-body .code-content {
860
+ font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
861
+ font-size: 13px;
862
+ line-height: 1.5;
863
+ white-space: pre;
864
+ overflow-x: auto;
865
+ color: var(--text);
866
+ tab-size: 2;
867
+ }
868
+
869
+ .file-content-body .code-content pre {
870
+ margin: 0;
871
+ }
872
+
873
+ .file-empty {
874
+ display: flex;
875
+ flex-direction: column;
876
+ align-items: center;
877
+ justify-content: center;
878
+ height: 100%;
879
+ color: var(--text-dim);
880
+ gap: 12px;
881
+ }
882
+
883
+ .file-empty .icon { font-size: 48px; opacity: 0.3; }
884
+ .file-empty p { font-size: 14px; text-align: center; }
885
+
886
+ .file-loading {
887
+ display: flex;
888
+ align-items: center;
889
+ justify-content: center;
890
+ height: 100%;
891
+ color: var(--text-dim);
892
+ font-size: 14px;
893
+ }
894
+
895
+ .file-error {
896
+ padding: 16px;
897
+ color: var(--red);
898
+ font-size: 14px;
899
+ }
900
+
901
+ /* File viewer back button (mobile) */
902
+ .files-back-btn {
903
+ display: none;
904
+ padding: 6px 12px;
905
+ border-radius: 6px;
906
+ border: 1px solid var(--border);
907
+ background: var(--bg);
908
+ color: var(--text);
909
+ font-size: 13px;
910
+ cursor: pointer;
911
+ margin-right: 8px;
912
+ flex-shrink: 0;
913
+ }
914
+
915
+ .files-back-btn:hover { background: var(--bg-hover); }
916
+
917
+ /* ── Editor mode (Phase 2) ───────────────────────────────── */
918
+ .file-editor {
919
+ display: flex;
920
+ flex-direction: column;
921
+ flex: 1;
922
+ overflow: hidden;
923
+ }
924
+
925
+ .file-editor textarea {
926
+ flex: 1;
927
+ width: 100%;
928
+ border: none;
929
+ background: #0d0d0d;
930
+ color: var(--text);
931
+ font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
932
+ font-size: 14px;
933
+ line-height: 1.5;
934
+ padding: 16px;
935
+ resize: none;
936
+ outline: none;
937
+ tab-size: 2;
938
+ box-sizing: border-box;
939
+ }
940
+
941
+ /* iOS zoom prevention: 16px minimum */
942
+ @supports (-webkit-touch-callout: none) {
943
+ .file-editor textarea { font-size: 16px; }
944
+ }
945
+
946
+ .editor-toolbar {
947
+ display: flex;
948
+ align-items: center;
949
+ gap: 8px;
950
+ padding: 8px 16px;
951
+ border-top: 1px solid var(--border);
952
+ background: var(--bg-panel);
953
+ flex-shrink: 0;
954
+ }
955
+
956
+ .editor-toolbar .btn {
957
+ padding: 6px 16px;
958
+ border-radius: 6px;
959
+ border: 1px solid var(--border);
960
+ font-size: 13px;
961
+ font-weight: 500;
962
+ cursor: pointer;
963
+ min-height: 36px;
964
+ }
965
+
966
+ .editor-toolbar .btn-save {
967
+ background: var(--accent);
968
+ color: #000;
969
+ border-color: var(--accent);
970
+ }
971
+
972
+ .editor-toolbar .btn-save:hover { opacity: 0.9; }
973
+
974
+ .editor-toolbar .btn-cancel {
975
+ background: var(--bg);
976
+ color: var(--text);
977
+ }
978
+
979
+ .editor-toolbar .btn-cancel:hover { background: var(--bg-hover); }
980
+
981
+ .editor-toolbar .shortcut-hint {
982
+ font-size: 11px;
983
+ color: var(--text-dim);
984
+ margin-left: auto;
985
+ }
986
+
987
+ .editor-toolbar .save-status {
988
+ font-size: 12px;
989
+ margin-left: 8px;
990
+ }
991
+
992
+ .editor-toolbar .save-status.saving { color: var(--text-dim); }
993
+ .editor-toolbar .save-status.saved { color: var(--accent); }
994
+ .editor-toolbar .save-status.error { color: var(--red); }
995
+
996
+ /* Toast notifications */
997
+ .toast-container {
998
+ position: fixed;
999
+ bottom: 24px;
1000
+ right: 24px;
1001
+ z-index: 9999;
1002
+ display: flex;
1003
+ flex-direction: column;
1004
+ gap: 8px;
1005
+ }
1006
+
1007
+ .toast {
1008
+ padding: 10px 16px;
1009
+ border-radius: 8px;
1010
+ font-size: 13px;
1011
+ color: #fff;
1012
+ animation: toastIn 0.2s ease-out;
1013
+ max-width: 360px;
1014
+ }
1015
+
1016
+ .toast.success { background: #166534; border: 1px solid #22c55e; }
1017
+ .toast.error { background: #7f1d1d; border: 1px solid #ef4444; }
1018
+ .toast.warning { background: #713f12; border: 1px solid #eab308; }
1019
+
1020
+ @keyframes toastIn {
1021
+ from { opacity: 0; transform: translateY(8px); }
1022
+ to { opacity: 1; transform: translateY(0); }
1023
+ }
1024
+
1025
+ /* Conflict dialog */
1026
+ .conflict-overlay {
1027
+ position: fixed;
1028
+ inset: 0;
1029
+ background: rgba(0,0,0,0.7);
1030
+ z-index: 10000;
1031
+ display: flex;
1032
+ align-items: center;
1033
+ justify-content: center;
1034
+ }
1035
+
1036
+ .conflict-dialog {
1037
+ background: var(--bg-panel);
1038
+ border: 1px solid var(--border);
1039
+ border-radius: 12px;
1040
+ padding: 24px;
1041
+ max-width: 400px;
1042
+ width: 90%;
1043
+ }
1044
+
1045
+ .conflict-dialog h3 {
1046
+ color: #eab308;
1047
+ margin: 0 0 12px;
1048
+ font-size: 16px;
1049
+ }
1050
+
1051
+ .conflict-dialog p {
1052
+ color: var(--text-dim);
1053
+ font-size: 13px;
1054
+ margin: 0 0 16px;
1055
+ line-height: 1.5;
1056
+ }
1057
+
1058
+ .conflict-dialog .conflict-actions {
1059
+ display: flex;
1060
+ gap: 8px;
1061
+ flex-wrap: wrap;
1062
+ }
1063
+
1064
+ .conflict-dialog .conflict-actions .btn {
1065
+ padding: 8px 16px;
1066
+ border-radius: 6px;
1067
+ border: 1px solid var(--border);
1068
+ font-size: 13px;
1069
+ cursor: pointer;
1070
+ min-height: 36px;
1071
+ }
1072
+
1073
+ .conflict-dialog .btn-overwrite {
1074
+ background: #7f1d1d;
1075
+ color: #fff;
1076
+ border-color: #ef4444;
1077
+ }
1078
+
1079
+ .conflict-dialog .btn-reload {
1080
+ background: var(--accent);
1081
+ color: #000;
1082
+ border-color: var(--accent);
1083
+ }
1084
+
1085
+ .conflict-dialog .btn-dismiss {
1086
+ background: var(--bg);
1087
+ color: var(--text);
1088
+ }
1089
+
525
1090
  /* ── Mobile responsive ─────────────────────────────────── */
526
1091
  @media (max-width: 768px) {
527
1092
  .app {
@@ -644,6 +1209,68 @@
644
1209
  .sidebar-header {
645
1210
  padding: 12px 14px;
646
1211
  }
1212
+
1213
+ /* Tab bar compact */
1214
+ .tab-bar {
1215
+ margin-left: 12px;
1216
+ }
1217
+
1218
+ .tab-bar .tab {
1219
+ padding: 5px 12px;
1220
+ font-size: 12px;
1221
+ }
1222
+
1223
+ /* File viewer mobile */
1224
+ .files-container {
1225
+ flex-direction: column;
1226
+ }
1227
+
1228
+ .file-tree {
1229
+ width: 100%;
1230
+ min-width: 100%;
1231
+ border-right: none;
1232
+ border-bottom: 1px solid var(--border);
1233
+ }
1234
+
1235
+ .app.files-active .file-tree {
1236
+ display: none;
1237
+ }
1238
+
1239
+ .app:not(.files-active) .file-content-panel {
1240
+ display: none;
1241
+ }
1242
+
1243
+ .app.files-active .files-back-btn {
1244
+ display: inline-block;
1245
+ }
1246
+
1247
+ .tree-item {
1248
+ padding: 12px 12px;
1249
+ font-size: 15px;
1250
+ }
1251
+
1252
+ .file-content-body {
1253
+ padding: 12px;
1254
+ }
1255
+
1256
+ .file-content-body .code-content {
1257
+ font-size: 12px;
1258
+ }
1259
+
1260
+ /* Editor mobile */
1261
+ .editor-toolbar {
1262
+ padding: 8px 12px;
1263
+ flex-wrap: wrap;
1264
+ }
1265
+
1266
+ .editor-toolbar .shortcut-hint {
1267
+ display: none;
1268
+ }
1269
+
1270
+ .editor-toolbar .btn {
1271
+ min-height: 44px;
1272
+ padding: 10px 16px;
1273
+ }
647
1274
  }
648
1275
  </style>
649
1276
  </head>
@@ -665,6 +1292,10 @@
665
1292
  <div class="header-left">
666
1293
  <img class="logo" src="/dashboard/logo.png" alt="Instar">
667
1294
  <h1>Instar Dashboard</h1>
1295
+ <nav class="tab-bar">
1296
+ <button class="tab active" data-tab="sessions" onclick="switchTab('sessions')">Sessions <span class="tab-count" id="tabSessionCount">0</span></button>
1297
+ <button class="tab" data-tab="files" onclick="switchTab('files')">Files</button>
1298
+ </nav>
668
1299
  </div>
669
1300
  <button class="wa-status-btn" id="waStatusBtn" onclick="toggleQrPanel()">WhatsApp</button>
670
1301
  <div class="status-badge" id="connStatus">
@@ -673,7 +1304,8 @@
673
1304
  </div>
674
1305
  </header>
675
1306
 
676
- <aside class="sidebar">
1307
+ <!-- Sessions tab -->
1308
+ <aside class="sidebar" id="sessionsTab">
677
1309
  <div class="sidebar-header">
678
1310
  <h2>Sessions</h2>
679
1311
  <span class="session-count" id="sessionCount">0</span>
@@ -721,6 +1353,47 @@
721
1353
  </div>
722
1354
  </div>
723
1355
  </main>
1356
+
1357
+ <!-- Files tab -->
1358
+ <div class="files-container" id="filesTab" style="display:none">
1359
+ <div class="file-tree" id="fileTree">
1360
+ <div class="file-tree-header">
1361
+ <h2>Files</h2>
1362
+ </div>
1363
+ <div class="file-tree-list" id="fileTreeList">
1364
+ <div class="file-loading" id="fileTreeLoading">Loading...</div>
1365
+ </div>
1366
+ </div>
1367
+ <div class="file-content-panel" id="fileContentPanel">
1368
+ <div class="file-content-header" id="fileContentHeader" style="display:none">
1369
+ <button class="files-back-btn" onclick="filesGoBack()">&larr;</button>
1370
+ <div class="breadcrumbs" id="fileBreadcrumbs"></div>
1371
+ <div class="file-badges" id="fileBadges"></div>
1372
+ </div>
1373
+ <div class="file-content-body" id="fileContentBody">
1374
+ <div class="file-empty" id="fileEmptyState">
1375
+ <div class="icon">&#x1F4C2;</div>
1376
+ <p>Select a file to view its contents</p>
1377
+ </div>
1378
+ </div>
1379
+ </div>
1380
+ </div>
1381
+ </div>
1382
+
1383
+ <!-- Toast notifications -->
1384
+ <div class="toast-container" id="toastContainer"></div>
1385
+
1386
+ <!-- Conflict resolution dialog -->
1387
+ <div class="conflict-overlay" id="conflictOverlay" style="display:none">
1388
+ <div class="conflict-dialog">
1389
+ <h3>File Modified Externally</h3>
1390
+ <p id="conflictMessage">This file was modified by another process since you started editing.</p>
1391
+ <div class="conflict-actions">
1392
+ <button class="btn btn-overwrite" onclick="handleConflictOverwrite()">Overwrite</button>
1393
+ <button class="btn btn-reload" onclick="handleConflictReload()">Reload</button>
1394
+ <button class="btn btn-dismiss" onclick="hideConflictDialog()">Cancel</button>
1395
+ </div>
1396
+ </div>
724
1397
  </div>
725
1398
 
726
1399
  <!-- WhatsApp QR panel (hidden by default) -->
@@ -732,6 +1405,12 @@
732
1405
  <div id="waQrContainer"></div>
733
1406
  </div>
734
1407
 
1408
+ <script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"
1409
+ integrity="sha384-H+hy9ULve6xfxRkWIh/YOtvDdpXgV2fmAGQkIDTxIgZwNoaoBal14Di2YTMR6MzR" crossorigin="anonymous"></script>
1410
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"
1411
+ integrity="sha384-eEu5CTj3qGvu9PdJuS+YlkNi7d2XxQROAFYOr59zgObtlcux1ae1Il3u7jvdCSWu" crossorigin="anonymous"></script>
1412
+ <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"
1413
+ integrity="sha384-RH2xi4eIQ/gjtbs9fUXM68sLSi99C7ZWBRX1vDrVv6GQXRibxXLbwO2NGZB74MbU" crossorigin="anonymous"></script>
735
1414
  <script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js"></script>
736
1415
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
737
1416
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
@@ -917,6 +1596,9 @@
917
1596
  const count = document.getElementById('sessionCount');
918
1597
 
919
1598
  count.textContent = sessions.length;
1599
+ // Keep tab bar count in sync
1600
+ const tabCount = document.getElementById('tabSessionCount');
1601
+ if (tabCount) tabCount.textContent = sessions.length;
920
1602
 
921
1603
  if (sessions.length === 0) {
922
1604
  empty.style.display = 'flex';
@@ -1312,6 +1994,771 @@
1312
1994
  sendKey('C-c');
1313
1995
  }
1314
1996
  });
1997
+
1998
+ // ── Tab System ─────────────────────────────────────────────
1999
+ let currentTab = 'sessions';
2000
+
2001
+ function switchTab(tabName) {
2002
+ if (tabName === currentTab) return;
2003
+ currentTab = tabName;
2004
+
2005
+ // Update tab buttons
2006
+ document.querySelectorAll('.tab-bar .tab').forEach(btn => {
2007
+ btn.classList.toggle('active', btn.dataset.tab === tabName);
2008
+ });
2009
+
2010
+ // Toggle tab content visibility
2011
+ const sessionsTab = document.getElementById('sessionsTab');
2012
+ const mainPanel = document.getElementById('mainPanel');
2013
+ const filesTab = document.getElementById('filesTab');
2014
+
2015
+ if (tabName === 'sessions') {
2016
+ sessionsTab.style.display = '';
2017
+ mainPanel.style.display = '';
2018
+ filesTab.style.display = 'none';
2019
+ } else if (tabName === 'files') {
2020
+ sessionsTab.style.display = 'none';
2021
+ mainPanel.style.display = 'none';
2022
+ filesTab.style.display = 'flex';
2023
+ // Load file tree on first switch
2024
+ if (!fileTreeLoaded) loadFileTree();
2025
+ }
2026
+
2027
+ // Update URL
2028
+ updateFileUrl();
2029
+ }
2030
+
2031
+ // ── File Viewer ────────────────────────────────────────────
2032
+ let fileTreeLoaded = false;
2033
+ let currentFilePath = '';
2034
+ let fileTreeCache = new Map(); // path -> entries
2035
+ let expandedDirs = new Set();
2036
+ let isEditing = false;
2037
+ let editOriginalContent = '';
2038
+ let editModifiedTimestamp = ''; // for optimistic concurrency
2039
+ let hasUnsavedChanges = false;
2040
+
2041
+ const FILE_ICONS = {
2042
+ directory: '\uD83D\uDCC1', // folder
2043
+ '.md': '\uD83D\uDCDD', // memo
2044
+ '.json': '\u2699\uFE0F', // gear
2045
+ '.ts': '\uD83D\uDCDC', // scroll
2046
+ '.js': '\uD83D\uDCDC',
2047
+ '.tsx': '\uD83D\uDCDC',
2048
+ '.jsx': '\uD83D\uDCDC',
2049
+ '.yaml': '\u2699\uFE0F',
2050
+ '.yml': '\u2699\uFE0F',
2051
+ '.sh': '\uD83D\uDCDC',
2052
+ '.py': '\uD83D\uDCDC',
2053
+ default: '\uD83D\uDCC4', // page
2054
+ };
2055
+
2056
+ function getFileIcon(name, type) {
2057
+ if (type === 'directory') return FILE_ICONS.directory;
2058
+ const ext = '.' + name.split('.').pop();
2059
+ return FILE_ICONS[ext] || FILE_ICONS.default;
2060
+ }
2061
+
2062
+ function getLanguage(filename) {
2063
+ const ext = filename.split('.').pop()?.toLowerCase();
2064
+ const langMap = {
2065
+ ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
2066
+ json: 'json', yaml: 'yaml', yml: 'yaml', sh: 'bash', bash: 'bash',
2067
+ py: 'python', rb: 'ruby', go: 'go', rs: 'rust', css: 'css',
2068
+ html: 'html', xml: 'xml', sql: 'sql', md: 'markdown',
2069
+ toml: 'toml', ini: 'ini', dockerfile: 'dockerfile',
2070
+ };
2071
+ return langMap[ext] || null;
2072
+ }
2073
+
2074
+ async function apiFetch(url) {
2075
+ const resp = await fetch(url, {
2076
+ headers: { 'Authorization': `Bearer ${token}` },
2077
+ });
2078
+ if (!resp.ok) {
2079
+ const data = await resp.json().catch(() => ({}));
2080
+ throw new Error(data.error || `HTTP ${resp.status}`);
2081
+ }
2082
+ return resp.json();
2083
+ }
2084
+
2085
+ async function loadFileTree() {
2086
+ fileTreeLoaded = true;
2087
+ const list = document.getElementById('fileTreeList');
2088
+ const loading = document.getElementById('fileTreeLoading');
2089
+
2090
+ try {
2091
+ loading.style.display = 'flex';
2092
+ const data = await apiFetch('/api/files/list?path=');
2093
+ loading.style.display = 'none';
2094
+
2095
+ if (!data.entries || data.entries.length === 0) {
2096
+ list.innerHTML = '<div class="file-empty"><p>No directories configured for viewing.<br>Ask your agent to set up the file viewer.</p></div>';
2097
+ return;
2098
+ }
2099
+
2100
+ fileTreeCache.set('', data.entries);
2101
+ renderFileTree();
2102
+ } catch (err) {
2103
+ loading.style.display = 'none';
2104
+ list.innerHTML = `<div class="file-error">Failed to load files: ${escapeHtml(err.message)}</div>`;
2105
+ }
2106
+ }
2107
+
2108
+ function renderFileTree() {
2109
+ const list = document.getElementById('fileTreeList');
2110
+ const rootEntries = fileTreeCache.get('') || [];
2111
+ list.innerHTML = '';
2112
+
2113
+ for (const entry of rootEntries) {
2114
+ const entryPath = entry.name;
2115
+ renderTreeNode(list, entry, entryPath);
2116
+ }
2117
+ }
2118
+
2119
+ function renderTreeNode(container, entry, entryPath) {
2120
+ const item = document.createElement('div');
2121
+ item.className = 'tree-item' + (entryPath === currentFilePath ? ' active' : '');
2122
+ item.dataset.path = entryPath;
2123
+
2124
+ const icon = document.createElement('span');
2125
+ icon.className = 'icon';
2126
+ icon.textContent = getFileIcon(entry.name, entry.type);
2127
+ item.appendChild(icon);
2128
+
2129
+ const name = document.createElement('span');
2130
+ name.className = 'name';
2131
+ name.textContent = entry.name;
2132
+ item.appendChild(name);
2133
+
2134
+ if (entry.type === 'directory') {
2135
+ const chevron = document.createElement('span');
2136
+ chevron.className = 'chevron' + (expandedDirs.has(entryPath) ? ' open' : '');
2137
+ chevron.textContent = '\u25B6'; // right triangle
2138
+ item.appendChild(chevron);
2139
+
2140
+ item.onclick = (e) => {
2141
+ e.stopPropagation();
2142
+ toggleDirectory(entryPath, item);
2143
+ };
2144
+
2145
+ container.appendChild(item);
2146
+
2147
+ // If expanded, show children
2148
+ if (expandedDirs.has(entryPath)) {
2149
+ const children = document.createElement('div');
2150
+ children.className = 'tree-children';
2151
+ children.id = 'tree-children-' + entryPath.replace(/[^a-zA-Z0-9]/g, '_');
2152
+
2153
+ const cached = fileTreeCache.get(entryPath);
2154
+ if (cached) {
2155
+ for (const child of cached) {
2156
+ const childPath = entryPath + '/' + child.name;
2157
+ renderTreeNode(children, child, childPath);
2158
+ }
2159
+ }
2160
+
2161
+ container.appendChild(children);
2162
+ }
2163
+ } else {
2164
+ item.onclick = (e) => {
2165
+ e.stopPropagation();
2166
+ openFile(entryPath);
2167
+ };
2168
+ container.appendChild(item);
2169
+ }
2170
+ }
2171
+
2172
+ async function toggleDirectory(dirPath, itemEl) {
2173
+ if (expandedDirs.has(dirPath)) {
2174
+ expandedDirs.delete(dirPath);
2175
+ } else {
2176
+ expandedDirs.add(dirPath);
2177
+ // Lazy load if not cached
2178
+ if (!fileTreeCache.has(dirPath)) {
2179
+ try {
2180
+ const data = await apiFetch('/api/files/list?path=' + encodeURIComponent(dirPath));
2181
+ fileTreeCache.set(dirPath, data.entries || []);
2182
+ } catch (err) {
2183
+ console.error('Failed to load directory:', err);
2184
+ expandedDirs.delete(dirPath);
2185
+ return;
2186
+ }
2187
+ }
2188
+ }
2189
+ renderFileTree();
2190
+ }
2191
+
2192
+ async function openFile(filePath) {
2193
+ // Guard: check unsaved changes before navigating away
2194
+ if (hasUnsavedChanges && !confirm('You have unsaved changes. Discard them?')) {
2195
+ return;
2196
+ }
2197
+ exitEditMode(true); // silent exit, no confirm
2198
+
2199
+ currentFilePath = filePath;
2200
+ const app = document.getElementById('app');
2201
+ app.classList.add('files-active');
2202
+
2203
+ // Update tree selection
2204
+ document.querySelectorAll('.tree-item').forEach(el => {
2205
+ el.classList.toggle('active', el.dataset.path === filePath);
2206
+ });
2207
+
2208
+ // Show header
2209
+ const header = document.getElementById('fileContentHeader');
2210
+ header.style.display = 'flex';
2211
+
2212
+ // Update breadcrumbs
2213
+ renderBreadcrumbs(filePath);
2214
+
2215
+ // Show loading
2216
+ const body = document.getElementById('fileContentBody');
2217
+ body.innerHTML = '<div class="file-loading">Loading...</div>';
2218
+
2219
+ try {
2220
+ const data = await apiFetch('/api/files/read?path=' + encodeURIComponent(filePath));
2221
+
2222
+ if (data.binary) {
2223
+ body.innerHTML = `<div class="file-empty"><div class="icon">&#x1F4E6;</div><p>Binary file (${formatFileSize(data.size)})<br>Preview not available</p></div>`;
2224
+ updateFileBadges(false);
2225
+ editModifiedTimestamp = '';
2226
+ updateFileUrl();
2227
+ return;
2228
+ }
2229
+
2230
+ const content = data.content || '';
2231
+ const filename = filePath.split('/').pop() || '';
2232
+ editModifiedTimestamp = data.modified || '';
2233
+ editOriginalContent = content;
2234
+
2235
+ // Render based on file type
2236
+ if (filename.endsWith('.md')) {
2237
+ renderMarkdown(body, content);
2238
+ } else {
2239
+ renderCode(body, content, filename);
2240
+ }
2241
+
2242
+ updateFileBadges(data.editable);
2243
+ updateFileUrl();
2244
+ } catch (err) {
2245
+ body.innerHTML = `<div class="file-error">Failed to load file: ${escapeHtml(err.message)}</div>`;
2246
+ }
2247
+ }
2248
+
2249
+ function renderBreadcrumbs(filePath) {
2250
+ const container = document.getElementById('fileBreadcrumbs');
2251
+ container.innerHTML = '';
2252
+
2253
+ const parts = filePath.split('/');
2254
+ let accumulated = '';
2255
+
2256
+ for (let i = 0; i < parts.length; i++) {
2257
+ if (i > 0) {
2258
+ const sep = document.createElement('span');
2259
+ sep.className = 'sep';
2260
+ sep.textContent = '/';
2261
+ container.appendChild(sep);
2262
+ }
2263
+
2264
+ const crumb = document.createElement('span');
2265
+ crumb.className = 'crumb' + (i === parts.length - 1 ? ' current' : '');
2266
+ crumb.textContent = parts[i];
2267
+
2268
+ if (i < parts.length - 1) {
2269
+ accumulated += (i > 0 ? '/' : '') + parts[i];
2270
+ const dirPath = accumulated;
2271
+ crumb.onclick = () => {
2272
+ // Navigate to this directory in the tree
2273
+ expandedDirs.add(dirPath);
2274
+ if (!fileTreeCache.has(dirPath)) {
2275
+ apiFetch('/api/files/list?path=' + encodeURIComponent(dirPath))
2276
+ .then(data => {
2277
+ fileTreeCache.set(dirPath, data.entries || []);
2278
+ renderFileTree();
2279
+ });
2280
+ }
2281
+ // On mobile, go back to tree
2282
+ filesGoBack();
2283
+ };
2284
+ } else {
2285
+ accumulated += (i > 0 ? '/' : '') + parts[i];
2286
+ }
2287
+
2288
+ container.appendChild(crumb);
2289
+ }
2290
+ }
2291
+
2292
+ function updateFileBadges(editable) {
2293
+ const badges = document.getElementById('fileBadges');
2294
+ badges.innerHTML = '';
2295
+
2296
+ // Read-only / editable badge
2297
+ const badge = document.createElement('span');
2298
+ badge.className = 'file-badge ' + (editable ? 'editable' : 'readonly');
2299
+ badge.textContent = editable ? 'Editable' : 'Read-only';
2300
+ badges.appendChild(badge);
2301
+
2302
+ // Edit button (only for editable files, hidden during edit mode)
2303
+ if (editable && !isEditing) {
2304
+ const editBtn = document.createElement('button');
2305
+ editBtn.className = 'file-badge copy-btn';
2306
+ editBtn.textContent = 'Edit';
2307
+ editBtn.onclick = () => enterEditMode();
2308
+ badges.appendChild(editBtn);
2309
+ }
2310
+
2311
+ // Copy button
2312
+ const copyBtn = document.createElement('button');
2313
+ copyBtn.className = 'file-badge copy-btn';
2314
+ copyBtn.textContent = 'Copy';
2315
+ copyBtn.onclick = () => {
2316
+ const body = document.getElementById('fileContentBody');
2317
+ // Get raw text content — from editor textarea if editing, otherwise from rendered view
2318
+ if (isEditing) {
2319
+ const ta = body.querySelector('.file-editor textarea');
2320
+ if (ta) {
2321
+ navigator.clipboard.writeText(ta.value).then(() => {
2322
+ copyBtn.textContent = 'Copied!';
2323
+ setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
2324
+ });
2325
+ return;
2326
+ }
2327
+ }
2328
+ const pre = body.querySelector('pre');
2329
+ const md = body.querySelector('.markdown-content');
2330
+ let text = '';
2331
+ if (pre) text = pre.textContent || '';
2332
+ else if (md) text = md.textContent || '';
2333
+ navigator.clipboard.writeText(text).then(() => {
2334
+ copyBtn.textContent = 'Copied!';
2335
+ setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
2336
+ });
2337
+ };
2338
+ badges.appendChild(copyBtn);
2339
+ }
2340
+
2341
+ function renderMarkdown(container, content) {
2342
+ if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') {
2343
+ // Fallback to code view if libs failed to load
2344
+ renderCode(container, content, 'file.md');
2345
+ return;
2346
+ }
2347
+
2348
+ const rawHtml = marked.parse(content);
2349
+ const cleanHtml = DOMPurify.sanitize(rawHtml);
2350
+
2351
+ const div = document.createElement('div');
2352
+ div.className = 'markdown-content';
2353
+ div.innerHTML = cleanHtml;
2354
+
2355
+ // Highlight code blocks within rendered markdown
2356
+ if (typeof hljs !== 'undefined') {
2357
+ div.querySelectorAll('pre code').forEach(block => {
2358
+ // Size guard: skip large blocks
2359
+ if (block.textContent.length > 200000) return;
2360
+ const lines = block.textContent.split('\n').length;
2361
+ if (lines > 2000) return;
2362
+ hljs.highlightElement(block);
2363
+ });
2364
+ }
2365
+
2366
+ container.innerHTML = '';
2367
+ container.appendChild(div);
2368
+ }
2369
+
2370
+ function renderCode(container, content, filename) {
2371
+ const div = document.createElement('div');
2372
+ div.className = 'code-content';
2373
+
2374
+ const pre = document.createElement('pre');
2375
+ const code = document.createElement('code');
2376
+
2377
+ code.textContent = content;
2378
+
2379
+ // Size guard: only highlight small files
2380
+ const shouldHighlight = typeof hljs !== 'undefined' &&
2381
+ content.length <= 200000 &&
2382
+ content.split('\n').length <= 2000;
2383
+
2384
+ if (shouldHighlight) {
2385
+ const lang = getLanguage(filename);
2386
+ if (lang) {
2387
+ code.className = 'language-' + lang;
2388
+ }
2389
+ pre.appendChild(code);
2390
+ div.appendChild(pre);
2391
+ container.innerHTML = '';
2392
+ container.appendChild(div);
2393
+ hljs.highlightElement(code);
2394
+ } else {
2395
+ pre.appendChild(code);
2396
+ div.appendChild(pre);
2397
+ container.innerHTML = '';
2398
+ container.appendChild(div);
2399
+ }
2400
+ }
2401
+
2402
+ function filesGoBack() {
2403
+ // Guard: check unsaved changes before navigating away
2404
+ if (hasUnsavedChanges && !confirm('You have unsaved changes. Discard them?')) {
2405
+ return;
2406
+ }
2407
+ exitEditMode(true); // silent exit
2408
+
2409
+ // Mobile: go back to file tree
2410
+ const app = document.getElementById('app');
2411
+ app.classList.remove('files-active');
2412
+ currentFilePath = '';
2413
+ document.querySelectorAll('.tree-item').forEach(el => el.classList.remove('active'));
2414
+ document.getElementById('fileContentHeader').style.display = 'none';
2415
+ document.getElementById('fileContentBody').innerHTML =
2416
+ '<div class="file-empty"><div class="icon">&#x1F4C2;</div><p>Select a file to view its contents</p></div>';
2417
+ updateFileUrl();
2418
+ }
2419
+
2420
+ function formatFileSize(bytes) {
2421
+ if (bytes < 1024) return bytes + ' B';
2422
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
2423
+ return (bytes / 1048576).toFixed(1) + ' MB';
2424
+ }
2425
+
2426
+ // ── Editor (Phase 2) ──────────────────────────────────────
2427
+
2428
+ function enterEditMode() {
2429
+ if (isEditing || !currentFilePath) return;
2430
+ isEditing = true;
2431
+ hasUnsavedChanges = false;
2432
+
2433
+ const body = document.getElementById('fileContentBody');
2434
+
2435
+ // Build editor UI
2436
+ const editor = document.createElement('div');
2437
+ editor.className = 'file-editor';
2438
+
2439
+ const toolbar = document.createElement('div');
2440
+ toolbar.className = 'editor-toolbar';
2441
+
2442
+ const saveBtn = document.createElement('button');
2443
+ saveBtn.className = 'btn btn-save';
2444
+ saveBtn.textContent = 'Save';
2445
+ saveBtn.onclick = saveFile;
2446
+
2447
+ const cancelBtn = document.createElement('button');
2448
+ cancelBtn.className = 'btn btn-cancel';
2449
+ cancelBtn.textContent = 'Cancel';
2450
+ cancelBtn.onclick = () => exitEditMode();
2451
+
2452
+ const hint = document.createElement('span');
2453
+ hint.className = 'shortcut-hint';
2454
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
2455
+ hint.textContent = isMac ? '\u2318S to save' : 'Ctrl+S to save';
2456
+
2457
+ const status = document.createElement('span');
2458
+ status.className = 'save-status';
2459
+ status.id = 'saveStatus';
2460
+
2461
+ toolbar.appendChild(saveBtn);
2462
+ toolbar.appendChild(cancelBtn);
2463
+ toolbar.appendChild(hint);
2464
+ toolbar.appendChild(status);
2465
+
2466
+ const textarea = document.createElement('textarea');
2467
+ textarea.value = editOriginalContent;
2468
+ textarea.spellcheck = false;
2469
+ textarea.autocomplete = 'off';
2470
+ textarea.autocapitalize = 'off';
2471
+
2472
+ // Track unsaved changes
2473
+ textarea.addEventListener('input', () => {
2474
+ hasUnsavedChanges = textarea.value !== editOriginalContent;
2475
+ });
2476
+
2477
+ editor.appendChild(toolbar);
2478
+ editor.appendChild(textarea);
2479
+
2480
+ body.innerHTML = '';
2481
+ body.appendChild(editor);
2482
+
2483
+ // Update badges (hides Edit button during editing)
2484
+ updateFileBadges(true);
2485
+
2486
+ // Focus textarea
2487
+ textarea.focus();
2488
+
2489
+ // iOS Safari: adjust for virtual keyboard using visualViewport
2490
+ if (window.visualViewport) {
2491
+ const adjustForKeyboard = () => {
2492
+ const vv = window.visualViewport;
2493
+ const offset = window.innerHeight - vv.height - vv.offsetTop;
2494
+ editor.style.paddingBottom = Math.max(0, offset) + 'px';
2495
+ };
2496
+ window.visualViewport.addEventListener('resize', adjustForKeyboard);
2497
+ window.visualViewport.addEventListener('scroll', adjustForKeyboard);
2498
+ // Clean up when leaving edit mode
2499
+ textarea._vpCleanup = () => {
2500
+ window.visualViewport.removeEventListener('resize', adjustForKeyboard);
2501
+ window.visualViewport.removeEventListener('scroll', adjustForKeyboard);
2502
+ };
2503
+ }
2504
+ }
2505
+
2506
+ function exitEditMode(silent) {
2507
+ if (!isEditing) return;
2508
+ if (!silent && hasUnsavedChanges && !confirm('You have unsaved changes. Discard them?')) {
2509
+ return;
2510
+ }
2511
+
2512
+ // Clean up visualViewport listeners
2513
+ const ta = document.querySelector('.file-editor textarea');
2514
+ if (ta && ta._vpCleanup) ta._vpCleanup();
2515
+
2516
+ isEditing = false;
2517
+ hasUnsavedChanges = false;
2518
+
2519
+ // Re-render the file in view mode
2520
+ if (currentFilePath) {
2521
+ openFile(currentFilePath);
2522
+ }
2523
+ }
2524
+
2525
+ async function saveFile() {
2526
+ if (!isEditing || !currentFilePath) return;
2527
+
2528
+ const textarea = document.querySelector('.file-editor textarea');
2529
+ if (!textarea) return;
2530
+
2531
+ const content = textarea.value;
2532
+ const status = document.getElementById('saveStatus');
2533
+ const saveBtn = document.querySelector('.editor-toolbar .btn-save');
2534
+
2535
+ // Show saving state
2536
+ if (status) { status.textContent = 'Saving...'; status.className = 'save-status saving'; }
2537
+ if (saveBtn) saveBtn.disabled = true;
2538
+
2539
+ try {
2540
+ const resp = await fetch('/api/files/save', {
2541
+ method: 'POST',
2542
+ headers: {
2543
+ 'Authorization': 'Bearer ' + token,
2544
+ 'Content-Type': 'application/json',
2545
+ 'X-Instar-Request': '1',
2546
+ },
2547
+ body: JSON.stringify({
2548
+ path: currentFilePath,
2549
+ content,
2550
+ expectedModified: editModifiedTimestamp,
2551
+ }),
2552
+ });
2553
+
2554
+ const data = await resp.json().catch(() => ({}));
2555
+
2556
+ if (resp.status === 409) {
2557
+ // Conflict — file modified externally
2558
+ showConflictDialog(data.currentModified);
2559
+ if (status) { status.textContent = 'Conflict'; status.className = 'save-status error'; }
2560
+ if (saveBtn) saveBtn.disabled = false;
2561
+ return;
2562
+ }
2563
+
2564
+ if (resp.status === 401) {
2565
+ // Session expired — stash content and re-auth
2566
+ sessionStorage.setItem('instar_edit_stash', JSON.stringify({
2567
+ path: currentFilePath,
2568
+ content,
2569
+ timestamp: Date.now(),
2570
+ }));
2571
+ showToast('Session expired. Please re-authenticate.', 'warning');
2572
+ if (saveBtn) saveBtn.disabled = false;
2573
+ return;
2574
+ }
2575
+
2576
+ if (!resp.ok) {
2577
+ throw new Error(data.error || 'HTTP ' + resp.status);
2578
+ }
2579
+
2580
+ // Success — update baseline
2581
+ editOriginalContent = content;
2582
+ editModifiedTimestamp = data.modified || '';
2583
+ hasUnsavedChanges = false;
2584
+
2585
+ if (status) { status.textContent = 'Saved'; status.className = 'save-status saved'; }
2586
+ showToast('File saved', 'success');
2587
+ setTimeout(() => {
2588
+ if (status && status.textContent === 'Saved') {
2589
+ status.textContent = '';
2590
+ status.className = 'save-status';
2591
+ }
2592
+ }, 3000);
2593
+ } catch (err) {
2594
+ if (status) { status.textContent = 'Error'; status.className = 'save-status error'; }
2595
+ showToast('Save failed: ' + err.message, 'error');
2596
+ } finally {
2597
+ if (saveBtn) saveBtn.disabled = false;
2598
+ }
2599
+ }
2600
+
2601
+ // ── Toast notifications ─────────────────────────────────────
2602
+
2603
+ function showToast(message, type) {
2604
+ const container = document.getElementById('toastContainer');
2605
+ const toast = document.createElement('div');
2606
+ toast.className = 'toast ' + (type || 'success');
2607
+ toast.textContent = message;
2608
+ container.appendChild(toast);
2609
+
2610
+ // Auto-dismiss after 4s
2611
+ setTimeout(() => {
2612
+ toast.style.opacity = '0';
2613
+ toast.style.transform = 'translateX(100%)';
2614
+ setTimeout(() => toast.remove(), 300);
2615
+ }, 4000);
2616
+ }
2617
+
2618
+ // ── Conflict resolution ─────────────────────────────────────
2619
+
2620
+ function showConflictDialog(serverModified) {
2621
+ document.getElementById('conflictOverlay').style.display = 'flex';
2622
+ document.getElementById('conflictOverlay').dataset.serverModified = serverModified || '';
2623
+ }
2624
+
2625
+ function hideConflictDialog() {
2626
+ document.getElementById('conflictOverlay').style.display = 'none';
2627
+ }
2628
+
2629
+ function handleConflictOverwrite() {
2630
+ hideConflictDialog();
2631
+ // Clear expectedModified to force overwrite
2632
+ editModifiedTimestamp = '';
2633
+ saveFile();
2634
+ }
2635
+
2636
+ async function handleConflictReload() {
2637
+ hideConflictDialog();
2638
+ // Reload file, discarding local changes
2639
+ hasUnsavedChanges = false;
2640
+ isEditing = false;
2641
+ await openFile(currentFilePath);
2642
+ // Re-enter edit mode with fresh content
2643
+ enterEditMode();
2644
+ }
2645
+
2646
+ // ── Keyboard shortcuts ──────────────────────────────────────
2647
+
2648
+ document.addEventListener('keydown', (e) => {
2649
+ // Cmd/Ctrl+S to save
2650
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
2651
+ if (isEditing) {
2652
+ e.preventDefault();
2653
+ saveFile();
2654
+ }
2655
+ }
2656
+ // Escape to exit edit mode
2657
+ if (e.key === 'Escape' && isEditing) {
2658
+ e.preventDefault();
2659
+ exitEditMode();
2660
+ }
2661
+ });
2662
+
2663
+ // ── Unsaved-changes guard ───────────────────────────────────
2664
+
2665
+ window.addEventListener('beforeunload', (e) => {
2666
+ if (hasUnsavedChanges) {
2667
+ e.preventDefault();
2668
+ e.returnValue = '';
2669
+ }
2670
+ });
2671
+
2672
+ // Restore stashed edit after re-auth
2673
+ function restoreEditStash() {
2674
+ const raw = sessionStorage.getItem('instar_edit_stash');
2675
+ if (!raw) return;
2676
+ try {
2677
+ const stash = JSON.parse(raw);
2678
+ // Only restore if recent (< 10 min) and path matches
2679
+ if (Date.now() - stash.timestamp > 600000) {
2680
+ sessionStorage.removeItem('instar_edit_stash');
2681
+ return;
2682
+ }
2683
+ if (stash.path && stash.content) {
2684
+ // Wait for file tree to load, then open + restore
2685
+ const restore = () => {
2686
+ if (!fileTreeLoaded) { setTimeout(restore, 100); return; }
2687
+ openFile(stash.path).then(() => {
2688
+ enterEditMode();
2689
+ const ta = document.querySelector('.file-editor textarea');
2690
+ if (ta) {
2691
+ ta.value = stash.content;
2692
+ hasUnsavedChanges = true;
2693
+ }
2694
+ showToast('Restored unsaved edits from before session timeout', 'warning');
2695
+ sessionStorage.removeItem('instar_edit_stash');
2696
+ });
2697
+ };
2698
+ setTimeout(restore, 200);
2699
+ }
2700
+ } catch { /* ignore bad stash */ }
2701
+ }
2702
+
2703
+ // ── Deep linking via query params ──────────────────────────
2704
+
2705
+ function updateFileUrl() {
2706
+ const params = new URLSearchParams(window.location.search);
2707
+ if (currentTab === 'files') {
2708
+ params.set('tab', 'files');
2709
+ if (currentFilePath) {
2710
+ params.set('path', currentFilePath);
2711
+ } else {
2712
+ params.delete('path');
2713
+ }
2714
+ } else {
2715
+ params.delete('tab');
2716
+ params.delete('path');
2717
+ }
2718
+ const qs = params.toString();
2719
+ const newUrl = window.location.pathname + (qs ? '?' + qs : '');
2720
+ history.replaceState(null, '', newUrl);
2721
+ }
2722
+
2723
+ function handleDeepLink() {
2724
+ const params = new URLSearchParams(window.location.search);
2725
+ const tab = params.get('tab');
2726
+ const filePath = params.get('path');
2727
+
2728
+ if (tab === 'files') {
2729
+ switchTab('files');
2730
+ if (filePath) {
2731
+ // Expand parent directories
2732
+ const parts = filePath.split('/');
2733
+ let accumulated = '';
2734
+ for (let i = 0; i < parts.length - 1; i++) {
2735
+ accumulated += (i > 0 ? '/' : '') + parts[i];
2736
+ expandedDirs.add(accumulated);
2737
+ }
2738
+ // Open the file after tree loads
2739
+ const waitForTree = () => {
2740
+ if (fileTreeLoaded) {
2741
+ openFile(filePath);
2742
+ } else {
2743
+ setTimeout(waitForTree, 100);
2744
+ }
2745
+ };
2746
+ waitForTree();
2747
+ }
2748
+ }
2749
+ }
2750
+
2751
+ // Handle deep links after authentication completes.
2752
+ // MutationObserver watches for the auth overlay being hidden.
2753
+ const deepLinkObserver = new MutationObserver(() => {
2754
+ if (document.getElementById('authOverlay').style.display === 'none') {
2755
+ deepLinkObserver.disconnect();
2756
+ handleDeepLink();
2757
+ }
2758
+ });
2759
+ deepLinkObserver.observe(document.getElementById('authOverlay'), {
2760
+ attributes: true, attributeFilter: ['style'],
2761
+ });
1315
2762
  </script>
1316
2763
  </body>
1317
2764
  </html>