glassbox 0.1.1 → 0.1.2
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 +1 -22
- package/dist/cli.js +537 -2033
- package/dist/client/app.global.js +10 -0
- package/dist/client/styles.css +1 -0
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { Hono as Hono3 } from "hono";
|
|
5
5
|
import { serve } from "@hono/node-server";
|
|
6
6
|
import { exec } from "child_process";
|
|
7
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
8
|
+
import { join as join3, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
7
10
|
|
|
8
11
|
// src/routes/api.ts
|
|
9
12
|
import { Hono } from "hono";
|
|
@@ -650,6 +653,324 @@ function getModeArgs(mode) {
|
|
|
650
653
|
}
|
|
651
654
|
}
|
|
652
655
|
|
|
656
|
+
// src/outline/parser.ts
|
|
657
|
+
var BRACE_LANGS = /* @__PURE__ */ new Set([
|
|
658
|
+
"javascript",
|
|
659
|
+
"typescript",
|
|
660
|
+
"java",
|
|
661
|
+
"go",
|
|
662
|
+
"rust",
|
|
663
|
+
"c",
|
|
664
|
+
"cpp",
|
|
665
|
+
"csharp",
|
|
666
|
+
"swift",
|
|
667
|
+
"php",
|
|
668
|
+
"kotlin",
|
|
669
|
+
"scala",
|
|
670
|
+
"dart",
|
|
671
|
+
"groovy",
|
|
672
|
+
"objectivec"
|
|
673
|
+
]);
|
|
674
|
+
var INDENT_LANGS = /* @__PURE__ */ new Set(["python", "ruby"]);
|
|
675
|
+
var EXT_TO_LANG = {
|
|
676
|
+
".js": "javascript",
|
|
677
|
+
".mjs": "javascript",
|
|
678
|
+
".cjs": "javascript",
|
|
679
|
+
".jsx": "javascript",
|
|
680
|
+
".ts": "typescript",
|
|
681
|
+
".tsx": "typescript",
|
|
682
|
+
".mts": "typescript",
|
|
683
|
+
".cts": "typescript",
|
|
684
|
+
".java": "java",
|
|
685
|
+
".go": "go",
|
|
686
|
+
".rs": "rust",
|
|
687
|
+
".c": "c",
|
|
688
|
+
".h": "c",
|
|
689
|
+
".cpp": "cpp",
|
|
690
|
+
".cc": "cpp",
|
|
691
|
+
".cxx": "cpp",
|
|
692
|
+
".hpp": "cpp",
|
|
693
|
+
".hh": "cpp",
|
|
694
|
+
".hxx": "cpp",
|
|
695
|
+
".cs": "csharp",
|
|
696
|
+
".swift": "swift",
|
|
697
|
+
".php": "php",
|
|
698
|
+
".kt": "kotlin",
|
|
699
|
+
".kts": "kotlin",
|
|
700
|
+
".scala": "scala",
|
|
701
|
+
".dart": "dart",
|
|
702
|
+
".groovy": "groovy",
|
|
703
|
+
".gvy": "groovy",
|
|
704
|
+
".m": "objectivec",
|
|
705
|
+
".mm": "objectivec",
|
|
706
|
+
".py": "python",
|
|
707
|
+
".pyw": "python",
|
|
708
|
+
".rb": "ruby",
|
|
709
|
+
".rake": "ruby"
|
|
710
|
+
};
|
|
711
|
+
function langFromPath(filePath) {
|
|
712
|
+
const dot = filePath.lastIndexOf(".");
|
|
713
|
+
if (dot === -1) return null;
|
|
714
|
+
return EXT_TO_LANG[filePath.slice(dot).toLowerCase()] || null;
|
|
715
|
+
}
|
|
716
|
+
function parseOutline(content, filePath) {
|
|
717
|
+
const lang = langFromPath(filePath);
|
|
718
|
+
if (!lang) return [];
|
|
719
|
+
if (BRACE_LANGS.has(lang)) return parseBraces(content, lang);
|
|
720
|
+
if (INDENT_LANGS.has(lang)) return parseIndent(content, lang);
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
var CLASS_PATTERNS = [
|
|
724
|
+
/^(?:export\s+)?(?:abstract\s+)?(?:public\s+|private\s+|protected\s+|internal\s+|static\s+|sealed\s+|final\s+)*(?:class|struct|enum|interface|trait|impl|namespace)\s+(\w+)/
|
|
725
|
+
];
|
|
726
|
+
var JS_TS_FUNC_PATTERNS = [
|
|
727
|
+
// function name(, async function name(, export function name(
|
|
728
|
+
/^(?:export\s+)?(?:export\s+default\s+)?(?:async\s+)?function\s*\*?\s+(\w+)/,
|
|
729
|
+
// Arrow function assigned to const/let/var: const name = (...) => or const name = async (
|
|
730
|
+
/^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?::\s*[^=]+\s*)?=>/
|
|
731
|
+
];
|
|
732
|
+
var JS_TS_METHOD_PATTERNS = [
|
|
733
|
+
// Method: [modifiers] name( — but require at least one modifier OR that we're in a class
|
|
734
|
+
/^(?:(?:public|private|protected|static|async|override|abstract|readonly|get|set)\s+)+(\w+)\s*(?:<[^>]*>)?\s*\(/,
|
|
735
|
+
// Plain method name( inside class body — no modifiers needed
|
|
736
|
+
/^(\w+)\s*(?:<[^>]*>)?\s*\(/
|
|
737
|
+
];
|
|
738
|
+
var GO_FUNC_PATTERNS = [
|
|
739
|
+
/^func\s+(?:\([^)]*\)\s+)?(\w+)\s*(?:\[.*?\])?\s*\(/
|
|
740
|
+
];
|
|
741
|
+
var RUST_FUNC_PATTERNS = [
|
|
742
|
+
/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)/
|
|
743
|
+
];
|
|
744
|
+
var SWIFT_FUNC_PATTERNS = [
|
|
745
|
+
/^(?:public\s+|private\s+|internal\s+|open\s+|static\s+|class\s+|override\s+|@\w+\s+)*func\s+(\w+)/
|
|
746
|
+
];
|
|
747
|
+
var TYPED_FUNC_PATTERNS = [
|
|
748
|
+
/^(?:public\s+|private\s+|protected\s+|static\s+|final\s+|abstract\s+|override\s+|virtual\s+|inline\s+|suspend\s+|open\s+)*(?:\w+(?:<[^>]*>)?(?:\[\])*\s+)+(\w+)\s*(?:<[^>]*>)?\s*\(/
|
|
749
|
+
];
|
|
750
|
+
var PHP_FUNC_PATTERNS = [
|
|
751
|
+
/^(?:public\s+|private\s+|protected\s+|static\s+|abstract\s+)*function\s+(\w+)/
|
|
752
|
+
];
|
|
753
|
+
function getFuncPatterns(lang) {
|
|
754
|
+
switch (lang) {
|
|
755
|
+
case "javascript":
|
|
756
|
+
case "typescript":
|
|
757
|
+
return { top: JS_TS_FUNC_PATTERNS, method: JS_TS_METHOD_PATTERNS };
|
|
758
|
+
case "go":
|
|
759
|
+
return { top: GO_FUNC_PATTERNS, method: [] };
|
|
760
|
+
case "rust":
|
|
761
|
+
return { top: RUST_FUNC_PATTERNS, method: RUST_FUNC_PATTERNS };
|
|
762
|
+
case "swift":
|
|
763
|
+
return { top: SWIFT_FUNC_PATTERNS, method: SWIFT_FUNC_PATTERNS };
|
|
764
|
+
case "php":
|
|
765
|
+
return { top: PHP_FUNC_PATTERNS, method: PHP_FUNC_PATTERNS };
|
|
766
|
+
case "c":
|
|
767
|
+
case "cpp":
|
|
768
|
+
case "objectivec":
|
|
769
|
+
return { top: TYPED_FUNC_PATTERNS, method: TYPED_FUNC_PATTERNS };
|
|
770
|
+
default:
|
|
771
|
+
return { top: TYPED_FUNC_PATTERNS, method: TYPED_FUNC_PATTERNS };
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
var SKIP_NAMES = /* @__PURE__ */ new Set([
|
|
775
|
+
"if",
|
|
776
|
+
"else",
|
|
777
|
+
"for",
|
|
778
|
+
"while",
|
|
779
|
+
"switch",
|
|
780
|
+
"catch",
|
|
781
|
+
"return",
|
|
782
|
+
"throw",
|
|
783
|
+
"new",
|
|
784
|
+
"delete",
|
|
785
|
+
"typeof",
|
|
786
|
+
"instanceof",
|
|
787
|
+
"void",
|
|
788
|
+
"import",
|
|
789
|
+
"export",
|
|
790
|
+
"from",
|
|
791
|
+
"require",
|
|
792
|
+
"case",
|
|
793
|
+
"default",
|
|
794
|
+
"break",
|
|
795
|
+
"continue",
|
|
796
|
+
"do",
|
|
797
|
+
"try",
|
|
798
|
+
"finally",
|
|
799
|
+
"with",
|
|
800
|
+
"yield",
|
|
801
|
+
"await",
|
|
802
|
+
"super",
|
|
803
|
+
"this"
|
|
804
|
+
]);
|
|
805
|
+
function parseBraces(content, lang) {
|
|
806
|
+
const lines = content.split("\n");
|
|
807
|
+
const root = [];
|
|
808
|
+
const stack = [];
|
|
809
|
+
let braceDepth = 0;
|
|
810
|
+
let inString = false;
|
|
811
|
+
let stringChar = "";
|
|
812
|
+
let inLineComment = false;
|
|
813
|
+
let inBlockComment = false;
|
|
814
|
+
let inTemplateLiteral = false;
|
|
815
|
+
const { top: topFuncPatterns, method: methodFuncPatterns } = getFuncPatterns(lang);
|
|
816
|
+
for (let i = 0; i < lines.length; i++) {
|
|
817
|
+
const line = lines[i];
|
|
818
|
+
const trimmed = line.trimStart();
|
|
819
|
+
const lineNum = i + 1;
|
|
820
|
+
if (!trimmed) continue;
|
|
821
|
+
const currentDepth = braceDepth;
|
|
822
|
+
let matched = false;
|
|
823
|
+
const directParent = stack.length > 0 ? stack[stack.length - 1].symbol : null;
|
|
824
|
+
const insideClass = directParent?.kind === "class";
|
|
825
|
+
if (!inBlockComment) {
|
|
826
|
+
for (const pat of CLASS_PATTERNS) {
|
|
827
|
+
const m = trimmed.match(pat);
|
|
828
|
+
if (m && m[1]) {
|
|
829
|
+
const sym = { name: m[1], kind: "class", line: lineNum, endLine: lineNum, children: [] };
|
|
830
|
+
pushSymbol(root, stack, sym, currentDepth);
|
|
831
|
+
matched = true;
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (!matched) {
|
|
836
|
+
const patterns = insideClass ? [...topFuncPatterns, ...methodFuncPatterns] : topFuncPatterns;
|
|
837
|
+
for (const pat of patterns) {
|
|
838
|
+
const m = trimmed.match(pat);
|
|
839
|
+
if (m && m[1] && !SKIP_NAMES.has(m[1])) {
|
|
840
|
+
const sym = { name: m[1], kind: "function", line: lineNum, endLine: lineNum, children: [] };
|
|
841
|
+
pushSymbol(root, stack, sym, currentDepth);
|
|
842
|
+
matched = true;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
for (let j = 0; j < line.length; j++) {
|
|
849
|
+
const ch = line[j];
|
|
850
|
+
const next = line[j + 1];
|
|
851
|
+
if (inLineComment) break;
|
|
852
|
+
if (inBlockComment) {
|
|
853
|
+
if (ch === "*" && next === "/") {
|
|
854
|
+
inBlockComment = false;
|
|
855
|
+
j++;
|
|
856
|
+
}
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (inString) {
|
|
860
|
+
if (ch === "\\") {
|
|
861
|
+
j++;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (ch === stringChar) {
|
|
865
|
+
inString = false;
|
|
866
|
+
}
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (inTemplateLiteral) {
|
|
870
|
+
if (ch === "\\") {
|
|
871
|
+
j++;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (ch === "`") {
|
|
875
|
+
inTemplateLiteral = false;
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (ch === "/" && next === "/") {
|
|
880
|
+
inLineComment = true;
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
if (ch === "/" && next === "*") {
|
|
884
|
+
inBlockComment = true;
|
|
885
|
+
j++;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (ch === '"' || ch === "'") {
|
|
889
|
+
inString = true;
|
|
890
|
+
stringChar = ch;
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (ch === "`" && (lang === "javascript" || lang === "typescript")) {
|
|
894
|
+
inTemplateLiteral = true;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (ch === "{") braceDepth++;
|
|
898
|
+
if (ch === "}") {
|
|
899
|
+
braceDepth--;
|
|
900
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= braceDepth) {
|
|
901
|
+
const closed = stack.pop();
|
|
902
|
+
closed.symbol.endLine = lineNum;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
inLineComment = false;
|
|
907
|
+
}
|
|
908
|
+
const lastLine = lines.length;
|
|
909
|
+
for (const item of stack) {
|
|
910
|
+
item.symbol.endLine = lastLine;
|
|
911
|
+
}
|
|
912
|
+
return root;
|
|
913
|
+
}
|
|
914
|
+
function pushSymbol(root, stack, sym, depth) {
|
|
915
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
916
|
+
stack.pop();
|
|
917
|
+
}
|
|
918
|
+
if (stack.length > 0) {
|
|
919
|
+
stack[stack.length - 1].symbol.children.push(sym);
|
|
920
|
+
} else {
|
|
921
|
+
root.push(sym);
|
|
922
|
+
}
|
|
923
|
+
stack.push({ symbol: sym, depth });
|
|
924
|
+
}
|
|
925
|
+
function parseIndent(content, lang) {
|
|
926
|
+
const lines = content.split("\n");
|
|
927
|
+
const root = [];
|
|
928
|
+
const stack = [];
|
|
929
|
+
const classRe = lang === "python" ? /^(\s*)class\s+(\w+)/ : /^(\s*)(?:class|module)\s+(\w+)/;
|
|
930
|
+
const funcRe = lang === "python" ? /^(\s*)(?:async\s+)?def\s+(\w+)/ : /^(\s*)def\s+(\w+)/;
|
|
931
|
+
for (let i = 0; i < lines.length; i++) {
|
|
932
|
+
const line = lines[i];
|
|
933
|
+
const lineNum = i + 1;
|
|
934
|
+
let m = line.match(classRe);
|
|
935
|
+
if (m) {
|
|
936
|
+
const indent = m[1].length;
|
|
937
|
+
const sym = { name: m[2], kind: "class", line: lineNum, endLine: lineNum, children: [] };
|
|
938
|
+
pushIndentSymbol(root, stack, sym, indent, lines, i);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
m = line.match(funcRe);
|
|
942
|
+
if (m) {
|
|
943
|
+
const indent = m[1].length;
|
|
944
|
+
const sym = { name: m[2], kind: "function", line: lineNum, endLine: lineNum, children: [] };
|
|
945
|
+
pushIndentSymbol(root, stack, sym, indent, lines, i);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return root;
|
|
949
|
+
}
|
|
950
|
+
function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
|
|
951
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
|
952
|
+
const closed = stack.pop();
|
|
953
|
+
closed.symbol.endLine = lineIdx;
|
|
954
|
+
}
|
|
955
|
+
let endLine = lines.length;
|
|
956
|
+
for (let j = lineIdx + 1; j < lines.length; j++) {
|
|
957
|
+
const l = lines[j];
|
|
958
|
+
if (l.trim() === "") continue;
|
|
959
|
+
const nextIndent = l.length - l.trimStart().length;
|
|
960
|
+
if (nextIndent <= indent) {
|
|
961
|
+
endLine = j;
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
sym.endLine = endLine;
|
|
966
|
+
if (stack.length > 0) {
|
|
967
|
+
stack[stack.length - 1].symbol.children.push(sym);
|
|
968
|
+
} else {
|
|
969
|
+
root.push(sym);
|
|
970
|
+
}
|
|
971
|
+
stack.push({ symbol: sym, indent });
|
|
972
|
+
}
|
|
973
|
+
|
|
653
974
|
// src/routes/api.ts
|
|
654
975
|
var apiRoutes = new Hono();
|
|
655
976
|
function resolveReviewId(c) {
|
|
@@ -789,6 +1110,25 @@ apiRoutes.get("/annotations/all", async (c) => {
|
|
|
789
1110
|
const annotations = await getAnnotationsForReview(reviewId);
|
|
790
1111
|
return c.json(annotations);
|
|
791
1112
|
});
|
|
1113
|
+
apiRoutes.get("/outline/:fileId", async (c) => {
|
|
1114
|
+
const repoRoot = c.get("repoRoot");
|
|
1115
|
+
const file = await getReviewFile(c.req.param("fileId"));
|
|
1116
|
+
if (!file) return c.json({ error: "Not found" }, 404);
|
|
1117
|
+
const diff = JSON.parse(file.diff_data || "{}");
|
|
1118
|
+
const isDeleted = diff.status === "deleted";
|
|
1119
|
+
let content = "";
|
|
1120
|
+
try {
|
|
1121
|
+
if (isDeleted) {
|
|
1122
|
+
content = getFileContent(file.file_path, "HEAD", repoRoot);
|
|
1123
|
+
} else {
|
|
1124
|
+
content = getFileContent(file.file_path, "working", repoRoot);
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
if (!content) return c.json({ symbols: [] });
|
|
1129
|
+
const symbols = parseOutline(content, file.file_path);
|
|
1130
|
+
return c.json({ symbols });
|
|
1131
|
+
});
|
|
792
1132
|
apiRoutes.get("/context/:fileId", async (c) => {
|
|
793
1133
|
const repoRoot = c.get("repoRoot");
|
|
794
1134
|
const file = await getReviewFile(c.req.param("fileId"));
|
|
@@ -885,2026 +1225,176 @@ function Layout({ title, reviewId, children }) {
|
|
|
885
1225
|
/* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
|
|
886
1226
|
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
|
887
1227
|
/* @__PURE__ */ jsx("title", { children: title }),
|
|
888
|
-
/* @__PURE__ */ jsx("
|
|
1228
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "/static/styles.css" })
|
|
889
1229
|
] }),
|
|
890
1230
|
/* @__PURE__ */ jsx("body", { "data-review-id": reviewId, children: [
|
|
891
1231
|
children,
|
|
892
|
-
/* @__PURE__ */ jsx("script", {
|
|
1232
|
+
/* @__PURE__ */ jsx("script", { src: "/static/app.js" })
|
|
893
1233
|
] })
|
|
894
1234
|
] });
|
|
895
1235
|
}
|
|
896
|
-
function getStyles() {
|
|
897
|
-
return `
|
|
898
|
-
:root {
|
|
899
|
-
--bg: #1e1e2e;
|
|
900
|
-
--bg-surface: #252536;
|
|
901
|
-
--bg-hover: #2d2d44;
|
|
902
|
-
--bg-active: #363652;
|
|
903
|
-
--text: #cdd6f4;
|
|
904
|
-
--text-dim: #8888aa;
|
|
905
|
-
--text-bright: #ffffff;
|
|
906
|
-
--accent: #89b4fa;
|
|
907
|
-
--accent-hover: #74a8fc;
|
|
908
|
-
--green: #a6e3a1;
|
|
909
|
-
--red: #f38ba8;
|
|
910
|
-
--yellow: #f9e2af;
|
|
911
|
-
--orange: #fab387;
|
|
912
|
-
--blue: #89b4fa;
|
|
913
|
-
--purple: #cba6f7;
|
|
914
|
-
--teal: #94e2d5;
|
|
915
|
-
--border: #363652;
|
|
916
|
-
--diff-add-bg: rgba(166, 227, 161, 0.1);
|
|
917
|
-
--diff-add-border: rgba(166, 227, 161, 0.3);
|
|
918
|
-
--diff-remove-bg: rgba(243, 139, 168, 0.1);
|
|
919
|
-
--diff-remove-border: rgba(243, 139, 168, 0.3);
|
|
920
|
-
--diff-context-bg: transparent;
|
|
921
|
-
--gutter-bg: #1a1a2e;
|
|
922
|
-
--gutter-text: #555577;
|
|
923
|
-
--sidebar-w: 300px;
|
|
924
|
-
--radius: 6px;
|
|
925
|
-
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
926
|
-
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
930
|
-
|
|
931
|
-
body {
|
|
932
|
-
font-family: var(--font-sans);
|
|
933
|
-
background: var(--bg);
|
|
934
|
-
color: var(--text);
|
|
935
|
-
height: 100vh;
|
|
936
|
-
overflow: hidden;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
.review-app {
|
|
940
|
-
display: flex;
|
|
941
|
-
height: 100vh;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/* Sidebar */
|
|
945
|
-
.sidebar {
|
|
946
|
-
width: var(--sidebar-w);
|
|
947
|
-
min-width: 200px;
|
|
948
|
-
max-width: 60vw;
|
|
949
|
-
background: var(--bg-surface);
|
|
950
|
-
border-right: 1px solid var(--border);
|
|
951
|
-
display: flex;
|
|
952
|
-
flex-direction: column;
|
|
953
|
-
overflow: hidden;
|
|
954
|
-
flex-shrink: 0;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
.sidebar-resize {
|
|
958
|
-
width: 4px;
|
|
959
|
-
cursor: col-resize;
|
|
960
|
-
background: transparent;
|
|
961
|
-
flex-shrink: 0;
|
|
962
|
-
transition: background 0.15s;
|
|
963
|
-
}
|
|
964
1236
|
|
|
965
|
-
.
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
.
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
color: var(--text-dim);
|
|
984
|
-
margin-top: 4px;
|
|
985
|
-
display: block;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
.sidebar-controls {
|
|
989
|
-
padding: 8px 16px;
|
|
990
|
-
border-bottom: 1px solid var(--border);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
.diff-mode-toggle {
|
|
994
|
-
display: flex;
|
|
995
|
-
gap: 4px;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
.file-filter {
|
|
999
|
-
padding: 8px 16px;
|
|
1000
|
-
border-bottom: 1px solid var(--border);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
.file-filter-input {
|
|
1004
|
-
width: 100%;
|
|
1005
|
-
padding: 5px 8px;
|
|
1006
|
-
background: var(--bg);
|
|
1007
|
-
color: var(--text);
|
|
1008
|
-
border: 1px solid var(--border);
|
|
1009
|
-
border-radius: var(--radius);
|
|
1010
|
-
font-family: var(--font-mono);
|
|
1011
|
-
font-size: 12px;
|
|
1012
|
-
outline: none;
|
|
1237
|
+
// src/components/fileList.tsx
|
|
1238
|
+
function buildFileTree(files) {
|
|
1239
|
+
const root = { name: "", children: [], files: [] };
|
|
1240
|
+
for (const f of files) {
|
|
1241
|
+
const parts = f.file_path.split("/");
|
|
1242
|
+
let node = root;
|
|
1243
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1244
|
+
let child = node.children.find((c) => c.name === parts[i]);
|
|
1245
|
+
if (!child) {
|
|
1246
|
+
child = { name: parts[i], children: [], files: [] };
|
|
1247
|
+
node.children.push(child);
|
|
1248
|
+
}
|
|
1249
|
+
node = child;
|
|
1250
|
+
}
|
|
1251
|
+
node.files.push(f);
|
|
1252
|
+
}
|
|
1253
|
+
compressTree(root);
|
|
1254
|
+
return root;
|
|
1013
1255
|
}
|
|
1014
|
-
|
|
1015
|
-
.
|
|
1016
|
-
|
|
1256
|
+
function compressTree(node) {
|
|
1257
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1258
|
+
let child = node.children[i];
|
|
1259
|
+
while (child.children.length === 1 && child.files.length === 0) {
|
|
1260
|
+
const grandchild = child.children[0];
|
|
1261
|
+
child = { name: child.name + "/" + grandchild.name, children: grandchild.children, files: grandchild.files };
|
|
1262
|
+
node.children[i] = child;
|
|
1263
|
+
}
|
|
1264
|
+
compressTree(child);
|
|
1265
|
+
}
|
|
1017
1266
|
}
|
|
1018
|
-
|
|
1019
|
-
.
|
|
1020
|
-
|
|
1267
|
+
function countFiles(node) {
|
|
1268
|
+
let count = node.files.length;
|
|
1269
|
+
for (const child of node.children) count += countFiles(child);
|
|
1270
|
+
return count;
|
|
1021
1271
|
}
|
|
1022
|
-
|
|
1023
|
-
.
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1272
|
+
function hasStale(node, staleCounts) {
|
|
1273
|
+
for (const f of node.files) {
|
|
1274
|
+
if (staleCounts[f.id]) return true;
|
|
1275
|
+
}
|
|
1276
|
+
for (const child of node.children) {
|
|
1277
|
+
if (hasStale(child, staleCounts)) return true;
|
|
1278
|
+
}
|
|
1279
|
+
return false;
|
|
1029
1280
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1281
|
+
function TreeView({ node, depth, annotationCounts, staleCounts }) {
|
|
1282
|
+
const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
|
|
1283
|
+
return /* @__PURE__ */ jsx("div", { children: [
|
|
1284
|
+
sortedChildren.map((child) => {
|
|
1285
|
+
const total = countFiles(child);
|
|
1286
|
+
const isCollapsible = total > 1;
|
|
1287
|
+
const stale = hasStale(child, staleCounts);
|
|
1288
|
+
return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
|
|
1289
|
+
/* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
1290
|
+
isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
|
|
1291
|
+
/* @__PURE__ */ jsx("span", { className: "folder-name", children: [
|
|
1292
|
+
child.name,
|
|
1293
|
+
"/"
|
|
1294
|
+
] }),
|
|
1295
|
+
stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
|
|
1296
|
+
] }),
|
|
1297
|
+
/* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
|
|
1298
|
+
] });
|
|
1299
|
+
}),
|
|
1300
|
+
node.files.map((f) => {
|
|
1301
|
+
const diff = JSON.parse(f.diff_data || "{}");
|
|
1302
|
+
const count = annotationCounts[f.id] || 0;
|
|
1303
|
+
const stale = staleCounts[f.id] || 0;
|
|
1304
|
+
const fileName = f.file_path.split("/").pop();
|
|
1305
|
+
return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
1306
|
+
/* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
|
|
1307
|
+
/* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
|
|
1308
|
+
/* @__PURE__ */ jsx("span", { className: `file-status ${diff.status || ""}`, children: diff.status || "" }),
|
|
1309
|
+
stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
|
|
1310
|
+
count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
|
|
1311
|
+
] });
|
|
1312
|
+
})
|
|
1313
|
+
] });
|
|
1036
1314
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
align-items: center;
|
|
1041
|
-
padding: 6px 16px 6px 16px;
|
|
1042
|
-
cursor: pointer;
|
|
1043
|
-
font-size: 13px;
|
|
1044
|
-
gap: 8px;
|
|
1045
|
-
border-left: 3px solid transparent;
|
|
1046
|
-
transition: background 0.1s;
|
|
1315
|
+
function FileList({ files, annotationCounts, staleCounts }) {
|
|
1316
|
+
const tree = buildFileTree(files);
|
|
1317
|
+
return /* @__PURE__ */ jsx("div", { className: "file-list", children: /* @__PURE__ */ jsx("div", { className: "file-list-items", children: /* @__PURE__ */ jsx(TreeView, { node: tree, depth: 0, annotationCounts, staleCounts }) }) });
|
|
1047
1318
|
}
|
|
1048
1319
|
|
|
1049
|
-
.
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1320
|
+
// src/components/diffView.tsx
|
|
1321
|
+
function DiffView({ file, diff, annotations, mode }) {
|
|
1322
|
+
const annotationsByLine = {};
|
|
1323
|
+
for (const a of annotations) {
|
|
1324
|
+
const key = `${a.line_number}:${a.side}`;
|
|
1325
|
+
if (!annotationsByLine[key]) annotationsByLine[key] = [];
|
|
1326
|
+
annotationsByLine[key].push(a);
|
|
1327
|
+
}
|
|
1328
|
+
return /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, children: [
|
|
1329
|
+
/* @__PURE__ */ jsx("div", { className: "diff-header", children: [
|
|
1330
|
+
/* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
|
|
1331
|
+
/* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
|
|
1332
|
+
] }),
|
|
1333
|
+
diff.isBinary ? /* @__PURE__ */ jsx("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx(UnifiedDiff, { hunks: diff.hunks, annotationsByLine }) : /* @__PURE__ */ jsx(SplitDiff, { hunks: diff.hunks, annotationsByLine })
|
|
1334
|
+
] });
|
|
1057
1335
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
.
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
flex-shrink: 0;
|
|
1121
|
-
text-align: center;
|
|
1122
|
-
transition: transform 0.1s;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
.folder-header.collapsed .folder-arrow {
|
|
1126
|
-
transform: rotate(-90deg);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
.folder-arrow-spacer {
|
|
1130
|
-
width: 12px;
|
|
1131
|
-
flex-shrink: 0;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
.folder-name {
|
|
1135
|
-
font-family: var(--font-mono);
|
|
1136
|
-
font-size: 12px;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
/* Main content */
|
|
1140
|
-
.main-content {
|
|
1141
|
-
flex: 1;
|
|
1142
|
-
overflow: auto;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
.welcome-message {
|
|
1146
|
-
display: flex;
|
|
1147
|
-
flex-direction: column;
|
|
1148
|
-
align-items: center;
|
|
1149
|
-
justify-content: center;
|
|
1150
|
-
height: 100%;
|
|
1151
|
-
color: var(--text-dim);
|
|
1152
|
-
gap: 8px;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
.welcome-message h3 { color: var(--text); }
|
|
1156
|
-
|
|
1157
|
-
/* Diff view */
|
|
1158
|
-
.diff-header {
|
|
1159
|
-
position: sticky;
|
|
1160
|
-
top: 0;
|
|
1161
|
-
z-index: 10;
|
|
1162
|
-
display: flex;
|
|
1163
|
-
align-items: center;
|
|
1164
|
-
justify-content: space-between;
|
|
1165
|
-
padding: 10px 16px;
|
|
1166
|
-
background: var(--bg-surface);
|
|
1167
|
-
border-bottom: 1px solid var(--border);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
.diff-header .file-path {
|
|
1171
|
-
font-family: var(--font-mono);
|
|
1172
|
-
font-size: 13px;
|
|
1173
|
-
color: var(--text-bright);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
.diff-header-actions {
|
|
1177
|
-
display: flex;
|
|
1178
|
-
gap: 8px;
|
|
1179
|
-
align-items: center;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
/* Split diff */
|
|
1183
|
-
.diff-table-split {
|
|
1184
|
-
width: 100%;
|
|
1185
|
-
font-family: var(--font-mono);
|
|
1186
|
-
font-size: 13px;
|
|
1187
|
-
line-height: 20px;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
.split-row {
|
|
1191
|
-
display: grid;
|
|
1192
|
-
grid-template-columns: 1fr 1fr;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
.split-row .diff-line {
|
|
1196
|
-
display: flex;
|
|
1197
|
-
min-width: 0;
|
|
1198
|
-
overflow: hidden;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
.split-row .diff-line.empty {
|
|
1202
|
-
background: var(--gutter-bg);
|
|
1203
|
-
min-height: 20px;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
.split-row .gutter {
|
|
1207
|
-
width: 50px;
|
|
1208
|
-
min-width: 50px;
|
|
1209
|
-
padding: 0 6px;
|
|
1210
|
-
text-align: right;
|
|
1211
|
-
color: var(--gutter-text);
|
|
1212
|
-
background: var(--gutter-bg);
|
|
1213
|
-
user-select: none;
|
|
1214
|
-
font-size: 12px;
|
|
1215
|
-
flex-shrink: 0;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
.split-left { border-right: 1px solid var(--border); }
|
|
1219
|
-
|
|
1220
|
-
.diff-table-split .hunk-separator {
|
|
1221
|
-
grid-column: 1 / -1;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
.diff-line {
|
|
1225
|
-
display: flex;
|
|
1226
|
-
min-height: 20px;
|
|
1227
|
-
border-bottom: 1px solid rgba(54,54,82,0.3);
|
|
1228
|
-
cursor: pointer;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
.diff-line:hover { filter: brightness(1.2); }
|
|
1232
|
-
|
|
1233
|
-
.diff-line.add { background: var(--diff-add-bg); }
|
|
1234
|
-
.diff-line.remove { background: var(--diff-remove-bg); }
|
|
1235
|
-
.diff-line.context { background: var(--diff-context-bg); }
|
|
1236
|
-
|
|
1237
|
-
.diff-line .gutter {
|
|
1238
|
-
width: 60px;
|
|
1239
|
-
min-width: 60px;
|
|
1240
|
-
padding: 0 8px;
|
|
1241
|
-
text-align: right;
|
|
1242
|
-
color: var(--gutter-text);
|
|
1243
|
-
background: var(--gutter-bg);
|
|
1244
|
-
user-select: none;
|
|
1245
|
-
font-size: 12px;
|
|
1246
|
-
display: flex;
|
|
1247
|
-
align-items: center;
|
|
1248
|
-
justify-content: flex-end;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
.diff-line .code {
|
|
1252
|
-
flex: 1;
|
|
1253
|
-
padding: 0 12px;
|
|
1254
|
-
white-space: pre;
|
|
1255
|
-
overflow-x: auto;
|
|
1256
|
-
tab-size: 4;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
.diff-line.add .code::before { content: '+'; color: var(--green); margin-right: 4px; }
|
|
1260
|
-
.diff-line.remove .code::before { content: '-'; color: var(--red); margin-right: 4px; }
|
|
1261
|
-
|
|
1262
|
-
/* Unified diff */
|
|
1263
|
-
.diff-table-unified {
|
|
1264
|
-
width: 100%;
|
|
1265
|
-
font-family: var(--font-mono);
|
|
1266
|
-
font-size: 13px;
|
|
1267
|
-
line-height: 20px;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
.diff-table-unified .diff-line {
|
|
1271
|
-
display: flex;
|
|
1272
|
-
min-width: 0;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
.diff-table-unified .gutter-old,
|
|
1276
|
-
.diff-table-unified .gutter-new {
|
|
1277
|
-
width: 50px;
|
|
1278
|
-
min-width: 50px;
|
|
1279
|
-
padding: 0 6px;
|
|
1280
|
-
text-align: right;
|
|
1281
|
-
color: var(--gutter-text);
|
|
1282
|
-
background: var(--gutter-bg);
|
|
1283
|
-
user-select: none;
|
|
1284
|
-
font-size: 12px;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
/* Hunk separator */
|
|
1288
|
-
.hunk-separator {
|
|
1289
|
-
padding: 4px 16px;
|
|
1290
|
-
background: rgba(137,180,250,0.05);
|
|
1291
|
-
color: var(--text-dim);
|
|
1292
|
-
font-size: 12px;
|
|
1293
|
-
font-family: var(--font-mono);
|
|
1294
|
-
border-top: 1px solid var(--border);
|
|
1295
|
-
border-bottom: 1px solid var(--border);
|
|
1296
|
-
cursor: pointer;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
.hunk-separator:hover { background: rgba(137,180,250,0.1); }
|
|
1300
|
-
|
|
1301
|
-
.expand-controls {
|
|
1302
|
-
display: flex;
|
|
1303
|
-
gap: 8px;
|
|
1304
|
-
padding: 2px 16px;
|
|
1305
|
-
background: rgba(137,180,250,0.05);
|
|
1306
|
-
border-bottom: 1px solid var(--border);
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
.expand-btn {
|
|
1310
|
-
font-size: 11px;
|
|
1311
|
-
color: var(--accent);
|
|
1312
|
-
background: none;
|
|
1313
|
-
border: none;
|
|
1314
|
-
cursor: pointer;
|
|
1315
|
-
padding: 2px 6px;
|
|
1316
|
-
border-radius: 3px;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
.expand-btn:hover { background: rgba(137,180,250,0.15); }
|
|
1320
|
-
|
|
1321
|
-
/* Annotation indicators */
|
|
1322
|
-
.has-annotation .gutter {
|
|
1323
|
-
position: relative;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
.has-annotation .gutter::after {
|
|
1327
|
-
content: '';
|
|
1328
|
-
position: absolute;
|
|
1329
|
-
right: 2px;
|
|
1330
|
-
top: 50%;
|
|
1331
|
-
transform: translateY(-50%);
|
|
1332
|
-
width: 6px;
|
|
1333
|
-
height: 6px;
|
|
1334
|
-
border-radius: 50%;
|
|
1335
|
-
background: var(--accent);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
/* Annotation inline display */
|
|
1339
|
-
.annotation-row {
|
|
1340
|
-
padding: 8px 16px 8px 76px;
|
|
1341
|
-
background: rgba(137,180,250,0.05);
|
|
1342
|
-
border-left: 3px solid var(--accent);
|
|
1343
|
-
font-family: var(--font-sans);
|
|
1344
|
-
font-size: 13px;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
.annotation-row .annotation-item {
|
|
1348
|
-
display: flex;
|
|
1349
|
-
gap: 8px;
|
|
1350
|
-
align-items: flex-start;
|
|
1351
|
-
padding: 4px 0;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
.annotation-category {
|
|
1355
|
-
font-size: 11px;
|
|
1356
|
-
font-weight: 600;
|
|
1357
|
-
padding: 1px 6px;
|
|
1358
|
-
border-radius: 3px;
|
|
1359
|
-
white-space: nowrap;
|
|
1360
|
-
flex-shrink: 0;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
.category-bug { color: var(--red); background: rgba(243,139,168,0.15); }
|
|
1364
|
-
.category-fix { color: var(--orange); background: rgba(250,179,135,0.15); }
|
|
1365
|
-
.category-style { color: var(--purple); background: rgba(203,166,247,0.15); }
|
|
1366
|
-
.category-pattern-follow { color: var(--green); background: rgba(166,227,161,0.15); }
|
|
1367
|
-
.category-pattern-avoid { color: var(--red); background: rgba(243,139,168,0.15); }
|
|
1368
|
-
.category-note { color: var(--blue); background: rgba(137,180,250,0.15); }
|
|
1369
|
-
.category-remember { color: var(--yellow); background: rgba(249,226,175,0.15); }
|
|
1370
|
-
|
|
1371
|
-
.annotation-text {
|
|
1372
|
-
flex: 1;
|
|
1373
|
-
color: var(--text);
|
|
1374
|
-
line-height: 1.4;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
.annotation-actions {
|
|
1378
|
-
display: flex;
|
|
1379
|
-
gap: 4px;
|
|
1380
|
-
flex-shrink: 0;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
/* Stale annotations */
|
|
1384
|
-
.annotation-stale {
|
|
1385
|
-
opacity: 0.7;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
.annotation-row:has(.annotation-stale) {
|
|
1389
|
-
border-left-color: var(--orange);
|
|
1390
|
-
background: rgba(250,179,135,0.05);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
.btn-keep { color: var(--orange); }
|
|
1394
|
-
.btn-keep:hover { background: rgba(250,179,135,0.15); }
|
|
1395
|
-
|
|
1396
|
-
.btn-icon { display: inline-flex; align-items: center; justify-content: center; padding: 3px 4px; }
|
|
1397
|
-
.btn-icon svg { display: block; }
|
|
1398
|
-
|
|
1399
|
-
.annotation-category[data-action="reclassify"] { cursor: pointer; }
|
|
1400
|
-
.annotation-category[data-action="reclassify"]:hover { filter: brightness(1.3); }
|
|
1401
|
-
|
|
1402
|
-
.reclassify-popup {
|
|
1403
|
-
background: var(--bg-surface);
|
|
1404
|
-
border: 1px solid var(--border);
|
|
1405
|
-
border-radius: var(--radius);
|
|
1406
|
-
padding: 4px 0;
|
|
1407
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
1408
|
-
min-width: 140px;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
.reclassify-option {
|
|
1412
|
-
padding: 6px 12px;
|
|
1413
|
-
cursor: pointer;
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
.reclassify-option:hover {
|
|
1417
|
-
background: var(--bg-hover);
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
.reclassify-option.active {
|
|
1421
|
-
background: rgba(137,180,250,0.1);
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
/* Stale indicator dot */
|
|
1425
|
-
.stale-dot {
|
|
1426
|
-
width: 6px;
|
|
1427
|
-
height: 6px;
|
|
1428
|
-
border-radius: 50%;
|
|
1429
|
-
background: var(--orange);
|
|
1430
|
-
flex-shrink: 0;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
/* Drag handle */
|
|
1434
|
-
.annotation-drag-handle {
|
|
1435
|
-
cursor: grab;
|
|
1436
|
-
color: var(--text-dim);
|
|
1437
|
-
font-size: 14px;
|
|
1438
|
-
line-height: 1;
|
|
1439
|
-
flex-shrink: 0;
|
|
1440
|
-
user-select: none;
|
|
1441
|
-
padding: 0 2px;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
.annotation-drag-handle:hover {
|
|
1445
|
-
color: var(--text);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
/* Drop target highlight */
|
|
1449
|
-
.diff-line.drag-over {
|
|
1450
|
-
outline: 2px solid var(--accent);
|
|
1451
|
-
outline-offset: -2px;
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
/* Annotation form */
|
|
1455
|
-
.annotation-form-container {
|
|
1456
|
-
padding: 12px 16px 12px 76px;
|
|
1457
|
-
background: rgba(137,180,250,0.08);
|
|
1458
|
-
border-left: 3px solid var(--accent);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
.annotation-form {
|
|
1462
|
-
display: flex;
|
|
1463
|
-
flex-direction: column;
|
|
1464
|
-
gap: 8px;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
.form-category-badge {
|
|
1468
|
-
cursor: pointer;
|
|
1469
|
-
align-self: flex-start;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
.form-category-badge:hover {
|
|
1473
|
-
filter: brightness(1.3);
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
.annotation-form textarea {
|
|
1477
|
-
width: 100%;
|
|
1478
|
-
min-height: 60px;
|
|
1479
|
-
padding: 8px;
|
|
1480
|
-
background: var(--bg);
|
|
1481
|
-
color: var(--text);
|
|
1482
|
-
border: 1px solid var(--border);
|
|
1483
|
-
border-radius: var(--radius);
|
|
1484
|
-
font-family: var(--font-sans);
|
|
1485
|
-
font-size: 13px;
|
|
1486
|
-
resize: vertical;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
.annotation-form textarea:focus {
|
|
1490
|
-
outline: none;
|
|
1491
|
-
border-color: var(--accent);
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
.annotation-form-actions {
|
|
1495
|
-
display: flex;
|
|
1496
|
-
gap: 8px;
|
|
1497
|
-
justify-content: flex-end;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
/* Buttons */
|
|
1501
|
-
.btn {
|
|
1502
|
-
padding: 6px 14px;
|
|
1503
|
-
border-radius: var(--radius);
|
|
1504
|
-
border: 1px solid var(--border);
|
|
1505
|
-
background: var(--bg-surface);
|
|
1506
|
-
color: var(--text);
|
|
1507
|
-
cursor: pointer;
|
|
1508
|
-
font-size: 13px;
|
|
1509
|
-
transition: background 0.15s;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
.btn:hover { background: var(--bg-hover); }
|
|
1513
|
-
.btn.active { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
|
1514
|
-
|
|
1515
|
-
.btn-sm { padding: 3px 10px; font-size: 12px; }
|
|
1516
|
-
.btn-xs { padding: 2px 6px; font-size: 11px; }
|
|
1517
|
-
|
|
1518
|
-
.btn-primary {
|
|
1519
|
-
background: var(--accent);
|
|
1520
|
-
color: var(--bg);
|
|
1521
|
-
border-color: var(--accent);
|
|
1522
|
-
font-weight: 600;
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
.btn-primary:hover { background: var(--accent-hover); }
|
|
1526
|
-
|
|
1527
|
-
.btn-danger { color: var(--red); }
|
|
1528
|
-
.btn-danger:hover { background: rgba(243,139,168,0.15); }
|
|
1529
|
-
|
|
1530
|
-
.btn-link {
|
|
1531
|
-
background: none;
|
|
1532
|
-
border: none;
|
|
1533
|
-
color: var(--accent);
|
|
1534
|
-
text-decoration: none;
|
|
1535
|
-
text-align: center;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
.btn-link:hover { text-decoration: underline; }
|
|
1539
|
-
|
|
1540
|
-
/* History */
|
|
1541
|
-
body:has(.history-page) {
|
|
1542
|
-
overflow: auto;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
.history-page {
|
|
1546
|
-
max-width: 800px;
|
|
1547
|
-
margin: 0 auto;
|
|
1548
|
-
padding: 40px 20px;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
.history-page h1 { margin-bottom: 24px; }
|
|
1552
|
-
|
|
1553
|
-
.history-item {
|
|
1554
|
-
padding: 16px;
|
|
1555
|
-
background: var(--bg-surface);
|
|
1556
|
-
border: 1px solid var(--border);
|
|
1557
|
-
border-radius: var(--radius);
|
|
1558
|
-
margin-bottom: 12px;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
.history-item {
|
|
1562
|
-
position: relative;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
.history-item h3 { font-size: 15px; margin-bottom: 4px; padding-right: 32px; }
|
|
1566
|
-
.history-item .meta { font-size: 12px; color: var(--text-dim); }
|
|
1567
|
-
|
|
1568
|
-
.history-item .delete-review-btn {
|
|
1569
|
-
position: absolute;
|
|
1570
|
-
top: 12px;
|
|
1571
|
-
right: 12px;
|
|
1572
|
-
background: none;
|
|
1573
|
-
border: none;
|
|
1574
|
-
color: var(--text-dim);
|
|
1575
|
-
cursor: pointer;
|
|
1576
|
-
padding: 4px;
|
|
1577
|
-
border-radius: var(--radius);
|
|
1578
|
-
font-size: 16px;
|
|
1579
|
-
line-height: 1;
|
|
1580
|
-
opacity: 0;
|
|
1581
|
-
transition: opacity 0.15s, color 0.15s;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
.history-item:hover .delete-review-btn { opacity: 1; }
|
|
1585
|
-
.history-item .delete-review-btn:hover { color: var(--red); background: rgba(243,139,168,0.15); }
|
|
1586
|
-
|
|
1587
|
-
.bulk-actions {
|
|
1588
|
-
display: flex;
|
|
1589
|
-
gap: 8px;
|
|
1590
|
-
margin: 12px 0;
|
|
1591
|
-
padding: 4px 0;
|
|
1592
|
-
align-items: center;
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
.bulk-actions span {
|
|
1596
|
-
font-size: 13px;
|
|
1597
|
-
color: var(--text-dim);
|
|
1598
|
-
margin-right: auto;
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
.status-badge {
|
|
1602
|
-
display: inline-block;
|
|
1603
|
-
font-size: 11px;
|
|
1604
|
-
padding: 1px 8px;
|
|
1605
|
-
border-radius: 10px;
|
|
1606
|
-
font-weight: 500;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
.status-badge.in_progress, .status-badge.in-progress { color: var(--yellow); background: rgba(249,226,175,0.15); }
|
|
1610
|
-
.status-badge.completed { color: var(--green); background: rgba(166,227,161,0.15); }
|
|
1611
|
-
|
|
1612
|
-
.history-item-link {
|
|
1613
|
-
text-decoration: none;
|
|
1614
|
-
color: inherit;
|
|
1615
|
-
display: block;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
.history-item-link:hover .history-item {
|
|
1619
|
-
border-color: var(--accent);
|
|
1620
|
-
background: var(--bg-hover);
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
.expanded-context {
|
|
1624
|
-
background: rgba(137,180,250,0.03);
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
/* Wrap mode */
|
|
1628
|
-
.wrap-lines .code {
|
|
1629
|
-
white-space: pre-wrap !important;
|
|
1630
|
-
word-break: break-all;
|
|
1631
|
-
overflow-x: visible !important;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
/* Hide horizontal scrollbars on code cells (still scrollable via trackpad) */
|
|
1635
|
-
.diff-line .code {
|
|
1636
|
-
scrollbar-width: none;
|
|
1637
|
-
}
|
|
1638
|
-
.diff-line .code::-webkit-scrollbar {
|
|
1639
|
-
height: 0;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
/* Controls layout */
|
|
1643
|
-
.sidebar-controls-row {
|
|
1644
|
-
display: flex;
|
|
1645
|
-
gap: 8px;
|
|
1646
|
-
align-items: center;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
.controls-divider {
|
|
1650
|
-
width: 1px;
|
|
1651
|
-
height: 18px;
|
|
1652
|
-
background: var(--border);
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
/* Complete modal */
|
|
1656
|
-
.modal-overlay {
|
|
1657
|
-
position: fixed;
|
|
1658
|
-
inset: 0;
|
|
1659
|
-
background: rgba(0,0,0,0.6);
|
|
1660
|
-
display: flex;
|
|
1661
|
-
align-items: center;
|
|
1662
|
-
justify-content: center;
|
|
1663
|
-
z-index: 100;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
.modal {
|
|
1667
|
-
background: var(--bg-surface);
|
|
1668
|
-
border: 1px solid var(--border);
|
|
1669
|
-
border-radius: 12px;
|
|
1670
|
-
padding: 24px;
|
|
1671
|
-
max-width: 480px;
|
|
1672
|
-
width: 90%;
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
.modal h3 { margin-bottom: 12px; }
|
|
1676
|
-
.modal p { margin-bottom: 16px; color: var(--text-dim); font-size: 14px; }
|
|
1677
|
-
|
|
1678
|
-
.modal-label { margin-bottom: 4px !important; font-size: 13px !important; color: var(--text-dim) !important; }
|
|
1679
|
-
|
|
1680
|
-
.modal-copyable {
|
|
1681
|
-
font-family: var(--font-mono);
|
|
1682
|
-
font-size: 12px;
|
|
1683
|
-
color: var(--accent);
|
|
1684
|
-
background: var(--bg);
|
|
1685
|
-
padding: 8px 12px;
|
|
1686
|
-
border-radius: var(--radius);
|
|
1687
|
-
border: 1px solid var(--border);
|
|
1688
|
-
margin-bottom: 16px;
|
|
1689
|
-
cursor: pointer;
|
|
1690
|
-
position: relative;
|
|
1691
|
-
word-break: break-all;
|
|
1692
|
-
transition: border-color 0.15s;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
.modal-copyable:hover { border-color: var(--accent); }
|
|
1696
|
-
|
|
1697
|
-
.modal-copyable.copied::after {
|
|
1698
|
-
content: 'Copied!';
|
|
1699
|
-
position: absolute;
|
|
1700
|
-
right: 8px;
|
|
1701
|
-
top: 50%;
|
|
1702
|
-
transform: translateY(-50%);
|
|
1703
|
-
font-size: 11px;
|
|
1704
|
-
color: var(--green);
|
|
1705
|
-
font-family: var(--font-sans);
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
.modal-gitignore {
|
|
1709
|
-
margin-bottom: 16px;
|
|
1710
|
-
padding-top: 12px;
|
|
1711
|
-
border-top: 1px solid var(--border);
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
.modal-actions {
|
|
1715
|
-
display: flex;
|
|
1716
|
-
gap: 8px;
|
|
1717
|
-
justify-content: flex-end;
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
/* Scrollbar */
|
|
1721
|
-
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
1722
|
-
::-webkit-scrollbar-track { background: transparent; }
|
|
1723
|
-
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
1724
|
-
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
|
|
1725
|
-
|
|
1726
|
-
/* Progress */
|
|
1727
|
-
.progress-bar {
|
|
1728
|
-
height: 3px;
|
|
1729
|
-
background: var(--border);
|
|
1730
|
-
border-radius: 2px;
|
|
1731
|
-
overflow: hidden;
|
|
1732
|
-
margin: 8px 16px;
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
.progress-bar-fill {
|
|
1736
|
-
height: 100%;
|
|
1737
|
-
background: var(--accent);
|
|
1738
|
-
transition: width 0.3s;
|
|
1739
|
-
}
|
|
1740
|
-
`;
|
|
1741
|
-
}
|
|
1742
|
-
function getClientScript() {
|
|
1743
|
-
return `
|
|
1744
|
-
(function() {
|
|
1745
|
-
const state = {
|
|
1746
|
-
reviewId: document.body.dataset.reviewId,
|
|
1747
|
-
currentFileId: null,
|
|
1748
|
-
diffMode: 'split',
|
|
1749
|
-
wrapLines: false,
|
|
1750
|
-
files: [],
|
|
1751
|
-
fileOrder: [],
|
|
1752
|
-
annotationCounts: {},
|
|
1753
|
-
staleCounts: {},
|
|
1754
|
-
filterText: '',
|
|
1755
|
-
_dragAnnotation: null,
|
|
1756
|
-
};
|
|
1757
|
-
|
|
1758
|
-
const CATEGORIES = [
|
|
1759
|
-
{ value: 'bug', label: 'Bug' },
|
|
1760
|
-
{ value: 'fix', label: 'Fix needed' },
|
|
1761
|
-
{ value: 'style', label: 'Style' },
|
|
1762
|
-
{ value: 'pattern-follow', label: 'Pattern to follow' },
|
|
1763
|
-
{ value: 'pattern-avoid', label: 'Pattern to avoid' },
|
|
1764
|
-
{ value: 'note', label: 'Note' },
|
|
1765
|
-
{ value: 'remember', label: 'Remember (for AI)' },
|
|
1766
|
-
];
|
|
1767
|
-
|
|
1768
|
-
// --- API ---
|
|
1769
|
-
async function api(path, opts = {}) {
|
|
1770
|
-
const separator = path.includes('?') ? '&' : '?';
|
|
1771
|
-
const url = '/api' + path + separator + 'reviewId=' + encodeURIComponent(state.reviewId);
|
|
1772
|
-
const res = await fetch(url, {
|
|
1773
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1774
|
-
...opts,
|
|
1775
|
-
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
1776
|
-
});
|
|
1777
|
-
return res.json();
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
// --- Init ---
|
|
1781
|
-
async function init() {
|
|
1782
|
-
await loadFiles();
|
|
1783
|
-
bindSidebarEvents();
|
|
1784
|
-
bindDiffModeToggle();
|
|
1785
|
-
bindWrapToggle();
|
|
1786
|
-
bindFileFilter();
|
|
1787
|
-
bindSidebarResize();
|
|
1788
|
-
bindCompleteButton();
|
|
1789
|
-
bindReopenButton();
|
|
1790
|
-
initScrollSync();
|
|
1791
|
-
updateProgress();
|
|
1792
|
-
document.addEventListener('dragend', () => {
|
|
1793
|
-
state._dragAnnotation = null;
|
|
1794
|
-
document.querySelectorAll('.diff-line.drag-over').forEach(d => d.classList.remove('drag-over'));
|
|
1795
|
-
});
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
async function loadFiles() {
|
|
1799
|
-
const data = await api('/files');
|
|
1800
|
-
state.files = data.files;
|
|
1801
|
-
state.annotationCounts = data.annotationCounts;
|
|
1802
|
-
state.staleCounts = data.staleCounts || {};
|
|
1803
|
-
renderFileList();
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
function renderFileList() {
|
|
1807
|
-
var list = document.querySelector('.file-list-items');
|
|
1808
|
-
if (!list) return;
|
|
1809
|
-
list.innerHTML = '';
|
|
1810
|
-
state.fileOrder = [];
|
|
1811
|
-
var filtered = state.files;
|
|
1812
|
-
if (state.filterText) {
|
|
1813
|
-
var q = state.filterText.toLowerCase();
|
|
1814
|
-
filtered = state.files.filter(function(f) {
|
|
1815
|
-
return f.file_path.toLowerCase().indexOf(q) !== -1;
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
|
-
var tree = buildFileTree(filtered);
|
|
1819
|
-
renderTreeNode(list, tree, 0);
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
function buildFileTree(files) {
|
|
1823
|
-
var root = { name: '', children: [], files: [] };
|
|
1824
|
-
files.forEach(function(f) {
|
|
1825
|
-
var parts = f.file_path.split('/');
|
|
1826
|
-
var node = root;
|
|
1827
|
-
for (var i = 0; i < parts.length - 1; i++) {
|
|
1828
|
-
var child = node.children.find(function(c) { return c.name === parts[i]; });
|
|
1829
|
-
if (!child) {
|
|
1830
|
-
child = { name: parts[i], children: [], files: [] };
|
|
1831
|
-
node.children.push(child);
|
|
1832
|
-
}
|
|
1833
|
-
node = child;
|
|
1834
|
-
}
|
|
1835
|
-
node.files.push(f);
|
|
1836
|
-
});
|
|
1837
|
-
compressTree(root);
|
|
1838
|
-
return root;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
function compressTree(node) {
|
|
1842
|
-
for (var i = 0; i < node.children.length; i++) {
|
|
1843
|
-
var child = node.children[i];
|
|
1844
|
-
while (child.children.length === 1 && child.files.length === 0) {
|
|
1845
|
-
var gc = child.children[0];
|
|
1846
|
-
child = { name: child.name + '/' + gc.name, children: gc.children, files: gc.files };
|
|
1847
|
-
node.children[i] = child;
|
|
1848
|
-
}
|
|
1849
|
-
compressTree(child);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
function countTreeFiles(node) {
|
|
1854
|
-
var count = node.files.length;
|
|
1855
|
-
node.children.forEach(function(c) { count += countTreeFiles(c); });
|
|
1856
|
-
return count;
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
function hasStaleInTree(node) {
|
|
1860
|
-
for (var i = 0; i < node.files.length; i++) {
|
|
1861
|
-
if (state.staleCounts[node.files[i].id]) return true;
|
|
1862
|
-
}
|
|
1863
|
-
for (var i = 0; i < node.children.length; i++) {
|
|
1864
|
-
if (hasStaleInTree(node.children[i])) return true;
|
|
1865
|
-
}
|
|
1866
|
-
return false;
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
function renderTreeNode(container, node, depth) {
|
|
1870
|
-
var sortedChildren = node.children.slice().sort(function(a, b) { return a.name.localeCompare(b.name); });
|
|
1871
|
-
|
|
1872
|
-
sortedChildren.forEach(function(child) {
|
|
1873
|
-
var total = countTreeFiles(child);
|
|
1874
|
-
var isCollapsible = total > 1;
|
|
1875
|
-
|
|
1876
|
-
var group = document.createElement('div');
|
|
1877
|
-
group.className = 'folder-group';
|
|
1878
|
-
|
|
1879
|
-
var header = document.createElement('div');
|
|
1880
|
-
header.className = 'folder-header' + (isCollapsible ? ' collapsible' : '');
|
|
1881
|
-
header.style.paddingLeft = (16 + depth * 12) + 'px';
|
|
1882
|
-
var stale = hasStaleInTree(child);
|
|
1883
|
-
header.innerHTML =
|
|
1884
|
-
(isCollapsible ? '<span class="folder-arrow">\u25BE</span>' : '<span class="folder-arrow-spacer"></span>') +
|
|
1885
|
-
'<span class="folder-name">' + esc(child.name) + '/</span>' +
|
|
1886
|
-
(stale ? '<span class="stale-dot"></span>' : '');
|
|
1887
|
-
|
|
1888
|
-
var content = document.createElement('div');
|
|
1889
|
-
content.className = 'folder-content';
|
|
1890
|
-
|
|
1891
|
-
if (isCollapsible) {
|
|
1892
|
-
header.addEventListener('click', function() {
|
|
1893
|
-
header.classList.toggle('collapsed');
|
|
1894
|
-
});
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
renderTreeNode(content, child, depth + 1);
|
|
1898
|
-
|
|
1899
|
-
group.appendChild(header);
|
|
1900
|
-
group.appendChild(content);
|
|
1901
|
-
container.appendChild(group);
|
|
1902
|
-
});
|
|
1903
|
-
|
|
1904
|
-
node.files.forEach(function(f) {
|
|
1905
|
-
var diff = JSON.parse(f.diff_data || '{}');
|
|
1906
|
-
var el = document.createElement('div');
|
|
1907
|
-
el.className = 'file-item' + (f.id === state.currentFileId ? ' active' : '');
|
|
1908
|
-
el.dataset.fileId = f.id;
|
|
1909
|
-
el.style.paddingLeft = (16 + depth * 12) + 'px';
|
|
1910
|
-
var count = state.annotationCounts[f.id] || 0;
|
|
1911
|
-
var staleCount = state.staleCounts[f.id] || 0;
|
|
1912
|
-
var fileName = f.file_path.split('/').pop();
|
|
1913
|
-
el.innerHTML =
|
|
1914
|
-
'<span class="status-dot ' + f.status + '"></span>' +
|
|
1915
|
-
'<span class="file-name" title="' + esc(f.file_path) + '">' + esc(fileName) + '</span>' +
|
|
1916
|
-
'<span class="file-status ' + (diff.status || '') + '">' + (diff.status || '') + '</span>' +
|
|
1917
|
-
(staleCount ? '<span class="stale-dot"></span>' : '') +
|
|
1918
|
-
(count ? '<span class="annotation-count">' + count + '</span>' : '');
|
|
1919
|
-
el.addEventListener('click', function() { selectFile(f.id); });
|
|
1920
|
-
container.appendChild(el);
|
|
1921
|
-
state.fileOrder.push(f.id);
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
function esc(s) {
|
|
1926
|
-
const d = document.createElement('div');
|
|
1927
|
-
d.textContent = s;
|
|
1928
|
-
return d.innerHTML;
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
// --- File Selection ---
|
|
1932
|
-
async function selectFile(fileId) {
|
|
1933
|
-
state.currentFileId = fileId;
|
|
1934
|
-
document.querySelectorAll('.file-item').forEach(el => {
|
|
1935
|
-
el.classList.toggle('active', el.dataset.fileId === fileId);
|
|
1936
|
-
});
|
|
1937
|
-
|
|
1938
|
-
const container = document.getElementById('diff-container');
|
|
1939
|
-
const welcome = document.querySelector('.welcome-message');
|
|
1940
|
-
if (welcome) welcome.style.display = 'none';
|
|
1941
|
-
container.style.display = 'block';
|
|
1942
|
-
|
|
1943
|
-
const res = await fetch('/file/' + fileId + '?mode=' + state.diffMode);
|
|
1944
|
-
container.innerHTML = await res.text();
|
|
1945
|
-
|
|
1946
|
-
// Reapply wrap mode
|
|
1947
|
-
container.classList.toggle('wrap-lines', state.wrapLines);
|
|
1948
|
-
|
|
1949
|
-
// Mark file as reviewed
|
|
1950
|
-
const file = state.files.find(f => f.id === fileId);
|
|
1951
|
-
if (file && file.status === 'pending') {
|
|
1952
|
-
await api('/files/' + fileId + '/status', { method: 'PATCH', body: { status: 'reviewed' } });
|
|
1953
|
-
file.status = 'reviewed';
|
|
1954
|
-
renderFileList();
|
|
1955
|
-
updateProgress();
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
bindDiffLineClicks();
|
|
1959
|
-
bindHunkExpanders();
|
|
1960
|
-
bindDragDrop();
|
|
1961
|
-
bindServerAnnotations();
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
// --- Context Expansion ---
|
|
1965
|
-
function bindHunkExpanders() {
|
|
1966
|
-
document.querySelectorAll('.hunk-separator').forEach(el => {
|
|
1967
|
-
el.addEventListener('click', async () => {
|
|
1968
|
-
const fileId = document.querySelector('.diff-view')?.dataset?.fileId;
|
|
1969
|
-
if (!fileId) return;
|
|
1970
|
-
|
|
1971
|
-
const hunkBlock = el.closest('.hunk-block');
|
|
1972
|
-
const prevBlock = hunkBlock?.previousElementSibling;
|
|
1973
|
-
|
|
1974
|
-
// Determine the gap: from end of previous hunk to start of this hunk
|
|
1975
|
-
const newStart = parseInt(el.dataset.newStart, 10);
|
|
1976
|
-
let gapStart = 1;
|
|
1977
|
-
if (prevBlock) {
|
|
1978
|
-
const prevSep = prevBlock.querySelector('.hunk-separator');
|
|
1979
|
-
if (prevSep) {
|
|
1980
|
-
gapStart = parseInt(prevSep.dataset.newStart, 10) + parseInt(prevSep.dataset.newCount, 10);
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
const gapEnd = newStart - 1;
|
|
1984
|
-
if (gapEnd < gapStart) return;
|
|
1985
|
-
|
|
1986
|
-
const data = await api('/context/' + fileId + '?start=' + gapStart + '&end=' + gapEnd);
|
|
1987
|
-
if (!data.lines || !data.lines.length) return;
|
|
1988
|
-
|
|
1989
|
-
// Build context lines and insert before the hunk separator
|
|
1990
|
-
const fragment = document.createDocumentFragment();
|
|
1991
|
-
data.lines.forEach(line => {
|
|
1992
|
-
const wrapper = document.createElement('div');
|
|
1993
|
-
const lineEl = document.createElement('div');
|
|
1994
|
-
lineEl.className = 'diff-line context expanded-context';
|
|
1995
|
-
lineEl.dataset.line = line.num;
|
|
1996
|
-
lineEl.dataset.side = 'new';
|
|
1997
|
-
|
|
1998
|
-
if (state.diffMode === 'split') {
|
|
1999
|
-
lineEl.innerHTML =
|
|
2000
|
-
'<span class="gutter">' + line.num + '</span>' +
|
|
2001
|
-
'<span class="code">' + esc(line.content) + '</span>';
|
|
2002
|
-
const row = document.createElement('div');
|
|
2003
|
-
row.className = 'split-row';
|
|
2004
|
-
const leftEl = lineEl.cloneNode(true);
|
|
2005
|
-
leftEl.dataset.side = 'old';
|
|
2006
|
-
row.appendChild(leftEl);
|
|
2007
|
-
row.appendChild(lineEl);
|
|
2008
|
-
wrapper.appendChild(row);
|
|
2009
|
-
} else {
|
|
2010
|
-
lineEl.innerHTML =
|
|
2011
|
-
'<span class="gutter-old">' + line.num + '</span>' +
|
|
2012
|
-
'<span class="gutter-new">' + line.num + '</span>' +
|
|
2013
|
-
'<span class="code">' + esc(line.content) + '</span>';
|
|
2014
|
-
wrapper.appendChild(lineEl);
|
|
2015
|
-
}
|
|
2016
|
-
fragment.appendChild(wrapper);
|
|
2017
|
-
});
|
|
2018
|
-
|
|
2019
|
-
el.replaceWith(fragment);
|
|
2020
|
-
bindDiffLineClicks();
|
|
2021
|
-
});
|
|
2022
|
-
});
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
// --- Diff Line Clicks ---
|
|
2026
|
-
function bindDiffLineClicks() {
|
|
2027
|
-
document.querySelectorAll('.diff-line').forEach(el => {
|
|
2028
|
-
el.addEventListener('click', (e) => {
|
|
2029
|
-
if (e.target.closest('.annotation-form-container') || e.target.closest('.annotation-row')) return;
|
|
2030
|
-
const line = parseInt(el.dataset.line, 10);
|
|
2031
|
-
const side = el.dataset.side || 'new';
|
|
2032
|
-
if (!isNaN(line)) showAnnotationForm(el, line, side);
|
|
2033
|
-
});
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
function buildCategoryBadge(value) {
|
|
2038
|
-
var cat = CATEGORIES.find(function(c) { return c.value === value; });
|
|
2039
|
-
return '<span class="annotation-category category-' + esc(value) + ' form-category-badge" data-category="' + esc(value) + '">' + esc(cat ? cat.label : value) + '</span>';
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
function bindCategoryBadgeClick(container) {
|
|
2043
|
-
var badge = container.querySelector('.form-category-badge');
|
|
2044
|
-
if (!badge) return;
|
|
2045
|
-
badge.addEventListener('click', function(e) {
|
|
2046
|
-
e.stopPropagation();
|
|
2047
|
-
showCategoryPicker(badge);
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
function showCategoryPicker(badge) {
|
|
2052
|
-
document.querySelectorAll('.reclassify-popup').forEach(function(el) { el.remove(); });
|
|
2053
|
-
|
|
2054
|
-
var current = badge.dataset.category;
|
|
2055
|
-
var popup = document.createElement('div');
|
|
2056
|
-
popup.className = 'reclassify-popup';
|
|
2057
|
-
popup.innerHTML = CATEGORIES.map(function(c) {
|
|
2058
|
-
return '<div class="reclassify-option' + (c.value === current ? ' active' : '') + '" data-value="' + c.value + '">' +
|
|
2059
|
-
'<span class="annotation-category category-' + c.value + '">' + c.label + '</span>' +
|
|
2060
|
-
'</div>';
|
|
2061
|
-
}).join('');
|
|
2062
|
-
|
|
2063
|
-
var rect = badge.getBoundingClientRect();
|
|
2064
|
-
popup.style.position = 'fixed';
|
|
2065
|
-
popup.style.left = rect.left + 'px';
|
|
2066
|
-
popup.style.top = (rect.bottom + 4) + 'px';
|
|
2067
|
-
popup.style.zIndex = '1000';
|
|
2068
|
-
|
|
2069
|
-
popup.addEventListener('click', function(e) {
|
|
2070
|
-
var opt = e.target.closest('.reclassify-option');
|
|
2071
|
-
if (!opt) return;
|
|
2072
|
-
e.stopPropagation();
|
|
2073
|
-
var val = opt.dataset.value;
|
|
2074
|
-
var cat = CATEGORIES.find(function(c) { return c.value === val; });
|
|
2075
|
-
badge.className = 'annotation-category category-' + val + ' form-category-badge';
|
|
2076
|
-
badge.dataset.category = val;
|
|
2077
|
-
badge.textContent = cat ? cat.label : val;
|
|
2078
|
-
popup.remove();
|
|
2079
|
-
});
|
|
2080
|
-
|
|
2081
|
-
document.body.appendChild(popup);
|
|
2082
|
-
var closePopup = function(e) {
|
|
2083
|
-
if (!popup.contains(e.target)) {
|
|
2084
|
-
popup.remove();
|
|
2085
|
-
document.removeEventListener('click', closePopup, true);
|
|
2086
|
-
}
|
|
2087
|
-
};
|
|
2088
|
-
setTimeout(function() { document.addEventListener('click', closePopup, true); }, 0);
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
function showAnnotationForm(afterEl, lineNumber, side) {
|
|
2092
|
-
// Remove any existing form
|
|
2093
|
-
document.querySelectorAll('.annotation-form-container').forEach(el => el.remove());
|
|
2094
|
-
|
|
2095
|
-
var defaultCategory = CATEGORIES[0].value;
|
|
2096
|
-
const container = document.createElement('div');
|
|
2097
|
-
container.className = 'annotation-form-container';
|
|
2098
|
-
container.innerHTML =
|
|
2099
|
-
'<div class="annotation-form">' +
|
|
2100
|
-
buildCategoryBadge(defaultCategory) +
|
|
2101
|
-
'<textarea placeholder="Enter your annotation..." autofocus></textarea>' +
|
|
2102
|
-
'<div class="annotation-form-actions">' +
|
|
2103
|
-
'<button class="btn btn-sm cancel-btn">Cancel</button>' +
|
|
2104
|
-
'<button class="btn btn-sm btn-primary annotation-save-btn">Save</button>' +
|
|
2105
|
-
'</div>' +
|
|
2106
|
-
'</div>';
|
|
2107
|
-
|
|
2108
|
-
// Insert after the clicked line (or after its annotation rows)
|
|
2109
|
-
let insertAfter = afterEl;
|
|
2110
|
-
let next = afterEl.nextElementSibling;
|
|
2111
|
-
while (next && (next.classList.contains('annotation-row'))) {
|
|
2112
|
-
insertAfter = next;
|
|
2113
|
-
next = next.nextElementSibling;
|
|
2114
|
-
}
|
|
2115
|
-
insertAfter.parentNode.insertBefore(container, insertAfter.nextSibling);
|
|
2116
|
-
|
|
2117
|
-
bindCategoryBadgeClick(container);
|
|
2118
|
-
|
|
2119
|
-
container.querySelector('.cancel-btn').addEventListener('click', () => {
|
|
2120
|
-
container.remove();
|
|
2121
|
-
});
|
|
2122
|
-
|
|
2123
|
-
const textarea = container.querySelector('textarea');
|
|
2124
|
-
textarea.focus();
|
|
2125
|
-
|
|
2126
|
-
// Handle Cmd/Ctrl+Enter to save
|
|
2127
|
-
textarea.addEventListener('keydown', (e) => {
|
|
2128
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
2129
|
-
saveAnnotation(container, lineNumber, side);
|
|
2130
|
-
}
|
|
2131
|
-
if (e.key === 'Escape') {
|
|
2132
|
-
container.remove();
|
|
2133
|
-
}
|
|
2134
|
-
});
|
|
2135
|
-
|
|
2136
|
-
container.querySelector('.annotation-save-btn').addEventListener('click', () => {
|
|
2137
|
-
saveAnnotation(container, lineNumber, side);
|
|
2138
|
-
});
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
async function saveAnnotation(container, lineNumber, side) {
|
|
2142
|
-
const content = container.querySelector('textarea').value.trim();
|
|
2143
|
-
const category = container.querySelector('.form-category-badge').dataset.category;
|
|
2144
|
-
if (!content) return;
|
|
2145
|
-
|
|
2146
|
-
const annotation = await api('/annotations', {
|
|
2147
|
-
method: 'POST',
|
|
2148
|
-
body: {
|
|
2149
|
-
reviewFileId: state.currentFileId,
|
|
2150
|
-
lineNumber,
|
|
2151
|
-
side,
|
|
2152
|
-
category,
|
|
2153
|
-
content,
|
|
2154
|
-
},
|
|
2155
|
-
});
|
|
2156
|
-
|
|
2157
|
-
container.remove();
|
|
2158
|
-
renderAnnotationInline(annotation, lineNumber, side);
|
|
2159
|
-
|
|
2160
|
-
// Update count
|
|
2161
|
-
state.annotationCounts[state.currentFileId] = (state.annotationCounts[state.currentFileId] || 0) + 1;
|
|
2162
|
-
renderFileList();
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
function renderAnnotationInline(annotation, lineNumber, side) {
|
|
2166
|
-
// Find the line element
|
|
2167
|
-
const lineEl = document.querySelector('.diff-line[data-line="' + lineNumber + '"][data-side="' + side + '"]');
|
|
2168
|
-
if (!lineEl) return;
|
|
2169
|
-
|
|
2170
|
-
lineEl.classList.add('has-annotation');
|
|
2171
|
-
|
|
2172
|
-
// Find or create annotation row after this line
|
|
2173
|
-
let annotationRow = lineEl.nextElementSibling;
|
|
2174
|
-
if (!annotationRow || !annotationRow.classList.contains('annotation-row')) {
|
|
2175
|
-
annotationRow = document.createElement('div');
|
|
2176
|
-
annotationRow.className = 'annotation-row';
|
|
2177
|
-
lineEl.parentNode.insertBefore(annotationRow, lineEl.nextSibling);
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
const item = document.createElement('div');
|
|
2181
|
-
item.className = 'annotation-item' + (annotation.is_stale ? ' annotation-stale' : '');
|
|
2182
|
-
item.dataset.annotationId = annotation.id;
|
|
2183
|
-
if (annotation.is_stale) item.dataset.isStale = 'true';
|
|
2184
|
-
item.innerHTML = buildAnnotationItemHtml(annotation);
|
|
2185
|
-
|
|
2186
|
-
bindAnnotationItemEvents(item, annotation, lineEl, annotationRow);
|
|
2187
|
-
annotationRow.appendChild(item);
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
var ICON_TRASH = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
|
|
2191
|
-
var ICON_EDIT = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>';
|
|
2192
|
-
|
|
2193
|
-
function buildAnnotationItemHtml(annotation) {
|
|
2194
|
-
return '<span class="annotation-drag-handle" draggable="true" title="Drag to move">\u2807</span>' +
|
|
2195
|
-
'<span class="annotation-category category-' + esc(annotation.category) + '" data-action="reclassify">' + esc(annotation.category) + '</span>' +
|
|
2196
|
-
'<span class="annotation-text">' + esc(annotation.content) + '</span>' +
|
|
2197
|
-
'<div class="annotation-actions">' +
|
|
2198
|
-
(annotation.is_stale ? '<button class="btn btn-xs btn-keep" data-action="keep">Keep</button>' : '') +
|
|
2199
|
-
'<button class="btn btn-xs btn-icon" data-action="edit" title="Edit">' + ICON_EDIT + '</button>' +
|
|
2200
|
-
'<button class="btn btn-xs btn-icon btn-danger" data-action="delete" title="Delete">' + ICON_TRASH + '</button>' +
|
|
2201
|
-
'</div>';
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
function bindAnnotationItemEvents(item, annotation, lineEl, annotationRow) {
|
|
2205
|
-
item.querySelector('[data-action="delete"]')?.addEventListener('click', async (e) => {
|
|
2206
|
-
e.stopPropagation();
|
|
2207
|
-
await api('/annotations/' + annotation.id, { method: 'DELETE' });
|
|
2208
|
-
item.remove();
|
|
2209
|
-
if (annotationRow && !annotationRow.querySelector('.annotation-item')) {
|
|
2210
|
-
annotationRow.remove();
|
|
2211
|
-
if (lineEl) lineEl.classList.remove('has-annotation');
|
|
2212
|
-
}
|
|
2213
|
-
state.annotationCounts[state.currentFileId] = Math.max(0, (state.annotationCounts[state.currentFileId] || 1) - 1);
|
|
2214
|
-
if (annotation.is_stale) {
|
|
2215
|
-
state.staleCounts[state.currentFileId] = Math.max(0, (state.staleCounts[state.currentFileId] || 1) - 1);
|
|
2216
|
-
}
|
|
2217
|
-
renderFileList();
|
|
2218
|
-
});
|
|
2219
|
-
|
|
2220
|
-
item.querySelector('[data-action="edit"]')?.addEventListener('click', (e) => {
|
|
2221
|
-
e.stopPropagation();
|
|
2222
|
-
editAnnotation(item, annotation);
|
|
2223
|
-
});
|
|
2224
|
-
|
|
2225
|
-
// Double-click to edit
|
|
2226
|
-
item.addEventListener('dblclick', (e) => {
|
|
2227
|
-
e.stopPropagation();
|
|
2228
|
-
if (item.querySelector('.annotation-form')) return;
|
|
2229
|
-
editAnnotation(item, annotation);
|
|
2230
|
-
});
|
|
2231
|
-
|
|
2232
|
-
// Click category to reclassify
|
|
2233
|
-
item.querySelector('[data-action="reclassify"]')?.addEventListener('click', (e) => {
|
|
2234
|
-
e.stopPropagation();
|
|
2235
|
-
showReclassifyPopup(e.target.closest('[data-action="reclassify"]'), item, annotation);
|
|
2236
|
-
});
|
|
2237
|
-
|
|
2238
|
-
item.querySelector('[data-action="keep"]')?.addEventListener('click', async (e) => {
|
|
2239
|
-
e.stopPropagation();
|
|
2240
|
-
await api('/annotations/' + annotation.id + '/keep', { method: 'POST' });
|
|
2241
|
-
annotation.is_stale = false;
|
|
2242
|
-
item.classList.remove('annotation-stale');
|
|
2243
|
-
delete item.dataset.isStale;
|
|
2244
|
-
item.innerHTML = buildAnnotationItemHtml(annotation);
|
|
2245
|
-
bindAnnotationItemEvents(item, annotation, lineEl, annotationRow);
|
|
2246
|
-
state.staleCounts[state.currentFileId] = Math.max(0, (state.staleCounts[state.currentFileId] || 1) - 1);
|
|
2247
|
-
renderFileList();
|
|
2248
|
-
});
|
|
2249
|
-
|
|
2250
|
-
// Drag handle
|
|
2251
|
-
var handle = item.querySelector('.annotation-drag-handle');
|
|
2252
|
-
if (handle) {
|
|
2253
|
-
handle.addEventListener('dragstart', (e) => {
|
|
2254
|
-
e.stopPropagation();
|
|
2255
|
-
state._dragAnnotation = { id: annotation.id, item: item, annotation: annotation };
|
|
2256
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
2257
|
-
e.dataTransfer.setData('text/plain', annotation.id);
|
|
2258
|
-
});
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
function showReclassifyPopup(badge, item, annotation) {
|
|
2263
|
-
// Remove any existing popup
|
|
2264
|
-
document.querySelectorAll('.reclassify-popup').forEach(el => el.remove());
|
|
2265
|
-
|
|
2266
|
-
var popup = document.createElement('div');
|
|
2267
|
-
popup.className = 'reclassify-popup';
|
|
2268
|
-
popup.innerHTML = CATEGORIES.map(function(c) {
|
|
2269
|
-
return '<div class="reclassify-option' + (c.value === annotation.category ? ' active' : '') + '" data-value="' + c.value + '">' +
|
|
2270
|
-
'<span class="annotation-category category-' + c.value + '">' + c.label + '</span>' +
|
|
2271
|
-
'</div>';
|
|
2272
|
-
}).join('');
|
|
2273
|
-
|
|
2274
|
-
// Position near the badge
|
|
2275
|
-
var rect = badge.getBoundingClientRect();
|
|
2276
|
-
popup.style.position = 'fixed';
|
|
2277
|
-
popup.style.left = rect.left + 'px';
|
|
2278
|
-
popup.style.top = (rect.bottom + 4) + 'px';
|
|
2279
|
-
popup.style.zIndex = '1000';
|
|
2280
|
-
|
|
2281
|
-
popup.addEventListener('click', async (e) => {
|
|
2282
|
-
var opt = e.target.closest('.reclassify-option');
|
|
2283
|
-
if (!opt) return;
|
|
2284
|
-
e.stopPropagation();
|
|
2285
|
-
var newCategory = opt.dataset.value;
|
|
2286
|
-
if (newCategory === annotation.category) { popup.remove(); return; }
|
|
2287
|
-
annotation.category = newCategory;
|
|
2288
|
-
await api('/annotations/' + annotation.id, { method: 'PATCH', body: { content: annotation.content, category: newCategory } });
|
|
2289
|
-
item.innerHTML = buildAnnotationItemHtml(annotation);
|
|
2290
|
-
var row = item.closest('.annotation-row');
|
|
2291
|
-
var lineElRef = row ? row.previousElementSibling : null;
|
|
2292
|
-
bindAnnotationItemEvents(item, annotation, lineElRef, row);
|
|
2293
|
-
popup.remove();
|
|
2294
|
-
});
|
|
2295
|
-
|
|
2296
|
-
document.body.appendChild(popup);
|
|
2297
|
-
|
|
2298
|
-
// Close on outside click
|
|
2299
|
-
var closePopup = function(e) {
|
|
2300
|
-
if (!popup.contains(e.target)) {
|
|
2301
|
-
popup.remove();
|
|
2302
|
-
document.removeEventListener('click', closePopup, true);
|
|
2303
|
-
}
|
|
2304
|
-
};
|
|
2305
|
-
setTimeout(function() {
|
|
2306
|
-
document.addEventListener('click', closePopup, true);
|
|
2307
|
-
}, 0);
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
function editAnnotation(item, annotation) {
|
|
2311
|
-
var annotationRow = item.closest('.annotation-row');
|
|
2312
|
-
var formContainer = document.createElement('div');
|
|
2313
|
-
formContainer.className = 'annotation-form-container';
|
|
2314
|
-
formContainer.innerHTML =
|
|
2315
|
-
'<div class="annotation-form">' +
|
|
2316
|
-
buildCategoryBadge(annotation.category) +
|
|
2317
|
-
'<textarea>' + esc(annotation.content) + '</textarea>' +
|
|
2318
|
-
'<div class="annotation-form-actions">' +
|
|
2319
|
-
'<button class="btn btn-sm cancel-edit">Cancel</button>' +
|
|
2320
|
-
'<button class="btn btn-sm btn-primary save-edit">Save</button>' +
|
|
2321
|
-
'</div>' +
|
|
2322
|
-
'</div>';
|
|
2323
|
-
|
|
2324
|
-
item.style.display = 'none';
|
|
2325
|
-
annotationRow.parentNode.insertBefore(formContainer, annotationRow.nextSibling);
|
|
2326
|
-
|
|
2327
|
-
bindCategoryBadgeClick(formContainer);
|
|
2328
|
-
|
|
2329
|
-
function cancelEdit() {
|
|
2330
|
-
item.style.display = '';
|
|
2331
|
-
formContainer.remove();
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
formContainer.querySelector('.cancel-edit').addEventListener('click', (e) => {
|
|
2335
|
-
e.stopPropagation();
|
|
2336
|
-
cancelEdit();
|
|
2337
|
-
});
|
|
2338
|
-
|
|
2339
|
-
formContainer.querySelector('.save-edit').addEventListener('click', async (e) => {
|
|
2340
|
-
e.stopPropagation();
|
|
2341
|
-
const content = formContainer.querySelector('textarea').value.trim();
|
|
2342
|
-
const category = formContainer.querySelector('.form-category-badge').dataset.category;
|
|
2343
|
-
if (!content) return;
|
|
2344
|
-
annotation.content = content;
|
|
2345
|
-
annotation.category = category;
|
|
2346
|
-
await api('/annotations/' + annotation.id, { method: 'PATCH', body: { content, category } });
|
|
2347
|
-
item.innerHTML = buildAnnotationItemHtml(annotation);
|
|
2348
|
-
rebindAnnotationActions(item, annotation);
|
|
2349
|
-
item.style.display = '';
|
|
2350
|
-
formContainer.remove();
|
|
2351
|
-
});
|
|
2352
|
-
|
|
2353
|
-
const textarea = formContainer.querySelector('textarea');
|
|
2354
|
-
textarea.focus();
|
|
2355
|
-
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
2356
|
-
textarea.addEventListener('keydown', (e) => {
|
|
2357
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
2358
|
-
formContainer.querySelector('.save-edit').click();
|
|
2359
|
-
}
|
|
2360
|
-
if (e.key === 'Escape') {
|
|
2361
|
-
cancelEdit();
|
|
2362
|
-
}
|
|
2363
|
-
});
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
function rebindAnnotationActions(item, annotation) {
|
|
2367
|
-
var row = item.closest('.annotation-row');
|
|
2368
|
-
var lineEl = row ? row.previousElementSibling : null;
|
|
2369
|
-
bindAnnotationItemEvents(item, annotation, lineEl, row);
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
// --- Drag & Drop for Annotations ---
|
|
2373
|
-
function bindDragDrop() {
|
|
2374
|
-
document.querySelectorAll('.diff-line').forEach(el => {
|
|
2375
|
-
el.addEventListener('dragover', (e) => {
|
|
2376
|
-
if (!state._dragAnnotation) return;
|
|
2377
|
-
e.preventDefault();
|
|
2378
|
-
e.dataTransfer.dropEffect = 'move';
|
|
2379
|
-
document.querySelectorAll('.diff-line.drag-over').forEach(d => d.classList.remove('drag-over'));
|
|
2380
|
-
el.classList.add('drag-over');
|
|
2381
|
-
});
|
|
2382
|
-
|
|
2383
|
-
el.addEventListener('dragleave', () => {
|
|
2384
|
-
el.classList.remove('drag-over');
|
|
2385
|
-
});
|
|
2386
|
-
|
|
2387
|
-
el.addEventListener('drop', async (e) => {
|
|
2388
|
-
e.preventDefault();
|
|
2389
|
-
el.classList.remove('drag-over');
|
|
2390
|
-
document.querySelectorAll('.diff-line.drag-over').forEach(d => d.classList.remove('drag-over'));
|
|
2391
|
-
if (!state._dragAnnotation) return;
|
|
2392
|
-
|
|
2393
|
-
const lineNum = parseInt(el.dataset.line, 10);
|
|
2394
|
-
const side = el.dataset.side || 'new';
|
|
2395
|
-
if (isNaN(lineNum)) return;
|
|
2396
|
-
|
|
2397
|
-
const drag = state._dragAnnotation;
|
|
2398
|
-
state._dragAnnotation = null;
|
|
2399
|
-
|
|
2400
|
-
await api('/annotations/' + drag.id + '/move', {
|
|
2401
|
-
method: 'PATCH',
|
|
2402
|
-
body: { lineNumber: lineNum, side: side },
|
|
2403
|
-
});
|
|
2404
|
-
|
|
2405
|
-
// Refresh the file view
|
|
2406
|
-
if (state.currentFileId) selectFile(state.currentFileId);
|
|
2407
|
-
});
|
|
2408
|
-
});
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
function bindServerAnnotations() {
|
|
2412
|
-
document.querySelectorAll('.annotation-item').forEach(item => {
|
|
2413
|
-
const id = item.dataset.annotationId;
|
|
2414
|
-
if (!id) return;
|
|
2415
|
-
const isStale = item.dataset.isStale === 'true';
|
|
2416
|
-
const category = item.querySelector('.annotation-category')?.textContent || '';
|
|
2417
|
-
const content = item.querySelector('.annotation-text')?.textContent || '';
|
|
2418
|
-
const annotation = { id: id, category: category, content: content, is_stale: isStale };
|
|
2419
|
-
var row = item.closest('.annotation-row');
|
|
2420
|
-
var lineEl = row ? row.previousElementSibling : null;
|
|
2421
|
-
bindAnnotationItemEvents(item, annotation, lineEl, row);
|
|
2422
|
-
});
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
// --- Diff Mode ---
|
|
2426
|
-
function bindDiffModeToggle() {
|
|
2427
|
-
document.querySelectorAll('[data-diff-mode]').forEach(btn => {
|
|
2428
|
-
btn.addEventListener('click', () => {
|
|
2429
|
-
state.diffMode = btn.dataset.diffMode;
|
|
2430
|
-
document.querySelectorAll('[data-diff-mode]').forEach(b => b.classList.toggle('active', b === btn));
|
|
2431
|
-
if (state.currentFileId) selectFile(state.currentFileId);
|
|
2432
|
-
});
|
|
2433
|
-
});
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
// --- File Filter ---
|
|
2437
|
-
function bindFileFilter() {
|
|
2438
|
-
var input = document.getElementById('file-filter');
|
|
2439
|
-
if (!input) return;
|
|
2440
|
-
var timer = null;
|
|
2441
|
-
input.addEventListener('input', function() {
|
|
2442
|
-
clearTimeout(timer);
|
|
2443
|
-
timer = setTimeout(function() {
|
|
2444
|
-
state.filterText = input.value;
|
|
2445
|
-
renderFileList();
|
|
2446
|
-
}, 150);
|
|
2447
|
-
});
|
|
2448
|
-
// Also handle Escape to clear
|
|
2449
|
-
input.addEventListener('keydown', function(e) {
|
|
2450
|
-
if (e.key === 'Escape') {
|
|
2451
|
-
input.value = '';
|
|
2452
|
-
state.filterText = '';
|
|
2453
|
-
renderFileList();
|
|
2454
|
-
input.blur();
|
|
2455
|
-
}
|
|
2456
|
-
});
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
// --- Sidebar Resize ---
|
|
2460
|
-
function bindSidebarResize() {
|
|
2461
|
-
var handle = document.getElementById('sidebar-resize');
|
|
2462
|
-
var sidebar = document.querySelector('.sidebar');
|
|
2463
|
-
if (!handle || !sidebar) return;
|
|
2464
|
-
|
|
2465
|
-
var dragging = false;
|
|
2466
|
-
var startX, startWidth;
|
|
2467
|
-
|
|
2468
|
-
handle.addEventListener('mousedown', function(e) {
|
|
2469
|
-
dragging = true;
|
|
2470
|
-
startX = e.clientX;
|
|
2471
|
-
startWidth = sidebar.offsetWidth;
|
|
2472
|
-
handle.classList.add('dragging');
|
|
2473
|
-
document.body.style.cursor = 'col-resize';
|
|
2474
|
-
document.body.style.userSelect = 'none';
|
|
2475
|
-
e.preventDefault();
|
|
2476
|
-
});
|
|
2477
|
-
|
|
2478
|
-
document.addEventListener('mousemove', function(e) {
|
|
2479
|
-
if (!dragging) return;
|
|
2480
|
-
var newWidth = startWidth + (e.clientX - startX);
|
|
2481
|
-
newWidth = Math.max(200, Math.min(newWidth, window.innerWidth * 0.6));
|
|
2482
|
-
sidebar.style.width = newWidth + 'px';
|
|
2483
|
-
});
|
|
2484
|
-
|
|
2485
|
-
document.addEventListener('mouseup', function() {
|
|
2486
|
-
if (!dragging) return;
|
|
2487
|
-
dragging = false;
|
|
2488
|
-
handle.classList.remove('dragging');
|
|
2489
|
-
document.body.style.cursor = '';
|
|
2490
|
-
document.body.style.userSelect = '';
|
|
2491
|
-
});
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
// --- Wrap Toggle ---
|
|
2495
|
-
function bindWrapToggle() {
|
|
2496
|
-
const btn = document.getElementById('wrap-toggle');
|
|
2497
|
-
if (!btn) return;
|
|
2498
|
-
btn.addEventListener('click', () => {
|
|
2499
|
-
state.wrapLines = !state.wrapLines;
|
|
2500
|
-
btn.classList.toggle('active', state.wrapLines);
|
|
2501
|
-
const container = document.getElementById('diff-container');
|
|
2502
|
-
if (container) {
|
|
2503
|
-
container.classList.toggle('wrap-lines', state.wrapLines);
|
|
2504
|
-
}
|
|
2505
|
-
// Reset scroll positions when toggling
|
|
2506
|
-
if (!state.wrapLines) {
|
|
2507
|
-
resetScrollSync();
|
|
2508
|
-
}
|
|
2509
|
-
});
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
// --- Scroll Sync (split mode, no-wrap) ---
|
|
2513
|
-
function initScrollSync() {
|
|
2514
|
-
const container = document.getElementById('diff-container');
|
|
2515
|
-
if (!container) return;
|
|
2516
|
-
|
|
2517
|
-
let lastScrollLeft = 0;
|
|
2518
|
-
let rafId = null;
|
|
2519
|
-
let syncing = false;
|
|
2520
|
-
|
|
2521
|
-
container.addEventListener('scroll', function(e) {
|
|
2522
|
-
if (syncing || state.wrapLines || state.diffMode !== 'split') return;
|
|
2523
|
-
const target = e.target;
|
|
2524
|
-
if (!target.classList || !target.classList.contains('code')) return;
|
|
2525
|
-
if (!target.closest('.split-row')) return;
|
|
2526
|
-
|
|
2527
|
-
const scrollLeft = target.scrollLeft;
|
|
2528
|
-
if (scrollLeft === lastScrollLeft) return;
|
|
2529
|
-
lastScrollLeft = scrollLeft;
|
|
2530
|
-
|
|
2531
|
-
if (rafId) cancelAnimationFrame(rafId);
|
|
2532
|
-
rafId = requestAnimationFrame(function() {
|
|
2533
|
-
syncing = true;
|
|
2534
|
-
container.querySelectorAll('.split-row .code').forEach(function(el) {
|
|
2535
|
-
if (el !== target && el.scrollLeft !== scrollLeft) {
|
|
2536
|
-
el.scrollLeft = scrollLeft;
|
|
2537
|
-
}
|
|
2538
|
-
});
|
|
2539
|
-
syncing = false;
|
|
2540
|
-
});
|
|
2541
|
-
}, true);
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
function resetScrollSync() {
|
|
2545
|
-
const container = document.getElementById('diff-container');
|
|
2546
|
-
if (!container) return;
|
|
2547
|
-
container.querySelectorAll('.split-row .code').forEach(function(el) {
|
|
2548
|
-
el.scrollLeft = 0;
|
|
2549
|
-
});
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
// --- Complete ---
|
|
2553
|
-
function bindCompleteButton() {
|
|
2554
|
-
const btn = document.getElementById('complete-review');
|
|
2555
|
-
if (!btn) return;
|
|
2556
|
-
btn.addEventListener('click', () => {
|
|
2557
|
-
showCompleteModal();
|
|
2558
|
-
});
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
|
-
function showCompleteModal() {
|
|
2562
|
-
var totalStale = 0;
|
|
2563
|
-
Object.keys(state.staleCounts).forEach(function(k) { totalStale += (state.staleCounts[k] || 0); });
|
|
2564
|
-
|
|
2565
|
-
const overlay = document.createElement('div');
|
|
2566
|
-
overlay.className = 'modal-overlay';
|
|
2567
|
-
|
|
2568
|
-
if (totalStale > 0) {
|
|
2569
|
-
overlay.innerHTML =
|
|
2570
|
-
'<div class="modal">' +
|
|
2571
|
-
'<h3>Stale Annotations</h3>' +
|
|
2572
|
-
'<p>There ' + (totalStale === 1 ? 'is 1 stale annotation' : 'are ' + totalStale + ' stale annotations') +
|
|
2573
|
-
' that could not be matched to the current diff. What would you like to do?</p>' +
|
|
2574
|
-
'<div class="modal-actions">' +
|
|
2575
|
-
'<button class="btn btn-sm modal-cancel">Cancel</button>' +
|
|
2576
|
-
'<button class="btn btn-sm btn-danger" data-stale-action="discard">Discard All Stale</button>' +
|
|
2577
|
-
'<button class="btn btn-sm btn-primary" data-stale-action="keep">Keep All & Complete</button>' +
|
|
2578
|
-
'</div>' +
|
|
2579
|
-
'</div>';
|
|
2580
|
-
|
|
2581
|
-
overlay.querySelector('.modal-cancel').addEventListener('click', () => overlay.remove());
|
|
2582
|
-
overlay.querySelector('[data-stale-action="discard"]').addEventListener('click', async () => {
|
|
2583
|
-
await api('/annotations/stale/delete-all', { method: 'POST' });
|
|
2584
|
-
state.staleCounts = {};
|
|
2585
|
-
renderFileList();
|
|
2586
|
-
overlay.remove();
|
|
2587
|
-
showCompleteModal();
|
|
2588
|
-
});
|
|
2589
|
-
overlay.querySelector('[data-stale-action="keep"]').addEventListener('click', async () => {
|
|
2590
|
-
await api('/annotations/stale/keep-all', { method: 'POST' });
|
|
2591
|
-
state.staleCounts = {};
|
|
2592
|
-
renderFileList();
|
|
2593
|
-
overlay.remove();
|
|
2594
|
-
showCompleteModal();
|
|
2595
|
-
});
|
|
2596
|
-
|
|
2597
|
-
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
2598
|
-
document.body.appendChild(overlay);
|
|
2599
|
-
return;
|
|
2600
|
-
}
|
|
2601
|
-
|
|
2602
|
-
overlay.innerHTML =
|
|
2603
|
-
'<div class="modal">' +
|
|
2604
|
-
'<h3>Complete Review</h3>' +
|
|
2605
|
-
'<p>This will generate a review summary that AI tools can read and act on. Annotations will be exported to .glassbox/ in the repository.</p>' +
|
|
2606
|
-
'<div class="modal-actions">' +
|
|
2607
|
-
'<button class="btn btn-sm modal-cancel">Cancel</button>' +
|
|
2608
|
-
'<button class="btn btn-sm btn-primary modal-confirm">Complete</button>' +
|
|
2609
|
-
'</div>' +
|
|
2610
|
-
'</div>';
|
|
2611
|
-
|
|
2612
|
-
overlay.querySelector('.modal-cancel').addEventListener('click', () => overlay.remove());
|
|
2613
|
-
overlay.querySelector('.modal-confirm').addEventListener('click', async () => {
|
|
2614
|
-
const result = await api('/review/complete', { method: 'POST' });
|
|
2615
|
-
var aiCommand = result.isCurrent
|
|
2616
|
-
? 'Read .glassbox/latest-review.md and apply the feedback.'
|
|
2617
|
-
: 'Read .glassbox/review-' + result.reviewId + '.md and apply the feedback.';
|
|
2618
|
-
|
|
2619
|
-
var gitignoreHtml = '';
|
|
2620
|
-
if (result.gitignorePrompt) {
|
|
2621
|
-
gitignoreHtml =
|
|
2622
|
-
'<div class="modal-gitignore">' +
|
|
2623
|
-
'<p class="modal-label">.glassbox/ is not in your .gitignore</p>' +
|
|
2624
|
-
'<div class="modal-actions" style="justify-content:flex-start;margin-top:4px">' +
|
|
2625
|
-
'<button class="btn btn-sm btn-primary" id="gitignore-add">Add to .gitignore</button>' +
|
|
2626
|
-
'<button class="btn btn-sm" id="gitignore-dismiss">Don\\'t ask for 30 days</button>' +
|
|
2627
|
-
'</div>' +
|
|
2628
|
-
'</div>';
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
overlay.querySelector('.modal').innerHTML =
|
|
2632
|
-
'<h3>Review Completed</h3>' +
|
|
2633
|
-
'<p class="modal-label">Review exported to:</p>' +
|
|
2634
|
-
'<div class="modal-copyable" data-copy="' + esc(result.exportPath) + '" title="Click to copy">' + esc(result.exportPath) + '</div>' +
|
|
2635
|
-
'<p class="modal-label">Tell your AI tool:</p>' +
|
|
2636
|
-
'<div class="modal-copyable" data-copy="' + esc(aiCommand) + '" title="Click to copy">' + esc(aiCommand) + '</div>' +
|
|
2637
|
-
gitignoreHtml +
|
|
2638
|
-
'<div class="modal-actions"><button class="btn btn-sm btn-primary" onclick="this.closest(\\'.modal-overlay\\').remove()">Done</button></div>';
|
|
2639
|
-
|
|
2640
|
-
// Bind copy-to-clipboard
|
|
2641
|
-
overlay.querySelectorAll('.modal-copyable').forEach(function(el) {
|
|
2642
|
-
el.addEventListener('click', function() {
|
|
2643
|
-
navigator.clipboard.writeText(el.dataset.copy);
|
|
2644
|
-
el.classList.add('copied');
|
|
2645
|
-
setTimeout(function() { el.classList.remove('copied'); }, 1500);
|
|
2646
|
-
});
|
|
2647
|
-
});
|
|
2648
|
-
|
|
2649
|
-
// Bind gitignore buttons
|
|
2650
|
-
var addBtn = overlay.querySelector('#gitignore-add');
|
|
2651
|
-
if (addBtn) {
|
|
2652
|
-
addBtn.addEventListener('click', async function() {
|
|
2653
|
-
await api('/gitignore/add', { method: 'POST' });
|
|
2654
|
-
var container = addBtn.closest('.modal-gitignore');
|
|
2655
|
-
container.innerHTML = '<p class="modal-label" style="color:var(--green)">Added .glassbox/ to .gitignore</p>';
|
|
2656
|
-
});
|
|
2657
|
-
}
|
|
2658
|
-
var dismissBtn = overlay.querySelector('#gitignore-dismiss');
|
|
2659
|
-
if (dismissBtn) {
|
|
2660
|
-
dismissBtn.addEventListener('click', async function() {
|
|
2661
|
-
await api('/gitignore/dismiss', { method: 'POST' });
|
|
2662
|
-
dismissBtn.closest('.modal-gitignore').remove();
|
|
2663
|
-
});
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
|
-
// Swap complete button to reopen
|
|
2667
|
-
const completeBtn = document.getElementById('complete-review');
|
|
2668
|
-
if (completeBtn) {
|
|
2669
|
-
const reopenBtn = document.createElement('button');
|
|
2670
|
-
reopenBtn.className = 'btn btn-primary';
|
|
2671
|
-
reopenBtn.id = 'reopen-review';
|
|
2672
|
-
reopenBtn.textContent = 'Reopen Review';
|
|
2673
|
-
completeBtn.replaceWith(reopenBtn);
|
|
2674
|
-
bindReopenButton();
|
|
2675
|
-
}
|
|
2676
|
-
});
|
|
2677
|
-
|
|
2678
|
-
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
2679
|
-
document.body.appendChild(overlay);
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
function bindReopenButton() {
|
|
2683
|
-
const btn = document.getElementById('reopen-review');
|
|
2684
|
-
if (!btn) return;
|
|
2685
|
-
btn.addEventListener('click', async () => {
|
|
2686
|
-
await api('/review/reopen', { method: 'POST' });
|
|
2687
|
-
// Swap reopen button to complete
|
|
2688
|
-
const completeBtn = document.createElement('button');
|
|
2689
|
-
completeBtn.className = 'btn btn-primary btn-complete';
|
|
2690
|
-
completeBtn.id = 'complete-review';
|
|
2691
|
-
completeBtn.textContent = 'Complete Review';
|
|
2692
|
-
btn.replaceWith(completeBtn);
|
|
2693
|
-
bindCompleteButton();
|
|
2694
|
-
});
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
// --- Progress ---
|
|
2698
|
-
function updateProgress() {
|
|
2699
|
-
const total = state.files.length;
|
|
2700
|
-
const reviewed = state.files.filter(f => f.status === 'reviewed').length;
|
|
2701
|
-
const summary = document.getElementById('progress-summary');
|
|
2702
|
-
if (summary) summary.textContent = reviewed + ' of ' + total + ' files reviewed';
|
|
2703
|
-
|
|
2704
|
-
// Update progress bar
|
|
2705
|
-
let bar = document.querySelector('.progress-bar');
|
|
2706
|
-
if (!bar) {
|
|
2707
|
-
bar = document.createElement('div');
|
|
2708
|
-
bar.className = 'progress-bar';
|
|
2709
|
-
bar.innerHTML = '<div class="progress-bar-fill"></div>';
|
|
2710
|
-
const controls = document.querySelector('.sidebar-controls');
|
|
2711
|
-
if (controls) controls.appendChild(bar);
|
|
2712
|
-
}
|
|
2713
|
-
const fill = bar.querySelector('.progress-bar-fill');
|
|
2714
|
-
if (fill) fill.style.width = (total ? (reviewed / total * 100) : 0) + '%';
|
|
2715
|
-
}
|
|
2716
|
-
|
|
2717
|
-
// --- Sidebar Events ---
|
|
2718
|
-
function bindSidebarEvents() {
|
|
2719
|
-
// Keyboard navigation
|
|
2720
|
-
document.addEventListener('keydown', (e) => {
|
|
2721
|
-
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
|
|
2722
|
-
if (e.key === 'j' || e.key === 'ArrowDown') {
|
|
2723
|
-
navigateFile(1);
|
|
2724
|
-
e.preventDefault();
|
|
2725
|
-
} else if (e.key === 'k' || e.key === 'ArrowUp') {
|
|
2726
|
-
navigateFile(-1);
|
|
2727
|
-
e.preventDefault();
|
|
2728
|
-
}
|
|
2729
|
-
});
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
function navigateFile(delta) {
|
|
2733
|
-
var order = state.fileOrder;
|
|
2734
|
-
var idx = order.indexOf(state.currentFileId);
|
|
2735
|
-
var next = idx + delta;
|
|
2736
|
-
if (next >= 0 && next < order.length) {
|
|
2737
|
-
selectFile(order[next]);
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
// Run
|
|
2742
|
-
init();
|
|
2743
|
-
})();
|
|
2744
|
-
`;
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
// src/components/fileList.tsx
|
|
2748
|
-
function buildFileTree(files) {
|
|
2749
|
-
const root = { name: "", children: [], files: [] };
|
|
2750
|
-
for (const f of files) {
|
|
2751
|
-
const parts = f.file_path.split("/");
|
|
2752
|
-
let node = root;
|
|
2753
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
2754
|
-
let child = node.children.find((c) => c.name === parts[i]);
|
|
2755
|
-
if (!child) {
|
|
2756
|
-
child = { name: parts[i], children: [], files: [] };
|
|
2757
|
-
node.children.push(child);
|
|
2758
|
-
}
|
|
2759
|
-
node = child;
|
|
2760
|
-
}
|
|
2761
|
-
node.files.push(f);
|
|
2762
|
-
}
|
|
2763
|
-
compressTree(root);
|
|
2764
|
-
return root;
|
|
2765
|
-
}
|
|
2766
|
-
function compressTree(node) {
|
|
2767
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
2768
|
-
let child = node.children[i];
|
|
2769
|
-
while (child.children.length === 1 && child.files.length === 0) {
|
|
2770
|
-
const grandchild = child.children[0];
|
|
2771
|
-
child = { name: child.name + "/" + grandchild.name, children: grandchild.children, files: grandchild.files };
|
|
2772
|
-
node.children[i] = child;
|
|
2773
|
-
}
|
|
2774
|
-
compressTree(child);
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
|
-
function countFiles(node) {
|
|
2778
|
-
let count = node.files.length;
|
|
2779
|
-
for (const child of node.children) count += countFiles(child);
|
|
2780
|
-
return count;
|
|
2781
|
-
}
|
|
2782
|
-
function hasStale(node, staleCounts) {
|
|
2783
|
-
for (const f of node.files) {
|
|
2784
|
-
if (staleCounts[f.id]) return true;
|
|
2785
|
-
}
|
|
2786
|
-
for (const child of node.children) {
|
|
2787
|
-
if (hasStale(child, staleCounts)) return true;
|
|
2788
|
-
}
|
|
2789
|
-
return false;
|
|
2790
|
-
}
|
|
2791
|
-
function TreeView({ node, depth, annotationCounts, staleCounts }) {
|
|
2792
|
-
const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
|
|
2793
|
-
return /* @__PURE__ */ jsx("div", { children: [
|
|
2794
|
-
sortedChildren.map((child) => {
|
|
2795
|
-
const total = countFiles(child);
|
|
2796
|
-
const isCollapsible = total > 1;
|
|
2797
|
-
const stale = hasStale(child, staleCounts);
|
|
2798
|
-
return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
|
|
2799
|
-
/* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
2800
|
-
isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
|
|
2801
|
-
/* @__PURE__ */ jsx("span", { className: "folder-name", children: [
|
|
2802
|
-
child.name,
|
|
2803
|
-
"/"
|
|
2804
|
-
] }),
|
|
2805
|
-
stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
|
|
2806
|
-
] }),
|
|
2807
|
-
/* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
|
|
2808
|
-
] });
|
|
2809
|
-
}),
|
|
2810
|
-
node.files.map((f) => {
|
|
2811
|
-
const diff = JSON.parse(f.diff_data || "{}");
|
|
2812
|
-
const count = annotationCounts[f.id] || 0;
|
|
2813
|
-
const stale = staleCounts[f.id] || 0;
|
|
2814
|
-
const fileName = f.file_path.split("/").pop();
|
|
2815
|
-
return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
2816
|
-
/* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
|
|
2817
|
-
/* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
|
|
2818
|
-
/* @__PURE__ */ jsx("span", { className: `file-status ${diff.status || ""}`, children: diff.status || "" }),
|
|
2819
|
-
stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
|
|
2820
|
-
count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
|
|
2821
|
-
] });
|
|
2822
|
-
})
|
|
2823
|
-
] });
|
|
2824
|
-
}
|
|
2825
|
-
function FileList({ files, annotationCounts, staleCounts }) {
|
|
2826
|
-
const tree = buildFileTree(files);
|
|
2827
|
-
return /* @__PURE__ */ jsx("div", { className: "file-list", children: /* @__PURE__ */ jsx("div", { className: "file-list-items", children: /* @__PURE__ */ jsx(TreeView, { node: tree, depth: 0, annotationCounts, staleCounts }) }) });
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
// src/components/diffView.tsx
|
|
2831
|
-
function DiffView({ file, diff, annotations, mode }) {
|
|
2832
|
-
const annotationsByLine = {};
|
|
2833
|
-
for (const a of annotations) {
|
|
2834
|
-
const key = `${a.line_number}:${a.side}`;
|
|
2835
|
-
if (!annotationsByLine[key]) annotationsByLine[key] = [];
|
|
2836
|
-
annotationsByLine[key].push(a);
|
|
2837
|
-
}
|
|
2838
|
-
return /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, children: [
|
|
2839
|
-
/* @__PURE__ */ jsx("div", { className: "diff-header", children: [
|
|
2840
|
-
/* @__PURE__ */ jsx("span", { className: "file-path", children: diff.filePath }),
|
|
2841
|
-
/* @__PURE__ */ jsx("div", { className: "diff-header-actions", children: /* @__PURE__ */ jsx("span", { className: `file-status ${diff.status}`, children: diff.status }) })
|
|
2842
|
-
] }),
|
|
2843
|
-
diff.isBinary ? /* @__PURE__ */ jsx("div", { className: "hunk-separator", children: "Binary file" }) : diff.status === "added" || diff.status === "deleted" || mode === "unified" ? /* @__PURE__ */ jsx(UnifiedDiff, { hunks: diff.hunks, annotationsByLine }) : /* @__PURE__ */ jsx(SplitDiff, { hunks: diff.hunks, annotationsByLine })
|
|
2844
|
-
] });
|
|
2845
|
-
}
|
|
2846
|
-
function SplitDiff({ hunks, annotationsByLine }) {
|
|
2847
|
-
return /* @__PURE__ */ jsx("div", { className: "diff-table-split", children: hunks.map((hunk, hunkIdx) => {
|
|
2848
|
-
const pairs = pairLines(hunk.lines);
|
|
2849
|
-
return /* @__PURE__ */ jsx("div", { className: "hunk-block", children: [
|
|
2850
|
-
/* @__PURE__ */ jsx(
|
|
2851
|
-
"div",
|
|
2852
|
-
{
|
|
2853
|
-
className: "hunk-separator",
|
|
2854
|
-
"data-hunk-idx": hunkIdx,
|
|
2855
|
-
"data-old-start": hunk.oldStart,
|
|
2856
|
-
"data-old-count": hunk.oldCount,
|
|
2857
|
-
"data-new-start": hunk.newStart,
|
|
2858
|
-
"data-new-count": hunk.newCount,
|
|
2859
|
-
children: [
|
|
2860
|
-
"@@ -",
|
|
2861
|
-
hunk.oldStart,
|
|
2862
|
-
",",
|
|
2863
|
-
hunk.oldCount,
|
|
2864
|
-
" +",
|
|
2865
|
-
hunk.newStart,
|
|
2866
|
-
",",
|
|
2867
|
-
hunk.newCount,
|
|
2868
|
-
" @@"
|
|
2869
|
-
]
|
|
2870
|
-
}
|
|
2871
|
-
),
|
|
2872
|
-
pairs.map((pair) => {
|
|
2873
|
-
const leftAnns = pair.left ? annotationsByLine[`${pair.left.oldNum}:old`] || [] : [];
|
|
2874
|
-
const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] || [] : [];
|
|
2875
|
-
const allAnns = [...leftAnns, ...rightAnns];
|
|
2876
|
-
return /* @__PURE__ */ jsx("div", { children: [
|
|
2877
|
-
/* @__PURE__ */ jsx("div", { className: "split-row", children: [
|
|
2878
|
-
/* @__PURE__ */ jsx(
|
|
2879
|
-
"div",
|
|
2880
|
-
{
|
|
2881
|
-
className: `diff-line split-left ${pair.left?.type || "empty"}`,
|
|
2882
|
-
"data-line": pair.left?.oldNum ?? "",
|
|
2883
|
-
"data-side": "old",
|
|
2884
|
-
children: [
|
|
2885
|
-
/* @__PURE__ */ jsx("span", { className: "gutter", children: pair.left?.oldNum ?? "" }),
|
|
2886
|
-
/* @__PURE__ */ jsx("span", { className: "code", children: pair.left ? raw(escapeHtml(pair.left.content)) : "" })
|
|
2887
|
-
]
|
|
2888
|
-
}
|
|
2889
|
-
),
|
|
2890
|
-
/* @__PURE__ */ jsx(
|
|
2891
|
-
"div",
|
|
2892
|
-
{
|
|
2893
|
-
className: `diff-line split-right ${pair.right?.type || "empty"}`,
|
|
2894
|
-
"data-line": pair.right?.newNum ?? "",
|
|
2895
|
-
"data-side": "new",
|
|
2896
|
-
children: [
|
|
2897
|
-
/* @__PURE__ */ jsx("span", { className: "gutter", children: pair.right?.newNum ?? "" }),
|
|
2898
|
-
/* @__PURE__ */ jsx("span", { className: "code", children: pair.right ? raw(escapeHtml(pair.right.content)) : "" })
|
|
2899
|
-
]
|
|
2900
|
-
}
|
|
2901
|
-
)
|
|
2902
|
-
] }),
|
|
2903
|
-
allAnns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: allAnns }) : null
|
|
2904
|
-
] });
|
|
2905
|
-
})
|
|
2906
|
-
] });
|
|
2907
|
-
}) });
|
|
1336
|
+
function SplitDiff({ hunks, annotationsByLine }) {
|
|
1337
|
+
return /* @__PURE__ */ jsx("div", { className: "diff-table-split", children: hunks.map((hunk, hunkIdx) => {
|
|
1338
|
+
const pairs = pairLines(hunk.lines);
|
|
1339
|
+
return /* @__PURE__ */ jsx("div", { className: "hunk-block", children: [
|
|
1340
|
+
/* @__PURE__ */ jsx(
|
|
1341
|
+
"div",
|
|
1342
|
+
{
|
|
1343
|
+
className: "hunk-separator",
|
|
1344
|
+
"data-hunk-idx": hunkIdx,
|
|
1345
|
+
"data-old-start": hunk.oldStart,
|
|
1346
|
+
"data-old-count": hunk.oldCount,
|
|
1347
|
+
"data-new-start": hunk.newStart,
|
|
1348
|
+
"data-new-count": hunk.newCount,
|
|
1349
|
+
children: [
|
|
1350
|
+
"@@ -",
|
|
1351
|
+
hunk.oldStart,
|
|
1352
|
+
",",
|
|
1353
|
+
hunk.oldCount,
|
|
1354
|
+
" +",
|
|
1355
|
+
hunk.newStart,
|
|
1356
|
+
",",
|
|
1357
|
+
hunk.newCount,
|
|
1358
|
+
" @@"
|
|
1359
|
+
]
|
|
1360
|
+
}
|
|
1361
|
+
),
|
|
1362
|
+
pairs.map((pair) => {
|
|
1363
|
+
const leftAnns = pair.left ? annotationsByLine[`${pair.left.oldNum}:old`] || [] : [];
|
|
1364
|
+
const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] || [] : [];
|
|
1365
|
+
const allAnns = [...leftAnns, ...rightAnns];
|
|
1366
|
+
return /* @__PURE__ */ jsx("div", { children: [
|
|
1367
|
+
/* @__PURE__ */ jsx("div", { className: "split-row", children: [
|
|
1368
|
+
/* @__PURE__ */ jsx(
|
|
1369
|
+
"div",
|
|
1370
|
+
{
|
|
1371
|
+
className: `diff-line split-left ${pair.left?.type || "empty"}`,
|
|
1372
|
+
"data-line": pair.left?.oldNum ?? "",
|
|
1373
|
+
"data-side": "old",
|
|
1374
|
+
children: [
|
|
1375
|
+
/* @__PURE__ */ jsx("span", { className: "gutter", children: pair.left?.oldNum ?? "" }),
|
|
1376
|
+
/* @__PURE__ */ jsx("span", { className: "code", children: pair.left ? raw(escapeHtml(pair.left.content)) : "" })
|
|
1377
|
+
]
|
|
1378
|
+
}
|
|
1379
|
+
),
|
|
1380
|
+
/* @__PURE__ */ jsx(
|
|
1381
|
+
"div",
|
|
1382
|
+
{
|
|
1383
|
+
className: `diff-line split-right ${pair.right?.type || "empty"}`,
|
|
1384
|
+
"data-line": pair.right?.newNum ?? "",
|
|
1385
|
+
"data-side": "new",
|
|
1386
|
+
children: [
|
|
1387
|
+
/* @__PURE__ */ jsx("span", { className: "gutter", children: pair.right?.newNum ?? "" }),
|
|
1388
|
+
/* @__PURE__ */ jsx("span", { className: "code", children: pair.right ? raw(escapeHtml(pair.right.content)) : "" })
|
|
1389
|
+
]
|
|
1390
|
+
}
|
|
1391
|
+
)
|
|
1392
|
+
] }),
|
|
1393
|
+
allAnns.length > 0 ? /* @__PURE__ */ jsx(AnnotationRows, { annotations: allAnns }) : null
|
|
1394
|
+
] });
|
|
1395
|
+
})
|
|
1396
|
+
] });
|
|
1397
|
+
}) });
|
|
2908
1398
|
}
|
|
2909
1399
|
function pairLines(lines) {
|
|
2910
1400
|
const pairs = [];
|
|
@@ -3173,14 +1663,6 @@ pageRoutes.get("/", async (c) => {
|
|
|
3173
1663
|
review.mode_args ? `: ${review.mode_args}` : ""
|
|
3174
1664
|
] })
|
|
3175
1665
|
] }),
|
|
3176
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-controls", children: /* @__PURE__ */ jsx("div", { className: "sidebar-controls-row", children: [
|
|
3177
|
-
/* @__PURE__ */ jsx("div", { className: "diff-mode-toggle", children: [
|
|
3178
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-sm active", "data-diff-mode": "split", children: "Split" }),
|
|
3179
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-sm", "data-diff-mode": "unified", children: "Unified" })
|
|
3180
|
-
] }),
|
|
3181
|
-
/* @__PURE__ */ jsx("span", { className: "controls-divider" }),
|
|
3182
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "wrap-toggle", children: "Wrap" })
|
|
3183
|
-
] }) }),
|
|
3184
1666
|
/* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
|
|
3185
1667
|
/* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts: {} }),
|
|
3186
1668
|
/* @__PURE__ */ jsx("div", { className: "sidebar-footer", children: [
|
|
@@ -3198,7 +1680,17 @@ pageRoutes.get("/", async (c) => {
|
|
|
3198
1680
|
] }),
|
|
3199
1681
|
/* @__PURE__ */ jsx("p", { className: "progress-summary", id: "progress-summary" })
|
|
3200
1682
|
] }),
|
|
3201
|
-
/* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" })
|
|
1683
|
+
/* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
|
|
1684
|
+
/* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
|
|
1685
|
+
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
|
|
1686
|
+
/* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
|
|
1687
|
+
/* @__PURE__ */ jsx("button", { className: "segment active", "data-diff-mode": "split", children: "Split" }),
|
|
1688
|
+
/* @__PURE__ */ jsx("button", { className: "segment", "data-diff-mode": "unified", children: "Unified" })
|
|
1689
|
+
] }),
|
|
1690
|
+
/* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "wrap-toggle", children: "Wrap" })
|
|
1691
|
+
] }),
|
|
1692
|
+
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-right", children: /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "language-btn", children: "Plain Text" }) })
|
|
1693
|
+
] })
|
|
3202
1694
|
] })
|
|
3203
1695
|
] }) });
|
|
3204
1696
|
return c.html(html.toString());
|
|
@@ -3236,14 +1728,6 @@ pageRoutes.get("/review/:reviewId", async (c) => {
|
|
|
3236
1728
|
review.mode_args ? `: ${review.mode_args}` : ""
|
|
3237
1729
|
] })
|
|
3238
1730
|
] }),
|
|
3239
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-controls", children: /* @__PURE__ */ jsx("div", { className: "sidebar-controls-row", children: [
|
|
3240
|
-
/* @__PURE__ */ jsx("div", { className: "diff-mode-toggle", children: [
|
|
3241
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-sm active", "data-diff-mode": "split", children: "Split" }),
|
|
3242
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-sm", "data-diff-mode": "unified", children: "Unified" })
|
|
3243
|
-
] }),
|
|
3244
|
-
/* @__PURE__ */ jsx("span", { className: "controls-divider" }),
|
|
3245
|
-
/* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "wrap-toggle", children: "Wrap" })
|
|
3246
|
-
] }) }),
|
|
3247
1731
|
/* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
|
|
3248
1732
|
/* @__PURE__ */ jsx(FileList, { files, annotationCounts, staleCounts: {} }),
|
|
3249
1733
|
/* @__PURE__ */ jsx("div", { className: "sidebar-footer", children: [
|
|
@@ -3262,7 +1746,17 @@ pageRoutes.get("/review/:reviewId", async (c) => {
|
|
|
3262
1746
|
] }),
|
|
3263
1747
|
/* @__PURE__ */ jsx("p", { className: "progress-summary", id: "progress-summary" })
|
|
3264
1748
|
] }),
|
|
3265
|
-
/* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" })
|
|
1749
|
+
/* @__PURE__ */ jsx("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
|
|
1750
|
+
/* @__PURE__ */ jsx("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
|
|
1751
|
+
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-left", children: [
|
|
1752
|
+
/* @__PURE__ */ jsx("div", { className: "segmented-control", children: [
|
|
1753
|
+
/* @__PURE__ */ jsx("button", { className: "segment active", "data-diff-mode": "split", children: "Split" }),
|
|
1754
|
+
/* @__PURE__ */ jsx("button", { className: "segment", "data-diff-mode": "unified", children: "Unified" })
|
|
1755
|
+
] }),
|
|
1756
|
+
/* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "wrap-toggle", children: "Wrap" })
|
|
1757
|
+
] }),
|
|
1758
|
+
/* @__PURE__ */ jsx("div", { className: "diff-toolbar-right", children: /* @__PURE__ */ jsx("button", { className: "toolbar-btn", id: "language-btn", children: "Plain Text" }) })
|
|
1759
|
+
] })
|
|
3266
1760
|
] })
|
|
3267
1761
|
] }) });
|
|
3268
1762
|
return c.html(html.toString());
|
|
@@ -3297,6 +1791,16 @@ async function startServer(port, reviewId, repoRoot) {
|
|
|
3297
1791
|
c.set("repoRoot", repoRoot);
|
|
3298
1792
|
await next();
|
|
3299
1793
|
});
|
|
1794
|
+
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
1795
|
+
const distDir = existsSync2(join3(selfDir, "client", "styles.css")) ? join3(selfDir, "client") : join3(selfDir, "..", "dist", "client");
|
|
1796
|
+
app.get("/static/styles.css", (c) => {
|
|
1797
|
+
const css = readFileSync3(join3(distDir, "styles.css"), "utf-8");
|
|
1798
|
+
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
1799
|
+
});
|
|
1800
|
+
app.get("/static/app.js", (c) => {
|
|
1801
|
+
const js = readFileSync3(join3(distDir, "app.global.js"), "utf-8");
|
|
1802
|
+
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
1803
|
+
});
|
|
3300
1804
|
app.route("/api", apiRoutes);
|
|
3301
1805
|
app.route("/", pageRoutes);
|
|
3302
1806
|
let actualPort = port;
|
|
@@ -3323,18 +1827,18 @@ async function startServer(port, reviewId, repoRoot) {
|
|
|
3323
1827
|
}
|
|
3324
1828
|
|
|
3325
1829
|
// src/update-check.ts
|
|
3326
|
-
import { readFileSync as
|
|
3327
|
-
import { join as
|
|
1830
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
|
|
1831
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
3328
1832
|
import { homedir as homedir3 } from "os";
|
|
3329
1833
|
import { get } from "https";
|
|
3330
|
-
import { fileURLToPath } from "url";
|
|
3331
|
-
var DATA_DIR =
|
|
3332
|
-
var CHECK_FILE =
|
|
1834
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1835
|
+
var DATA_DIR = join4(homedir3(), ".glassbox");
|
|
1836
|
+
var CHECK_FILE = join4(DATA_DIR, "last-update-check");
|
|
3333
1837
|
var PACKAGE_NAME = "glassbox";
|
|
3334
1838
|
function getCurrentVersion() {
|
|
3335
1839
|
try {
|
|
3336
|
-
const dir =
|
|
3337
|
-
const pkg = JSON.parse(
|
|
1840
|
+
const dir = dirname2(fileURLToPath2(import.meta.url));
|
|
1841
|
+
const pkg = JSON.parse(readFileSync4(join4(dir, "..", "package.json"), "utf-8"));
|
|
3338
1842
|
return pkg.version;
|
|
3339
1843
|
} catch {
|
|
3340
1844
|
return "0.0.0";
|
|
@@ -3342,8 +1846,8 @@ function getCurrentVersion() {
|
|
|
3342
1846
|
}
|
|
3343
1847
|
function getLastCheckDate() {
|
|
3344
1848
|
try {
|
|
3345
|
-
if (
|
|
3346
|
-
return
|
|
1849
|
+
if (existsSync3(CHECK_FILE)) {
|
|
1850
|
+
return readFileSync4(CHECK_FILE, "utf-8").trim();
|
|
3347
1851
|
}
|
|
3348
1852
|
} catch {
|
|
3349
1853
|
}
|
|
@@ -3637,7 +2141,7 @@ async function main() {
|
|
|
3637
2141
|
}
|
|
3638
2142
|
const { mode, port, resume, forceUpdateCheck, debug } = parsed;
|
|
3639
2143
|
if (debug) {
|
|
3640
|
-
console.log(`Build timestamp: ${"2026-03-
|
|
2144
|
+
console.log(`Build timestamp: ${"2026-03-06T01:38:17.060Z"}`);
|
|
3641
2145
|
}
|
|
3642
2146
|
await checkForUpdates(forceUpdateCheck);
|
|
3643
2147
|
const cwd = process.cwd();
|