@in-the-loop-labs/pair-review 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -38
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/index.html +270 -623
- package/public/js/index.js +1071 -0
- package/public/js/local.js +80 -0
- package/public/js/modules/analysis-history.js +5 -1
- package/public/local.html +45 -2
- package/src/ai/index.js +1 -0
- package/src/ai/pi-provider.js +859 -0
- package/src/ai/provider.js +32 -8
- package/src/ai/stream-parser.js +171 -2
- package/src/config.js +1 -1
- package/src/database.js +170 -40
- package/src/local-review.js +9 -0
- package/src/routes/local.js +390 -41
package/public/index.html
CHANGED
|
@@ -474,6 +474,17 @@
|
|
|
474
474
|
cursor: not-allowed;
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
.btn-browse {
|
|
478
|
+
background-color: var(--ai-subtle);
|
|
479
|
+
color: var(--ai-primary);
|
|
480
|
+
border: 1px solid var(--ai-border);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.btn-browse:hover:not(:disabled) {
|
|
484
|
+
background-color: var(--ai-glow);
|
|
485
|
+
border-color: var(--ai-primary);
|
|
486
|
+
}
|
|
487
|
+
|
|
477
488
|
.start-review-error {
|
|
478
489
|
margin-top: 12px;
|
|
479
490
|
padding: 10px 14px;
|
|
@@ -494,6 +505,17 @@
|
|
|
494
505
|
display: block;
|
|
495
506
|
}
|
|
496
507
|
|
|
508
|
+
.start-review-error.info {
|
|
509
|
+
color: var(--color-fg-muted);
|
|
510
|
+
background-color: rgba(9, 105, 218, 0.08);
|
|
511
|
+
border-color: rgba(9, 105, 218, 0.3);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
[data-theme="dark"] .start-review-error.info {
|
|
515
|
+
background-color: rgba(56, 139, 253, 0.1);
|
|
516
|
+
border-color: rgba(56, 139, 253, 0.3);
|
|
517
|
+
}
|
|
518
|
+
|
|
497
519
|
.start-review-loading {
|
|
498
520
|
display: none;
|
|
499
521
|
align-items: center;
|
|
@@ -691,6 +713,14 @@
|
|
|
691
713
|
white-space: nowrap;
|
|
692
714
|
}
|
|
693
715
|
|
|
716
|
+
.local-table .col-actions {
|
|
717
|
+
text-align: right;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.local-table .btn-repo-settings {
|
|
721
|
+
margin-right: 4px;
|
|
722
|
+
}
|
|
723
|
+
|
|
694
724
|
.btn-delete-worktree,
|
|
695
725
|
.btn-repo-settings {
|
|
696
726
|
display: inline-flex;
|
|
@@ -797,6 +827,181 @@
|
|
|
797
827
|
display: none;
|
|
798
828
|
}
|
|
799
829
|
|
|
830
|
+
/* Tab Container Styles */
|
|
831
|
+
.tab-container {
|
|
832
|
+
width: 100%;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.tab-bar {
|
|
836
|
+
display: flex;
|
|
837
|
+
gap: 0;
|
|
838
|
+
border-bottom: 1px solid var(--color-border-secondary);
|
|
839
|
+
margin-bottom: 16px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.tab-btn {
|
|
843
|
+
padding: 8px 16px;
|
|
844
|
+
font-family: var(--font-sans);
|
|
845
|
+
font-size: 13px;
|
|
846
|
+
font-weight: 500;
|
|
847
|
+
color: var(--color-text-secondary);
|
|
848
|
+
background: transparent;
|
|
849
|
+
border: none;
|
|
850
|
+
border-bottom: 2px solid transparent;
|
|
851
|
+
cursor: pointer;
|
|
852
|
+
transition: all var(--transition-fast);
|
|
853
|
+
white-space: nowrap;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.tab-btn:hover {
|
|
857
|
+
color: var(--color-text-primary);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.tab-btn.active {
|
|
861
|
+
color: var(--ai-primary);
|
|
862
|
+
border-bottom-color: var(--ai-primary);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.tab-pane {
|
|
866
|
+
display: none;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.tab-pane.active {
|
|
870
|
+
display: block;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/* Local Reviews Table - tighter spacing */
|
|
874
|
+
.recent-reviews-table.local-table th,
|
|
875
|
+
.recent-reviews-table.local-table td {
|
|
876
|
+
padding: 8px 10px;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.col-local-path {
|
|
880
|
+
max-width: 260px;
|
|
881
|
+
overflow: hidden;
|
|
882
|
+
text-overflow: ellipsis;
|
|
883
|
+
white-space: nowrap;
|
|
884
|
+
/* RTL direction + LTR alignment makes ellipsis truncate from the left, showing the end of long paths */
|
|
885
|
+
direction: rtl;
|
|
886
|
+
text-align: left;
|
|
887
|
+
color: var(--color-text-tertiary);
|
|
888
|
+
font-family: var(--font-mono);
|
|
889
|
+
font-size: 12px;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.col-local-name {
|
|
893
|
+
max-width: 180px;
|
|
894
|
+
overflow: hidden;
|
|
895
|
+
text-overflow: ellipsis;
|
|
896
|
+
white-space: nowrap;
|
|
897
|
+
color: var(--ai-primary);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.col-local-sha {
|
|
901
|
+
font-family: var(--font-mono);
|
|
902
|
+
font-size: 12px;
|
|
903
|
+
color: var(--color-text-tertiary);
|
|
904
|
+
white-space: nowrap;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.col-local-name a {
|
|
908
|
+
color: var(--ai-primary);
|
|
909
|
+
text-decoration: none;
|
|
910
|
+
font-weight: 500;
|
|
911
|
+
font-family: var(--font-mono);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.col-local-name a:hover {
|
|
915
|
+
color: var(--ai-secondary);
|
|
916
|
+
text-decoration: underline;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.col-local-name em {
|
|
920
|
+
font-style: italic;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/* Delete session button */
|
|
924
|
+
.btn-delete-session {
|
|
925
|
+
display: inline-flex;
|
|
926
|
+
align-items: center;
|
|
927
|
+
justify-content: center;
|
|
928
|
+
width: 28px;
|
|
929
|
+
height: 28px;
|
|
930
|
+
padding: 0;
|
|
931
|
+
background: transparent;
|
|
932
|
+
border: 1px solid transparent;
|
|
933
|
+
border-radius: var(--radius-md);
|
|
934
|
+
color: var(--color-text-tertiary);
|
|
935
|
+
cursor: pointer;
|
|
936
|
+
transition: all var(--transition-fast);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.btn-delete-session:hover {
|
|
940
|
+
background-color: rgba(208, 36, 47, 0.08);
|
|
941
|
+
border-color: rgba(208, 36, 47, 0.3);
|
|
942
|
+
color: var(--color-danger);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
[data-theme="dark"] .btn-delete-session:hover {
|
|
946
|
+
background-color: rgba(248, 81, 73, 0.1);
|
|
947
|
+
border-color: rgba(248, 81, 73, 0.3);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/* Inline delete confirmation — use higher specificity to override tbody tr:hover */
|
|
951
|
+
.recent-reviews-table tbody tr.delete-confirm-row td {
|
|
952
|
+
background-color: rgba(208, 36, 47, 0.04);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
[data-theme="dark"] .recent-reviews-table tbody tr.delete-confirm-row td {
|
|
956
|
+
background-color: rgba(248, 81, 73, 0.06);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.delete-confirm-inner {
|
|
960
|
+
display: flex;
|
|
961
|
+
align-items: center;
|
|
962
|
+
gap: 8px;
|
|
963
|
+
font-size: 13px;
|
|
964
|
+
color: var(--color-text-secondary);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
.delete-confirm-inner .btn-confirm-yes {
|
|
968
|
+
padding: 2px 10px;
|
|
969
|
+
font-size: 12px;
|
|
970
|
+
font-weight: 500;
|
|
971
|
+
background: var(--color-danger);
|
|
972
|
+
color: #ffffff;
|
|
973
|
+
border: none;
|
|
974
|
+
border-radius: var(--radius-sm);
|
|
975
|
+
cursor: pointer;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.delete-confirm-inner .btn-confirm-yes:hover {
|
|
979
|
+
background: var(--color-danger-hover);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.delete-confirm-inner .btn-confirm-no {
|
|
983
|
+
padding: 2px 10px;
|
|
984
|
+
font-size: 12px;
|
|
985
|
+
font-weight: 500;
|
|
986
|
+
background: transparent;
|
|
987
|
+
color: var(--color-text-secondary);
|
|
988
|
+
border: 1px solid var(--color-border-primary);
|
|
989
|
+
border-radius: var(--radius-sm);
|
|
990
|
+
cursor: pointer;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.delete-confirm-inner .btn-confirm-no:hover {
|
|
994
|
+
background: var(--color-bg-secondary);
|
|
995
|
+
color: var(--color-text-primary);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/* Start review section spacing inside unified tabs */
|
|
999
|
+
.tab-pane .start-review-section {
|
|
1000
|
+
max-width: 100%;
|
|
1001
|
+
margin-bottom: 24px;
|
|
1002
|
+
padding-top: 0;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
800
1005
|
/* Initial loading state - hide content until JS determines what to show */
|
|
801
1006
|
.loading-hidden {
|
|
802
1007
|
display: none !important;
|
|
@@ -863,41 +1068,76 @@
|
|
|
863
1068
|
<!-- Main Content -->
|
|
864
1069
|
<main class="main-content">
|
|
865
1070
|
<div class="welcome-section" id="welcome-section">
|
|
866
|
-
<!--
|
|
867
|
-
<div class="
|
|
868
|
-
<
|
|
869
|
-
<
|
|
870
|
-
type="
|
|
871
|
-
class="
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
<
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1071
|
+
<!-- Unified Tab Section -->
|
|
1072
|
+
<div class="recent-reviews-section" id="recent-reviews-section">
|
|
1073
|
+
<div class="section-header" id="recent-reviews-header">
|
|
1074
|
+
<div class="tab-bar" id="unified-tab-bar">
|
|
1075
|
+
<button class="tab-btn active" data-tab="pr-tab" type="button">Pull Requests</button>
|
|
1076
|
+
<button class="tab-btn" data-tab="local-tab" type="button">Local Reviews</button>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
|
|
1080
|
+
<!-- Pull Requests Tab: Input + Listing -->
|
|
1081
|
+
<div class="tab-pane active" id="pr-tab">
|
|
1082
|
+
<div class="start-review-section">
|
|
1083
|
+
<form class="start-review-form" id="start-review-form">
|
|
1084
|
+
<input
|
|
1085
|
+
type="text"
|
|
1086
|
+
class="start-review-input"
|
|
1087
|
+
id="pr-url-input"
|
|
1088
|
+
placeholder="Enter GitHub or Graphite PR URL"
|
|
1089
|
+
autocomplete="off"
|
|
1090
|
+
spellcheck="false"
|
|
1091
|
+
>
|
|
1092
|
+
<button type="submit" class="start-review-btn" id="start-review-btn">
|
|
1093
|
+
Start Review
|
|
1094
|
+
</button>
|
|
1095
|
+
</form>
|
|
1096
|
+
<div class="start-review-error" id="start-review-error-pr"></div>
|
|
1097
|
+
<div class="start-review-loading" id="start-review-loading-pr">
|
|
1098
|
+
<div class="spinner"></div>
|
|
1099
|
+
<span id="start-review-loading-text-pr">Creating worktree and fetching PR data...</span>
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
<div id="recent-reviews-container" class="recent-reviews-loading">
|
|
1103
|
+
Loading recent reviews...
|
|
1104
|
+
</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
|
|
1107
|
+
<!-- Local Reviews Tab: Input + Listing -->
|
|
1108
|
+
<div class="tab-pane" id="local-tab">
|
|
1109
|
+
<div class="start-review-section">
|
|
1110
|
+
<form class="start-review-form" id="start-local-form">
|
|
1111
|
+
<input
|
|
1112
|
+
type="text"
|
|
1113
|
+
class="start-review-input"
|
|
1114
|
+
id="local-path-input"
|
|
1115
|
+
placeholder="Enter directory path (e.g. /Users/me/project)"
|
|
1116
|
+
autocomplete="off"
|
|
1117
|
+
spellcheck="false"
|
|
1118
|
+
>
|
|
1119
|
+
<button type="button" class="start-review-btn btn-browse" id="browse-local-btn" title="Browse for directory">
|
|
1120
|
+
Browse
|
|
1121
|
+
</button>
|
|
1122
|
+
<button type="submit" class="start-review-btn" id="start-local-btn">
|
|
1123
|
+
Review Local
|
|
1124
|
+
</button>
|
|
1125
|
+
</form>
|
|
1126
|
+
<div class="start-review-error" id="start-review-error-local"></div>
|
|
1127
|
+
<div class="start-review-loading" id="start-review-loading-local">
|
|
1128
|
+
<div class="spinner"></div>
|
|
1129
|
+
<span id="start-review-loading-text-local">Starting local review...</span>
|
|
1130
|
+
</div>
|
|
1131
|
+
</div>
|
|
1132
|
+
<div id="local-reviews-container" class="recent-reviews-loading">
|
|
1133
|
+
Loading local reviews...
|
|
1134
|
+
</div>
|
|
885
1135
|
</div>
|
|
886
1136
|
</div>
|
|
887
1137
|
|
|
888
1138
|
<!-- Usage Info (shown when no reviews, hidden initially during loading) -->
|
|
889
1139
|
<!-- Content is populated from help modal via JS to avoid duplication -->
|
|
890
1140
|
<div class="usage-info loading-hidden" id="usage-info"></div>
|
|
891
|
-
|
|
892
|
-
<!-- Recent Reviews Section -->
|
|
893
|
-
<div class="recent-reviews-section" id="recent-reviews-section">
|
|
894
|
-
<div class="section-header loading-hidden" id="recent-reviews-header">
|
|
895
|
-
<h3 class="section-title">Recent Reviews</h3>
|
|
896
|
-
</div>
|
|
897
|
-
<div id="recent-reviews-container" class="recent-reviews-loading">
|
|
898
|
-
Loading recent reviews...
|
|
899
|
-
</div>
|
|
900
|
-
</div>
|
|
901
1141
|
</div>
|
|
902
1142
|
</main>
|
|
903
1143
|
</div>
|
|
@@ -941,599 +1181,6 @@
|
|
|
941
1181
|
</div>
|
|
942
1182
|
</div>
|
|
943
1183
|
|
|
944
|
-
<script>
|
|
945
|
-
/**
|
|
946
|
-
* Theme Management
|
|
947
|
-
*/
|
|
948
|
-
function initTheme() {
|
|
949
|
-
const savedTheme = localStorage.getItem('theme');
|
|
950
|
-
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
951
|
-
const theme = savedTheme || (prefersDark ? 'dark' : 'light');
|
|
952
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
function toggleTheme() {
|
|
956
|
-
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
957
|
-
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
958
|
-
document.documentElement.setAttribute('data-theme', newTheme);
|
|
959
|
-
localStorage.setItem('theme', newTheme);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Initialize theme on page load
|
|
963
|
-
initTheme();
|
|
964
|
-
|
|
965
|
-
// Set up theme toggle button
|
|
966
|
-
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
|
|
967
|
-
|
|
968
|
-
/**
|
|
969
|
-
* Help Modal Management
|
|
970
|
-
*/
|
|
971
|
-
function openHelpModal() {
|
|
972
|
-
const overlay = document.getElementById('help-modal-overlay');
|
|
973
|
-
overlay.classList.add('visible');
|
|
974
|
-
document.body.style.overflow = 'hidden';
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function closeHelpModal() {
|
|
978
|
-
const overlay = document.getElementById('help-modal-overlay');
|
|
979
|
-
overlay.classList.remove('visible');
|
|
980
|
-
document.body.style.overflow = '';
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// Set up help button
|
|
984
|
-
document.getElementById('help-btn').addEventListener('click', openHelpModal);
|
|
985
|
-
|
|
986
|
-
// Set up close button
|
|
987
|
-
document.getElementById('help-modal-close').addEventListener('click', closeHelpModal);
|
|
988
|
-
|
|
989
|
-
// Close on overlay click (but not modal click)
|
|
990
|
-
document.getElementById('help-modal-overlay').addEventListener('click', function(e) {
|
|
991
|
-
if (e.target === this) {
|
|
992
|
-
closeHelpModal();
|
|
993
|
-
}
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
// Close on Escape key
|
|
997
|
-
document.addEventListener('keydown', function(e) {
|
|
998
|
-
if (e.key === 'Escape') {
|
|
999
|
-
const overlay = document.getElementById('help-modal-overlay');
|
|
1000
|
-
if (overlay.classList.contains('visible')) {
|
|
1001
|
-
closeHelpModal();
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
// Listen for system theme changes
|
|
1007
|
-
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
1008
|
-
if (!localStorage.getItem('theme')) {
|
|
1009
|
-
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* Format a relative time string from a date
|
|
1015
|
-
* @param {string} dateString - ISO date string
|
|
1016
|
-
* @returns {string} Human-readable relative time
|
|
1017
|
-
*/
|
|
1018
|
-
function formatRelativeTime(dateString) {
|
|
1019
|
-
const date = new Date(dateString);
|
|
1020
|
-
const now = new Date();
|
|
1021
|
-
const diffMs = now - date;
|
|
1022
|
-
const diffSecs = Math.floor(diffMs / 1000);
|
|
1023
|
-
const diffMins = Math.floor(diffSecs / 60);
|
|
1024
|
-
const diffHours = Math.floor(diffMins / 60);
|
|
1025
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
1026
|
-
|
|
1027
|
-
if (diffSecs < 60) {
|
|
1028
|
-
return 'Just now';
|
|
1029
|
-
} else if (diffMins < 60) {
|
|
1030
|
-
return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
|
|
1031
|
-
} else if (diffHours < 24) {
|
|
1032
|
-
return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
|
|
1033
|
-
} else if (diffDays < 7) {
|
|
1034
|
-
return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
|
|
1035
|
-
} else if (diffDays < 30) {
|
|
1036
|
-
const weeks = Math.floor(diffDays / 7);
|
|
1037
|
-
return `${weeks} week${weeks !== 1 ? 's' : ''} ago`;
|
|
1038
|
-
} else {
|
|
1039
|
-
const months = Math.floor(diffDays / 30);
|
|
1040
|
-
return `${months} month${months !== 1 ? 's' : ''} ago`;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* Render a single recent review table row
|
|
1046
|
-
* @param {Object} worktree - Worktree data
|
|
1047
|
-
* @returns {string} HTML string for the table row
|
|
1048
|
-
*/
|
|
1049
|
-
function renderRecentReviewRow(worktree) {
|
|
1050
|
-
const [owner, repo] = worktree.repository.split('/');
|
|
1051
|
-
const link = `/pr/${owner}/${repo}/${worktree.pr_number}`;
|
|
1052
|
-
const settingsLink = `/repo-settings.html?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`;
|
|
1053
|
-
const relativeTime = formatRelativeTime(worktree.last_accessed_at);
|
|
1054
|
-
|
|
1055
|
-
const authorDisplay = worktree.author
|
|
1056
|
-
? `<a href="https://github.com/${encodeURIComponent(worktree.author)}" target="_blank" rel="noopener">${escapeHtml(worktree.author)}</a>`
|
|
1057
|
-
: '';
|
|
1058
|
-
|
|
1059
|
-
return `
|
|
1060
|
-
<tr>
|
|
1061
|
-
<td class="col-repo">${worktree.repository}</td>
|
|
1062
|
-
<td class="col-pr"><a href="${link}">#${worktree.pr_number}</a></td>
|
|
1063
|
-
<td class="col-title" title="${escapeHtml(worktree.pr_title)}">${escapeHtml(worktree.pr_title)}</td>
|
|
1064
|
-
<td class="col-author">${authorDisplay}</td>
|
|
1065
|
-
<td class="col-time">${relativeTime}</td>
|
|
1066
|
-
<td class="col-actions">
|
|
1067
|
-
<a
|
|
1068
|
-
href="${settingsLink}"
|
|
1069
|
-
class="btn-repo-settings"
|
|
1070
|
-
title="Repository settings"
|
|
1071
|
-
>
|
|
1072
|
-
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
1073
|
-
<path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"/>
|
|
1074
|
-
</svg>
|
|
1075
|
-
</a>
|
|
1076
|
-
<button
|
|
1077
|
-
class="btn-delete-worktree"
|
|
1078
|
-
data-worktree-id="${worktree.id}"
|
|
1079
|
-
data-repository="${escapeHtml(worktree.repository)}"
|
|
1080
|
-
data-pr-number="${worktree.pr_number}"
|
|
1081
|
-
title="Delete worktree"
|
|
1082
|
-
>
|
|
1083
|
-
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
1084
|
-
<path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19a1.75 1.75 0 001.741-1.575l.66-6.6a.75.75 0 00-1.492-.15l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path>
|
|
1085
|
-
</svg>
|
|
1086
|
-
</button>
|
|
1087
|
-
</td>
|
|
1088
|
-
</tr>
|
|
1089
|
-
`;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
/**
|
|
1093
|
-
* Escape HTML special characters
|
|
1094
|
-
* @param {string} text - Text to escape
|
|
1095
|
-
* @returns {string} Escaped text
|
|
1096
|
-
*/
|
|
1097
|
-
function escapeHtml(text) {
|
|
1098
|
-
const div = document.createElement('div');
|
|
1099
|
-
div.textContent = text || '';
|
|
1100
|
-
return div.innerHTML;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
/**
|
|
1104
|
-
* Delete a worktree
|
|
1105
|
-
* @param {HTMLElement} button - The delete button element
|
|
1106
|
-
*/
|
|
1107
|
-
async function deleteWorktree(button) {
|
|
1108
|
-
const worktreeId = button.dataset.worktreeId;
|
|
1109
|
-
const repository = button.dataset.repository;
|
|
1110
|
-
const prNumber = button.dataset.prNumber;
|
|
1111
|
-
|
|
1112
|
-
if (!confirm(`Delete worktree for ${repository} #${prNumber}?`)) {
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
try {
|
|
1117
|
-
const response = await fetch(`/api/worktrees/${worktreeId}`, {
|
|
1118
|
-
method: 'DELETE'
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
|
-
if (!response.ok) {
|
|
1122
|
-
const data = await response.json().catch(() => ({}));
|
|
1123
|
-
throw new Error(data.error || 'Failed to delete worktree');
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Reload the recent reviews list
|
|
1127
|
-
await loadRecentReviews();
|
|
1128
|
-
|
|
1129
|
-
} catch (error) {
|
|
1130
|
-
console.error('Error deleting worktree:', error);
|
|
1131
|
-
alert('Failed to delete worktree: ' + error.message);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// Event delegation for delete buttons and show-more button
|
|
1136
|
-
document.addEventListener('click', function(event) {
|
|
1137
|
-
const deleteBtn = event.target.closest('.btn-delete-worktree');
|
|
1138
|
-
if (deleteBtn) {
|
|
1139
|
-
event.preventDefault();
|
|
1140
|
-
event.stopPropagation();
|
|
1141
|
-
deleteWorktree(deleteBtn);
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const showMoreBtn = event.target.closest('#btn-show-more');
|
|
1146
|
-
if (showMoreBtn) {
|
|
1147
|
-
event.preventDefault();
|
|
1148
|
-
loadMoreReviews();
|
|
1149
|
-
}
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
|
-
/** Pagination state for the recent reviews list */
|
|
1153
|
-
const recentReviewsPagination = {
|
|
1154
|
-
/** ISO timestamp of the last loaded item (cursor for next fetch) */
|
|
1155
|
-
lastTimestamp: null,
|
|
1156
|
-
/** Number of worktrees to fetch per page */
|
|
1157
|
-
pageSize: 10,
|
|
1158
|
-
/** Whether the server has indicated more results exist */
|
|
1159
|
-
hasMore: false
|
|
1160
|
-
};
|
|
1161
|
-
|
|
1162
|
-
/**
|
|
1163
|
-
* Fetch and display recent reviews (initial load).
|
|
1164
|
-
* Resets pagination state and renders the full table from scratch.
|
|
1165
|
-
*/
|
|
1166
|
-
async function loadRecentReviews() {
|
|
1167
|
-
const container = document.getElementById('recent-reviews-container');
|
|
1168
|
-
const section = document.getElementById('recent-reviews-section');
|
|
1169
|
-
const sectionHeader = document.getElementById('recent-reviews-header');
|
|
1170
|
-
const usageInfo = document.getElementById('usage-info');
|
|
1171
|
-
|
|
1172
|
-
// Reset pagination state
|
|
1173
|
-
recentReviewsPagination.lastTimestamp = null;
|
|
1174
|
-
recentReviewsPagination.hasMore = false;
|
|
1175
|
-
|
|
1176
|
-
try {
|
|
1177
|
-
const response = await fetch(`/api/worktrees/recent?limit=${recentReviewsPagination.pageSize}`);
|
|
1178
|
-
|
|
1179
|
-
if (!response.ok) {
|
|
1180
|
-
throw new Error('Failed to fetch recent reviews');
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
const data = await response.json();
|
|
1184
|
-
|
|
1185
|
-
if (!data.success || !data.worktrees || data.worktrees.length === 0) {
|
|
1186
|
-
// Show friendly empty state with usage info
|
|
1187
|
-
container.innerHTML = `
|
|
1188
|
-
<div class="recent-reviews-empty">
|
|
1189
|
-
<p>No reviews yet. Paste a PR URL above to get started.</p>
|
|
1190
|
-
</div>
|
|
1191
|
-
`;
|
|
1192
|
-
container.classList.remove('recent-reviews-loading');
|
|
1193
|
-
// Show usage info when no reviews exist
|
|
1194
|
-
if (usageInfo) usageInfo.classList.remove('loading-hidden');
|
|
1195
|
-
// Keep header hidden for empty state
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
// Update pagination state — track the cursor for the next page
|
|
1200
|
-
recentReviewsPagination.lastTimestamp = data.worktrees[data.worktrees.length - 1].last_accessed_at;
|
|
1201
|
-
recentReviewsPagination.hasMore = !!data.hasMore;
|
|
1202
|
-
|
|
1203
|
-
// Hide usage info when there are reviews (keep loading-hidden class)
|
|
1204
|
-
// Show the section header
|
|
1205
|
-
if (sectionHeader) sectionHeader.classList.remove('loading-hidden');
|
|
1206
|
-
|
|
1207
|
-
// Render the table of recent reviews
|
|
1208
|
-
const html = `
|
|
1209
|
-
<table class="recent-reviews-table">
|
|
1210
|
-
<thead>
|
|
1211
|
-
<tr>
|
|
1212
|
-
<th>Repository</th>
|
|
1213
|
-
<th>PR</th>
|
|
1214
|
-
<th>Title</th>
|
|
1215
|
-
<th>Author</th>
|
|
1216
|
-
<th>Last Opened</th>
|
|
1217
|
-
<th>Actions</th>
|
|
1218
|
-
</tr>
|
|
1219
|
-
</thead>
|
|
1220
|
-
<tbody id="recent-reviews-tbody">
|
|
1221
|
-
${data.worktrees.map(renderRecentReviewRow).join('')}
|
|
1222
|
-
</tbody>
|
|
1223
|
-
</table>
|
|
1224
|
-
${renderShowMoreButton(data.hasMore)}
|
|
1225
|
-
`;
|
|
1226
|
-
container.innerHTML = html;
|
|
1227
|
-
container.classList.remove('recent-reviews-loading');
|
|
1228
|
-
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
console.error('Error loading recent reviews:', error);
|
|
1231
|
-
// Hide the section on error, show usage info as fallback
|
|
1232
|
-
section.style.display = 'none';
|
|
1233
|
-
if (usageInfo) usageInfo.classList.remove('loading-hidden');
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
/**
|
|
1238
|
-
* Render the "Show more" button HTML.
|
|
1239
|
-
* @param {boolean} hasMore - Whether more results are available
|
|
1240
|
-
* @returns {string} HTML string for the show-more container
|
|
1241
|
-
*/
|
|
1242
|
-
function renderShowMoreButton(hasMore) {
|
|
1243
|
-
if (!hasMore) return '';
|
|
1244
|
-
return `
|
|
1245
|
-
<div class="show-more-container" id="show-more-container">
|
|
1246
|
-
<button class="btn-show-more" id="btn-show-more" type="button">
|
|
1247
|
-
<span class="btn-show-more-text">Show more</span>
|
|
1248
|
-
<span class="spinner"></span>
|
|
1249
|
-
</button>
|
|
1250
|
-
</div>
|
|
1251
|
-
`;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
/**
|
|
1255
|
-
* Load the next page of worktrees and append them to the existing table.
|
|
1256
|
-
* Called when the "Show more" button is clicked.
|
|
1257
|
-
*/
|
|
1258
|
-
async function loadMoreReviews() {
|
|
1259
|
-
const btn = document.getElementById('btn-show-more');
|
|
1260
|
-
const tbody = document.getElementById('recent-reviews-tbody');
|
|
1261
|
-
if (!btn || !tbody) return;
|
|
1262
|
-
|
|
1263
|
-
// Show loading state on the button
|
|
1264
|
-
btn.classList.add('loading');
|
|
1265
|
-
btn.disabled = true;
|
|
1266
|
-
|
|
1267
|
-
try {
|
|
1268
|
-
const { lastTimestamp, pageSize } = recentReviewsPagination;
|
|
1269
|
-
const params = new URLSearchParams({ limit: pageSize });
|
|
1270
|
-
if (lastTimestamp) params.set('before', lastTimestamp);
|
|
1271
|
-
const response = await fetch(`/api/worktrees/recent?${params}`);
|
|
1272
|
-
|
|
1273
|
-
if (!response.ok) {
|
|
1274
|
-
throw new Error('Failed to fetch more reviews');
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
const data = await response.json();
|
|
1278
|
-
|
|
1279
|
-
// Guard against stale response if the table was refreshed (e.g. by a delete) while loading
|
|
1280
|
-
if (!document.contains(btn)) return;
|
|
1281
|
-
|
|
1282
|
-
if (!data.success || !data.worktrees || data.worktrees.length === 0) {
|
|
1283
|
-
// No more results - remove the button
|
|
1284
|
-
const showMoreContainer = document.getElementById('show-more-container');
|
|
1285
|
-
if (showMoreContainer) showMoreContainer.remove();
|
|
1286
|
-
recentReviewsPagination.hasMore = false;
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// Append new rows to the existing table body
|
|
1291
|
-
tbody.insertAdjacentHTML('beforeend', data.worktrees.map(renderRecentReviewRow).join(''));
|
|
1292
|
-
|
|
1293
|
-
// Update pagination state — advance the cursor
|
|
1294
|
-
recentReviewsPagination.lastTimestamp = data.worktrees[data.worktrees.length - 1].last_accessed_at;
|
|
1295
|
-
recentReviewsPagination.hasMore = !!data.hasMore;
|
|
1296
|
-
|
|
1297
|
-
// Update or remove the "Show more" button
|
|
1298
|
-
if (!data.hasMore) {
|
|
1299
|
-
const showMoreContainer = document.getElementById('show-more-container');
|
|
1300
|
-
if (showMoreContainer) showMoreContainer.remove();
|
|
1301
|
-
} else {
|
|
1302
|
-
// Reset button state
|
|
1303
|
-
btn.classList.remove('loading');
|
|
1304
|
-
btn.disabled = false;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
} catch (error) {
|
|
1308
|
-
console.error('Error loading more reviews:', error);
|
|
1309
|
-
// Reset button state and show error so the user knows what happened
|
|
1310
|
-
btn.classList.remove('loading');
|
|
1311
|
-
btn.disabled = false;
|
|
1312
|
-
const textEl = btn.querySelector('.btn-show-more-text');
|
|
1313
|
-
if (textEl) {
|
|
1314
|
-
textEl.textContent = 'Failed to load — click to retry';
|
|
1315
|
-
// Restore original text after a brief delay so the user sees the error
|
|
1316
|
-
setTimeout(() => { textEl.textContent = 'Show more'; }, 4000);
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
/**
|
|
1322
|
-
* Parse a PR URL using the backend API
|
|
1323
|
-
* Supports GitHub and Graphite URLs (with or without protocol)
|
|
1324
|
-
* @param {string} url - The PR URL to parse
|
|
1325
|
-
* @returns {Promise<Object|null>} { owner, repo, prNumber } or null if invalid
|
|
1326
|
-
*/
|
|
1327
|
-
async function parsePRUrl(url) {
|
|
1328
|
-
if (!url || typeof url !== 'string') {
|
|
1329
|
-
return null;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
try {
|
|
1333
|
-
const response = await fetch('/api/parse-pr-url', {
|
|
1334
|
-
method: 'POST',
|
|
1335
|
-
headers: {
|
|
1336
|
-
'Content-Type': 'application/json'
|
|
1337
|
-
},
|
|
1338
|
-
body: JSON.stringify({ url: url.trim() })
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
const data = await response.json();
|
|
1342
|
-
|
|
1343
|
-
if (data.valid) {
|
|
1344
|
-
return {
|
|
1345
|
-
owner: data.owner,
|
|
1346
|
-
repo: data.repo,
|
|
1347
|
-
prNumber: data.prNumber
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
return null;
|
|
1352
|
-
} catch (e) {
|
|
1353
|
-
console.error('Error parsing PR URL:', e);
|
|
1354
|
-
return null;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
/**
|
|
1359
|
-
* Set loading state for the start review form
|
|
1360
|
-
* @param {boolean} loading - Whether to show loading state
|
|
1361
|
-
* @param {string} text - Optional loading text
|
|
1362
|
-
*/
|
|
1363
|
-
function setStartReviewLoading(loading, text = 'Creating worktree and fetching PR data...') {
|
|
1364
|
-
const form = document.getElementById('start-review-form');
|
|
1365
|
-
const input = document.getElementById('pr-url-input');
|
|
1366
|
-
const btn = document.getElementById('start-review-btn');
|
|
1367
|
-
const loadingEl = document.getElementById('start-review-loading');
|
|
1368
|
-
const loadingText = document.getElementById('start-review-loading-text');
|
|
1369
|
-
const errorEl = document.getElementById('start-review-error');
|
|
1370
|
-
|
|
1371
|
-
if (loading) {
|
|
1372
|
-
input.disabled = true;
|
|
1373
|
-
btn.disabled = true;
|
|
1374
|
-
btn.textContent = 'Starting...';
|
|
1375
|
-
loadingEl.classList.add('visible');
|
|
1376
|
-
loadingText.textContent = text;
|
|
1377
|
-
errorEl.classList.remove('visible');
|
|
1378
|
-
} else {
|
|
1379
|
-
input.disabled = false;
|
|
1380
|
-
btn.disabled = false;
|
|
1381
|
-
btn.textContent = 'Start Review';
|
|
1382
|
-
loadingEl.classList.remove('visible');
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
/**
|
|
1387
|
-
* Show error message for the start review form
|
|
1388
|
-
* @param {string} message - Error message to display
|
|
1389
|
-
*/
|
|
1390
|
-
function showStartReviewError(message) {
|
|
1391
|
-
const errorEl = document.getElementById('start-review-error');
|
|
1392
|
-
errorEl.textContent = message;
|
|
1393
|
-
errorEl.classList.add('visible');
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
/**
|
|
1397
|
-
* Handle start review form submission
|
|
1398
|
-
* @param {Event} event - Form submit event
|
|
1399
|
-
*/
|
|
1400
|
-
async function handleStartReview(event) {
|
|
1401
|
-
event.preventDefault();
|
|
1402
|
-
|
|
1403
|
-
const input = document.getElementById('pr-url-input');
|
|
1404
|
-
const url = input.value.trim();
|
|
1405
|
-
|
|
1406
|
-
// Clear previous errors
|
|
1407
|
-
const errorEl = document.getElementById('start-review-error');
|
|
1408
|
-
errorEl.classList.remove('visible');
|
|
1409
|
-
|
|
1410
|
-
// Validate input
|
|
1411
|
-
if (!url) {
|
|
1412
|
-
showStartReviewError('Please enter a GitHub PR URL');
|
|
1413
|
-
input.focus();
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// Show loading state while parsing
|
|
1418
|
-
setStartReviewLoading(true, 'Validating PR URL...');
|
|
1419
|
-
|
|
1420
|
-
// Parse the URL using the backend API
|
|
1421
|
-
const parsed = await parsePRUrl(url);
|
|
1422
|
-
if (!parsed) {
|
|
1423
|
-
setStartReviewLoading(false);
|
|
1424
|
-
showStartReviewError('Invalid PR URL. Please enter a GitHub or Graphite PR URL (e.g., https://github.com/owner/repo/pull/123)');
|
|
1425
|
-
input.focus();
|
|
1426
|
-
return;
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
// Update loading state
|
|
1430
|
-
setStartReviewLoading(true, 'Fetching PR data from GitHub...');
|
|
1431
|
-
|
|
1432
|
-
try {
|
|
1433
|
-
// Call the API to create the worktree
|
|
1434
|
-
const response = await fetch('/api/worktrees/create', {
|
|
1435
|
-
method: 'POST',
|
|
1436
|
-
headers: {
|
|
1437
|
-
'Content-Type': 'application/json'
|
|
1438
|
-
},
|
|
1439
|
-
body: JSON.stringify({
|
|
1440
|
-
owner: parsed.owner,
|
|
1441
|
-
repo: parsed.repo,
|
|
1442
|
-
prNumber: parsed.prNumber
|
|
1443
|
-
})
|
|
1444
|
-
});
|
|
1445
|
-
|
|
1446
|
-
const data = await response.json();
|
|
1447
|
-
|
|
1448
|
-
if (!response.ok) {
|
|
1449
|
-
throw new Error(data.error || 'Failed to create worktree');
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
if (!data.success) {
|
|
1453
|
-
throw new Error(data.error || 'Failed to create worktree');
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// Update loading text before redirect
|
|
1457
|
-
setStartReviewLoading(true, 'Redirecting to review...');
|
|
1458
|
-
|
|
1459
|
-
// Redirect to the review page
|
|
1460
|
-
window.location.href = data.reviewUrl;
|
|
1461
|
-
|
|
1462
|
-
} catch (error) {
|
|
1463
|
-
console.error('Error starting review:', error);
|
|
1464
|
-
setStartReviewLoading(false);
|
|
1465
|
-
showStartReviewError(error.message || 'An unexpected error occurred. Please try again.');
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
/**
|
|
1470
|
-
* Update command examples based on whether running via npx or installed
|
|
1471
|
-
* @param {boolean} isNpx - True if running via npx
|
|
1472
|
-
*/
|
|
1473
|
-
function updateCommandExamples(isNpx) {
|
|
1474
|
-
const baseCmd = isNpx ? 'npx @in-the-loop-labs/pair-review' : 'pair-review';
|
|
1475
|
-
const cmdExamples = document.querySelectorAll('.cmd-example');
|
|
1476
|
-
cmdExamples.forEach(el => {
|
|
1477
|
-
const args = el.dataset.args || '';
|
|
1478
|
-
el.textContent = args ? `${baseCmd} ${args}` : baseCmd;
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
/**
|
|
1483
|
-
* Fetch config from server and update UI accordingly
|
|
1484
|
-
*/
|
|
1485
|
-
async function loadConfigAndUpdateUI() {
|
|
1486
|
-
try {
|
|
1487
|
-
const response = await fetch('/api/config');
|
|
1488
|
-
if (response.ok) {
|
|
1489
|
-
const config = await response.json();
|
|
1490
|
-
updateCommandExamples(config.is_running_via_npx);
|
|
1491
|
-
} else {
|
|
1492
|
-
// Fallback: assume installed (shorter command)
|
|
1493
|
-
updateCommandExamples(false);
|
|
1494
|
-
}
|
|
1495
|
-
} catch (error) {
|
|
1496
|
-
console.error('Error loading config:', error);
|
|
1497
|
-
// Fallback: assume installed (shorter command)
|
|
1498
|
-
updateCommandExamples(false);
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
// Load recent reviews when the page loads
|
|
1503
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
1504
|
-
// Load config and update command examples based on npx detection
|
|
1505
|
-
loadConfigAndUpdateUI().then(() => {
|
|
1506
|
-
// Sync help content to usage-info section AFTER command examples are updated
|
|
1507
|
-
const helpContent = document.querySelector('.help-modal-content');
|
|
1508
|
-
const usageInfo = document.getElementById('usage-info');
|
|
1509
|
-
if (helpContent && usageInfo) {
|
|
1510
|
-
// Clear any existing content
|
|
1511
|
-
usageInfo.innerHTML = '';
|
|
1512
|
-
// Clone the content nodes safely
|
|
1513
|
-
Array.from(helpContent.childNodes).forEach(node => {
|
|
1514
|
-
usageInfo.appendChild(node.cloneNode(true));
|
|
1515
|
-
});
|
|
1516
|
-
}
|
|
1517
|
-
});
|
|
1518
|
-
|
|
1519
|
-
loadRecentReviews();
|
|
1520
|
-
|
|
1521
|
-
// Set up start review form handler
|
|
1522
|
-
const form = document.getElementById('start-review-form');
|
|
1523
|
-
if (form) {
|
|
1524
|
-
form.addEventListener('submit', handleStartReview);
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// Allow Enter key to submit
|
|
1528
|
-
const input = document.getElementById('pr-url-input');
|
|
1529
|
-
if (input) {
|
|
1530
|
-
input.addEventListener('keypress', function(e) {
|
|
1531
|
-
if (e.key === 'Enter') {
|
|
1532
|
-
form.dispatchEvent(new Event('submit'));
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
});
|
|
1537
|
-
</script>
|
|
1184
|
+
<script src="/js/index.js"></script>
|
|
1538
1185
|
</body>
|
|
1539
1186
|
</html>
|