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.
- package/dashboard/index.html +1448 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +44 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +50 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +22 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/knowledge/ProbeRegistry.d.ts +54 -0
- package/dist/knowledge/ProbeRegistry.d.ts.map +1 -0
- package/dist/knowledge/ProbeRegistry.js +119 -0
- package/dist/knowledge/ProbeRegistry.js.map +1 -0
- package/dist/knowledge/SelfKnowledgeTree.d.ts +100 -0
- package/dist/knowledge/SelfKnowledgeTree.d.ts.map +1 -0
- package/dist/knowledge/SelfKnowledgeTree.js +476 -0
- package/dist/knowledge/SelfKnowledgeTree.js.map +1 -0
- package/dist/knowledge/TreeGenerator.d.ts +57 -0
- package/dist/knowledge/TreeGenerator.d.ts.map +1 -0
- package/dist/knowledge/TreeGenerator.js +486 -0
- package/dist/knowledge/TreeGenerator.js.map +1 -0
- package/dist/knowledge/TreeSynthesis.d.ts +24 -0
- package/dist/knowledge/TreeSynthesis.d.ts.map +1 -0
- package/dist/knowledge/TreeSynthesis.js +61 -0
- package/dist/knowledge/TreeSynthesis.js.map +1 -0
- package/dist/knowledge/TreeTraversal.d.ts +91 -0
- package/dist/knowledge/TreeTraversal.d.ts.map +1 -0
- package/dist/knowledge/TreeTraversal.js +347 -0
- package/dist/knowledge/TreeTraversal.js.map +1 -0
- package/dist/knowledge/TreeTriage.d.ts +32 -0
- package/dist/knowledge/TreeTriage.d.ts.map +1 -0
- package/dist/knowledge/TreeTriage.js +127 -0
- package/dist/knowledge/TreeTriage.js.map +1 -0
- package/dist/knowledge/types.d.ts +180 -0
- package/dist/knowledge/types.d.ts.map +1 -0
- package/dist/knowledge/types.js +28 -0
- package/dist/knowledge/types.js.map +1 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +18 -2
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +4 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/fileRoutes.d.ts +16 -0
- package/dist/server/fileRoutes.d.ts.map +1 -0
- package/dist/server/fileRoutes.js +488 -0
- package/dist/server/fileRoutes.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +17 -17
- package/upgrades/0.18.5.md +25 -0
- package/upgrades/0.18.6.md +25 -0
package/dashboard/index.html
CHANGED
|
@@ -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
|
-
|
|
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()">←</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">📂</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">📦</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">📂</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>
|