@youtyan/code-viewer 0.13.0 → 0.13.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/web/app.js +150 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -875,6 +875,17 @@ Details: ${JSON.stringify(output)}` : "";
875
875
  };
876
876
  }
877
877
 
878
+ // web-src/core/file-refresh.ts
879
+ function rawFileInfoSignature(info) {
880
+ if (info.missing) return "missing";
881
+ if (info.size == null && !info.updated_at && !info.commit_updated_at)
882
+ return null;
883
+ return `${info.size ?? ""}|${info.updated_at ?? ""}|${info.commit_updated_at ?? ""}`;
884
+ }
885
+ function fileSignatureUnchanged(stored, key, sig) {
886
+ return stored !== null && stored.key === key && stored.sig === sig;
887
+ }
888
+
878
889
  // web-src/core/focus-scope.ts
879
890
  function isEditableKeyTarget(target) {
880
891
  if (!target) return false;
@@ -25714,6 +25725,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
25714
25725
  (f2) => `${fileKey2(f2)}\0${f2.path}\0${f2.old_path || ""}\0${f2.status || "M"}`
25715
25726
  ).join("\n");
25716
25727
  }
25728
+ function stripLoadUrlGeneration(url) {
25729
+ const queryStart = url.indexOf("?");
25730
+ if (queryStart === -1) return url;
25731
+ const params = new URLSearchParams(url.slice(queryStart + 1));
25732
+ if (!params.has("generation")) return url;
25733
+ params.delete("generation");
25734
+ const s2 = params.toString();
25735
+ return s2 ? `${url.slice(0, queryStart)}?${s2}` : url.slice(0, queryStart);
25736
+ }
25737
+ function diffResponseSignature(data) {
25738
+ return JSON.stringify({ ...data, generation: void 0 });
25739
+ }
25717
25740
  function computeCardSignature(f2) {
25718
25741
  return [
25719
25742
  fileKey2(f2),
@@ -25725,12 +25748,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
25725
25748
  f2.size_class || "small",
25726
25749
  f2.force_layout || "",
25727
25750
  f2.highlight ? 1 : 0,
25728
- f2.load_url,
25729
- f2.preview_url || "",
25751
+ stripLoadUrlGeneration(f2.load_url),
25752
+ f2.preview_url ? stripLoadUrlGeneration(f2.preview_url) : "",
25730
25753
  f2.estimated_height_px || 0,
25731
25754
  f2.untracked ? 1 : 0
25732
25755
  ].join("\0");
25733
25756
  }
25757
+ function cardNeedsLoad(card) {
25758
+ if (card.classList.contains("loading")) return false;
25759
+ if (card.dataset.staleLoading === "1") return false;
25760
+ if (card.classList.contains("loaded")) return card.dataset.stale === "1";
25761
+ return true;
25762
+ }
25734
25763
  function ensureLazyObserver() {
25735
25764
  if (!lazyObserver) {
25736
25765
  lazyObserver = new IntersectionObserver(
@@ -25738,8 +25767,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
25738
25767
  for (const entry of entries) {
25739
25768
  if (!entry.isIntersecting) continue;
25740
25769
  const card = entry.target;
25741
- if (card.classList.contains("loaded") || card.classList.contains("loading"))
25742
- continue;
25770
+ if (!cardNeedsLoad(card)) continue;
25743
25771
  const f2 = card._file || STATE.files.find((x) => x.path === card.dataset.path);
25744
25772
  if (f2) enqueueLoad(f2, card, 0);
25745
25773
  }
@@ -25761,15 +25789,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
25761
25789
  return false;
25762
25790
  if (!changedPathsCoverPath(changedPaths, file.path)) return false;
25763
25791
  card.dataset.reqId = String(++CLIENT_REQ_SEQ);
25792
+ delete card.dataset.stale;
25793
+ delete card.dataset.staleLoading;
25764
25794
  card.style.minHeight = `${measuredHeight ?? card.offsetHeight}px`;
25765
25795
  card.classList.remove("loaded", "loading", "error");
25766
25796
  card.classList.add("pending");
25767
25797
  card._diffData = null;
25798
+ card._loadedSig = null;
25799
+ card._loadedSigUrl = null;
25768
25800
  const indicator = card.querySelector(".loading-indicator");
25769
25801
  if (indicator) indicator.hidden = false;
25770
25802
  activatePendingCard(card, file);
25771
25803
  return true;
25772
25804
  }
25805
+ function revalidateCardSilently(card, file) {
25806
+ if (!card.classList.contains("loaded")) return false;
25807
+ card.dataset.reqId = String(++CLIENT_REQ_SEQ);
25808
+ card.dataset.stale = "1";
25809
+ card._file = file;
25810
+ activatePendingCard(card, file);
25811
+ return true;
25812
+ }
25773
25813
  function updateSidebarStats(files) {
25774
25814
  for (const f2 of files) {
25775
25815
  const li = document.querySelector(
@@ -25800,8 +25840,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
25800
25840
  delete card.dataset.manualRendered;
25801
25841
  delete card.dataset.manualLoad;
25802
25842
  delete card.dataset.manualMode;
25843
+ delete card.dataset.stale;
25844
+ delete card.dataset.staleLoading;
25803
25845
  card.style.minHeight = `${file.estimated_height_px || 80}px`;
25804
25846
  card._diffData = null;
25847
+ card._loadedSig = null;
25848
+ card._loadedSigUrl = null;
25805
25849
  card._file = file;
25806
25850
  }
25807
25851
  function renderShell(meta, changedPaths) {
@@ -25877,23 +25921,37 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
25877
25921
  activatePendingCard(card, f2);
25878
25922
  invalidatedCards++;
25879
25923
  sidebarNeedsStatsUpdate = true;
25880
- } else {
25924
+ } else if (sigChanged) {
25881
25925
  const stats = card.querySelector(".gdp-shell-header .stats");
25882
25926
  if (stats) {
25883
25927
  stats.innerHTML = '<span class="a">+' + (f2.additions || 0) + '</span><span class="d">−' + (f2.deletions || 0) + "</span>";
25884
25928
  }
25885
25929
  card._file = f2;
25886
- const didInvalidate = sigChanged ? invalidateLoadedCard(card, f2, null, measuredHeights.get(key)) : invalidateLoadedCard(
25930
+ const didInvalidate = invalidateLoadedCard(
25887
25931
  card,
25888
25932
  f2,
25889
- changedPaths,
25933
+ null,
25890
25934
  measuredHeights.get(key)
25891
25935
  );
25892
25936
  if (didInvalidate) invalidatedCards++;
25893
- else if ((sigChanged || pathHint) && card.classList.contains("pending")) {
25937
+ else if (card.classList.contains("pending")) {
25894
25938
  activatePendingCard(card, f2);
25895
25939
  }
25896
- if (sigChanged) sidebarNeedsStatsUpdate = true;
25940
+ sidebarNeedsStatsUpdate = true;
25941
+ } else {
25942
+ card._file = f2;
25943
+ if (!revalidateCardSilently(card, f2)) {
25944
+ const didInvalidate = invalidateLoadedCard(
25945
+ card,
25946
+ f2,
25947
+ changedPaths,
25948
+ measuredHeights.get(key)
25949
+ );
25950
+ if (didInvalidate) invalidatedCards++;
25951
+ else if (card.classList.contains("pending")) {
25952
+ activatePendingCard(card, f2);
25953
+ }
25954
+ }
25897
25955
  }
25898
25956
  }
25899
25957
  if (sidebarNeedsStatsUpdate && canUpdateSidebar)
@@ -25988,8 +26046,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
25988
26046
  entries.forEach((entry) => {
25989
26047
  if (!entry.isIntersecting) return;
25990
26048
  const card = entry.target;
25991
- if (card.classList.contains("loaded") || card.classList.contains("loading"))
25992
- return;
26049
+ if (!cardNeedsLoad(card)) return;
25993
26050
  const f2 = card._file || STATE.files.find((x) => x.path === card.dataset.path);
25994
26051
  if (!f2) return;
25995
26052
  enqueueLoad(f2, card, 0);
@@ -26030,8 +26087,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26030
26087
  while (ACTIVE_LOADS < MAX_PARALLEL && LOAD_QUEUE.length) {
26031
26088
  const item = LOAD_QUEUE.shift();
26032
26089
  if (item.epoch !== LOAD_EPOCH) continue;
26033
- if (item.card.classList.contains("loaded") || item.card.classList.contains("loading"))
26034
- continue;
26090
+ if (!cardNeedsLoad(item.card)) continue;
26035
26091
  ACTIVE_LOADS++;
26036
26092
  loadFile(item.file, item.card).finally(() => {
26037
26093
  if (item.epoch === LOAD_EPOCH) {
@@ -26126,20 +26182,29 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26126
26182
  });
26127
26183
  }
26128
26184
  function loadFile(file, card, urlOverride, options) {
26129
- card.classList.remove("pending");
26130
- card.classList.add("loading");
26131
- if (lazyObserver) lazyObserver.unobserve(card);
26185
+ const silent = card.classList.contains("loaded") && card.dataset.stale === "1";
26132
26186
  const indicator = card.querySelector(".loading-indicator");
26133
- if (indicator) indicator.hidden = false;
26187
+ if (silent) {
26188
+ card.dataset.staleLoading = "1";
26189
+ } else {
26190
+ card.classList.remove("pending");
26191
+ card.classList.add("loading");
26192
+ if (indicator) indicator.hidden = false;
26193
+ }
26194
+ if (lazyObserver) lazyObserver.unobserve(card);
26134
26195
  const url = urlOverride || (card.dataset.manualMode === "full" ? file.load_url : file.preview_url || file.load_url);
26135
26196
  const myGen = getServerGeneration();
26136
26197
  const myReq = ++CLIENT_REQ_SEQ;
26137
26198
  card.dataset.reqId = String(myReq);
26138
26199
  const retryStale = () => {
26139
26200
  if (String(myReq) !== card.dataset.reqId) return;
26140
- card.classList.remove("loading");
26141
- card.classList.add("pending");
26142
- if (indicator) indicator.hidden = true;
26201
+ if (silent) {
26202
+ delete card.dataset.staleLoading;
26203
+ } else {
26204
+ card.classList.remove("loading");
26205
+ card.classList.add("pending");
26206
+ if (indicator) indicator.hidden = true;
26207
+ }
26143
26208
  const fresh = card._file || STATE.files.find((x) => x.path === card.dataset.path);
26144
26209
  if (fresh && card.isConnected) enqueueLoad(fresh, card, 0);
26145
26210
  };
@@ -26158,11 +26223,32 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26158
26223
  retryStale();
26159
26224
  return;
26160
26225
  }
26226
+ const strippedUrl = stripLoadUrlGeneration(url);
26227
+ if (silent) {
26228
+ const sig = diffResponseSignature(data);
26229
+ if (card._loadedSigUrl === strippedUrl && card._loadedSig === sig) {
26230
+ delete card.dataset.staleLoading;
26231
+ delete card.dataset.stale;
26232
+ return;
26233
+ }
26234
+ }
26161
26235
  if (!options?.immediate) await nextIdle();
26162
26236
  if (String(myReq) !== card.dataset.reqId) return;
26237
+ if (silent) {
26238
+ delete card.dataset.staleLoading;
26239
+ delete card.dataset.stale;
26240
+ }
26241
+ card._loadedSig = diffResponseSignature(data);
26242
+ card._loadedSigUrl = strippedUrl;
26163
26243
  renderFile(file, data, card);
26164
26244
  }).catch((error2) => {
26165
26245
  if (String(myReq) !== card.dataset.reqId) return;
26246
+ if (silent) {
26247
+ delete card.dataset.staleLoading;
26248
+ delete card.dataset.stale;
26249
+ console.error("[code-viewer] silent diff revalidation failed", error2);
26250
+ return;
26251
+ }
26166
26252
  console.error("[code-viewer] failed to load diff", error2);
26167
26253
  card.classList.remove("loading");
26168
26254
  card.classList.add("error");
@@ -36086,6 +36172,7 @@ code-viewer annotate add-db --db app.db --tab query \\
36086
36172
  async function loadRawFileInfo(target) {
36087
36173
  try {
36088
36174
  const res = await fetch(buildRawFileUrl(target), { method: "HEAD" });
36175
+ if (res.status === 404) return { missing: true };
36089
36176
  if (!res.ok) return {};
36090
36177
  const rawSize = res.headers.get("content-length");
36091
36178
  const size = rawSize == null ? NaN : Number(rawSize);
@@ -47326,12 +47413,53 @@ ${error2.stack}` : ""}`
47326
47413
  );
47327
47414
  }
47328
47415
  }
47416
+ let fileRouteSignature = null;
47417
+ let fileRouteSignatureSeed = null;
47418
+ let fileRouteSignatureCheck = null;
47419
+ function fileRouteSignatureKey(route) {
47420
+ return `${route.view || "blob"}\0${route.path}\0${route.ref || "worktree"}`;
47421
+ }
47422
+ async function readFileRouteSignature(route) {
47423
+ const info = await REPO_VIEW.loadRawFileInfo({
47424
+ path: route.path,
47425
+ ref: route.ref || "worktree"
47426
+ });
47427
+ return rawFileInfoSignature(info);
47428
+ }
47429
+ function seedFileRouteSignature(route) {
47430
+ const key = fileRouteSignatureKey(route);
47431
+ const seed = trackLoad(readFileRouteSignature(route)).then((sig) => {
47432
+ if (fileRouteSignatureSeed === seed) fileRouteSignatureSeed = null;
47433
+ if (sig === null) return;
47434
+ fileRouteSignature = { key, sig };
47435
+ });
47436
+ fileRouteSignatureSeed = seed;
47437
+ }
47438
+ function refreshFileRouteIfChanged(route) {
47439
+ if (fileRouteSignatureCheck) return;
47440
+ const key = fileRouteSignatureKey(route);
47441
+ const pendingSeed = fileRouteSignatureSeed ?? Promise.resolve();
47442
+ fileRouteSignatureCheck = pendingSeed.then(() => trackLoad(readFileRouteSignature(route))).then((sig) => {
47443
+ fileRouteSignatureCheck = null;
47444
+ if (sig === null) return;
47445
+ const routeNow = STATE.route;
47446
+ if (routeNow.screen !== "file" || !isBlobOrBlameFileRoute(routeNow) || fileRouteSignatureKey(routeNow) !== key)
47447
+ return;
47448
+ if (fileSignatureUnchanged(fileRouteSignature, key, sig)) return;
47449
+ fileRouteSignature = { key, sig };
47450
+ dispatchFileRoute(routeNow, { refresh: true });
47451
+ });
47452
+ fileRouteSignatureCheck.catch(() => {
47453
+ fileRouteSignatureCheck = null;
47454
+ });
47455
+ }
47329
47456
  function dispatchFileRoute(route, options = {}) {
47330
47457
  if (route.view === "blob") {
47331
47458
  setStatus("live");
47332
47459
  removeFileHistoryShell2();
47333
47460
  BLAME_VIEW.removeBlamePage();
47334
47461
  applySourceRouteToShell(options);
47462
+ seedFileRouteSignature(route);
47335
47463
  return true;
47336
47464
  }
47337
47465
  if (route.view === "blame") {
@@ -47339,6 +47467,7 @@ ${error2.stack}` : ""}`
47339
47467
  cancelActiveSourceLoad("navigation");
47340
47468
  removeFileHistoryShell2();
47341
47469
  void BLAME_VIEW.renderBlamePage({ path: route.path, ref: route.ref });
47470
+ seedFileRouteSignature(route);
47342
47471
  return true;
47343
47472
  }
47344
47473
  if (route.view === "history") {
@@ -49510,7 +49639,7 @@ ${error2.stack}` : ""}`
49510
49639
  if (isBlobOrBlameFileRoute(route)) {
49511
49640
  const viewingPath = route.path;
49512
49641
  if (viewingPath && !changedPathsCoverPath(paths, viewingPath)) return;
49513
- dispatchFileRoute(route, { refresh: true });
49642
+ refreshFileRouteIfChanged(route);
49514
49643
  return;
49515
49644
  }
49516
49645
  if (route.screen === "repo") {