getgloss 0.12.0 → 0.12.3

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.0",
13
+ version: "0.12.3",
14
14
  description: "Local browser-based diff review for coding-agent loops.",
15
15
  type: "module",
16
16
  packageManager: "pnpm@10.33.2",
@@ -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
  }
@@ -1013,6 +1017,25 @@ async function resolveSourcePeek({
1013
1017
  }
1014
1018
  throw new Error(`No definition found for ${symbol}`);
1015
1019
  }
1020
+ async function readSourcePeekRange({
1021
+ lineCount,
1022
+ repoRoot,
1023
+ sourceFilePath,
1024
+ sourceRef,
1025
+ startLine
1026
+ }) {
1027
+ const content = await readRepoText(repoRoot, sourceRef, sourceFilePath);
1028
+ const limited = limitContentRange(content, startLine, lineCount);
1029
+ return {
1030
+ filePath: sourceFilePath,
1031
+ startLine: limited.startLine,
1032
+ totalLines: limited.totalLines,
1033
+ content: limited.content,
1034
+ truncated: limited.truncated,
1035
+ hasMoreAbove: limited.hasMoreAbove,
1036
+ hasMoreBelow: limited.hasMoreBelow
1037
+ };
1038
+ }
1016
1039
  async function resolveImportedTarget({
1017
1040
  aliasConfig,
1018
1041
  importedName,
@@ -1191,37 +1214,86 @@ function responseForMatch({
1191
1214
  language: languageForPath(filePath),
1192
1215
  content: limited.content,
1193
1216
  truncated: limited.truncated,
1217
+ totalLines: limited.totalLines,
1218
+ hasMoreAbove: limited.hasMoreAbove,
1219
+ hasMoreBelow: limited.hasMoreBelow,
1194
1220
  matchReason
1195
1221
  };
1196
1222
  }
1197
1223
  function limitContentAroundLine(content, line) {
1224
+ const lines = splitFileLines2(content);
1225
+ const totalLines = sourceTotalLines(lines);
1198
1226
  if (Buffer.byteLength(content, "utf8") <= SOURCE_PEEK_MAX_BYTES) {
1199
- return { content, startLine: 1, truncated: false };
1227
+ return {
1228
+ content,
1229
+ startLine: 1,
1230
+ totalLines,
1231
+ truncated: false,
1232
+ hasMoreAbove: false,
1233
+ hasMoreBelow: false
1234
+ };
1235
+ }
1236
+ if (lines.length === 0) {
1237
+ return emptySourceWindow();
1200
1238
  }
1201
- const lines = splitFileLines2(content);
1202
1239
  const targetIndex = Math.max(0, line - 1);
1203
1240
  const preferredStartIndex = Math.max(0, targetIndex - SOURCE_PEEK_TARGET_CONTEXT_LINES);
1204
1241
  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;
1242
+ return limitLinesByIndexes(lines, preferredStartIndex, preferredEndIndex, targetIndex);
1243
+ }
1244
+ function limitContentRange(content, startLine, lineCount) {
1245
+ const lines = splitFileLines2(content);
1246
+ if (lines.length === 0) {
1247
+ return emptySourceWindow();
1248
+ }
1249
+ const cappedLineCount = Math.min(lineCount, SOURCE_PEEK_RANGE_MAX_LINES);
1250
+ const startIndex = clampNumber(startLine - 1, 0, lines.length - 1);
1251
+ const endIndex = Math.min(lines.length, startIndex + cappedLineCount);
1252
+ return limitLinesByIndexes(lines, startIndex, endIndex, null);
1253
+ }
1254
+ function limitLinesByIndexes(lines, startIndex, endIndex, targetIndex) {
1255
+ let limitedStartIndex = startIndex;
1256
+ let limitedEndIndex = endIndex;
1257
+ let limitedContent = lines.slice(limitedStartIndex, limitedEndIndex).join("\n");
1258
+ let truncatedByBytes = false;
1259
+ while (Buffer.byteLength(limitedContent, "utf8") > SOURCE_PEEK_MAX_BYTES && limitedEndIndex - limitedStartIndex > 1) {
1260
+ if (targetIndex !== null && targetIndex - limitedStartIndex > limitedEndIndex - targetIndex - 1) {
1261
+ limitedStartIndex += 1;
1211
1262
  } else {
1212
- endIndex -= 1;
1263
+ limitedEndIndex -= 1;
1213
1264
  }
1214
- limitedContent = lines.slice(startIndex, endIndex).join("\n");
1265
+ limitedContent = lines.slice(limitedStartIndex, limitedEndIndex).join("\n");
1266
+ truncatedByBytes = true;
1215
1267
  }
1216
1268
  if (Buffer.byteLength(limitedContent, "utf8") > SOURCE_PEEK_MAX_BYTES) {
1217
1269
  limitedContent = truncateUtf8(limitedContent, SOURCE_PEEK_MAX_BYTES);
1270
+ truncatedByBytes = true;
1218
1271
  }
1219
1272
  return {
1220
1273
  content: limitedContent,
1221
- startLine: startIndex + 1,
1222
- truncated: true
1274
+ startLine: limitedStartIndex + 1,
1275
+ totalLines: sourceTotalLines(lines),
1276
+ truncated: truncatedByBytes || limitedStartIndex > 0 || limitedEndIndex < lines.length,
1277
+ hasMoreAbove: limitedStartIndex > 0,
1278
+ hasMoreBelow: limitedEndIndex < lines.length
1279
+ };
1280
+ }
1281
+ function emptySourceWindow() {
1282
+ return {
1283
+ content: "",
1284
+ startLine: 1,
1285
+ totalLines: 1,
1286
+ truncated: false,
1287
+ hasMoreAbove: false,
1288
+ hasMoreBelow: false
1223
1289
  };
1224
1290
  }
1291
+ function sourceTotalLines(lines) {
1292
+ return Math.max(1, lines.length);
1293
+ }
1294
+ function clampNumber(value, min, max) {
1295
+ return Math.min(max, Math.max(min, value));
1296
+ }
1225
1297
  function truncateUtf8(value, maxBytes) {
1226
1298
  const buffer = Buffer.from(value, "utf8");
1227
1299
  if (buffer.byteLength <= maxBytes) {
@@ -2914,6 +2986,46 @@ function createApp(origin2, options = {}) {
2914
2986
  return c.json({ error: `source peek unavailable: ${formatError(error)}` }, 404);
2915
2987
  }
2916
2988
  });
2989
+ app.post("/api/reviews/:id/source-peek/range", async (c) => {
2990
+ const id = c.req.param("id");
2991
+ const existing = await reviewStore.get(id);
2992
+ if (!existing) {
2993
+ return c.json({ error: "review not found" }, 404);
2994
+ }
2995
+ const parsed = await readJsonBody(c, isSourcePeekRangeRequest, "source peek range request");
2996
+ if (!parsed.ok) {
2997
+ return parsed.response;
2998
+ }
2999
+ const body = parsed.body;
3000
+ const turn = body.turnId ? await reviewStore.getTurn(id, body.turnId) : null;
3001
+ if (body.turnId && !turn) {
3002
+ return c.json({ error: "turn not found" }, 404);
3003
+ }
3004
+ const diffPayload = turn?.diff ?? existing.diff;
3005
+ const repoRoot = path9.resolve(diffPayload.cwd);
3006
+ const pathError = validateContextPath(repoRoot, body.filePath, "filePath");
3007
+ if (pathError) {
3008
+ return c.json({ error: pathError }, 400);
3009
+ }
3010
+ const source = await resolveContextSource(diffPayload, body.source);
3011
+ if (!source.ok) {
3012
+ return c.json({ error: source.error }, source.status);
3013
+ }
3014
+ const sourceRef = body.side === "L" ? source.oldRef : source.newRef;
3015
+ try {
3016
+ return c.json(
3017
+ await readSourcePeekRange({
3018
+ repoRoot,
3019
+ sourceFilePath: body.filePath,
3020
+ sourceRef,
3021
+ startLine: body.startLine,
3022
+ lineCount: body.lineCount
3023
+ })
3024
+ );
3025
+ } catch (error) {
3026
+ return c.json({ error: `source range unavailable: ${formatError(error)}` }, 404);
3027
+ }
3028
+ });
2917
3029
  app.post("/api/reviews/:id/files/content", async (c) => {
2918
3030
  const id = c.req.param("id");
2919
3031
  const existing = await reviewStore.get(id);