getgloss 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ import path from "path";
10
10
  // package.json
11
11
  var package_default = {
12
12
  name: "getgloss",
13
- version: "0.12.1",
13
+ version: "0.13.0",
14
14
  description: "Local browser-based diff review for coding-agent loops.",
15
15
  type: "module",
16
16
  packageManager: "pnpm@10.33.2",
@@ -67,8 +67,8 @@ var package_default = {
67
67
  tsup: "8.5.1",
68
68
  tsx: "4.22.3",
69
69
  typescript: "5.9.3",
70
- vite: "6.4.2",
71
- vitest: "3.2.4"
70
+ vite: "6.4.3",
71
+ vitest: "3.2.6"
72
72
  },
73
73
  keywords: [
74
74
  "diff",
@@ -202,6 +202,7 @@ var DIFF_SCOPE_MODES = ["working", "branch", "explicit"];
202
202
  var DIFF_FALLBACK_REASONS = ["working-tree-clean", "missing-branch-base"];
203
203
  var DIFF_CONTEXT_MAX_LINES = 500;
204
204
  var SOURCE_PEEK_MAX_BYTES = 35e4;
205
+ var SOURCE_PEEK_RANGE_MAX_LINES = 240;
205
206
  var REVIEW_SCOPE_MODES = ["all", "single", "range"];
206
207
  var RESOLUTION_STATUSES = ["partial", "resolved"];
207
208
  var OPEN_FILE_SCOPES = ["review", "repo"];
@@ -272,6 +273,9 @@ function isDiffContextRequest(value) {
272
273
  function isSourcePeekRequest(value) {
273
274
  return isRecord(value) && isString(value.filePath) && isNullableString(value.oldPath) && isOptionalString(value.turnId) && isDiffContextSource(value.source) && isOneOf(value.side, SIDES) && isPositiveInteger(value.line) && isNonNegativeInteger(value.column) && isIdentifier(value.symbol);
274
275
  }
276
+ function isSourcePeekRangeRequest(value) {
277
+ return isRecord(value) && isString(value.filePath) && isOptionalString(value.turnId) && isDiffContextSource(value.source) && isOneOf(value.side, SIDES) && isPositiveInteger(value.startLine) && isPositiveInteger(value.lineCount) && value.lineCount <= SOURCE_PEEK_RANGE_MAX_LINES;
278
+ }
275
279
  function isSubmitReviewRequest(value) {
276
280
  return isRecord(value) && isArrayOf(value.comments, isComment) && isOptional(value.reviewScope, isReviewScope);
277
281
  }
@@ -361,7 +365,13 @@ function isDiffLine(value) {
361
365
  return isRecord(value) && isOneOf(value.type, DIFF_LINE_TYPES) && isNullableNumber(value.oldLine) && isNullableNumber(value.newLine) && isString(value.content);
362
366
  }
363
367
  function isComment(value) {
364
- return isRecord(value) && isString(value.id) && isString(value.filePath) && isNumber(value.startLine) && isNumber(value.endLine) && isOneOf(value.side, SIDES) && isString(value.body) && isString(value.originalSnippet) && isString(value.createdAt);
368
+ if (!isRecord(value)) {
369
+ return false;
370
+ }
371
+ if (value.kind === "general") {
372
+ return isString(value.id) && isString(value.body) && isString(value.createdAt);
373
+ }
374
+ return isString(value.id) && (value.kind === void 0 || value.kind === "line") && isString(value.filePath) && isNumber(value.startLine) && isNumber(value.endLine) && isOneOf(value.side, SIDES) && isString(value.body) && isString(value.originalSnippet) && isString(value.createdAt);
365
375
  }
366
376
  function isResolvedComment(value) {
367
377
  return isRecord(value) && isString(value.commentId) && value.status === "resolved" && isOptionalString(value.summary) && isString(value.resolvedAt);
@@ -576,11 +586,28 @@ import { Hono } from "hono";
576
586
  import { streamSSE } from "hono/streaming";
577
587
 
578
588
  // src/shared/comments.ts
589
+ function isLineComment(comment) {
590
+ return comment.kind === void 0 || comment.kind === "line";
591
+ }
579
592
  function compareCommentsByLocation(a, b) {
593
+ const aIsLine = isLineComment(a);
594
+ const bIsLine = isLineComment(b);
595
+ if (!aIsLine || !bIsLine) {
596
+ if (aIsLine !== bIsLine) {
597
+ return aIsLine ? 1 : -1;
598
+ }
599
+ return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
600
+ }
580
601
  return a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side);
581
602
  }
582
603
  function countCommentFiles(comments) {
583
- return new Set(comments.map((comment) => comment.filePath)).size;
604
+ const filePaths = /* @__PURE__ */ new Set();
605
+ for (const comment of comments) {
606
+ if (isLineComment(comment)) {
607
+ filePaths.add(comment.filePath);
608
+ }
609
+ }
610
+ return filePaths.size;
584
611
  }
585
612
  function formatLineRange(range, options = {}) {
586
613
  const startLine = Math.min(range.startLine, range.endLine);
@@ -777,6 +804,17 @@ function summarizeDiffFiles(files) {
777
804
  );
778
805
  }
779
806
 
807
+ // src/shared/file-order.ts
808
+ function compareFilePaths(leftPath, rightPath, leftTieBreaker = "", rightTieBreaker = "") {
809
+ return leftPath.localeCompare(rightPath, void 0, { sensitivity: "base" }) || leftPath.localeCompare(rightPath) || leftTieBreaker.localeCompare(rightTieBreaker, void 0, { sensitivity: "base" }) || leftTieBreaker.localeCompare(rightTieBreaker);
810
+ }
811
+ function compareDiffFiles(left, right) {
812
+ return compareFilePaths(left.path, right.path, left.oldPath ?? "", right.oldPath ?? "");
813
+ }
814
+ function sortDiffFiles(files) {
815
+ return files.toSorted(compareDiffFiles);
816
+ }
817
+
780
818
  // src/shared/git-diff.ts
781
819
  var DIFF_ARGS = ["diff", "--no-color", "--find-renames", "--find-copies"];
782
820
  async function git(args, cwd) {
@@ -785,7 +823,7 @@ async function git(args, cwd) {
785
823
  }
786
824
  async function captureCommitRangeDiff(fromSha, toSha, repoRoot) {
787
825
  const rawDiff = await git([...DIFF_ARGS, `${fromSha}^`, toSha, "--"], repoRoot);
788
- const files = parseUnifiedDiff(rawDiff);
826
+ const files = sortDiffFiles(parseUnifiedDiff(rawDiff));
789
827
  return {
790
828
  stats: summarizeDiffFiles(files),
791
829
  rawDiff,
@@ -1013,6 +1051,25 @@ async function resolveSourcePeek({
1013
1051
  }
1014
1052
  throw new Error(`No definition found for ${symbol}`);
1015
1053
  }
1054
+ async function readSourcePeekRange({
1055
+ lineCount,
1056
+ repoRoot,
1057
+ sourceFilePath,
1058
+ sourceRef,
1059
+ startLine
1060
+ }) {
1061
+ const content = await readRepoText(repoRoot, sourceRef, sourceFilePath);
1062
+ const limited = limitContentRange(content, startLine, lineCount);
1063
+ return {
1064
+ filePath: sourceFilePath,
1065
+ startLine: limited.startLine,
1066
+ totalLines: limited.totalLines,
1067
+ content: limited.content,
1068
+ truncated: limited.truncated,
1069
+ hasMoreAbove: limited.hasMoreAbove,
1070
+ hasMoreBelow: limited.hasMoreBelow
1071
+ };
1072
+ }
1016
1073
  async function resolveImportedTarget({
1017
1074
  aliasConfig,
1018
1075
  importedName,
@@ -1191,37 +1248,86 @@ function responseForMatch({
1191
1248
  language: languageForPath(filePath),
1192
1249
  content: limited.content,
1193
1250
  truncated: limited.truncated,
1251
+ totalLines: limited.totalLines,
1252
+ hasMoreAbove: limited.hasMoreAbove,
1253
+ hasMoreBelow: limited.hasMoreBelow,
1194
1254
  matchReason
1195
1255
  };
1196
1256
  }
1197
1257
  function limitContentAroundLine(content, line) {
1258
+ const lines = splitFileLines2(content);
1259
+ const totalLines = sourceTotalLines(lines);
1198
1260
  if (Buffer.byteLength(content, "utf8") <= SOURCE_PEEK_MAX_BYTES) {
1199
- return { content, startLine: 1, truncated: false };
1261
+ return {
1262
+ content,
1263
+ startLine: 1,
1264
+ totalLines,
1265
+ truncated: false,
1266
+ hasMoreAbove: false,
1267
+ hasMoreBelow: false
1268
+ };
1269
+ }
1270
+ if (lines.length === 0) {
1271
+ return emptySourceWindow();
1200
1272
  }
1201
- const lines = splitFileLines2(content);
1202
1273
  const targetIndex = Math.max(0, line - 1);
1203
1274
  const preferredStartIndex = Math.max(0, targetIndex - SOURCE_PEEK_TARGET_CONTEXT_LINES);
1204
1275
  const preferredEndIndex = Math.min(lines.length, preferredStartIndex + SOURCE_PEEK_CONTEXT_LINES);
1205
- let startIndex = preferredStartIndex;
1206
- let endIndex = preferredEndIndex;
1207
- let limitedContent = lines.slice(startIndex, endIndex).join("\n");
1208
- while (Buffer.byteLength(limitedContent, "utf8") > SOURCE_PEEK_MAX_BYTES && endIndex - startIndex > 1) {
1209
- if (targetIndex - startIndex > endIndex - targetIndex - 1) {
1210
- startIndex += 1;
1276
+ return limitLinesByIndexes(lines, preferredStartIndex, preferredEndIndex, targetIndex);
1277
+ }
1278
+ function limitContentRange(content, startLine, lineCount) {
1279
+ const lines = splitFileLines2(content);
1280
+ if (lines.length === 0) {
1281
+ return emptySourceWindow();
1282
+ }
1283
+ const cappedLineCount = Math.min(lineCount, SOURCE_PEEK_RANGE_MAX_LINES);
1284
+ const startIndex = clampNumber(startLine - 1, 0, lines.length - 1);
1285
+ const endIndex = Math.min(lines.length, startIndex + cappedLineCount);
1286
+ return limitLinesByIndexes(lines, startIndex, endIndex, null);
1287
+ }
1288
+ function limitLinesByIndexes(lines, startIndex, endIndex, targetIndex) {
1289
+ let limitedStartIndex = startIndex;
1290
+ let limitedEndIndex = endIndex;
1291
+ let limitedContent = lines.slice(limitedStartIndex, limitedEndIndex).join("\n");
1292
+ let truncatedByBytes = false;
1293
+ while (Buffer.byteLength(limitedContent, "utf8") > SOURCE_PEEK_MAX_BYTES && limitedEndIndex - limitedStartIndex > 1) {
1294
+ if (targetIndex !== null && targetIndex - limitedStartIndex > limitedEndIndex - targetIndex - 1) {
1295
+ limitedStartIndex += 1;
1211
1296
  } else {
1212
- endIndex -= 1;
1297
+ limitedEndIndex -= 1;
1213
1298
  }
1214
- limitedContent = lines.slice(startIndex, endIndex).join("\n");
1299
+ limitedContent = lines.slice(limitedStartIndex, limitedEndIndex).join("\n");
1300
+ truncatedByBytes = true;
1215
1301
  }
1216
1302
  if (Buffer.byteLength(limitedContent, "utf8") > SOURCE_PEEK_MAX_BYTES) {
1217
1303
  limitedContent = truncateUtf8(limitedContent, SOURCE_PEEK_MAX_BYTES);
1304
+ truncatedByBytes = true;
1218
1305
  }
1219
1306
  return {
1220
1307
  content: limitedContent,
1221
- startLine: startIndex + 1,
1222
- truncated: true
1308
+ startLine: limitedStartIndex + 1,
1309
+ totalLines: sourceTotalLines(lines),
1310
+ truncated: truncatedByBytes || limitedStartIndex > 0 || limitedEndIndex < lines.length,
1311
+ hasMoreAbove: limitedStartIndex > 0,
1312
+ hasMoreBelow: limitedEndIndex < lines.length
1223
1313
  };
1224
1314
  }
1315
+ function emptySourceWindow() {
1316
+ return {
1317
+ content: "",
1318
+ startLine: 1,
1319
+ totalLines: 1,
1320
+ truncated: false,
1321
+ hasMoreAbove: false,
1322
+ hasMoreBelow: false
1323
+ };
1324
+ }
1325
+ function sourceTotalLines(lines) {
1326
+ return Math.max(1, lines.length);
1327
+ }
1328
+ function clampNumber(value, min, max) {
1329
+ return Math.min(max, Math.max(min, value));
1330
+ }
1225
1331
  function truncateUtf8(value, maxBytes) {
1226
1332
  const buffer = Buffer.from(value, "utf8");
1227
1333
  if (buffer.byteLength <= maxBytes) {
@@ -1833,9 +1939,11 @@ function languageForSnippet(filePath, snippet) {
1833
1939
  }
1834
1940
  function serializeFeedbackMarkdown(bundle) {
1835
1941
  const comments = bundle.comments.toSorted(compareCommentsByLocation);
1942
+ const generalComments = comments.filter((comment) => !isLineComment(comment));
1943
+ const lineComments = comments.filter(isLineComment);
1836
1944
  const commentsByFile = /* @__PURE__ */ new Map();
1837
1945
  const files = [];
1838
- for (const comment of comments) {
1946
+ for (const comment of lineComments) {
1839
1947
  const fileComments = commentsByFile.get(comment.filePath);
1840
1948
  if (fileComments) {
1841
1949
  fileComments.push(comment);
@@ -1853,6 +1961,12 @@ function serializeFeedbackMarkdown(bundle) {
1853
1961
  `Files: ${files.length} Comments: ${comments.length}`,
1854
1962
  ""
1855
1963
  ];
1964
+ if (generalComments.length > 0) {
1965
+ lines.push("## General comments", "");
1966
+ for (const comment of generalComments) {
1967
+ lines.push(`### ${comment.id}`, comment.body.trim(), "");
1968
+ }
1969
+ }
1856
1970
  for (const filePath of files) {
1857
1971
  lines.push(`## ${filePath}`, "");
1858
1972
  for (const comment of commentsByFile.get(filePath) ?? []) {
@@ -2914,6 +3028,46 @@ function createApp(origin2, options = {}) {
2914
3028
  return c.json({ error: `source peek unavailable: ${formatError(error)}` }, 404);
2915
3029
  }
2916
3030
  });
3031
+ app.post("/api/reviews/:id/source-peek/range", async (c) => {
3032
+ const id = c.req.param("id");
3033
+ const existing = await reviewStore.get(id);
3034
+ if (!existing) {
3035
+ return c.json({ error: "review not found" }, 404);
3036
+ }
3037
+ const parsed = await readJsonBody(c, isSourcePeekRangeRequest, "source peek range request");
3038
+ if (!parsed.ok) {
3039
+ return parsed.response;
3040
+ }
3041
+ const body = parsed.body;
3042
+ const turn = body.turnId ? await reviewStore.getTurn(id, body.turnId) : null;
3043
+ if (body.turnId && !turn) {
3044
+ return c.json({ error: "turn not found" }, 404);
3045
+ }
3046
+ const diffPayload = turn?.diff ?? existing.diff;
3047
+ const repoRoot = path9.resolve(diffPayload.cwd);
3048
+ const pathError = validateContextPath(repoRoot, body.filePath, "filePath");
3049
+ if (pathError) {
3050
+ return c.json({ error: pathError }, 400);
3051
+ }
3052
+ const source = await resolveContextSource(diffPayload, body.source);
3053
+ if (!source.ok) {
3054
+ return c.json({ error: source.error }, source.status);
3055
+ }
3056
+ const sourceRef = body.side === "L" ? source.oldRef : source.newRef;
3057
+ try {
3058
+ return c.json(
3059
+ await readSourcePeekRange({
3060
+ repoRoot,
3061
+ sourceFilePath: body.filePath,
3062
+ sourceRef,
3063
+ startLine: body.startLine,
3064
+ lineCount: body.lineCount
3065
+ })
3066
+ );
3067
+ } catch (error) {
3068
+ return c.json({ error: `source range unavailable: ${formatError(error)}` }, 404);
3069
+ }
3070
+ });
2917
3071
  app.post("/api/reviews/:id/files/content", async (c) => {
2918
3072
  const id = c.req.param("id");
2919
3073
  const existing = await reviewStore.get(id);