@youtyan/code-viewer 0.6.1 → 0.6.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/web/app.js CHANGED
@@ -21,6 +21,48 @@
21
21
  throw Error('Dynamic require of "' + x + '" is not supported');
22
22
  });
23
23
 
24
+ // web-src/core/diff-file-kinds.ts
25
+ var HEAVY_SIZE_CLASSES = new Set(["medium", "large", "huge"]);
26
+ function classifyDiffFileKind(file) {
27
+ const status = (file.status || "")[0]?.toUpperCase();
28
+ const heavy = HEAVY_SIZE_CLASSES.has(file.size_class || "");
29
+ const media = !!file.media_kind;
30
+ return {
31
+ added: status === "A",
32
+ deleted: status === "D",
33
+ renamed: status === "R",
34
+ heavy,
35
+ binary: !media && (file.size_class === "binary" || !!file.binary),
36
+ media
37
+ };
38
+ }
39
+ function summarizeDiffFileKinds(files) {
40
+ const counts = {
41
+ added: 0,
42
+ deleted: 0,
43
+ renamed: 0,
44
+ heavy: 0,
45
+ binary: 0,
46
+ media: 0
47
+ };
48
+ for (const file of files) {
49
+ const kind = classifyDiffFileKind(file);
50
+ if (kind.added)
51
+ counts.added++;
52
+ if (kind.deleted)
53
+ counts.deleted++;
54
+ if (kind.renamed)
55
+ counts.renamed++;
56
+ if (kind.heavy)
57
+ counts.heavy++;
58
+ if (kind.binary)
59
+ counts.binary++;
60
+ if (kind.media)
61
+ counts.media++;
62
+ }
63
+ return counts;
64
+ }
65
+
24
66
  // web-src/core/file-path-copy.ts
25
67
  function filePathClipboardText(path) {
26
68
  return path || "";
@@ -55,6 +97,13 @@ ${lines.join(`
55
97
 
56
98
  // web-src/core/ai-context-copy.ts
57
99
  var AI_CONTEXT_LARGE_SELECTION_LINE_THRESHOLD = 300;
100
+ var DATABASE_QUERY_SQL_MAX_CHARS = 200;
101
+ function truncateOneLine(text, maxChars) {
102
+ const collapsed = text.replace(/\s+/g, " ").trim();
103
+ if (collapsed.length <= maxChars)
104
+ return collapsed;
105
+ return `${collapsed.slice(0, maxChars)}...`;
106
+ }
58
107
  function lineRange(line) {
59
108
  return typeof line === "number" ? { start: line, end: line } : { start: line.start, end: line.end };
60
109
  }
@@ -104,13 +153,14 @@ ${lines.join(`
104
153
  `)}
105
154
  \`\`\``;
106
155
  }
107
- function historyLine(route) {
108
- if (!route.commit)
109
- return "";
110
- const ref = route.ref && route.ref !== "worktree" && route.ref !== "HEAD" ? ` (ref: ${route.ref})` : "";
111
- return `commit: ${route.commit}${ref}`;
156
+ function historyLine(route, diffFrom, diffTo) {
157
+ if (route.commit) {
158
+ const ref = route.ref && route.ref !== "worktree" && route.ref !== "HEAD" ? ` (ref: ${route.ref})` : "";
159
+ return `commit: ${route.commit}${ref}`;
160
+ }
161
+ return `History: ${diffFrom}..${diffTo}${commitOrRefSuffix(route)}`;
112
162
  }
113
- function databaseLine(route) {
163
+ function databaseLine(route, querySql) {
114
164
  const parts = [];
115
165
  if (route.db)
116
166
  parts.push(`db=${route.db}`);
@@ -123,16 +173,52 @@ ${lines.join(`
123
173
  if (route.diffBefore && route.diffAfter) {
124
174
  parts.push(`snapshot=${route.diffBefore}..${route.diffAfter}`);
125
175
  }
126
- return parts.length > 0 ? `database: ${parts.join(", ")}` : "";
176
+ if (route.tab === "query" && querySql) {
177
+ const sql = truncateOneLine(querySql, DATABASE_QUERY_SQL_MAX_CHARS);
178
+ if (sql)
179
+ parts.push(`sql=${sql}`);
180
+ }
181
+ return parts.length > 0 ? `database: ${parts.join(", ")}` : "database";
182
+ }
183
+ function diffOverviewLine(from, to, meta, viewedFiles) {
184
+ const base = `Diff: ${from}..${to}`;
185
+ if (!meta?.totals)
186
+ return base;
187
+ const parts = [
188
+ `${meta.totals.files} file${meta.totals.files === 1 ? "" : "s"}`,
189
+ `+${meta.totals.additions}/-${meta.totals.deletions}`
190
+ ];
191
+ const viewedTotal = meta.files.length;
192
+ if (viewedFiles && viewedTotal > 0) {
193
+ const viewed = meta.files.filter((file) => viewedFiles.has(file.path)).length;
194
+ parts.push(`${viewed}/${viewedTotal} viewed`);
195
+ }
196
+ const kinds = summarizeDiffFileKinds(meta.files);
197
+ const kindParts = [];
198
+ if (kinds.added)
199
+ kindParts.push(`${kinds.added} added`);
200
+ if (kinds.deleted)
201
+ kindParts.push(`${kinds.deleted} deleted`);
202
+ if (kinds.renamed)
203
+ kindParts.push(`${kinds.renamed} renamed`);
204
+ if (kinds.heavy)
205
+ kindParts.push(`${kinds.heavy} heavy`);
206
+ if (kinds.binary)
207
+ kindParts.push(`${kinds.binary} binary`);
208
+ if (kinds.media)
209
+ kindParts.push(`${kinds.media} media`);
210
+ if (kindParts.length > 0)
211
+ parts.push(kindParts.join(", "));
212
+ return `${base} (${parts.join(", ")})`;
127
213
  }
128
214
  function aiContextClipboardText(snapshot) {
129
215
  const { route } = snapshot;
130
216
  if (route.screen === "database")
131
- return databaseLine(route);
217
+ return databaseLine(route, snapshot.databaseQuerySql);
132
218
  if (route.screen === "history")
133
- return historyLine(route);
219
+ return historyLine(route, snapshot.diffFrom, snapshot.diffTo);
134
220
  if (route.screen === "diff" && !route.path) {
135
- return `Diff: ${snapshot.diffFrom}..${snapshot.diffTo}`;
221
+ return diffOverviewLine(snapshot.diffFrom, snapshot.diffTo, snapshot.diffMeta, snapshot.viewedFiles);
136
222
  }
137
223
  return referenceLine(route, snapshot.selectionCode);
138
224
  }
@@ -401,6 +487,30 @@ ${lines.join(`
401
487
  "M7.47 2.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L8 3.81 4.28 7.53a.75.75 0 0 1-1.06-1.06Z",
402
488
  "M7.47 6.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L8 7.81l-3.72 3.72a.75.75 0 1 1-1.06-1.06Z"
403
489
  ];
490
+ var PLAY_16_PATH = "M4.75 3.625a.75.75 0 0 1 1.148-.635l6 3.875a.75.75 0 0 1 0 1.27l-6 3.875A.75.75 0 0 1 4.75 11.375Z";
491
+ var PAUSE_16_PATHS = [
492
+ "M4.75 3A.75.75 0 0 1 5.5 2.25h1A.75.75 0 0 1 7.25 3v10a.75.75 0 0 1-.75.75h-1A.75.75 0 0 1 4.75 13Z",
493
+ "M8.75 3a.75.75 0 0 1 .75-.75h1a.75.75 0 0 1 .75.75v10a.75.75 0 0 1-.75.75h-1a.75.75 0 0 1-.75-.75Z"
494
+ ];
495
+ var PREVIOUS_16_PATHS = [
496
+ "M3.75 3a.75.75 0 0 1 .75.75v8.5a.75.75 0 0 1-1.5 0v-8.5A.75.75 0 0 1 3.75 3Z",
497
+ "M12.22 3.22a.749.749 0 0 1 0 1.06L8.5 8l3.72 3.72a.749.749 0 1 1-1.06 1.06L6.91 8.53a.749.749 0 0 1 0-1.06l4.25-4.25a.749.749 0 0 1 1.06 0Z"
498
+ ];
499
+ var NEXT_16_PATHS = [
500
+ "M12.25 3a.75.75 0 0 1 .75.75v8.5a.75.75 0 0 1-1.5 0v-8.5a.75.75 0 0 1 .75-.75Z",
501
+ "M3.78 3.22a.749.749 0 0 1 1.06 0l4.25 4.25a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06-1.06L7.5 8 3.78 4.28a.749.749 0 0 1 0-1.06Z"
502
+ ];
503
+ var VOLUME_UNMUTED_16_PATHS = [
504
+ "M7.563 2.069A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.188.61L3.766 11.5H1.75A1.75 1.75 0 0 1 0 9.75v-3.5C0 5.284.784 4.5 1.75 4.5h2.016l3.046-2.36a.75.75 0 0 1 .751-.071ZM6.5 4.279 4.484 5.841A.75.75 0 0 1 4.025 6H1.75a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h2.275a.75.75 0 0 1 .459.159L6.5 11.721Z",
505
+ "M10.28 5.22a.749.749 0 0 1 1.06 0 3.93 3.93 0 0 1 0 5.56.749.749 0 1 1-1.06-1.06 2.43 2.43 0 0 0 0-3.44.749.749 0 0 1 0-1.06Z",
506
+ "M12.39 3.11a.749.749 0 0 1 1.06 0 6.91 6.91 0 0 1 0 9.78.749.749 0 0 1-1.06-1.06 5.41 5.41 0 0 0 0-7.66.749.749 0 0 1 0-1.06Z"
507
+ ];
508
+ var VOLUME_MUTED_16_PATHS = [
509
+ VOLUME_UNMUTED_16_PATHS[0],
510
+ "M10.28 6.22 11.5 7.44l1.22-1.22a.749.749 0 1 1 1.06 1.06L12.56 8.5l1.22 1.22a.749.749 0 1 1-1.06 1.06L11.5 9.56l-1.22 1.22a.749.749 0 1 1-1.06-1.06l1.22-1.22-1.22-1.22a.749.749 0 1 1 1.06-1.06Z"
511
+ ];
512
+ var X_16_PATH = "M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z";
513
+ var SYNC_16_PATH = "M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z";
404
514
  function iconSvg(className, paths) {
405
515
  const pathList = Array.isArray(paths) ? paths : [paths];
406
516
  return '<svg class="octicon ' + className + '" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">' + pathList.map((path) => `<path fill="currentColor" d="${path}"></path>`).join("") + "</svg>";
@@ -409,6 +519,12 @@ ${lines.join(`
409
519
  var COMMENT_DISCUSSION_16_PATH = "M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z";
410
520
  var PULSE_16_PATH = "M6 2c.306 0 .582.187.696.471L10 10.731l1.304-3.26A.751.751 0 0 1 12 7h3.25a.75.75 0 0 1 0 1.5h-2.742l-1.812 4.528a.751.751 0 0 1-1.392 0L6 4.77 4.696 8.03A.75.75 0 0 1 4 8.5H.75a.75.75 0 0 1 0-1.5h2.742l1.812-4.529A.751.751 0 0 1 6 2Z";
411
521
  var MOON_16_PATH = "M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.499 5.499 0 1 0 7.678-7.678Z";
522
+ var SEARCH_16_PATH = "M10.68 11.74a6 6 0 0 1-7.917-8.984 6 6 0 0 1 8.997 7.905l3.05 3.05a.75.75 0 1 1-1.06 1.06l-3.07-3.03ZM11.5 7a4.499 4.499 0 1 1-8.997 0A4.499 4.499 0 0 1 11.5 7Z";
523
+ var DOWNLOAD_16_PATHS = [
524
+ "M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z",
525
+ "M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"
526
+ ];
527
+ var LINK_16_PATH = "m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z";
412
528
 
413
529
  // web-src/core/keyboard.ts
414
530
  function isImeComposing(event) {
@@ -522,7 +638,8 @@ ${lines.join(`
522
638
  { action: "toggle-theme", key: "t" },
523
639
  { action: "open-help", key: "?", shift: true },
524
640
  { action: "copy-ai-context", key: "y" },
525
- { action: "copy-ai-context-with-code", key: "y", shift: true }
641
+ { action: "copy-ai-context-with-code", key: "y", shift: true },
642
+ { action: "next-unviewed-file", key: "n" }
526
643
  ];
527
644
  function resolveKeymapAction(event, context) {
528
645
  const key = event.key.toLowerCase();
@@ -1553,6 +1670,11 @@ ${lines.join(`
1553
1670
  }
1554
1671
 
1555
1672
  // web-src/views/annotations-player.ts
1673
+ function setIconButton(button, label, iconClass, paths) {
1674
+ button.innerHTML = iconSvg(iconClass, paths);
1675
+ button.title = label;
1676
+ button.setAttribute("aria-label", label);
1677
+ }
1556
1678
  function createAnnotationsPlayer(deps) {
1557
1679
  const bar = deps.$("#annotation-player");
1558
1680
  const toggleBtn = deps.$("#annotation-player-toggle");
@@ -1609,9 +1731,11 @@ ${lines.join(`
1609
1731
  }
1610
1732
  function render(state) {
1611
1733
  bar.classList.toggle("playing", state.status === "playing");
1612
- toggleBtn.textContent = state.status === "playing" ? "" : "";
1734
+ setIconButton(toggleBtn, state.status === "playing" ? "Pause annotation playback" : "Play annotation playback", state.status === "playing" ? "octicon-pause" : "octicon-play", state.status === "playing" ? PAUSE_16_PATHS : PLAY_16_PATH);
1735
+ setIconButton(prevBtn, "Previous annotation", "octicon-skip-back", PREVIOUS_16_PATHS);
1736
+ setIconButton(nextBtn, "Next annotation", "octicon-skip-forward", NEXT_16_PATHS);
1613
1737
  progress.textContent = state.status === "idle" || state.index < 0 ? `${state.total}` : `${state.index + 1}/${state.total}`;
1614
- muteBtn.textContent = state.muted ? "\uD83D\uDD07" : "\uD83D\uDD0A";
1738
+ setIconButton(muteBtn, state.muted ? "Unmute annotation playback" : "Mute annotation playback", state.muted ? "octicon-volume-muted" : "octicon-volume-unmuted", state.muted ? VOLUME_MUTED_16_PATHS : VOLUME_UNMUTED_16_PATHS);
1615
1739
  prevBtn.disabled = state.status === "idle";
1616
1740
  nextBtn.disabled = state.status === "idle";
1617
1741
  }
@@ -8206,6 +8330,14 @@ ${frontmatter.yaml}
8206
8330
  const annotationListCountEl = $("#annotation-list-count");
8207
8331
  const annotationCaptureDb = $("#annotation-capture-db");
8208
8332
  annotationCaptureDb.innerHTML = iconSvg("octicon-plus", PLUS_16_PATH);
8333
+ const annotationDetailPrev = $("#annotation-detail-prev");
8334
+ const annotationDetailNext = $("#annotation-detail-next");
8335
+ annotationDetailPrev.innerHTML = iconSvg("octicon-skip-back", PREVIOUS_16_PATHS);
8336
+ annotationDetailPrev.title = "previous annotation";
8337
+ annotationDetailPrev.setAttribute("aria-label", "previous annotation");
8338
+ annotationDetailNext.innerHTML = iconSvg("octicon-skip-forward", NEXT_16_PATHS);
8339
+ annotationDetailNext.title = "next annotation";
8340
+ annotationDetailNext.setAttribute("aria-label", "next annotation");
8209
8341
  function updateDatabaseCaptureButton() {
8210
8342
  annotationCaptureDb.hidden = deps.getRoute().screen !== "database";
8211
8343
  }
@@ -8842,11 +8974,10 @@ ${frontmatter.yaml}
8842
8974
  edit.addEventListener("click", () => {
8843
8975
  openAnnotationEditForm(entry);
8844
8976
  });
8845
- const prev = $("#annotation-detail-prev");
8846
- head?.insertBefore(copyRef, prev);
8847
- head?.insertBefore(edit, prev);
8848
- $("#annotation-detail-prev").disabled = index <= 0;
8849
- $("#annotation-detail-next").disabled = index >= session.entries.length - 1;
8977
+ head?.insertBefore(copyRef, annotationDetailPrev);
8978
+ head?.insertBefore(edit, annotationDetailPrev);
8979
+ annotationDetailPrev.disabled = index <= 0;
8980
+ annotationDetailNext.disabled = index >= session.entries.length - 1;
8850
8981
  annotationDetail.hidden = false;
8851
8982
  setAnnotationPanelOpen(true);
8852
8983
  updateActiveHighlights();
@@ -9077,10 +9208,10 @@ ${frontmatter.yaml}
9077
9208
  postAnnotationAction({ action: "clear" });
9078
9209
  });
9079
9210
  $("#annotation-detail-close").addEventListener("click", hideAnnotationDetail);
9080
- $("#annotation-detail-prev").addEventListener("click", () => {
9211
+ annotationDetailPrev.addEventListener("click", () => {
9081
9212
  stepAnnotation(-1);
9082
9213
  });
9083
- $("#annotation-detail-next").addEventListener("click", () => {
9214
+ annotationDetailNext.addEventListener("click", () => {
9084
9215
  stepAnnotation(1);
9085
9216
  });
9086
9217
  $("#annotation-detail-location").addEventListener("click", (e2) => {
@@ -9659,6 +9790,15 @@ ${frontmatter.yaml}
9659
9790
  schemaTab: "Schema",
9660
9791
  selectDatastore: "Select datastore",
9661
9792
  selectSchema: "Select PostgreSQL schema",
9793
+ refreshDatastores: "Refresh datastores",
9794
+ refreshDatastoresShort: "Refresh",
9795
+ refreshDatastoresBusy: "Refreshing...",
9796
+ refreshDatastoresTitle: "Refresh the datastore list",
9797
+ refreshDatastoresUnchanged: "No datastore changes",
9798
+ refreshDatastoresChanged: (added, removed) => [
9799
+ added > 0 ? `+${added} datastore${added === 1 ? "" : "s"}` : "",
9800
+ removed > 0 ? `-${removed} datastore${removed === 1 ? "" : "s"}` : ""
9801
+ ].filter(Boolean).join(" / "),
9662
9802
  toolbar: "Datastore tools",
9663
9803
  query: "Query",
9664
9804
  queryTitle: "Query Editor",
@@ -9674,6 +9814,8 @@ ${frontmatter.yaml}
9674
9814
  closeTab: (label) => `Close ${label}`,
9675
9815
  loadingSchema: "Loading schema…",
9676
9816
  noDatastores: "No datastores found",
9817
+ noDatastoresHint: "Start a database service or add a SQLite file, then refresh this list.",
9818
+ noDatastoreTab: "No datastore",
9677
9819
  dockerLimitReached: "Docker discovery reached the service limit; some compose services may be hidden.",
9678
9820
  inferFkLabel: "Rails FK inference",
9679
9821
  inferFkTitle: "Infer FK from Rails-style <name>_id → <names>.id",
@@ -9683,12 +9825,29 @@ ${frontmatter.yaml}
9683
9825
  grid: {
9684
9826
  searchPlaceholder: "Search all columns…",
9685
9827
  columnFilterPlaceholder: (column) => `${column}…`,
9828
+ clearFiltersLabel: "Clear filters",
9829
+ clearFiltersAction: (count) => `Clear ${count} active filter${count === 1 ? "" : "s"}`,
9830
+ refreshLabel: "Reload table",
9831
+ refreshFilteredLabel: "Reload filtered rows",
9832
+ refreshingLabel: "Reloading...",
9833
+ refreshAction: "Reload this table, keeping search and column filters",
9834
+ refreshActionWithFilters: (count) => `Reload this table, keeping ${count} active filter${count === 1 ? "" : "s"}`,
9835
+ refreshResultChanged: (delta, total) => {
9836
+ const abs = Math.abs(delta).toLocaleString();
9837
+ const sign = delta > 0 ? "+" : "-";
9838
+ return `Rows ${sign}${abs} (${total} now)`;
9839
+ },
9840
+ refreshResultUnchanged: (total) => `Rows unchanged (${total})`,
9686
9841
  exportAction: "Export",
9687
9842
  foreignKeyHint: "Foreign key — click to view related rows",
9688
9843
  relatedEmpty: "No matching row in the referenced table",
9844
+ filteredEmptyTitle: (count) => `No rows match ${count} active filter${count === 1 ? "" : "s"}`,
9845
+ filteredEmptyHint: "The table was loaded, but the current search or column filters hide every row.",
9846
+ filteredEmptyAction: "Clear filters",
9689
9847
  statusRows: (n2) => `${n2} rows`,
9690
9848
  statusSort: (column, dir) => `Sort: ${column} ${dir}`,
9691
- statusFilters: (n2) => `${n2} filter(s)`
9849
+ statusFilters: (n2) => `${n2} filter(s)`,
9850
+ statusRefreshing: (filters) => filters > 0 ? `Reloading with ${filters} active filter${filters === 1 ? "" : "s"}…` : "Reloading current table…"
9692
9851
  },
9693
9852
  edit: {
9694
9853
  editMode: "Edit",
@@ -9712,6 +9871,9 @@ ${frontmatter.yaml}
9712
9871
  indexes: "Indexes",
9713
9872
  triggers: "Triggers",
9714
9873
  ddl: "DDL",
9874
+ refreshLabel: "Refresh schema",
9875
+ refreshAction: "Refresh this table schema",
9876
+ refreshingLabel: "Refreshing...",
9715
9877
  copyDdl: "Copy DDL",
9716
9878
  copied: "Copied!",
9717
9879
  colName: "Column",
@@ -9749,8 +9911,10 @@ ${frontmatter.yaml}
9749
9911
  statusExplain: (ms) => `Explain (${ms}ms)`
9750
9912
  },
9751
9913
  history: {
9752
- refresh: "Refresh",
9753
- refreshTitle: "Refresh history",
9914
+ refresh: "Refresh history",
9915
+ refreshTitle: "Refresh query history",
9916
+ refreshResultAdded: (count) => `+${count} queries`,
9917
+ refreshResultUnchanged: "No new queries",
9754
9918
  clearAll: "Clear All",
9755
9919
  clearTitle: "Delete all query history",
9756
9920
  selectPlaceholder: "Select a query to view details",
@@ -9761,6 +9925,10 @@ ${frontmatter.yaml}
9761
9925
  delete: "Delete",
9762
9926
  confirmDelete: "Confirm delete",
9763
9927
  confirmClear: "Confirm clear",
9928
+ executorAi: "AI",
9929
+ executorUser: "User",
9930
+ rowsLabel: (rows, truncated) => `${rows}${truncated ? "+" : ""} rows`,
9931
+ elapsedLabel: (ms) => `${ms}ms`,
9764
9932
  truncatedRows: (saved, total) => `Showing ${saved} of ${total} rows`
9765
9933
  },
9766
9934
  sessionLog: {
@@ -9944,6 +10112,15 @@ ${frontmatter.yaml}
9944
10112
  schemaTab: "スキーマ",
9945
10113
  selectDatastore: "データストアを選択",
9946
10114
  selectSchema: "PostgreSQL スキーマを選択",
10115
+ refreshDatastores: "データストアを更新",
10116
+ refreshDatastoresShort: "更新",
10117
+ refreshDatastoresBusy: "更新中...",
10118
+ refreshDatastoresTitle: "データストア一覧を更新",
10119
+ refreshDatastoresUnchanged: "データストアに変化なし",
10120
+ refreshDatastoresChanged: (added, removed) => [
10121
+ added > 0 ? `+${added} データストア` : "",
10122
+ removed > 0 ? `-${removed} データストア` : ""
10123
+ ].filter(Boolean).join(" / "),
9947
10124
  toolbar: "データストアツール",
9948
10125
  query: "クエリ",
9949
10126
  queryTitle: "クエリエディタ",
@@ -9959,6 +10136,8 @@ ${frontmatter.yaml}
9959
10136
  closeTab: (label) => `${label} を閉じる`,
9960
10137
  loadingSchema: "スキーマを読み込み中…",
9961
10138
  noDatastores: "データストアが見つかりません",
10139
+ noDatastoresHint: "DB サービスを起動するか SQLite ファイルを追加してから、一覧を更新してください。",
10140
+ noDatastoreTab: "未検出",
9962
10141
  dockerLimitReached: "Docker のサービス数が上限に達しました。一部の compose サービスは表示されていない可能性があります。",
9963
10142
  inferFkLabel: "Rails FK 推測",
9964
10143
  inferFkTitle: "Rails 命名規約 (<name>_id → <names>.id) から FK を推測",
@@ -9968,12 +10147,29 @@ ${frontmatter.yaml}
9968
10147
  grid: {
9969
10148
  searchPlaceholder: "全カラムを検索…",
9970
10149
  columnFilterPlaceholder: (column) => `${column}…`,
10150
+ clearFiltersLabel: "フィルタ解除",
10151
+ clearFiltersAction: (count) => `有効なフィルタ ${count} 件を解除`,
10152
+ refreshLabel: "表を再読込",
10153
+ refreshFilteredLabel: "絞り込み再読込",
10154
+ refreshingLabel: "更新中...",
10155
+ refreshAction: "検索/列フィルタを保持して、この表だけ再読み込み",
10156
+ refreshActionWithFilters: (count) => `有効なフィルタ ${count} 件を保持して、この表だけ再読み込み`,
10157
+ refreshResultChanged: (delta, total) => {
10158
+ const abs = Math.abs(delta).toLocaleString();
10159
+ const sign = delta > 0 ? "+" : "-";
10160
+ return `行数 ${sign}${abs} (現在 ${total})`;
10161
+ },
10162
+ refreshResultUnchanged: (total) => `行数変化なし (${total})`,
9971
10163
  exportAction: "エクスポート",
9972
10164
  foreignKeyHint: "外部キー: クリックして関連データを表示",
9973
10165
  relatedEmpty: "参照先に該当する行がありません",
10166
+ filteredEmptyTitle: (count) => `フィルタ ${count} 件に一致する行がありません`,
10167
+ filteredEmptyHint: "表は読み込めていますが、現在の検索/列フィルタですべての行が隠れています。",
10168
+ filteredEmptyAction: "フィルタ解除",
9974
10169
  statusRows: (n2) => `${n2} 行`,
9975
10170
  statusSort: (column, dir) => `並び替え: ${column} ${dir}`,
9976
- statusFilters: (n2) => `フィルタ ${n2} 件`
10171
+ statusFilters: (n2) => `フィルタ ${n2} 件`,
10172
+ statusRefreshing: (filters) => filters > 0 ? `フィルタ ${filters} 件を保持して再読み込み中…` : "この表を再読み込み中…"
9977
10173
  },
9978
10174
  edit: {
9979
10175
  editMode: "編集",
@@ -9997,6 +10193,9 @@ ${frontmatter.yaml}
9997
10193
  indexes: "インデックス",
9998
10194
  triggers: "トリガー",
9999
10195
  ddl: "DDL",
10196
+ refreshLabel: "スキーマを更新",
10197
+ refreshAction: "この表のスキーマを再読み込み",
10198
+ refreshingLabel: "更新中...",
10000
10199
  copyDdl: "DDL をコピー",
10001
10200
  copied: "コピーしました",
10002
10201
  colName: "カラム",
@@ -10034,8 +10233,10 @@ ${frontmatter.yaml}
10034
10233
  statusExplain: (ms) => `Explain (${ms}ms)`
10035
10234
  },
10036
10235
  history: {
10037
- refresh: "更新",
10038
- refreshTitle: "履歴を更新",
10236
+ refresh: "クエリ履歴を更新",
10237
+ refreshTitle: "クエリ履歴を再読み込み",
10238
+ refreshResultAdded: (count) => `+${count} 件`,
10239
+ refreshResultUnchanged: "新しいクエリはありません",
10039
10240
  clearAll: "すべて削除",
10040
10241
  clearTitle: "クエリ履歴をすべて削除",
10041
10242
  selectPlaceholder: "クエリを選択すると詳細が表示されます",
@@ -10046,6 +10247,10 @@ ${frontmatter.yaml}
10046
10247
  delete: "削除",
10047
10248
  confirmDelete: "削除を確認",
10048
10249
  confirmClear: "全削除を確認",
10250
+ executorAi: "AI",
10251
+ executorUser: "ユーザー",
10252
+ rowsLabel: (rows, truncated) => `${rows}${truncated ? "+" : ""} 行`,
10253
+ elapsedLabel: (ms) => `${ms}ms`,
10049
10254
  truncatedRows: (saved, total) => `全 ${total} 行中 ${saved} 行を表示`
10050
10255
  },
10051
10256
  sessionLog: {
@@ -11861,16 +12066,22 @@ ${frontmatter.yaml}
11861
12066
  const toolbar = document.createElement("div");
11862
12067
  toolbar.className = "db-query-history-toolbar";
11863
12068
  const refreshBtn = document.createElement("button");
11864
- refreshBtn.className = "db-query-history-action";
12069
+ refreshBtn.className = "db-query-history-action db-query-history-refresh";
11865
12070
  refreshBtn.type = "button";
11866
- refreshBtn.textContent = text2().history.refresh;
11867
- refreshBtn.title = text2().history.refreshTitle;
12071
+ refreshBtn.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
12072
+ const refreshLabel = document.createElement("span");
12073
+ refreshLabel.className = "db-query-history-action-label";
12074
+ refreshBtn.appendChild(refreshLabel);
12075
+ const refreshResult = document.createElement("span");
12076
+ refreshResult.className = "db-refresh-result db-query-history-refresh-result";
12077
+ refreshResult.setAttribute("aria-live", "polite");
12078
+ refreshResult.hidden = true;
11868
12079
  const clearBtn = document.createElement("button");
11869
12080
  clearBtn.className = "db-query-history-action db-query-history-danger";
11870
12081
  clearBtn.type = "button";
11871
12082
  clearBtn.textContent = text2().history.clearAll;
11872
12083
  clearBtn.title = text2().history.clearTitle;
11873
- toolbar.append(refreshBtn, clearBtn);
12084
+ toolbar.append(refreshBtn, refreshResult, clearBtn);
11874
12085
  const body = document.createElement("div");
11875
12086
  body.className = "db-query-history-body-split";
11876
12087
  const listCol = document.createElement("div");
@@ -11894,6 +12105,50 @@ ${frontmatter.yaml}
11894
12105
  let inFlightRefresh = null;
11895
12106
  const entryRowsById = new Map;
11896
12107
  let selectedEntryRow = null;
12108
+ let refreshResultState = { type: "none" };
12109
+ function syncRefreshButtonLabel() {
12110
+ refreshLabel.textContent = text2().history.refresh;
12111
+ refreshBtn.title = text2().history.refreshTitle;
12112
+ refreshBtn.setAttribute("aria-label", text2().history.refreshTitle);
12113
+ }
12114
+ function setRefreshBusy(isBusy) {
12115
+ refreshBtn.disabled = isBusy;
12116
+ refreshBtn.classList.toggle("spinning", isBusy);
12117
+ refreshBtn.setAttribute("aria-busy", isBusy ? "true" : "false");
12118
+ }
12119
+ function refreshResultText() {
12120
+ switch (refreshResultState.type) {
12121
+ case "none":
12122
+ return "";
12123
+ case "added":
12124
+ return text2().history.refreshResultAdded(refreshResultState.count);
12125
+ case "unchanged":
12126
+ return text2().history.refreshResultUnchanged;
12127
+ default: {
12128
+ const exhaustive = refreshResultState;
12129
+ return exhaustive;
12130
+ }
12131
+ }
12132
+ }
12133
+ function syncRefreshResult() {
12134
+ const message = refreshResultText();
12135
+ refreshResult.textContent = message;
12136
+ refreshResult.hidden = message.length === 0;
12137
+ refreshResult.classList.toggle("changed", refreshResultState.type === "added");
12138
+ }
12139
+ function clearRefreshResult() {
12140
+ refreshResultState = { type: "none" };
12141
+ syncRefreshResult();
12142
+ }
12143
+ function setRefreshResult(previousEntries, nextEntries) {
12144
+ const previousIds = new Set(previousEntries.map((entry) => entry.id));
12145
+ const added = nextEntries.filter((entry) => !previousIds.has(entry.id)).length;
12146
+ refreshResultState = added > 0 ? { type: "added", count: added } : { type: "unchanged" };
12147
+ syncRefreshResult();
12148
+ }
12149
+ syncRefreshButtonLabel();
12150
+ setRefreshBusy(false);
12151
+ syncRefreshResult();
11897
12152
  function currentRefreshParams() {
11898
12153
  const dbId = callbacks.getDbId();
11899
12154
  const schema = callbacks.getSchema();
@@ -11930,6 +12185,10 @@ ${frontmatter.yaml}
11930
12185
  if (!options.force && refreshKey === lastRefreshKey && now - lastRefreshAt < 1000) {
11931
12186
  return;
11932
12187
  }
12188
+ const previousEntries = options.announceResult ? [...entries] : null;
12189
+ if (options.announceResult)
12190
+ clearRefreshResult();
12191
+ setRefreshBusy(true);
11933
12192
  const promise = (async () => {
11934
12193
  const res = await fetch(`/_db/history${params}`);
11935
12194
  if (!res.ok)
@@ -11938,6 +12197,8 @@ ${frontmatter.yaml}
11938
12197
  if (currentRefreshParams().key !== refreshKey)
11939
12198
  return;
11940
12199
  entries = state.entries;
12200
+ if (previousEntries)
12201
+ setRefreshResult(previousEntries, entries);
11941
12202
  lastRefreshKey = refreshKey;
11942
12203
  lastRefreshAt = Date.now();
11943
12204
  if (selectedEntryId && !entries.some((entry) => entry.id === selectedEntryId)) {
@@ -11949,8 +12210,10 @@ ${frontmatter.yaml}
11949
12210
  try {
11950
12211
  await promise;
11951
12212
  } finally {
11952
- if (inFlightRefresh?.promise === promise)
12213
+ if (inFlightRefresh?.promise === promise) {
11953
12214
  inFlightRefresh = null;
12215
+ setRefreshBusy(false);
12216
+ }
11954
12217
  }
11955
12218
  }
11956
12219
  function render() {
@@ -11987,6 +12250,7 @@ ${frontmatter.yaml}
11987
12250
  }
11988
12251
  function renderDetail(entry) {
11989
12252
  detailCol.innerHTML = "";
12253
+ const meta = renderEntryMeta(entry, "detail");
11990
12254
  const actions = document.createElement("div");
11991
12255
  actions.className = "db-query-history-detail-actions";
11992
12256
  const useBtn = document.createElement("button");
@@ -12021,7 +12285,7 @@ ${frontmatter.yaml}
12021
12285
  const sqlBlock = document.createElement("pre");
12022
12286
  sqlBlock.className = "db-query-history-sql";
12023
12287
  sqlBlock.textContent = entry.sql;
12024
- detailCol.append(actions, sqlBlock);
12288
+ detailCol.append(meta, actions, sqlBlock);
12025
12289
  if (entry.body) {
12026
12290
  const bodyBlock = document.createElement("div");
12027
12291
  bodyBlock.className = "db-query-history-body";
@@ -12047,26 +12311,30 @@ ${frontmatter.yaml}
12047
12311
  }
12048
12312
  item.dataset.id = entry.id;
12049
12313
  entryRowsById.set(entry.id, item);
12314
+ const meta = renderEntryMeta(entry, "entry");
12315
+ const title = document.createElement("div");
12316
+ title.className = "db-query-history-entry-title";
12317
+ title.textContent = entry.title || (entry.sql.length > 80 ? `${entry.sql.slice(0, 80)}...` : entry.sql);
12318
+ item.append(meta, title);
12319
+ item.addEventListener("click", () => selectEntry(entry));
12320
+ return item;
12321
+ }
12322
+ function renderEntryMeta(entry, mode) {
12050
12323
  const meta = document.createElement("div");
12051
- meta.className = "db-query-history-entry-meta";
12324
+ meta.className = mode === "detail" ? "db-query-history-detail-meta" : "db-query-history-entry-meta";
12052
12325
  const byIcon = document.createElement("span");
12053
12326
  byIcon.className = "db-query-history-by";
12054
- byIcon.textContent = entry.executedBy === "ai" ? "[AI]" : "[User]";
12327
+ const executor = entry.executedBy === "ai" ? text2().history.executorAi : text2().history.executorUser;
12328
+ byIcon.textContent = mode === "detail" ? executor : `[${executor}]`;
12055
12329
  const time = document.createElement("span");
12056
12330
  time.className = "db-query-history-time";
12057
12331
  time.textContent = formatTime(entry.executedAt);
12058
12332
  time.title = entry.executedAt;
12059
12333
  const stats = document.createElement("span");
12060
12334
  stats.className = "db-query-history-stats";
12061
- const truncMark = entry.truncated ? "+" : "";
12062
- stats.textContent = `${entry.rowCount}${truncMark} rows, ${entry.elapsedMs}ms`;
12335
+ stats.textContent = `${text2().history.rowsLabel(entry.rowCount, entry.truncated)}, ${text2().history.elapsedLabel(entry.elapsedMs)}`;
12063
12336
  meta.append(byIcon, time, stats);
12064
- const title = document.createElement("div");
12065
- title.className = "db-query-history-entry-title";
12066
- title.textContent = entry.title || (entry.sql.length > 80 ? `${entry.sql.slice(0, 80)}...` : entry.sql);
12067
- item.append(meta, title);
12068
- item.addEventListener("click", () => selectEntry(entry));
12069
- return item;
12337
+ return meta;
12070
12338
  }
12071
12339
  function renderPreviewTable(entry) {
12072
12340
  const wrapper = document.createElement("div");
@@ -12175,17 +12443,19 @@ ${frontmatter.yaml}
12175
12443
  body: JSON.stringify(dbId ? { db: dbId, ...schema ? { schema } : {} } : {})
12176
12444
  });
12177
12445
  entries = [];
12446
+ clearRefreshResult();
12178
12447
  render();
12179
12448
  } catch {}
12180
12449
  });
12181
12450
  refreshBtn.addEventListener("click", () => {
12182
- refresh({ force: true });
12451
+ refresh({ force: true, announceResult: true });
12183
12452
  });
12184
12453
  function clear() {
12185
12454
  entries = [];
12186
12455
  expandedIds.clear();
12187
12456
  lastRefreshKey = null;
12188
12457
  lastRefreshAt = 0;
12458
+ clearRefreshResult();
12189
12459
  if (clearConfirmTimer) {
12190
12460
  clearTimeout(clearConfirmTimer);
12191
12461
  clearConfirmTimer = null;
@@ -12196,8 +12466,8 @@ ${frontmatter.yaml}
12196
12466
  render();
12197
12467
  }
12198
12468
  function localize() {
12199
- refreshBtn.textContent = text2().history.refresh;
12200
- refreshBtn.title = text2().history.refreshTitle;
12469
+ syncRefreshButtonLabel();
12470
+ syncRefreshResult();
12201
12471
  if (clearBtn.dataset.confirm !== "1") {
12202
12472
  clearBtn.textContent = text2().history.clearAll;
12203
12473
  }
@@ -14261,7 +14531,25 @@ ${frontmatter.yaml}
14261
14531
  const el = document.createElement("div");
14262
14532
  el.className = "db-schema-view";
14263
14533
  el.hidden = true;
14534
+ let refreshBusy = false;
14535
+ let refreshBtn = null;
14536
+ let refreshLabel = null;
14264
14537
  let lastArgs = null;
14538
+ function syncRefreshButton() {
14539
+ if (!refreshBtn || !refreshLabel)
14540
+ return;
14541
+ const t2 = text2().schema;
14542
+ refreshBtn.title = t2.refreshAction;
14543
+ refreshBtn.setAttribute("aria-label", t2.refreshAction);
14544
+ refreshBtn.setAttribute("aria-busy", refreshBusy ? "true" : "false");
14545
+ refreshBtn.disabled = refreshBusy;
14546
+ refreshBtn.classList.toggle("spinning", refreshBusy);
14547
+ refreshLabel.textContent = refreshBusy ? t2.refreshingLabel : t2.refreshLabel;
14548
+ }
14549
+ function setRefreshBusy(busy) {
14550
+ refreshBusy = busy;
14551
+ syncRefreshButton();
14552
+ }
14265
14553
  function render(table2, columns, indexes, extra) {
14266
14554
  lastArgs = { table: table2, columns, indexes, extra };
14267
14555
  const t2 = text2().schema;
@@ -14269,7 +14557,29 @@ ${frontmatter.yaml}
14269
14557
  el.innerHTML = "";
14270
14558
  const header = document.createElement("div");
14271
14559
  header.className = "db-schema-header";
14272
- header.textContent = `Schema: ${table2}`;
14560
+ const headerTitle = document.createElement("span");
14561
+ headerTitle.className = "db-schema-header-title";
14562
+ headerTitle.textContent = `Schema: ${table2}`;
14563
+ header.appendChild(headerTitle);
14564
+ if (deps.onRefresh) {
14565
+ refreshBtn = document.createElement("button");
14566
+ refreshBtn.type = "button";
14567
+ refreshBtn.className = "db-btn db-btn-icon db-grid-refresh db-schema-refresh";
14568
+ refreshBtn.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
14569
+ refreshLabel = document.createElement("span");
14570
+ refreshLabel.className = "db-grid-refresh-label";
14571
+ refreshBtn.appendChild(refreshLabel);
14572
+ refreshBtn.addEventListener("click", () => {
14573
+ if (refreshBusy)
14574
+ return;
14575
+ deps.onRefresh?.();
14576
+ });
14577
+ header.appendChild(refreshBtn);
14578
+ syncRefreshButton();
14579
+ } else {
14580
+ refreshBtn = null;
14581
+ refreshLabel = null;
14582
+ }
14273
14583
  el.appendChild(header);
14274
14584
  const colSectionHeader = document.createElement("div");
14275
14585
  colSectionHeader.className = "db-schema-section-header";
@@ -14432,13 +14742,16 @@ ${frontmatter.yaml}
14432
14742
  el.hidden = true;
14433
14743
  el.innerHTML = "";
14434
14744
  lastArgs = null;
14745
+ refreshBtn = null;
14746
+ refreshLabel = null;
14747
+ refreshBusy = false;
14435
14748
  }
14436
14749
  function localize() {
14437
14750
  if (!lastArgs || el.hidden)
14438
14751
  return;
14439
14752
  render(lastArgs.table, lastArgs.columns, lastArgs.indexes, lastArgs.extra);
14440
14753
  }
14441
- return { el, render, clear, localize };
14754
+ return { el, render, clear, localize, setRefreshBusy };
14442
14755
  }
14443
14756
 
14444
14757
  // web-src/views/database/session-log.ts
@@ -15866,7 +16179,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15866
16179
  filterBar.className = "db-grid-filter-bar";
15867
16180
  const filterIcon = document.createElement("span");
15868
16181
  filterIcon.className = "db-grid-filter-icon";
15869
- filterIcon.textContent = "\uD83D\uDD0D";
16182
+ filterIcon.setAttribute("aria-hidden", "true");
16183
+ filterIcon.innerHTML = iconSvg("octicon-search", SEARCH_16_PATH);
15870
16184
  const filterInput = document.createElement("input");
15871
16185
  filterInput.type = "search";
15872
16186
  filterInput.className = "db-grid-filter-input";
@@ -15874,9 +16188,24 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15874
16188
  filterInput.autocomplete = "off";
15875
16189
  const filterClear = document.createElement("button");
15876
16190
  filterClear.type = "button";
15877
- filterClear.className = "db-btn db-btn-icon db-grid-filter-clear";
15878
- filterClear.textContent = "×";
16191
+ filterClear.className = "db-btn db-btn-sm db-grid-filter-clear";
16192
+ filterClear.textContent = text2().grid.clearFiltersLabel;
15879
16193
  filterClear.hidden = true;
16194
+ const refreshBtn = document.createElement("button");
16195
+ refreshBtn.type = "button";
16196
+ refreshBtn.className = "db-btn db-btn-icon db-grid-refresh";
16197
+ refreshBtn.title = text2().grid.refreshAction;
16198
+ refreshBtn.setAttribute("aria-label", text2().grid.refreshAction);
16199
+ refreshBtn.setAttribute("aria-busy", "false");
16200
+ refreshBtn.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
16201
+ const refreshLabel = document.createElement("span");
16202
+ refreshLabel.className = "db-grid-refresh-label";
16203
+ refreshLabel.textContent = text2().grid.refreshLabel;
16204
+ refreshBtn.appendChild(refreshLabel);
16205
+ const refreshResult = document.createElement("span");
16206
+ refreshResult.className = "db-refresh-result db-grid-refresh-result";
16207
+ refreshResult.setAttribute("aria-live", "polite");
16208
+ refreshResult.hidden = true;
15880
16209
  const exportWrap = document.createElement("div");
15881
16210
  exportWrap.className = "db-grid-export";
15882
16211
  const exportBtn = document.createElement("button");
@@ -15884,7 +16213,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15884
16213
  exportBtn.className = "db-btn db-btn-icon db-grid-export-toggle";
15885
16214
  exportBtn.title = text2().grid.exportAction;
15886
16215
  exportBtn.setAttribute("aria-label", text2().grid.exportAction);
15887
- exportBtn.textContent = "";
16216
+ exportBtn.innerHTML = iconSvg("octicon-download", DOWNLOAD_16_PATHS);
15888
16217
  const exportMenu = document.createElement("div");
15889
16218
  exportMenu.className = "db-grid-export-menu";
15890
16219
  exportMenu.hidden = true;
@@ -15944,7 +16273,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15944
16273
  const editStatus = document.createElement("span");
15945
16274
  editStatus.className = "db-grid-edit-status";
15946
16275
  editWrap.append(newRowBtn, commitBtn, discardBtn, editStatus);
15947
- filterBar.append(filterIcon, filterInput, filterClear, editWrap, exportWrap);
16276
+ const filterActions = document.createElement("div");
16277
+ filterActions.className = "db-grid-filter-actions";
16278
+ filterActions.append(refreshBtn, refreshResult, exportWrap);
16279
+ filterBar.append(filterIcon, filterInput, filterClear, editWrap, filterActions);
15948
16280
  const headerWrap = document.createElement("div");
15949
16281
  headerWrap.className = "db-grid-header-wrap";
15950
16282
  const headerRow = document.createElement("div");
@@ -15961,6 +16293,31 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15961
16293
  spacer.className = "db-grid-spacer";
15962
16294
  const body = document.createElement("div");
15963
16295
  body.className = "db-grid-body";
16296
+ const filteredEmpty = document.createElement("div");
16297
+ filteredEmpty.className = "db-pane-empty";
16298
+ filteredEmpty.hidden = true;
16299
+ const filteredEmptyIcon = document.createElement("div");
16300
+ filteredEmptyIcon.className = "db-pane-empty-icon";
16301
+ filteredEmptyIcon.innerHTML = iconSvg("octicon-search", SEARCH_16_PATH);
16302
+ const filteredEmptyTitle = document.createElement("div");
16303
+ filteredEmptyTitle.className = "db-pane-empty-title";
16304
+ const filteredEmptyHint = document.createElement("div");
16305
+ filteredEmptyHint.className = "db-pane-empty-hint";
16306
+ const filteredEmptyActions = document.createElement("div");
16307
+ filteredEmptyActions.className = "db-pane-empty-actions";
16308
+ filteredEmptyActions.hidden = true;
16309
+ const filteredEmptyReloadAction = document.createElement("button");
16310
+ filteredEmptyReloadAction.type = "button";
16311
+ filteredEmptyReloadAction.className = "db-btn db-btn-primary db-btn-sm";
16312
+ filteredEmptyReloadAction.hidden = true;
16313
+ filteredEmptyReloadAction.disabled = true;
16314
+ const filteredEmptyAction = document.createElement("button");
16315
+ filteredEmptyAction.type = "button";
16316
+ filteredEmptyAction.className = "db-btn db-btn-sm";
16317
+ filteredEmptyAction.hidden = true;
16318
+ filteredEmptyAction.disabled = true;
16319
+ filteredEmptyActions.append(filteredEmptyReloadAction, filteredEmptyAction);
16320
+ filteredEmpty.append(filteredEmptyIcon, filteredEmptyTitle, filteredEmptyHint, filteredEmptyActions);
15964
16321
  const detailPanel = document.createElement("div");
15965
16322
  detailPanel.className = "db-grid-detail-panel";
15966
16323
  detailPanel.hidden = true;
@@ -15968,7 +16325,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
15968
16325
  detailResize.className = "db-grid-detail-resize";
15969
16326
  detailResize.addEventListener("mousedown", startDetailResize);
15970
16327
  detailPanel.appendChild(detailResize);
15971
- viewport.append(spacer, body);
16328
+ viewport.append(spacer, body, filteredEmpty);
15972
16329
  el.append(filterBar, headerWrap, filterRowWrap, viewport, detailPanel);
15973
16330
  function panelMaxHeight() {
15974
16331
  const containerH = el.clientHeight || window.innerHeight;
@@ -16046,6 +16403,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16046
16403
  let loadController = new AbortController;
16047
16404
  let rafId = 0;
16048
16405
  let statusEl = null;
16406
+ let isRefreshing = false;
16407
+ let refreshResultState = null;
16049
16408
  let filterTimer = null;
16050
16409
  let selectedRowIndex = -1;
16051
16410
  let activeCellRowIndex = -1;
@@ -16140,19 +16499,109 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16140
16499
  syncContentWidth();
16141
16500
  renderViewport();
16142
16501
  }
16143
- function collectFilters() {
16502
+ function collectColumnFilters() {
16144
16503
  const filters = [];
16145
16504
  for (const [col, val] of columnFilters) {
16146
16505
  if (val)
16147
16506
  filters.push({ column: col, value: val });
16148
16507
  }
16149
- if (globalSearchValue && filters.length === 0) {
16508
+ return filters;
16509
+ }
16510
+ function collectFilters() {
16511
+ const filters = collectColumnFilters();
16512
+ if (globalSearchValue) {
16150
16513
  for (const col of columnNames) {
16151
- filters.push({ column: col, value: globalSearchValue });
16514
+ if (!filters.some((filter) => filter.column === col && filter.value === globalSearchValue)) {
16515
+ filters.push({ column: col, value: globalSearchValue });
16516
+ }
16152
16517
  }
16153
16518
  }
16154
16519
  return filters;
16155
16520
  }
16521
+ function activeFilterCount() {
16522
+ return columnFilters.size + (globalSearchValue ? 1 : 0);
16523
+ }
16524
+ function syncRefreshButton() {
16525
+ const count = activeFilterCount();
16526
+ const t2 = text2().grid;
16527
+ refreshLabel.textContent = isRefreshing ? t2.refreshingLabel : count > 0 ? t2.refreshFilteredLabel : t2.refreshLabel;
16528
+ const action = count > 0 ? t2.refreshActionWithFilters(count) : t2.refreshAction;
16529
+ refreshBtn.classList.toggle("has-filters", count > 0);
16530
+ refreshBtn.title = action;
16531
+ refreshBtn.setAttribute("aria-label", action);
16532
+ refreshBtn.setAttribute("aria-busy", isRefreshing ? "true" : "false");
16533
+ }
16534
+ function syncRefreshResult() {
16535
+ const result = refreshResultState;
16536
+ refreshResult.hidden = !result;
16537
+ refreshResult.classList.toggle("changed", !!result && result.delta !== 0);
16538
+ if (!result) {
16539
+ refreshResult.textContent = "";
16540
+ return;
16541
+ }
16542
+ const t2 = text2().grid;
16543
+ const total = result.totalRows.toLocaleString();
16544
+ refreshResult.textContent = result.delta === 0 ? t2.refreshResultUnchanged(total) : t2.refreshResultChanged(result.delta, total);
16545
+ }
16546
+ function clearRefreshResult() {
16547
+ refreshResultState = null;
16548
+ syncRefreshResult();
16549
+ }
16550
+ function setRefreshResult(previousTotalRows, nextTotalRows) {
16551
+ refreshResultState = {
16552
+ delta: nextTotalRows - previousTotalRows,
16553
+ totalRows: nextTotalRows
16554
+ };
16555
+ syncRefreshResult();
16556
+ }
16557
+ function syncFilterClearButton() {
16558
+ const count = activeFilterCount();
16559
+ const t2 = text2().grid;
16560
+ filterClear.hidden = count === 0;
16561
+ filterClear.textContent = t2.clearFiltersLabel;
16562
+ const action = t2.clearFiltersAction(count);
16563
+ filterClear.title = action;
16564
+ filterClear.setAttribute("aria-label", action);
16565
+ }
16566
+ function syncFilteredEmptyState() {
16567
+ const count = activeFilterCount();
16568
+ const show = totalRows === 0 && count > 0;
16569
+ filteredEmpty.hidden = !show;
16570
+ filteredEmptyActions.hidden = !show;
16571
+ filteredEmptyReloadAction.hidden = !show;
16572
+ filteredEmptyReloadAction.disabled = !show || isRefreshing;
16573
+ filteredEmptyAction.hidden = !show;
16574
+ filteredEmptyAction.disabled = !show;
16575
+ if (!show)
16576
+ return;
16577
+ const t2 = text2().grid;
16578
+ const reloadAction = t2.refreshActionWithFilters(count);
16579
+ filteredEmptyTitle.textContent = t2.filteredEmptyTitle(count);
16580
+ filteredEmptyHint.textContent = t2.filteredEmptyHint;
16581
+ filteredEmptyReloadAction.textContent = isRefreshing ? t2.refreshingLabel : t2.refreshFilteredLabel;
16582
+ filteredEmptyReloadAction.title = reloadAction;
16583
+ filteredEmptyReloadAction.setAttribute("aria-label", reloadAction);
16584
+ filteredEmptyReloadAction.setAttribute("aria-busy", isRefreshing ? "true" : "false");
16585
+ filteredEmptyAction.textContent = t2.filteredEmptyAction;
16586
+ filteredEmptyAction.title = t2.clearFiltersAction(count);
16587
+ filteredEmptyAction.setAttribute("aria-label", t2.clearFiltersAction(count));
16588
+ }
16589
+ function clearAllFilters() {
16590
+ globalSearchValue = "";
16591
+ filterInput.value = "";
16592
+ columnFilters.clear();
16593
+ clearRefreshResult();
16594
+ filterRow.querySelectorAll(".db-grid-col-filter").forEach((input) => {
16595
+ input.value = "";
16596
+ });
16597
+ syncFilterClearButton();
16598
+ syncRefreshButton();
16599
+ syncFilteredEmptyState();
16600
+ }
16601
+ function clearFiltersAndReload() {
16602
+ clearAllFilters();
16603
+ invalidateData();
16604
+ }
16156
16605
  function resetSelectionAndDetail() {
16157
16606
  selectedRowIndex = -1;
16158
16607
  detailPanel.hidden = true;
@@ -16169,11 +16618,60 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16169
16618
  function invalidateData() {
16170
16619
  pageCache = new Map;
16171
16620
  pendingPages = new Map;
16621
+ clearRefreshResult();
16172
16622
  startNewLoadGeneration();
16173
16623
  viewport.scrollTop = 0;
16174
16624
  resetSelectionAndDetail();
16175
16625
  return ensurePage(0);
16176
16626
  }
16627
+ async function refreshCurrentTable() {
16628
+ if (!currentTable || refreshBtn.disabled)
16629
+ return;
16630
+ const refreshTable = currentTable;
16631
+ const previousTotalRows = totalRows;
16632
+ const refreshFilterKey = JSON.stringify(collectFilters());
16633
+ const scrollTop = viewport.scrollTop;
16634
+ const pageStart = Math.floor(scrollTop / ROW_HEIGHT / PAGE_SIZE) * PAGE_SIZE;
16635
+ refreshBtn.disabled = true;
16636
+ refreshBtn.classList.add("spinning");
16637
+ isRefreshing = true;
16638
+ clearRefreshResult();
16639
+ syncRefreshButton();
16640
+ syncFilteredEmptyState();
16641
+ updateStatus();
16642
+ if (filterTimer) {
16643
+ clearTimeout(filterTimer);
16644
+ filterTimer = null;
16645
+ }
16646
+ pageCache = new Map;
16647
+ pendingPages = new Map;
16648
+ startNewLoadGeneration();
16649
+ const refreshGeneration = loadGeneration;
16650
+ resetSelectionAndDetail();
16651
+ viewport.scrollTop = scrollTop;
16652
+ renderViewport();
16653
+ try {
16654
+ await ensurePage(pageStart);
16655
+ } finally {
16656
+ const endedInError = statusEl?.classList.contains("db-pane-error");
16657
+ isRefreshing = false;
16658
+ refreshBtn.classList.remove("spinning");
16659
+ refreshBtn.disabled = false;
16660
+ syncRefreshButton();
16661
+ syncFilteredEmptyState();
16662
+ if (!endedInError) {
16663
+ if (loadGeneration === refreshGeneration && JSON.stringify(collectFilters()) === refreshFilterKey) {
16664
+ setRefreshResult(previousTotalRows, totalRows);
16665
+ callbacks.onRefreshComplete?.({
16666
+ table: refreshTable,
16667
+ totalRows,
16668
+ filters: collectFilters()
16669
+ });
16670
+ }
16671
+ updateStatus();
16672
+ }
16673
+ }
16674
+ }
16177
16675
  function clear() {
16178
16676
  cleanupResize();
16179
16677
  clearActiveCell();
@@ -16182,10 +16680,13 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16182
16680
  columnNames = [];
16183
16681
  totalRows = 0;
16184
16682
  sort = null;
16683
+ isRefreshing = false;
16185
16684
  columnFilters.clear();
16186
16685
  globalSearchValue = "";
16686
+ clearRefreshResult();
16187
16687
  filterInput.value = "";
16188
- filterClear.hidden = true;
16688
+ syncFilterClearButton();
16689
+ syncRefreshButton();
16189
16690
  filterRow.innerHTML = "";
16190
16691
  pageCache = new Map;
16191
16692
  pendingPages = new Map;
@@ -16278,7 +16779,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16278
16779
  header.className = "db-related-header";
16279
16780
  const title = document.createElement("span");
16280
16781
  title.className = "db-related-title";
16281
- title.textContent = "\uD83D\uDD17";
16782
+ title.setAttribute("aria-hidden", "true");
16783
+ title.innerHTML = iconSvg("octicon-link", LINK_16_PATH);
16282
16784
  relatedCrumbEl = document.createElement("span");
16283
16785
  relatedCrumbEl.className = "db-related-crumbs";
16284
16786
  const copyBtn = document.createElement("button");
@@ -16582,7 +17084,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16582
17084
  cell.classList.add("db-grid-header-fk");
16583
17085
  const fkIcon = document.createElement("span");
16584
17086
  fkIcon.className = "db-grid-header-fk-icon";
16585
- fkIcon.textContent = "\uD83D\uDD17";
17087
+ fkIcon.innerHTML = iconSvg("octicon-link", LINK_16_PATH);
16586
17088
  fkIcon.title = text2().grid.foreignKeyHint;
16587
17089
  label.appendChild(fkIcon);
16588
17090
  }
@@ -16668,6 +17170,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16668
17170
  } else {
16669
17171
  columnFilters.delete(col.name);
16670
17172
  }
17173
+ clearRefreshResult();
17174
+ syncFilterClearButton();
17175
+ syncRefreshButton();
16671
17176
  scheduleFilter();
16672
17177
  });
16673
17178
  input.addEventListener("keydown", (e2) => {
@@ -16676,6 +17181,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16676
17181
  if (e2.key === "Escape") {
16677
17182
  input.value = "";
16678
17183
  columnFilters.delete(col.name);
17184
+ clearRefreshResult();
17185
+ syncFilterClearButton();
17186
+ syncRefreshButton();
16679
17187
  scheduleFilter();
16680
17188
  }
16681
17189
  });
@@ -16687,6 +17195,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
16687
17195
  if (filterTimer)
16688
17196
  clearTimeout(filterTimer);
16689
17197
  filterTimer = setTimeout(() => {
17198
+ filterTimer = null;
16690
17199
  invalidateData();
16691
17200
  }, FILTER_DEBOUNCE_MS);
16692
17201
  }
@@ -17065,6 +17574,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17065
17574
  for (let i2 = startRow;i2 < endRow; i2++) {
17066
17575
  body.appendChild(i2 < totalRows ? buildDataRow(i2) : buildDraftRow(i2 - totalRows));
17067
17576
  }
17577
+ syncFilteredEmptyState();
17068
17578
  if (focusRestore) {
17069
17579
  const next = body.querySelector(`.db-grid-cell-input[data-edit-row="${focusRestore.row}"][data-edit-col="${focusRestore.col}"]`);
17070
17580
  if (next) {
@@ -17114,15 +17624,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17114
17624
  const parts = [t2.statusRows(totalRows.toLocaleString())];
17115
17625
  if (sort)
17116
17626
  parts.push(t2.statusSort(sort.column, sort.direction.toUpperCase()));
17117
- const activeFilterCount = columnFilters.size + (globalSearchValue ? 1 : 0);
17118
- if (activeFilterCount > 0)
17119
- parts.push(t2.statusFilters(activeFilterCount));
17627
+ const filterCount = activeFilterCount();
17628
+ if (filterCount > 0)
17629
+ parts.push(t2.statusFilters(filterCount));
17630
+ if (isRefreshing)
17631
+ parts.push(t2.statusRefreshing(filterCount));
17120
17632
  const textNode = statusEl.firstChild;
17121
17633
  if (textNode && textNode.nodeType === Node.TEXT_NODE) {
17122
17634
  textNode.textContent = `${parts.join(" | ")} `;
17123
17635
  } else {
17124
17636
  statusEl.insertBefore(document.createTextNode(`${parts.join(" | ")} `), statusEl.firstChild);
17125
17637
  }
17638
+ syncRefreshResult();
17126
17639
  }
17127
17640
  function showError(message) {
17128
17641
  clear();
@@ -17161,13 +17674,14 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17161
17674
  if (state.search !== undefined) {
17162
17675
  globalSearchValue = state.search;
17163
17676
  filterInput.value = state.search;
17164
- filterClear.hidden = !globalSearchValue;
17165
17677
  }
17166
17678
  columnFilters.clear();
17167
17679
  for (const filter of state.filters || []) {
17168
17680
  if (filter.column && filter.value)
17169
17681
  columnFilters.set(filter.column, filter.value);
17170
17682
  }
17683
+ syncFilterClearButton();
17684
+ syncRefreshButton();
17171
17685
  sort = state.sort || null;
17172
17686
  const targetRowIndex = state.row && state.row > 0 ? state.row - 1 : -1;
17173
17687
  renderHeader();
@@ -17180,7 +17694,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17180
17694
  }
17181
17695
  }
17182
17696
  function getState() {
17183
- const filters = collectFilters().filter((filter) => filter.value !== globalSearchValue);
17697
+ const filters = collectColumnFilters();
17184
17698
  return {
17185
17699
  ...globalSearchValue ? { search: globalSearchValue } : {},
17186
17700
  ...filters.length ? { filters } : {},
@@ -17190,7 +17704,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17190
17704
  }
17191
17705
  filterInput.addEventListener("input", () => {
17192
17706
  globalSearchValue = filterInput.value.trim();
17193
- filterClear.hidden = !globalSearchValue;
17707
+ clearRefreshResult();
17708
+ syncFilterClearButton();
17709
+ syncRefreshButton();
17194
17710
  scheduleFilter();
17195
17711
  });
17196
17712
  filterInput.addEventListener("keydown", (e2) => {
@@ -17199,15 +17715,24 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17199
17715
  if (e2.key === "Escape") {
17200
17716
  filterInput.value = "";
17201
17717
  globalSearchValue = "";
17202
- filterClear.hidden = true;
17718
+ clearRefreshResult();
17719
+ syncFilterClearButton();
17720
+ syncRefreshButton();
17203
17721
  scheduleFilter();
17204
17722
  }
17205
17723
  });
17206
17724
  filterClear.addEventListener("click", () => {
17207
- filterInput.value = "";
17208
- globalSearchValue = "";
17209
- filterClear.hidden = true;
17210
- invalidateData();
17725
+ clearFiltersAndReload();
17726
+ });
17727
+ filteredEmptyAction.addEventListener("click", () => {
17728
+ clearFiltersAndReload();
17729
+ filterInput.focus?.();
17730
+ });
17731
+ filteredEmptyReloadAction.addEventListener("click", () => {
17732
+ refreshCurrentTable();
17733
+ });
17734
+ refreshBtn.addEventListener("click", () => {
17735
+ refreshCurrentTable();
17211
17736
  });
17212
17737
  const onViewportScroll = () => {
17213
17738
  headerWrap.scrollLeft = viewport.scrollLeft;
@@ -17236,6 +17761,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17236
17761
  function localize() {
17237
17762
  const t2 = text2();
17238
17763
  filterInput.placeholder = t2.grid.searchPlaceholder;
17764
+ syncFilterClearButton();
17765
+ syncRefreshButton();
17766
+ syncRefreshResult();
17767
+ syncFilteredEmptyState();
17239
17768
  exportBtn.title = t2.grid.exportAction;
17240
17769
  exportBtn.setAttribute("aria-label", t2.grid.exportAction);
17241
17770
  newRowBtn.textContent = t2.edit.newRow;
@@ -17517,6 +18046,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17517
18046
  return {
17518
18047
  el,
17519
18048
  load,
18049
+ refresh: refreshCurrentTable,
17520
18050
  showError,
17521
18051
  applyState,
17522
18052
  getState,
@@ -17575,7 +18105,14 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17575
18105
  filterInput.className = "db-table-filter";
17576
18106
  filterInput.type = "text";
17577
18107
  filterInput.placeholder = "Filter tables...";
17578
- filterWrap.appendChild(filterInput);
18108
+ const filterClear = document.createElement("button");
18109
+ filterClear.className = "db-table-filter-clear";
18110
+ filterClear.type = "button";
18111
+ filterClear.title = "Clear table filter";
18112
+ filterClear.setAttribute("aria-label", "Clear table filter");
18113
+ filterClear.innerHTML = iconSvg("octicon-x", X_16_PATH);
18114
+ filterClear.hidden = true;
18115
+ filterWrap.append(filterInput, filterClear);
17579
18116
  const el = document.createElement("div");
17580
18117
  el.className = "db-table-list";
17581
18118
  wrapper.append(filterWrap, el);
@@ -17735,13 +18272,37 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17735
18272
  }
17736
18273
  }
17737
18274
  }
18275
+ function syncFilterClearButton() {
18276
+ filterClear.hidden = filterInput.value.length === 0;
18277
+ }
18278
+ function clearFilter() {
18279
+ if (!filterInput.value)
18280
+ return;
18281
+ filterInput.value = "";
18282
+ renderFiltered(allTables, "");
18283
+ filterInput.focus();
18284
+ }
17738
18285
  function renderFiltered(tables, filter) {
17739
18286
  el.innerHTML = "";
18287
+ syncFilterClearButton();
17740
18288
  const filtered = filter ? tables.filter((t2) => t2.name.toLowerCase().includes(filter.toLowerCase())) : tables;
17741
18289
  if (filtered.length === 0) {
17742
18290
  const empty = document.createElement("div");
17743
18291
  empty.className = "db-table-list-empty";
17744
18292
  empty.textContent = filter ? "No matching tables" : "No tables found";
18293
+ if (filter) {
18294
+ const actions = document.createElement("div");
18295
+ actions.className = "db-pane-empty-actions db-table-list-empty-actions";
18296
+ const clear = document.createElement("button");
18297
+ clear.type = "button";
18298
+ clear.className = "db-btn db-btn-sm";
18299
+ clear.textContent = "Clear filter";
18300
+ clear.title = "Clear table filter";
18301
+ clear.setAttribute("aria-label", "Clear table filter");
18302
+ clear.addEventListener("click", clearFilter);
18303
+ actions.appendChild(clear);
18304
+ empty.appendChild(actions);
18305
+ }
17745
18306
  el.appendChild(empty);
17746
18307
  return;
17747
18308
  }
@@ -17811,23 +18372,37 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17811
18372
  filterInput.value = "";
17812
18373
  renderFiltered(tables, "");
17813
18374
  }
17814
- filterInput.addEventListener("input", () => {
18375
+ function handleFilterInput() {
17815
18376
  renderFiltered(allTables, filterInput.value);
17816
- });
18377
+ }
18378
+ filterInput.addEventListener("input", handleFilterInput);
18379
+ filterClear.addEventListener("click", clearFilter);
17817
18380
  function setActive(table2) {
17818
18381
  activeTable = table2;
17819
18382
  el.querySelectorAll(".db-table-item").forEach((item) => {
17820
18383
  item.classList.toggle("active", item.dataset.table === table2);
17821
18384
  });
17822
18385
  }
18386
+ function updateRowCount(table2, rowCount) {
18387
+ allTables = allTables.map((entry) => entry.name === table2 ? { ...entry, rowCount } : entry);
18388
+ for (const node of el.querySelectorAll(".db-table-node")) {
18389
+ if (node.dataset.table !== table2)
18390
+ continue;
18391
+ const count = node.querySelector(".db-table-count");
18392
+ if (count)
18393
+ count.textContent = rowCount != null ? formatRowCount(rowCount) : "";
18394
+ }
18395
+ }
17823
18396
  function dispose() {
17824
18397
  closeContextMenu();
17825
18398
  allTables = [];
17826
18399
  expandedTables.clear();
17827
18400
  columnCache.clear();
17828
18401
  activeTable = null;
18402
+ filterInput.removeEventListener("input", handleFilterInput);
18403
+ filterClear.removeEventListener("click", clearFilter);
17829
18404
  }
17830
- return { el: wrapper, render, setActive, dispose };
18405
+ return { el: wrapper, render, setActive, updateRowCount, dispose };
17831
18406
  }
17832
18407
  function formatRowCount(n2) {
17833
18408
  if (n2 >= 1e6)
@@ -17981,11 +18556,14 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17981
18556
  return value;
17982
18557
  }
17983
18558
  let lastFiles = [];
18559
+ let noDatastoresAvailable = false;
17984
18560
  let currentSchema = initial.schema ?? null;
17985
18561
  let currentTable = initial.table ?? null;
17986
18562
  let loadGeneration = 0;
17987
18563
  const tableSelectGuard = createAbortGuard();
17988
18564
  let historyRefreshPending = null;
18565
+ let isRefreshingDatastores = false;
18566
+ let datastoreRefreshResult = null;
17989
18567
  const dbSelect = document.createElement("select");
17990
18568
  dbSelect.className = "db-file-select";
17991
18569
  reloc(() => {
@@ -17997,9 +18575,29 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
17997
18575
  reloc(() => {
17998
18576
  schemaSelect.title = paneText().nav.selectSchema;
17999
18577
  });
18578
+ const dbRefreshBtn = makeIconButton({
18579
+ label: paneText().nav.refreshDatastores,
18580
+ title: paneText().nav.refreshDatastoresTitle,
18581
+ pathD: SYNC_16_PATH,
18582
+ onClick: () => {
18583
+ refreshDatastoreList();
18584
+ }
18585
+ });
18586
+ dbRefreshBtn.classList.add("db-refresh-btn");
18587
+ const dbRefreshLabel = document.createElement("span");
18588
+ dbRefreshLabel.className = "db-refresh-label";
18589
+ dbRefreshBtn.appendChild(dbRefreshLabel);
18590
+ const dbRefreshResult = document.createElement("span");
18591
+ dbRefreshResult.className = "db-refresh-result";
18592
+ dbRefreshResult.hidden = true;
18593
+ dbRefreshResult.setAttribute("aria-live", "polite");
18594
+ syncDbRefreshButton();
18595
+ const dbSelectRow = document.createElement("div");
18596
+ dbSelectRow.className = "db-select-row";
18597
+ dbSelectRow.append(dbSelect, dbRefreshBtn, dbRefreshResult);
18000
18598
  const dbToolbar = document.createElement("div");
18001
18599
  dbToolbar.className = "db-toolbar";
18002
- dbToolbar.append(dbSelect, schemaSelect);
18600
+ dbToolbar.append(dbSelectRow, schemaSelect);
18003
18601
  const tabBar = document.createElement("div");
18004
18602
  tabBar.className = "db-tab-bar";
18005
18603
  const tabData = createInnerTab("Data", true);
@@ -18073,7 +18671,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18073
18671
  const searchBtn = makeIconButton({
18074
18672
  label: "Search",
18075
18673
  title: "Search across tables",
18076
- pathD: ICON_PATH_SEARCH,
18674
+ pathD: SEARCH_16_PATH,
18077
18675
  onClick: () => setActiveTab("search")
18078
18676
  });
18079
18677
  const snapshotBtn = makeIconButton({
@@ -18091,6 +18689,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18091
18689
  });
18092
18690
  reloc(() => {
18093
18691
  const t2 = paneText().nav;
18692
+ localizeIconButton(dbRefreshBtn, t2.refreshDatastores, t2.refreshDatastoresTitle);
18693
+ syncDbRefreshButton();
18094
18694
  localizeIconButton(queryBtn, t2.query, t2.queryTitle);
18095
18695
  localizeIconButton(erBtn, t2.er, t2.erTitle);
18096
18696
  localizeIconButton(searchBtn, t2.search, t2.searchTitle);
@@ -18145,7 +18745,13 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18145
18745
  },
18146
18746
  getText: () => paneText(),
18147
18747
  getEditable: () => isSqlKind(currentDbInfo?.kind),
18148
- applyMutations: (mutations) => applyRowMutations(mutations)
18748
+ applyMutations: (mutations) => applyRowMutations(mutations),
18749
+ onRefreshComplete: ({ table: table2, filters }) => {
18750
+ if (filters.length === 0) {
18751
+ return;
18752
+ }
18753
+ refreshTableRowCount(table2);
18754
+ }
18149
18755
  });
18150
18756
  const editModeToggle = makePrefToggle({
18151
18757
  title: paneText().edit.editModeTitle,
@@ -18177,7 +18783,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18177
18783
  });
18178
18784
  if (initial.sqlDraft)
18179
18785
  queryEditor.setSql(initial.sqlDraft, { silent: true });
18180
- const schemaView = createSchemaView({ getText: () => paneText() });
18786
+ const schemaView = createSchemaView({
18787
+ getText: () => paneText(),
18788
+ onRefresh: () => refreshCurrentSchemaView()
18789
+ });
18181
18790
  const erDiagram = createErDiagram({ getText: () => paneText() });
18182
18791
  const globalSearchView = createGlobalSearchView({
18183
18792
  getDbId: () => currentDbInfo?.id || null,
@@ -18268,9 +18877,30 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18268
18877
  s3Explorer.el.hidden = true;
18269
18878
  s3Explorer.sidebarSlot.hidden = true;
18270
18879
  explorerSidebarHost.append(redisExplorer.sidebarSlot, esExplorer.sidebarSlot, s3Explorer.sidebarSlot);
18880
+ const noDatastoresPane = document.createElement("div");
18881
+ noDatastoresPane.className = "db-no-datastores";
18882
+ noDatastoresPane.hidden = true;
18883
+ function renderNoDatastoresEmpty() {
18884
+ const t2 = paneText().nav;
18885
+ setPaneEmpty(noDatastoresPane, t2.noDatastores, {
18886
+ hint: t2.noDatastoresHint,
18887
+ iconPath: ICON_PATH_SNAPSHOT
18888
+ });
18889
+ const actionRow = document.createElement("div");
18890
+ actionRow.className = "db-no-datastores-actions";
18891
+ const refresh = document.createElement("button");
18892
+ refresh.type = "button";
18893
+ refresh.className = "db-btn db-btn-primary db-no-datastores-action";
18894
+ refresh.textContent = t2.refreshDatastores;
18895
+ refresh.addEventListener("click", () => {
18896
+ refreshDatastoreList();
18897
+ });
18898
+ actionRow.appendChild(refresh);
18899
+ noDatastoresPane.querySelector(".db-pane-empty")?.appendChild(actionRow);
18900
+ }
18271
18901
  const mainContent = document.createElement("div");
18272
18902
  mainContent.className = "db-main-content";
18273
- mainContent.append(tabBar, grid.el, queryEditor.el, schemaView.el, erDiagram.el, globalSearchView.el, snapshotView.el, redisExplorer.el, esExplorer.el, s3Explorer.el);
18903
+ mainContent.append(tabBar, grid.el, queryEditor.el, schemaView.el, erDiagram.el, globalSearchView.el, snapshotView.el, redisExplorer.el, esExplorer.el, s3Explorer.el, noDatastoresPane);
18274
18904
  queryEditor.el.hidden = true;
18275
18905
  globalSearchView.el.hidden = true;
18276
18906
  snapshotView.el.hidden = true;
@@ -18381,6 +19011,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18381
19011
  function applyVisibility() {
18382
19012
  const sqlMode = isSqlKind(currentDbInfo?.kind);
18383
19013
  const visibility = computeVisibility(currentDbInfo?.kind, currentTab, userPrefersHistoryOpen);
19014
+ noDatastoresPane.hidden = !noDatastoresAvailable;
18384
19015
  toolsSection.hidden = visibility.toolsHidden;
18385
19016
  prefsBar.hidden = visibility.toolsHidden;
18386
19017
  historyDock.hidden = visibility.historyTabStripHidden;
@@ -18401,6 +19032,28 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18401
19032
  esExplorer.sidebarSlot.hidden = visibility.esHidden;
18402
19033
  s3Explorer.sidebarSlot.hidden = visibility.s3Hidden;
18403
19034
  explorerSidebarHost.hidden = visibility.redisHidden && visibility.esHidden && visibility.s3Hidden;
19035
+ if (noDatastoresAvailable) {
19036
+ toolsSection.hidden = true;
19037
+ prefsBar.hidden = true;
19038
+ historyDock.hidden = true;
19039
+ historyResizer.hidden = true;
19040
+ historyPane.hidden = true;
19041
+ tableList.el.hidden = true;
19042
+ tabBar.hidden = true;
19043
+ grid.el.hidden = true;
19044
+ queryEditor.el.hidden = true;
19045
+ schemaView.el.hidden = true;
19046
+ erDiagram.el.hidden = true;
19047
+ globalSearchView.el.hidden = true;
19048
+ snapshotView.el.hidden = true;
19049
+ redisExplorer.el.hidden = true;
19050
+ esExplorer.el.hidden = true;
19051
+ s3Explorer.el.hidden = true;
19052
+ redisExplorer.sidebarSlot.hidden = true;
19053
+ esExplorer.sidebarSlot.hidden = true;
19054
+ s3Explorer.sidebarSlot.hidden = true;
19055
+ explorerSidebarHost.hidden = true;
19056
+ }
18404
19057
  if (!sqlMode) {
18405
19058
  queryBtn.classList.remove("active");
18406
19059
  erBtn.classList.remove("active");
@@ -18551,11 +19204,59 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18551
19204
  errorPrefix: "failed to fetch schema"
18552
19205
  });
18553
19206
  }
19207
+ function syncCachedTableRowCount(table2, rowCount) {
19208
+ if (!schemaCache)
19209
+ return;
19210
+ let changed = false;
19211
+ const tables = schemaCache.tables.map((entry) => {
19212
+ if (entry.name !== table2 || entry.rowCount === rowCount)
19213
+ return entry;
19214
+ changed = true;
19215
+ return { ...entry, rowCount };
19216
+ });
19217
+ if (!changed)
19218
+ return;
19219
+ schemaCache = { ...schemaCache, tables };
19220
+ tableList.updateRowCount(table2, rowCount);
19221
+ }
19222
+ async function fetchTableRowCount(table2, dbId, schema) {
19223
+ const params = new URLSearchParams({ db: dbId, table: table2 });
19224
+ if (schema)
19225
+ params.set("schema", schema);
19226
+ return logSqlFetch({
19227
+ url: `/_db/table-count?${params}`,
19228
+ kind: "query",
19229
+ label: `_db/table-count ${table2}`,
19230
+ errorPrefix: "failed to fetch table count",
19231
+ rowCountOf: (r2) => r2.rowCount ?? undefined
19232
+ });
19233
+ }
19234
+ async function refreshTableRowCount(table2) {
19235
+ const dbId = currentDbInfo?.id;
19236
+ if (!dbId)
19237
+ return;
19238
+ const schema = currentSchema;
19239
+ const generation = loadGeneration;
19240
+ try {
19241
+ const data = await fetchTableRowCount(table2, dbId, schema);
19242
+ if (generation !== loadGeneration || currentDbInfo?.id !== dbId || currentSchema !== schema || currentTable !== table2) {
19243
+ return;
19244
+ }
19245
+ syncCachedTableRowCount(table2, data.rowCount);
19246
+ } catch (err) {
19247
+ if (!isAbortError2(err)) {
19248
+ console.warn(`failed to refresh table count: ${errorMessage(err)}`);
19249
+ }
19250
+ }
19251
+ }
18554
19252
  async function fetchTablePage(table2, offset, limit, sort, filters, signal, eq = []) {
18555
19253
  if (!currentDbInfo)
18556
19254
  throw new Error("no database selected");
19255
+ const requestDbId = currentDbInfo.id;
19256
+ const requestSchema = currentSchema;
19257
+ const requestGeneration = loadGeneration;
18557
19258
  const params = new URLSearchParams({
18558
- db: currentDbInfo.id,
19259
+ db: requestDbId,
18559
19260
  table: table2,
18560
19261
  offset: String(offset),
18561
19262
  limit: String(limit)
@@ -18571,7 +19272,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18571
19272
  if (eq.length > 0) {
18572
19273
  params.set("eq", JSON.stringify(eq));
18573
19274
  }
18574
- return logSqlFetch({
19275
+ const data = await logSqlFetch({
18575
19276
  url: `/_db/table?${params}`,
18576
19277
  init: signal ? { signal } : undefined,
18577
19278
  kind: "query",
@@ -18580,6 +19281,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18580
19281
  errorPrefix: "failed to fetch table",
18581
19282
  rowCountOf: (r2) => r2.rows.length
18582
19283
  });
19284
+ if (filters.length === 0 && eq.length === 0 && currentDbInfo?.id === requestDbId && currentSchema === requestSchema && currentTable === table2 && requestGeneration === loadGeneration) {
19285
+ syncCachedTableRowCount(table2, data.totalRows);
19286
+ }
19287
+ return data;
18583
19288
  }
18584
19289
  function fetchRelatedPage(table2, offset, limit, sort, filters, eq, signal) {
18585
19290
  return fetchTablePage(table2, offset, limit, sort, filters, signal, eq);
@@ -18891,6 +19596,47 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18891
19596
  const columns = await fetchColumns(table2);
18892
19597
  schemaView.render(table2, columns, schemaCache?.indexes || []);
18893
19598
  }
19599
+ async function refreshCurrentSchemaView() {
19600
+ if (!currentDbInfo || !currentTable)
19601
+ return;
19602
+ const generation = loadGeneration;
19603
+ const dbId = currentDbInfo.id;
19604
+ const table2 = currentTable;
19605
+ schemaView.setRefreshBusy(true);
19606
+ try {
19607
+ const schema = await fetchSchema(dbId);
19608
+ if (generation !== loadGeneration || currentDbInfo?.id !== dbId || currentTable !== table2) {
19609
+ return;
19610
+ }
19611
+ currentSchema = schema.schema || currentSchema;
19612
+ schemaCache = schema;
19613
+ tableList.render(schema.tables);
19614
+ const nextTable = schema.tables.some((entry) => entry.name === table2) ? table2 : schema.tables[0]?.name;
19615
+ if (!nextTable) {
19616
+ currentTable = null;
19617
+ grid.clear();
19618
+ schemaView.clear();
19619
+ erDiagram.clear();
19620
+ cb.onStateChange();
19621
+ return;
19622
+ }
19623
+ currentTable = nextTable;
19624
+ tableList.setActive(nextTable);
19625
+ const columns = await fetchColumns(nextTable);
19626
+ if (generation !== loadGeneration || currentDbInfo?.id !== dbId || currentTable !== nextTable) {
19627
+ return;
19628
+ }
19629
+ schemaView.render(nextTable, columns, schema.indexes || []);
19630
+ cb.onStateChange();
19631
+ } catch (err) {
19632
+ if (generation !== loadGeneration || currentDbInfo?.id !== dbId || isAbortError2(err)) {
19633
+ return;
19634
+ }
19635
+ setTableListStatus(errorMessage(err), { error: true });
19636
+ } finally {
19637
+ schemaView.setRefreshBusy(false);
19638
+ }
19639
+ }
18894
19640
  async function showDdl(table2) {
18895
19641
  if (!currentDbInfo)
18896
19642
  return;
@@ -18935,6 +19681,54 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18935
19681
  schemaSelect.addEventListener("change", () => {
18936
19682
  handleSchemaSelectChange();
18937
19683
  });
19684
+ function syncDbRefreshButton() {
19685
+ const text2 = paneText().nav;
19686
+ dbRefreshLabel.textContent = isRefreshingDatastores ? text2.refreshDatastoresBusy : text2.refreshDatastoresShort;
19687
+ dbRefreshBtn.setAttribute("aria-busy", isRefreshingDatastores ? "true" : "false");
19688
+ syncDatastoreRefreshResult();
19689
+ }
19690
+ function syncDatastoreRefreshResult() {
19691
+ const result = datastoreRefreshResult;
19692
+ dbRefreshResult.hidden = !result;
19693
+ dbRefreshResult.classList.toggle("changed", !!result && (result.added > 0 || result.removed > 0));
19694
+ if (!result)
19695
+ return;
19696
+ const text2 = paneText().nav;
19697
+ dbRefreshResult.textContent = result.added > 0 || result.removed > 0 ? text2.refreshDatastoresChanged(result.added, result.removed) : text2.refreshDatastoresUnchanged;
19698
+ }
19699
+ function diffDatastoreFiles(before, after) {
19700
+ const beforeIds = new Set(before.map((file) => file.id));
19701
+ const afterIds = new Set(after.map((file) => file.id));
19702
+ let added = 0;
19703
+ let removed = 0;
19704
+ for (const id of afterIds) {
19705
+ if (!beforeIds.has(id))
19706
+ added++;
19707
+ }
19708
+ for (const id of beforeIds) {
19709
+ if (!afterIds.has(id))
19710
+ removed++;
19711
+ }
19712
+ return { added, removed };
19713
+ }
19714
+ async function refreshDatastoreList() {
19715
+ if (dbRefreshBtn.disabled)
19716
+ return;
19717
+ const beforeFiles = [...lastFiles];
19718
+ isRefreshingDatastores = true;
19719
+ dbRefreshBtn.disabled = true;
19720
+ dbRefreshBtn.classList.add("spinning");
19721
+ syncDbRefreshButton();
19722
+ try {
19723
+ await outerDeps.refreshDatastores();
19724
+ datastoreRefreshResult = cb.isActive() ? diffDatastoreFiles(beforeFiles, lastFiles) : null;
19725
+ } finally {
19726
+ isRefreshingDatastores = false;
19727
+ dbRefreshBtn.classList.remove("spinning");
19728
+ dbRefreshBtn.disabled = false;
19729
+ syncDbRefreshButton();
19730
+ }
19731
+ }
18938
19732
  async function handleSchemaSelectChange() {
18939
19733
  if (!currentDbInfo || !isPostgresKind(currentDbInfo.kind))
18940
19734
  return;
@@ -18963,6 +19757,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18963
19757
  const dbId = dbSelect.value;
18964
19758
  if (!dbId)
18965
19759
  return;
19760
+ datastoreRefreshResult = null;
19761
+ syncDatastoreRefreshResult();
18966
19762
  const generation = ++loadGeneration;
18967
19763
  const file = lastFiles.find((f2) => f2.id === dbId);
18968
19764
  const option = dbSelect.selectedOptions[0];
@@ -18998,6 +19794,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18998
19794
  return;
18999
19795
  const files = filesResponse.files;
19000
19796
  lastFiles = files;
19797
+ noDatastoresAvailable = files.length === 0;
19001
19798
  if (filesResponse.truncated) {
19002
19799
  showDockerNotice(paneText().nav.dockerLimitReached);
19003
19800
  } else {
@@ -19010,6 +19807,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19010
19807
  opt.textContent = paneText().nav.noDatastores;
19011
19808
  dbSelect.appendChild(opt);
19012
19809
  dbSelect.disabled = true;
19810
+ currentDbInfo = null;
19811
+ currentSchema = null;
19812
+ currentTable = null;
19813
+ schemaCache = null;
19814
+ renderSchemaOptions([], null);
19815
+ tableList.render([]);
19816
+ grid.clear();
19817
+ schemaView.clear();
19818
+ erDiagram.clear();
19819
+ redisExplorer.clear();
19820
+ esExplorer.clear();
19821
+ s3Explorer.clear();
19822
+ renderNoDatastoresEmpty();
19823
+ applyVisibility();
19013
19824
  cb.onStateChange();
19014
19825
  return;
19015
19826
  }
@@ -19204,6 +20015,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19204
20015
  return target;
19205
20016
  }
19206
20017
  function getLabel() {
20018
+ if (noDatastoresAvailable)
20019
+ return paneText().nav.noDatastoreTab;
19207
20020
  if (!currentDbInfo)
19208
20021
  return labelFromDbId(initial.dbId);
19209
20022
  const suffix = currentSchema ? ` / ${currentSchema}` : "";
@@ -19242,6 +20055,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19242
20055
  function localizePane() {
19243
20056
  for (const fn of paneLocalizers)
19244
20057
  fn();
20058
+ if (noDatastoresAvailable)
20059
+ renderNoDatastoresEmpty();
19245
20060
  grid.localize();
19246
20061
  schemaView.localize();
19247
20062
  erDiagram.localize();
@@ -19266,7 +20081,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19266
20081
  }
19267
20082
  var ICON_PATH_QUERY = "M5.72 4.22a.75.75 0 0 1 0 1.06L2.81 8l2.91 2.72a.75.75 0 1 1-1.06 1.06L1.22 8.53a.75.75 0 0 1 0-1.06l3.44-3.25a.75.75 0 0 1 1.06 0Zm4.56 0a.75.75 0 0 1 1.06 0l3.44 3.25a.75.75 0 0 1 0 1.06l-3.44 3.25a.75.75 0 1 1-1.06-1.06L13.19 8l-2.91-2.72a.75.75 0 0 1 0-1.06Z";
19268
20083
  var ICON_PATH_ER = "M1.5 1.75A.75.75 0 0 1 2.25 1h4.5a.75.75 0 0 1 .75.75v3.5h2v-1.5A.75.75 0 0 1 10.25 3h3.5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75V6.5h-2v3h2v-.75A.75.75 0 0 1 10.25 8h3.5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75v-1.5h-2v3.5a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-3.5A.75.75 0 0 1 2.25 9h4.5a.75.75 0 0 1 .75.75v.75h-.5v-1H3v3h3V11h.5v-.75a.75.75 0 0 0-.75-.75H3V6.5h2.5V2.5H3Z";
19269
- var ICON_PATH_SEARCH = "M10.68 11.74a6 6 0 0 1-7.917-8.984 6 6 0 0 1 8.997 7.905l3.05 3.05a.75.75 0 1 1-1.06 1.06l-3.07-3.03ZM11.5 7a4.499 4.499 0 1 1-8.997 0A4.499 4.499 0 0 1 11.5 7Z";
19270
20084
  var ICON_PATH_SNAPSHOT = "M3.5 1.75A1.75 1.75 0 0 1 5.25 0h5.5A1.75 1.75 0 0 1 12.5 1.75v.5h1.75A1.75 1.75 0 0 1 16 4v9.25A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25V4a1.75 1.75 0 0 1 1.75-1.75H3.5v-.5Zm1.5.5v.5h6v-.5a.25.25 0 0 0-.25-.25h-5.5a.25.25 0 0 0-.25.25Zm-3.25 2a.25.25 0 0 0-.25.25v9.25c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V4a.25.25 0 0 0-.25-.25H1.75ZM8 6a2.75 2.75 0 1 0 0 5.5 2.75 2.75 0 0 0 0-5.5Z";
19271
20085
  var ICON_PATH_EDIT_MODE = "M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.247.247 0 0 0-.064.108l-.558 1.953 1.953-.558a.249.249 0 0 0 .108-.064l6.286-6.286Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354L12.427 2.49Z";
19272
20086
  function makeIconButton(opts) {
@@ -19897,6 +20711,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19897
20711
  setDbUiPref,
19898
20712
  onDbUiPrefChange,
19899
20713
  loadSqlHistory,
20714
+ refreshDatastores,
19900
20715
  sessionLog
19901
20716
  }, {
19902
20717
  tabId: id,
@@ -20208,6 +21023,36 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20208
21023
  }).then(() => doEnter(seq, db, schema, table2, view, options));
20209
21024
  await enterQueue;
20210
21025
  }
21026
+ async function refreshDatastores() {
21027
+ const seq = lifecycleSeq;
21028
+ enterQueue = enterQueue.catch(() => {
21029
+ return;
21030
+ }).then(() => doRefreshDatastores(seq));
21031
+ await enterQueue;
21032
+ }
21033
+ async function doRefreshDatastores(seq) {
21034
+ if (seq !== lifecycleSeq)
21035
+ return;
21036
+ dbFilesCache = null;
21037
+ if (!mounted || !activeTabId)
21038
+ return;
21039
+ const id = activeTabId;
21040
+ const entry = tabsById.get(id);
21041
+ if (!entry)
21042
+ return;
21043
+ const pendingInitialEnter = ensureInitialEnter(id);
21044
+ if (pendingInitialEnter)
21045
+ await pendingInitialEnter;
21046
+ if (!mounted || activeTabId !== id)
21047
+ return;
21048
+ const state = entry.pane.getState();
21049
+ await entry.pane.enter(state.dbId ?? undefined, state.schema ?? undefined, state.table ?? undefined, state.view, { autoSelectFirst: state.dbId !== null });
21050
+ if (!mounted || activeTabId !== id)
21051
+ return;
21052
+ refreshChipLabel(id);
21053
+ syncActiveRoute();
21054
+ scheduleSave();
21055
+ }
20211
21056
  function leave() {
20212
21057
  if (!mounted)
20213
21058
  return;
@@ -20285,6 +21130,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20285
21130
  leave,
20286
21131
  handleSse,
20287
21132
  localize,
21133
+ refresh: refreshDatastores,
20288
21134
  getDbUiPref,
20289
21135
  setDbUiPref,
20290
21136
  onDbUiPrefChange
@@ -20483,6 +21329,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20483
21329
  getServerGeneration,
20484
21330
  setServerGeneration,
20485
21331
  invalidateRepoSidebar,
21332
+ diffText,
20486
21333
  getDiffRoot,
20487
21334
  getEmptyPane,
20488
21335
  isEmbeddedDiffMode
@@ -20543,7 +21390,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20543
21390
  if (!button)
20544
21391
  return;
20545
21392
  button.setAttribute("aria-pressed", expanded ? "true" : "false");
20546
- button.title = expanded ? "Collapse expanded lines" : "Expand all lines";
21393
+ const label = expanded ? "Collapse expanded lines" : "Expand all lines";
21394
+ button.title = label;
21395
+ button.setAttribute("aria-label", label);
20547
21396
  button.innerHTML = expanded ? iconSvg("octicon-fold", COLLAPSE_ALL_16_PATHS) : iconSvg("octicon-unfold", EXPAND_ALL_16_PATHS);
20548
21397
  }
20549
21398
  function setProjectBranch(branch) {
@@ -20554,25 +21403,131 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20554
21403
  el.textContent = branch;
20555
21404
  el.title = branch ? `Current branch: ${branch}` : "";
20556
21405
  }
21406
+ let metaFilesForViewedProgress = [];
21407
+ function viewedProgressFor(files) {
21408
+ const total = files.length;
21409
+ const viewed = files.filter((file) => STATE.viewedFiles.has(file.path)).length;
21410
+ return { viewed, total };
21411
+ }
21412
+ function applyViewedProgressChipState(chip, viewed, total) {
21413
+ chip.textContent = diffText().viewedProgress(viewed, total);
21414
+ chip.classList.toggle("chip-viewed-empty", viewed === 0);
21415
+ chip.classList.toggle("chip-viewed-partial", viewed > 0 && viewed < total);
21416
+ chip.classList.toggle("chip-viewed-done", total > 0 && viewed === total);
21417
+ }
21418
+ function syncViewedProgressChip() {
21419
+ const chip = document.querySelector("#meta .chip-viewed");
21420
+ if (chip) {
21421
+ const { viewed, total } = viewedProgressFor(metaFilesForViewedProgress);
21422
+ if (total <= 0)
21423
+ chip.remove();
21424
+ else
21425
+ applyViewedProgressChipState(chip, viewed, total);
21426
+ }
21427
+ syncNextUnviewedButton();
21428
+ }
21429
+ function applyNextUnviewedButtonState(button, hasUnviewed) {
21430
+ const text2 = diffText();
21431
+ button.disabled = !hasUnviewed;
21432
+ button.textContent = hasUnviewed ? text2.nextUnviewed : text2.allViewed;
21433
+ button.title = hasUnviewed ? text2.nextUnviewedTitle : text2.allViewedTitle;
21434
+ }
21435
+ function syncNextUnviewedButton() {
21436
+ const button = document.querySelector("#meta .chip-next-unviewed");
21437
+ if (!button)
21438
+ return;
21439
+ const { total } = viewedProgressFor(metaFilesForViewedProgress);
21440
+ if (total <= 0) {
21441
+ button.remove();
21442
+ return;
21443
+ }
21444
+ applyNextUnviewedButtonState(button, nextUnviewedFilePath() !== null);
21445
+ }
21446
+ function nextUnviewedFilePath() {
21447
+ if (isRepositorySidebarMode())
21448
+ return null;
21449
+ const items = $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
21450
+ if (items.length === 0)
21451
+ return null;
21452
+ const currentIndex = items.findIndex((li) => li.classList.contains("active"));
21453
+ for (let offset = 1;offset <= items.length; offset++) {
21454
+ const idx = (currentIndex + offset + items.length) % items.length;
21455
+ const li = items[idx];
21456
+ const path = li.dataset.path || "";
21457
+ const viewed = STATE.viewedFiles.has(path) || li.classList.contains("viewed");
21458
+ if (path && !viewed)
21459
+ return path;
21460
+ }
21461
+ return null;
21462
+ }
21463
+ function scrollToNextUnviewedFile() {
21464
+ const path = nextUnviewedFilePath();
21465
+ if (!path)
21466
+ return false;
21467
+ scrollToFile(path, undefined, { reveal: true });
21468
+ return true;
21469
+ }
20557
21470
  function renderMeta(meta) {
20558
21471
  const el = $("#meta");
20559
21472
  if (!meta) {
20560
21473
  el.textContent = "";
21474
+ metaFilesForViewedProgress = [];
20561
21475
  return;
20562
21476
  }
20563
21477
  setProjectName(meta.project || "");
20564
21478
  setProjectBranch(meta.branch || "");
21479
+ metaFilesForViewedProgress = meta.files || [];
20565
21480
  el.innerHTML = "";
21481
+ const text2 = diffText();
20566
21482
  if (meta.totals) {
20567
- const t2 = document.createElement("span");
20568
- t2.className = "num";
20569
- t2.innerHTML = '<span class="add">+' + meta.totals.additions + "</span> " + '<span class="del">−' + meta.totals.deletions + "</span> " + "<span>" + meta.totals.files + " files</span>";
20570
- el.appendChild(t2);
21483
+ const files = document.createElement("span");
21484
+ files.className = "chip chip-files";
21485
+ files.textContent = text2.files(meta.totals.files);
21486
+ el.appendChild(files);
21487
+ const add2 = document.createElement("span");
21488
+ add2.className = "chip chip-add";
21489
+ add2.textContent = `+${meta.totals.additions}`;
21490
+ el.appendChild(add2);
21491
+ const del = document.createElement("span");
21492
+ del.className = "chip chip-del";
21493
+ del.textContent = `−${meta.totals.deletions}`;
21494
+ el.appendChild(del);
21495
+ }
21496
+ const kinds = summarizeDiffFileKinds(meta.files);
21497
+ const kindChip = (className, label, count) => {
21498
+ if (count <= 0)
21499
+ return;
21500
+ const chip = document.createElement("span");
21501
+ chip.className = `chip ${className}`;
21502
+ chip.textContent = `${count} ${label}`;
21503
+ el.appendChild(chip);
21504
+ };
21505
+ kindChip("chip-added", text2.kindAdded, kinds.added);
21506
+ kindChip("chip-deleted", text2.kindDeleted, kinds.deleted);
21507
+ kindChip("chip-renamed", text2.kindRenamed, kinds.renamed);
21508
+ kindChip("chip-heavy", text2.kindHeavy, kinds.heavy);
21509
+ kindChip("chip-binary", text2.kindBinary, kinds.binary);
21510
+ kindChip("chip-media", text2.kindMedia, kinds.media);
21511
+ const viewedProgress = viewedProgressFor(metaFilesForViewedProgress);
21512
+ if (viewedProgress.total > 0) {
21513
+ const viewed = document.createElement("span");
21514
+ viewed.className = "chip chip-viewed";
21515
+ viewed.title = text2.viewedProgressTitle;
21516
+ applyViewedProgressChipState(viewed, viewedProgress.viewed, viewedProgress.total);
21517
+ el.appendChild(viewed);
21518
+ const nextUnviewed = document.createElement("button");
21519
+ nextUnviewed.type = "button";
21520
+ nextUnviewed.className = "chip chip-next-unviewed";
21521
+ applyNextUnviewedButtonState(nextUnviewed, viewedProgress.viewed < viewedProgress.total);
21522
+ nextUnviewed.addEventListener("click", () => {
21523
+ scrollToNextUnviewedFile();
21524
+ });
21525
+ el.appendChild(nextUnviewed);
20571
21526
  }
20572
21527
  const u2 = document.createElement("span");
20573
- u2.className = "updated-at";
20574
- u2.title = "last updated";
20575
- u2.textContent = `updated ${new Date().toLocaleTimeString([], { hour12: false })}`;
21528
+ u2.className = "chip chip-updated";
21529
+ u2.title = text2.updatedTitle;
21530
+ u2.textContent = text2.updated(new Date().toLocaleTimeString([], { hour12: false }));
20576
21531
  el.appendChild(u2);
20577
21532
  }
20578
21533
  let SUPPRESS_SPY_UNTIL = 0;
@@ -20629,13 +21584,13 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20629
21584
  window.addEventListener("touchmove", () => {
20630
21585
  REANCHOR_UNTIL = 0;
20631
21586
  }, { passive: true });
20632
- function scrollToFile(path, line) {
21587
+ function scrollToFile(path, line, options) {
20633
21588
  const card = document.querySelector(diffCardSelector(path));
20634
21589
  if (!card)
20635
21590
  return;
20636
21591
  if (line)
20637
21592
  REANCHOR_UNTIL = performance.now() + 4000;
20638
- markActive(path);
21593
+ markActive(path, { reveal: options?.reveal });
20639
21594
  SUPPRESS_SPY_UNTIL = performance.now() + 1500;
20640
21595
  const onEnd = () => {
20641
21596
  SUPPRESS_SPY_UNTIL = 0;
@@ -20663,6 +21618,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20663
21618
  const viewed = STATE.viewedFiles.has(path);
20664
21619
  syncViewedCardDisplay(card, viewed);
20665
21620
  });
21621
+ syncViewedProgressChip();
20666
21622
  }
20667
21623
  let CLIENT_REQ_SEQ = 0;
20668
21624
  const LOAD_QUEUE = [];
@@ -20876,6 +21832,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20876
21832
  setupScrollSpy();
20877
21833
  scrollSpyInstalled = true;
20878
21834
  }
21835
+ syncViewedProgressChip();
20879
21836
  return {
20880
21837
  structureChanged: false,
20881
21838
  invalidatedCards,
@@ -21213,7 +22170,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21213
22170
  const button = card.querySelector(".gdp-file-toggle");
21214
22171
  if (button) {
21215
22172
  button.setAttribute("aria-expanded", collapsed ? "false" : "true");
21216
- button.title = collapsed ? "Expand file" : "Collapse file";
22173
+ const toggleLabel = collapsed ? "Expand file" : "Collapse file";
22174
+ button.title = toggleLabel;
22175
+ button.setAttribute("aria-label", toggleLabel);
21217
22176
  }
21218
22177
  const unfold = card.querySelector(".gdp-file-unfold");
21219
22178
  if (unfold)
@@ -21310,6 +22269,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21310
22269
  toggle.type = "button";
21311
22270
  toggle.className = "gdp-file-header-icon gdp-file-toggle";
21312
22271
  toggle.title = "Collapse file";
22272
+ toggle.setAttribute("aria-label", "Collapse file");
21313
22273
  toggle.setAttribute("aria-expanded", "true");
21314
22274
  toggle.innerHTML = iconSvg("octicon-chevron-down", CHEVRON_DOWN_16_PATH);
21315
22275
  toggle.addEventListener("click", (e2) => {
@@ -21331,6 +22291,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21331
22291
  copy.type = "button";
21332
22292
  copy.className = "gdp-file-header-icon gdp-copy-path";
21333
22293
  copy.title = "copy file path";
22294
+ copy.setAttribute("aria-label", "copy file path");
21334
22295
  copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
21335
22296
  copy.addEventListener("click", async (e2) => {
21336
22297
  e2.stopPropagation();
@@ -21671,6 +22632,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21671
22632
  addExpandHunksUI,
21672
22633
  scheduleIdleHighlight,
21673
22634
  scrollToFile,
22635
+ scrollToNextUnviewedFile,
21674
22636
  prefetchByPath,
21675
22637
  applyDiffRouteFocus,
21676
22638
  clearDiffLineFocus,
@@ -21899,12 +22861,17 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21899
22861
  deps.clearLoadQueue();
21900
22862
  if (deps.empty) {
21901
22863
  deps.empty.classList.remove("hidden");
22864
+ deps.empty.classList.remove("empty-with-actions");
22865
+ const text2 = deps.emptyText();
21902
22866
  const h2 = deps.empty.querySelector("h2");
21903
22867
  if (h2)
21904
- h2.textContent = "No commit selected";
22868
+ h2.textContent = text2.noCommitSelectedTitle;
21905
22869
  const p2 = deps.empty.querySelector("p");
21906
22870
  if (p2)
21907
- p2.textContent = "Select a commit from the list to see its changes.";
22871
+ p2.textContent = text2.noCommitSelectedBody;
22872
+ const actions = deps.empty.querySelector(".empty-actions");
22873
+ if (actions)
22874
+ actions.hidden = true;
21908
22875
  }
21909
22876
  deps.setStatus("live");
21910
22877
  }
@@ -21962,14 +22929,37 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21962
22929
  en: {
21963
22930
  worktreeLabel: "Uncommitted changes (Working tree)",
21964
22931
  bodyExpandClose: "Collapse",
21965
- bodyExpandMore: (n2) => `Show more (${n2} lines)`
22932
+ bodyExpandMore: (n2) => `Show more (${n2} lines)`,
22933
+ refreshLabel: "Refresh",
22934
+ refreshLabelPending: "Update",
22935
+ refreshTitle: "Refresh commit history",
22936
+ refreshTitlePending: "History may have changed. Refresh",
22937
+ refreshPendingStatus: "History may have changed",
22938
+ refreshResultUpdated: (sha) => `Updated: ${sha} is now latest`,
22939
+ refreshResultUnchanged: "No new commits",
22940
+ refreshResultUnchangedInView: "No new commits in this view",
22941
+ filterClearLabel: "Clear",
22942
+ filterClearTitle: "Clear commit filter"
21966
22943
  },
21967
22944
  ja: {
21968
22945
  worktreeLabel: "未コミット変更 (Working tree)",
21969
22946
  bodyExpandClose: "閉じる",
21970
- bodyExpandMore: (n2) => `もっと見る (${n2} 行)`
22947
+ bodyExpandMore: (n2) => `もっと見る (${n2} 行)`,
22948
+ refreshLabel: "更新",
22949
+ refreshLabelPending: "更新あり",
22950
+ refreshTitle: "コミット履歴を更新",
22951
+ refreshTitlePending: "新しい履歴がある可能性があります。更新",
22952
+ refreshPendingStatus: "新しい履歴がある可能性があります",
22953
+ refreshResultUpdated: (sha) => `更新: ${sha} が最新です`,
22954
+ refreshResultUnchanged: "新しいコミットはありません",
22955
+ refreshResultUnchangedInView: "この表示では新しいコミットはありません",
22956
+ filterClearLabel: "解除",
22957
+ filterClearTitle: "コミットフィルタを解除"
21971
22958
  }
21972
22959
  };
22960
+ function historyText(lang) {
22961
+ return HISTORY_TEXT[lang];
22962
+ }
21973
22963
  function historyWorktreeLabel(lang) {
21974
22964
  return HISTORY_TEXT[lang].worktreeLabel;
21975
22965
  }
@@ -21990,6 +22980,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21990
22980
  title.className = "history-title";
21991
22981
  title.textContent = "Commits";
21992
22982
  panelHead.appendChild(title);
22983
+ const refreshButton = document.createElement("button");
22984
+ refreshButton.type = "button";
22985
+ refreshButton.className = "history-refresh";
22986
+ refreshButton.title = HISTORY_TEXT.en.refreshTitle;
22987
+ refreshButton.setAttribute("aria-label", HISTORY_TEXT.en.refreshTitle);
22988
+ refreshButton.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
22989
+ const refreshLabel = document.createElement("span");
22990
+ refreshLabel.className = "history-refresh-label";
22991
+ refreshLabel.textContent = HISTORY_TEXT.en.refreshLabel;
22992
+ refreshButton.appendChild(refreshLabel);
22993
+ const refreshResult = document.createElement("span");
22994
+ refreshResult.className = "db-refresh-result history-refresh-result";
22995
+ refreshResult.setAttribute("aria-live", "polite");
22996
+ refreshResult.hidden = true;
21993
22997
  if (page) {
21994
22998
  const refMount = document.createElement("span");
21995
22999
  refMount.dataset.refSelectorMount = "";
@@ -21998,6 +23002,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21998
23002
  refMount.dataset.title = "history ref";
21999
23003
  panelHead.appendChild(refMount);
22000
23004
  }
23005
+ panelHead.append(refreshButton, refreshResult);
22001
23006
  const filterWrap = document.createElement("div");
22002
23007
  filterWrap.className = "history-filter-wrap";
22003
23008
  const filterInput = document.createElement("input");
@@ -22008,7 +23013,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22008
23013
  filterInput.type = "search";
22009
23014
  filterInput.placeholder = page ? "filter commits… (message, sha, author:name, path:file)" : "filter commits… (message, sha, author:name)";
22010
23015
  filterInput.autocomplete = "off";
22011
- filterWrap.appendChild(filterInput);
23016
+ const filterClearButton = document.createElement("button");
23017
+ filterClearButton.type = "button";
23018
+ if (page)
23019
+ filterClearButton.id = "history-filter-clear";
23020
+ filterClearButton.className = "history-filter-clear";
23021
+ filterClearButton.hidden = true;
23022
+ filterClearButton.textContent = HISTORY_TEXT.en.filterClearLabel;
23023
+ filterClearButton.title = HISTORY_TEXT.en.filterClearTitle;
23024
+ filterClearButton.setAttribute("aria-label", HISTORY_TEXT.en.filterClearTitle);
23025
+ filterWrap.append(filterInput, filterClearButton);
22012
23026
  const banner = document.createElement("div");
22013
23027
  if (page)
22014
23028
  banner.id = "history-banner";
@@ -22031,7 +23045,17 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22031
23045
  status.hidden = true;
22032
23046
  status.setAttribute("role", "status");
22033
23047
  panel.append(panelHead, filterWrap, banner, list2, sentinel, status);
22034
- return { panel, list: list2, banner, status, sentinel, filterInput };
23048
+ return {
23049
+ panel,
23050
+ list: list2,
23051
+ banner,
23052
+ status,
23053
+ sentinel,
23054
+ filterInput,
23055
+ filterClearButton,
23056
+ refreshButton,
23057
+ refreshResult
23058
+ };
22035
23059
  }
22036
23060
  function buildHistoryCommitInfoDom(options = {}) {
22037
23061
  const variant = options.variant || "page";
@@ -22129,6 +23153,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22129
23153
  status: deps.$("#history-status"),
22130
23154
  sentinel: deps.$("#history-sentinel"),
22131
23155
  filterInput: document.querySelector("#history-filter"),
23156
+ filterClearButton: document.querySelector("#history-filter-clear"),
23157
+ refreshButton: document.querySelector(".history-refresh"),
23158
+ refreshResult: document.querySelector(".history-refresh-result"),
22132
23159
  commitInfo: document.querySelector("#history-commit-info")
22133
23160
  };
22134
23161
  let activeMount = defaultMount;
@@ -22139,6 +23166,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22139
23166
  let sentinel = defaultMount.sentinel;
22140
23167
  let attachedList = null;
22141
23168
  let attachedFilterInput = null;
23169
+ let attachedFilterClearButton = null;
23170
+ let attachedRefreshButton = null;
22142
23171
  let filterTimer = null;
22143
23172
  let observer = null;
22144
23173
  let ref = "HEAD";
@@ -22152,6 +23181,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22152
23181
  let mode = "history";
22153
23182
  let routeRef = "HEAD";
22154
23183
  let pathFilter = "";
23184
+ let refreshStatus = { type: "none" };
23185
+ let freshSha = "";
22155
23186
  function historyScopeFromRoute(route = deps.getRoute()) {
22156
23187
  if (route.screen === "history") {
22157
23188
  const nextRef = route.ref || "HEAD";
@@ -22175,6 +23206,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22175
23206
  }
22176
23207
  return null;
22177
23208
  }
23209
+ function currentRefreshScopeKey() {
23210
+ const scope = historyScopeFromRoute();
23211
+ if (!scope)
23212
+ return "";
23213
+ return [
23214
+ scope.mode,
23215
+ scope.logRef,
23216
+ scope.routeRef,
23217
+ scope.pathFilter,
23218
+ query
23219
+ ].join("\x00");
23220
+ }
22178
23221
  function worktreeDiffRange() {
22179
23222
  return { from: "HEAD", to: "worktree" };
22180
23223
  }
@@ -22186,6 +23229,39 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22186
23229
  statusEl.textContent = message;
22187
23230
  statusEl.hidden = !message;
22188
23231
  }
23232
+ function latestCommitSha() {
23233
+ return commits.find((commit) => commit.sha !== HISTORY_WORKTREE_COMMIT)?.sha || "";
23234
+ }
23235
+ function clearRefreshResult() {
23236
+ refreshStatus = { type: "none" };
23237
+ freshSha = "";
23238
+ syncRefreshResult();
23239
+ }
23240
+ function setRefreshResult(previousTopSha, nextTopSha) {
23241
+ freshSha = previousTopSha && nextTopSha && previousTopSha !== nextTopSha ? nextTopSha : "";
23242
+ refreshStatus = freshSha ? { type: "updated", sha: nextTopSha.slice(0, 7) } : { type: "unchanged", scoped: Boolean(query || pathFilter) };
23243
+ renderList();
23244
+ }
23245
+ function refreshStatusMessage() {
23246
+ const text2 = historyText(deps.getLanguage());
23247
+ switch (refreshStatus.type) {
23248
+ case "pending":
23249
+ return text2.refreshPendingStatus;
23250
+ case "updated":
23251
+ return text2.refreshResultUpdated(refreshStatus.sha);
23252
+ case "unchanged":
23253
+ return refreshStatus.scoped ? text2.refreshResultUnchangedInView : text2.refreshResultUnchanged;
23254
+ case "none":
23255
+ return "";
23256
+ default: {
23257
+ const exhaustive = refreshStatus;
23258
+ return exhaustive;
23259
+ }
23260
+ }
23261
+ }
23262
+ function syncRefreshStatusText() {
23263
+ setStatusText(loading ? "loading..." : refreshStatusMessage() || (commits.length ? "" : "no commits"));
23264
+ }
22189
23265
  function commitInfoElement() {
22190
23266
  return activeMount.commitInfo || document.querySelector("#history-commit-info");
22191
23267
  }
@@ -22257,13 +23333,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22257
23333
  }
22258
23334
  function commitRow(commit) {
22259
23335
  const active = commit.sha === selectedSha ? " active" : "";
22260
- return `<li class="history-item${active}" data-sha="${deps.escapeHtml(commit.sha)}">` + `<span class="subject" title="${deps.escapeHtml(commit.subject)}">${deps.escapeHtml(commit.subject)}</span>` + `<span class="meta2">` + `<span class="sha">${deps.escapeHtml(commit.sha.slice(0, 7))}</span>` + `<span class="author">${deps.escapeHtml(commit.author)}</span>` + `<span class="when">${deps.escapeHtml(displayWhen(commit.when))}</span>` + `</span>` + `</li>`;
23336
+ const fresh = commit.sha === freshSha ? " history-item-fresh" : "";
23337
+ return `<li class="history-item${active}${fresh}" data-sha="${deps.escapeHtml(commit.sha)}">` + `<span class="subject" title="${deps.escapeHtml(commit.subject)}">${deps.escapeHtml(commit.subject)}</span>` + `<span class="meta2">` + `<span class="sha">${deps.escapeHtml(commit.sha.slice(0, 7))}</span>` + `<span class="author">${deps.escapeHtml(commit.author)}</span>` + `<span class="when">${deps.escapeHtml(displayWhen(commit.when))}</span>` + `</span>` + `</li>`;
22261
23338
  }
22262
23339
  function worktreeRow() {
22263
23340
  const active = selectedSha === HISTORY_WORKTREE_COMMIT ? " active" : "";
22264
23341
  return `<li class="history-item history-item-worktree${active}" data-sha="${HISTORY_WORKTREE_COMMIT}">` + `<span class="subject" title="${deps.escapeHtml(historyWorktreeLabel(deps.getLanguage()))}">${deps.escapeHtml(historyWorktreeLabel(deps.getLanguage()))}</span>` + `<span class="meta2">` + `<span class="sha">HEAD..worktree</span>` + `<span class="author">Working tree</span>` + `</span>` + `</li>`;
22265
23342
  }
22266
23343
  function renderList() {
23344
+ syncRefreshButton(activeMount.refreshButton);
23345
+ syncRefreshResult(activeMount.refreshResult);
22267
23346
  const now = new Date;
22268
23347
  const html = mode === "history" ? [worktreeRow()] : [];
22269
23348
  let lastGroup = "";
@@ -22280,7 +23359,42 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22280
23359
  html.push(commitRow(commit));
22281
23360
  }
22282
23361
  list2.innerHTML = html.join("");
22283
- setStatusText(loading ? "loading..." : commits.length ? "" : "no commits");
23362
+ syncRefreshStatusText();
23363
+ }
23364
+ function syncRefreshButton(button) {
23365
+ if (!button)
23366
+ return;
23367
+ const text2 = historyText(deps.getLanguage());
23368
+ const hasPendingUpdate = refreshStatus.type === "pending";
23369
+ const title = hasPendingUpdate ? text2.refreshTitlePending : text2.refreshTitle;
23370
+ button.title = title;
23371
+ button.setAttribute("aria-label", title);
23372
+ button.classList.toggle("has-update", hasPendingUpdate);
23373
+ const label = button.querySelector(".history-refresh-label");
23374
+ if (label) {
23375
+ label.textContent = hasPendingUpdate ? text2.refreshLabelPending : text2.refreshLabel;
23376
+ }
23377
+ }
23378
+ function syncRefreshResult(result) {
23379
+ const el = result ?? activeMount.refreshResult;
23380
+ if (!el)
23381
+ return;
23382
+ const message = refreshStatus.type === "pending" || refreshStatus.type === "updated" || refreshStatus.type === "unchanged" ? refreshStatusMessage() : "";
23383
+ el.textContent = message;
23384
+ el.hidden = message.length === 0;
23385
+ el.classList.toggle("changed", refreshStatus.type === "updated");
23386
+ el.classList.toggle("pending", refreshStatus.type === "pending");
23387
+ }
23388
+ function syncFilterClearButton(button) {
23389
+ const clearButton = button ?? activeMount.filterClearButton;
23390
+ if (!clearButton)
23391
+ return;
23392
+ const input = activeMount.filterInput ?? null;
23393
+ const text2 = historyText(deps.getLanguage());
23394
+ clearButton.textContent = text2.filterClearLabel;
23395
+ clearButton.title = text2.filterClearTitle;
23396
+ clearButton.setAttribute("aria-label", text2.filterClearTitle);
23397
+ clearButton.hidden = !(input?.value || "");
22284
23398
  }
22285
23399
  async function updateCommitInfo(commit) {
22286
23400
  const info = commitInfoElement();
@@ -22559,6 +23673,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22559
23673
  selectionGeneration++;
22560
23674
  selectedSha = "";
22561
23675
  setBanner("");
23676
+ clearRefreshResult();
22562
23677
  await updateCommitInfo(null);
22563
23678
  renderList();
22564
23679
  await loadNextPage();
@@ -22629,6 +23744,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22629
23744
  query = value;
22630
23745
  generation++;
22631
23746
  selectionGeneration++;
23747
+ clearRefreshResult();
22632
23748
  commits = [];
22633
23749
  hasMore = false;
22634
23750
  loading = false;
@@ -22648,6 +23764,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22648
23764
  const input = activeFilterInputFromEvent(event);
22649
23765
  if (!input)
22650
23766
  return;
23767
+ syncFilterClearButton();
22651
23768
  if (filterTimer)
22652
23769
  clearTimeout(filterTimer);
22653
23770
  filterTimer = setTimeout(() => {
@@ -22663,13 +23780,54 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22663
23780
  return;
22664
23781
  if (e2.key === "Escape" && input.value) {
22665
23782
  input.value = "";
23783
+ syncFilterClearButton();
22666
23784
  applyFilter("");
22667
23785
  e2.stopPropagation();
22668
23786
  }
22669
23787
  }
23788
+ function handleFilterClearClick() {
23789
+ const input = attachedFilterInput;
23790
+ if (!input?.value)
23791
+ return;
23792
+ if (filterTimer) {
23793
+ clearTimeout(filterTimer);
23794
+ filterTimer = null;
23795
+ }
23796
+ input.value = "";
23797
+ syncFilterClearButton();
23798
+ applyFilter("");
23799
+ input.focus?.();
23800
+ }
23801
+ async function handleRefreshClick() {
23802
+ const button = attachedRefreshButton;
23803
+ if (button?.disabled)
23804
+ return;
23805
+ const refreshScopeKey = currentRefreshScopeKey();
23806
+ const previousTopSha = latestCommitSha();
23807
+ clearRefreshResult();
23808
+ if (button) {
23809
+ button.disabled = true;
23810
+ button.classList.add("spinning");
23811
+ }
23812
+ try {
23813
+ await enterHistory({ mount: activeMount, force: true });
23814
+ if (!refreshScopeKey || currentRefreshScopeKey() !== refreshScopeKey)
23815
+ return;
23816
+ setRefreshResult(previousTopSha, latestCommitSha());
23817
+ } finally {
23818
+ if (button) {
23819
+ button.classList.remove("spinning");
23820
+ button.disabled = false;
23821
+ }
23822
+ }
23823
+ }
22670
23824
  function activateMount(mount) {
22671
- if (activeMount === mount && attachedList === mount.list)
23825
+ if (activeMount === mount && attachedList === mount.list) {
23826
+ syncRefreshButton(mount.refreshButton);
23827
+ syncRefreshResult(mount.refreshResult);
23828
+ syncFilterClearButton(mount.filterClearButton);
22672
23829
  return;
23830
+ }
22673
23831
  if (attachedList) {
22674
23832
  attachedList.removeEventListener("click", handleListClick);
22675
23833
  attachedList = null;
@@ -22680,6 +23838,14 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22680
23838
  attachedFilterInput.removeEventListener("keydown", handleFilterKeydown);
22681
23839
  attachedFilterInput = null;
22682
23840
  }
23841
+ if (attachedFilterClearButton) {
23842
+ attachedFilterClearButton.removeEventListener("click", handleFilterClearClick);
23843
+ attachedFilterClearButton = null;
23844
+ }
23845
+ if (attachedRefreshButton) {
23846
+ attachedRefreshButton.removeEventListener("click", handleRefreshClick);
23847
+ attachedRefreshButton = null;
23848
+ }
22683
23849
  if (filterTimer) {
22684
23850
  clearTimeout(filterTimer);
22685
23851
  filterTimer = null;
@@ -22701,6 +23867,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22701
23867
  input.addEventListener("keydown", handleFilterKeydown);
22702
23868
  attachedFilterInput = input;
22703
23869
  }
23870
+ const filterClearButton = mount.filterClearButton ?? null;
23871
+ if (filterClearButton) {
23872
+ syncFilterClearButton(filterClearButton);
23873
+ filterClearButton.addEventListener("click", handleFilterClearClick);
23874
+ attachedFilterClearButton = filterClearButton;
23875
+ }
23876
+ const refreshButton = mount.refreshButton ?? null;
23877
+ if (refreshButton) {
23878
+ syncRefreshButton(refreshButton);
23879
+ syncRefreshResult(mount.refreshResult);
23880
+ refreshButton.addEventListener("click", handleRefreshClick);
23881
+ attachedRefreshButton = refreshButton;
23882
+ }
22704
23883
  observer = new IntersectionObserver((entries) => {
22705
23884
  if (!entries.some((entry) => entry.isIntersecting))
22706
23885
  return;
@@ -22737,6 +23916,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22737
23916
  enterHistory,
22738
23917
  leaveHistory,
22739
23918
  onRefPicked,
23919
+ localize: () => {
23920
+ syncRefreshButton(activeMount.refreshButton);
23921
+ syncRefreshResult(activeMount.refreshResult);
23922
+ syncFilterClearButton(activeMount.filterClearButton);
23923
+ renderList();
23924
+ },
23925
+ notePossibleUpdate: () => {
23926
+ refreshStatus = { type: "pending" };
23927
+ syncRefreshButton(activeMount.refreshButton);
23928
+ syncRefreshResult(activeMount.refreshResult);
23929
+ syncRefreshStatusText();
23930
+ },
22740
23931
  isWorktreeSelected: () => selectedSha === HISTORY_WORKTREE_COMMIT
22741
23932
  };
22742
23933
  }
@@ -22765,15 +23956,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22765
23956
  const diffPane = document.createElement("main");
22766
23957
  diffPane.className = "gdp-file-history-diff-pane";
22767
23958
  const commitInfo = buildHistoryCommitInfoDom({ variant: "file" });
23959
+ const emptyText = deps.emptyText();
22768
23960
  const empty = document.createElement("div");
22769
23961
  empty.className = "empty gdp-file-history-empty hidden";
22770
23962
  const emptyIcon = document.createElement("div");
22771
- emptyIcon.className = "emoji";
22772
- emptyIcon.textContent = "";
23963
+ emptyIcon.className = "empty-icon";
23964
+ emptyIcon.innerHTML = iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH);
22773
23965
  const emptyTitle = document.createElement("h2");
22774
- emptyTitle.textContent = "No commit selected";
23966
+ emptyTitle.textContent = emptyText.noCommitSelectedTitle;
22775
23967
  const emptyBody = document.createElement("p");
22776
- emptyBody.textContent = "Select a commit from the list to see its changes.";
23968
+ emptyBody.textContent = emptyText.noCommitSelectedBody;
22777
23969
  empty.append(emptyIcon, emptyTitle, emptyBody);
22778
23970
  const diffHost = document.createElement("div");
22779
23971
  diffHost.className = "gdp-file-history-diff";
@@ -22823,8 +24015,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22823
24015
  {
22824
24016
  selectors: [{ action: "open-help" }],
22825
24017
  description: {
22826
- en: "Open this help (keybindings)",
22827
- ja: "このヘルプ(キーバインド)を開く"
24018
+ en: "Open quick help",
24019
+ ja: "クイックヘルプを開く"
22828
24020
  }
22829
24021
  },
22830
24022
  {
@@ -22851,6 +24043,13 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22851
24043
  ja: "前 / 次の注釈へ移動"
22852
24044
  }
22853
24045
  },
24046
+ {
24047
+ selectors: [{ action: "next-unviewed-file" }],
24048
+ description: {
24049
+ en: "Jump to the next unviewed file",
24050
+ ja: "次の未確認ファイルへ移動"
24051
+ }
24052
+ },
22854
24053
  {
22855
24054
  selectors: [{ action: "layout-unified" }, { action: "layout-split" }],
22856
24055
  description: {
@@ -23096,8 +24295,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23096
24295
  addUnique(labels, item.label);
23097
24296
  return [labels.join(" / "), row.description[language]];
23098
24297
  }
23099
- function buildHelpKeybindingGroups(language, bindings = DEFAULT_KEY_BINDINGS) {
23100
- return HELP_KEYBINDING_GROUPS.map((group) => ({
24298
+ function buildHelpKeybindingGroups(language, bindings = DEFAULT_KEY_BINDINGS, onlyTitlesEn) {
24299
+ const groups = onlyTitlesEn ? HELP_KEYBINDING_GROUPS.filter((group) => onlyTitlesEn.includes(group.title.en)) : HELP_KEYBINDING_GROUPS;
24300
+ return groups.map((group) => ({
23101
24301
  title: group.title[language],
23102
24302
  rows: group.rows.map((row) => buildRow(row, language, bindings))
23103
24303
  }));
@@ -23469,7 +24669,7 @@ code-viewer annotate add-db --db app.db --tab query \\
23469
24669
  ],
23470
24670
  [
23471
24671
  "Data tab",
23472
- "Paginated grid with sort, filter, cell copy, and CSV/JSON export (capped at 100k rows; export respects the current filter and sort). Toggle Edit mode in the prefs bar to enable inline editing — double-click a cell to edit, queue insert/delete via row actions, and commit the batch atomically. Edited rows/cells highlight in yellow until committed."
24672
+ "Paginated grid with sort, filter, cell copy, CSV/JSON export (capped at 100k rows; export respects the current filter and sort), and a table-only reload action that keeps global search and column filters. Reload results are announced beside the button so row-count changes are visible. Toggle Edit mode in the prefs bar to enable inline editing — double-click a cell to edit, queue insert/delete via row actions, and commit the batch atomically. Edited rows/cells highlight in yellow until committed."
23473
24673
  ],
23474
24674
  [
23475
24675
  "Detail footer & related panel",
@@ -23477,7 +24677,7 @@ code-viewer annotate add-db --db app.db --tab query \\
23477
24677
  ],
23478
24678
  [
23479
24679
  "Schema tab",
23480
- "Column definitions, indexes, foreign keys, triggers, and DDL."
24680
+ "Column definitions, indexes, foreign keys, triggers, and DDL, with an in-tab refresh action for reloading the current table structure."
23481
24681
  ],
23482
24682
  [
23483
24683
  "Query editor",
@@ -24121,7 +25321,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24121
25321
  ],
24122
25322
  [
24123
25323
  "Data タブ",
24124
- "ページネーション付きグリッド。ソート、フィルター、セルコピー、CSV/JSON エクスポート(最大10万行、現在のフィルター/ソートを反映)。設定バーで Edit モードを ON にするとインライン編集が可能 — セルをダブルクリックで編集、行操作で挿入/削除を予約、コミット単位で一括適用。未コミットの編集行/セルは黄色でハイライト。"
25324
+ "ページネーション付きグリッド。ソート、フィルター、セルコピー、CSV/JSON エクスポート(最大10万行、現在のフィルター/ソートを反映)、全体検索と列フィルターを保持した表だけの再読み込みに対応。再読み込み結果はボタン横に表示され、行数変化に気づけます。設定バーで Edit モードを ON にするとインライン編集が可能 — セルをダブルクリックで編集、行操作で挿入/削除を予約、コミット単位で一括適用。未コミットの編集行/セルは黄色でハイライト。"
24125
25325
  ],
24126
25326
  [
24127
25327
  "詳細フッタ・関連パネル",
@@ -24129,7 +25329,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24129
25329
  ],
24130
25330
  [
24131
25331
  "Schema タブ",
24132
- "カラム定義、インデックス、外部キー、トリガー、DDL"
25332
+ "カラム定義、インデックス、外部キー、トリガー、DDL。現在の表構造だけを再読み込みするタブ内更新にも対応。"
24133
25333
  ],
24134
25334
  [
24135
25335
  "クエリエディター",
@@ -24801,6 +26001,7 @@ code-viewer query snapshot delete --id snap-abc123`
24801
26001
  const button = document.createElement("button");
24802
26002
  button.className = "gdp-expand-btn";
24803
26003
  button.title = spec.title;
26004
+ button.setAttribute("aria-label", spec.title);
24804
26005
  button.innerHTML = '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">' + '<path fill="currentColor" d="' + EXPAND_ICON_PATHS[spec.direction] + '"/></svg>';
24805
26006
  button.addEventListener("click", (e2) => {
24806
26007
  e2.stopPropagation();
@@ -25139,6 +26340,86 @@ code-viewer query snapshot delete --id snap-abc123`
25139
26340
  };
25140
26341
  }
25141
26342
 
26343
+ // web-src/views/quick-help.ts
26344
+ var QUICK_HELP_GROUP_TITLES_EN = ["Global", "Main Panel"];
26345
+ function createQuickHelp(deps) {
26346
+ const popover = deps.$("#quick-help-popover");
26347
+ const trigger = deps.$("#quick-help-btn");
26348
+ const titleEl = deps.$("#quick-help-title");
26349
+ const closeBtn = deps.$("#quick-help-close");
26350
+ const groupsHost = deps.$("#quick-help-groups");
26351
+ const fullLink = deps.$("#quick-help-full-link");
26352
+ function isOpen() {
26353
+ return !popover.hidden;
26354
+ }
26355
+ function renderContent() {
26356
+ groupsHost.innerHTML = "";
26357
+ const groups = buildHelpKeybindingGroups(deps.getLanguage(), undefined, QUICK_HELP_GROUP_TITLES_EN);
26358
+ for (const group of groups) {
26359
+ const section = document.createElement("section");
26360
+ section.className = "gdp-help-group";
26361
+ const title = document.createElement("h3");
26362
+ title.textContent = group.title;
26363
+ section.append(title, renderHelpTable(group.rows));
26364
+ groupsHost.appendChild(section);
26365
+ }
26366
+ }
26367
+ function applyText() {
26368
+ const text2 = deps.getText();
26369
+ titleEl.textContent = text2.panelTitle;
26370
+ popover.setAttribute("aria-label", text2.panelTitle);
26371
+ closeBtn.setAttribute("aria-label", text2.close);
26372
+ fullLink.textContent = text2.viewAll;
26373
+ }
26374
+ function open() {
26375
+ applyText();
26376
+ renderContent();
26377
+ popover.hidden = false;
26378
+ closeBtn.focus();
26379
+ }
26380
+ function close() {
26381
+ if (!isOpen())
26382
+ return;
26383
+ popover.hidden = true;
26384
+ }
26385
+ function toggle() {
26386
+ if (isOpen())
26387
+ close();
26388
+ else
26389
+ open();
26390
+ }
26391
+ trigger.addEventListener("click", toggle);
26392
+ closeBtn.addEventListener("click", close);
26393
+ fullLink.addEventListener("click", (e2) => {
26394
+ e2.preventDefault();
26395
+ close();
26396
+ deps.openFullKeybindings();
26397
+ });
26398
+ document.addEventListener("keydown", (e2) => {
26399
+ if (isImeComposing(e2))
26400
+ return;
26401
+ if (e2.key !== "Escape")
26402
+ return;
26403
+ if (!isOpen())
26404
+ return;
26405
+ close();
26406
+ });
26407
+ document.addEventListener("mousedown", (e2) => {
26408
+ if (!isOpen())
26409
+ return;
26410
+ const target = e2.target;
26411
+ if (popover.contains(target) || trigger.contains(target))
26412
+ return;
26413
+ close();
26414
+ });
26415
+ function localize() {
26416
+ applyText();
26417
+ if (isOpen())
26418
+ renderContent();
26419
+ }
26420
+ return { open, close, toggle, isOpen, localize };
26421
+ }
26422
+
25142
26423
  // web-src/views/ref-picker.ts
25143
26424
  function createRefPicker(deps) {
25144
26425
  function wireRefSelectorInput(input, onPick) {
@@ -25570,7 +26851,11 @@ code-viewer query snapshot delete --id snap-abc123`
25570
26851
  trackLoad,
25571
26852
  getRepoSidebarRef,
25572
26853
  setRepoSidebarRef,
25573
- isTestPath
26854
+ isTestPath,
26855
+ sidebarToggleTitle,
26856
+ openDirectoryInOsTitle,
26857
+ omittedDirectoryBadge,
26858
+ commitEntryBadge
25574
26859
  } = deps;
25575
26860
  const VIRTUAL_SIDEBAR_THRESHOLD = 3000;
25576
26861
  const VIRTUAL_SIDEBAR_ROW_HEIGHT = 29;
@@ -25647,8 +26932,9 @@ code-viewer query snapshot delete --id snap-abc123`
25647
26932
  button = createSidebarToggleButton();
25648
26933
  bindSidebarToggleButton(button);
25649
26934
  button.setAttribute("aria-pressed", STATE.sidebarHidden ? "true" : "false");
25650
- button.title = STATE.sidebarHidden ? "show sidebar" : "hide sidebar";
25651
- button.setAttribute("aria-label", STATE.sidebarHidden ? "show sidebar" : "hide sidebar");
26935
+ const toggleTitle = sidebarToggleTitle(STATE.sidebarHidden);
26936
+ button.title = toggleTitle;
26937
+ button.setAttribute("aria-label", toggleTitle);
25652
26938
  syncSidebarToggleIcon(button);
25653
26939
  return button;
25654
26940
  }
@@ -25813,12 +27099,13 @@ code-viewer query snapshot delete --id snap-abc123`
25813
27099
  if (dir.children_omitted) {
25814
27100
  const omitted = document.createElement("span");
25815
27101
  omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
25816
- omitted.textContent = dir.children_omitted_reason === "heavy" ? "skipped" : "private";
25817
- omitted.title = dir.children_omitted_reason === "heavy" ? "Tree expansion is skipped, but the directory detail can be opened" : "This directory cannot be opened from the browser";
27102
+ const badge = omittedDirectoryBadge(dir.children_omitted_reason);
27103
+ omitted.textContent = badge.label;
27104
+ omitted.title = badge.title;
25818
27105
  label.appendChild(omitted);
25819
27106
  }
25820
27107
  li.appendChild(label);
25821
- li.appendChild(createOpenPathButton(dir.path, "directory", "open this folder in OS"));
27108
+ li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
25822
27109
  const collapsed = STATE.collapsedDirs.has(dir.path);
25823
27110
  if (collapsed)
25824
27111
  li.classList.add("collapsed");
@@ -25866,40 +27153,7 @@ code-viewer query snapshot delete --id snap-abc123`
25866
27153
  ul.appendChild(childUl);
25867
27154
  } else {
25868
27155
  const f2 = item.file;
25869
- const li = document.createElement("li");
25870
- li.className = "tree-file";
25871
- li.tabIndex = -1;
25872
- li.dataset.path = f2.path;
25873
- li.dataset.type = "blob";
25874
- li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
25875
- li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
25876
- const spacer = document.createElement("span");
25877
- spacer.className = "chev-spacer";
25878
- li.appendChild(spacer);
25879
- if (f2.status) {
25880
- li.appendChild(fileBadge(f2.status));
25881
- } else {
25882
- const icon = document.createElement("span");
25883
- icon.className = "d2h-icon-wrapper";
25884
- icon.innerHTML = fileEntryIcon();
25885
- li.appendChild(icon);
25886
- }
25887
- const name = document.createElement("span");
25888
- name.className = "name";
25889
- name.textContent = f2.path.split("/").pop();
25890
- name.title = f2.path;
25891
- li.appendChild(name);
25892
- li.addEventListener("click", () => {
25893
- if (onFileClick)
25894
- onFileClick(f2);
25895
- else
25896
- scrollToFile(f2.path);
25897
- scheduleMainSurfaceFocus();
25898
- });
25899
- if (!onFileClick)
25900
- li.addEventListener("mouseenter", () => prefetchByPath(f2.path), {
25901
- passive: true
25902
- });
27156
+ const li = createTreeFileRow(f2, depth, onFileClick);
25903
27157
  ul.appendChild(li);
25904
27158
  }
25905
27159
  }
@@ -25983,7 +27237,8 @@ code-viewer query snapshot delete --id snap-abc123`
25983
27237
  order: dir.minOrder + (index + 1) / 1e5,
25984
27238
  path: entry.path,
25985
27239
  display_path: entry.path,
25986
- type: entry.type,
27240
+ type: meta.ref === "worktree" && entry.type === "commit" && !entry.submodule ? "tree" : entry.type,
27241
+ submodule: entry.submodule,
25987
27242
  children_omitted: entry.children_omitted,
25988
27243
  children_omitted_reason: entry.children_omitted_reason
25989
27244
  }));
@@ -26033,12 +27288,13 @@ code-viewer query snapshot delete --id snap-abc123`
26033
27288
  if (dir.children_omitted) {
26034
27289
  const omitted = document.createElement("span");
26035
27290
  omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
26036
- omitted.textContent = dir.children_omitted_reason === "heavy" ? "skipped" : "private";
26037
- omitted.title = dir.children_omitted_reason === "heavy" ? "Tree expansion is skipped, but the directory detail can be opened" : "This directory cannot be opened from the browser";
27291
+ const badge = omittedDirectoryBadge(dir.children_omitted_reason);
27292
+ omitted.textContent = badge.label;
27293
+ omitted.title = badge.title;
26038
27294
  label.appendChild(omitted);
26039
27295
  }
26040
27296
  li.appendChild(label);
26041
- li.appendChild(createOpenPathButton(dir.path, "directory", "open this folder in OS"));
27297
+ li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
26042
27298
  const updateIcon = () => {
26043
27299
  setFolderIcon(dirIcon, li.classList.contains("collapsed"));
26044
27300
  };
@@ -26104,12 +27360,37 @@ code-viewer query snapshot delete --id snap-abc123`
26104
27360
  }
26105
27361
  return li;
26106
27362
  }
27363
+ function fileKindTag(f2) {
27364
+ if (f2.type === "commit") {
27365
+ const badge = commitEntryBadge(f2.submodule);
27366
+ const tag2 = document.createElement("span");
27367
+ tag2.className = `kind-tag ${f2.submodule ? "submodule" : "gitlink"}`;
27368
+ tag2.textContent = badge.label;
27369
+ tag2.title = badge.title;
27370
+ return tag2;
27371
+ }
27372
+ const kind = classifyDiffFileKind(f2);
27373
+ if (!kind.heavy && !kind.binary && !kind.media)
27374
+ return null;
27375
+ const tag = document.createElement("span");
27376
+ const isBinaryLike = kind.binary || kind.media;
27377
+ tag.className = `kind-tag ${isBinaryLike ? "binary" : "heavy"}`;
27378
+ tag.textContent = isBinaryLike ? "B" : "!";
27379
+ tag.title = isBinaryLike ? "binary/media file" : "large diff";
27380
+ return tag;
27381
+ }
27382
+ function sidebarEntryIcon(f2) {
27383
+ return f2.type === "commit" ? iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH) : fileEntryIcon();
27384
+ }
26107
27385
  function createTreeFileRow(f2, depth, onFileClick) {
26108
27386
  const li = document.createElement("li");
26109
27387
  li.className = "tree-file";
26110
27388
  li.tabIndex = -1;
26111
27389
  li.dataset.path = f2.path;
26112
- li.dataset.type = "blob";
27390
+ li.dataset.type = f2.type || "blob";
27391
+ if (f2.type === "commit") {
27392
+ li.title = commitEntryBadge(f2.submodule).title;
27393
+ }
26113
27394
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
26114
27395
  li.classList.toggle("hidden-by-tests", STATE.hideTests && !isRepositorySidebarMode() && isTestPath(f2.path || ""));
26115
27396
  li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
@@ -26121,7 +27402,7 @@ code-viewer query snapshot delete --id snap-abc123`
26121
27402
  } else {
26122
27403
  const icon = document.createElement("span");
26123
27404
  icon.className = "d2h-icon-wrapper";
26124
- icon.innerHTML = fileEntryIcon();
27405
+ icon.innerHTML = sidebarEntryIcon(f2);
26125
27406
  li.appendChild(icon);
26126
27407
  }
26127
27408
  const name = document.createElement("span");
@@ -26129,6 +27410,9 @@ code-viewer query snapshot delete --id snap-abc123`
26129
27410
  name.textContent = f2.path.split("/").pop();
26130
27411
  name.title = f2.path;
26131
27412
  li.appendChild(name);
27413
+ const kindTag = fileKindTag(f2);
27414
+ if (kindTag)
27415
+ li.appendChild(kindTag);
26132
27416
  li.addEventListener("click", () => {
26133
27417
  if (onFileClick)
26134
27418
  onFileClick(f2);
@@ -26336,13 +27620,17 @@ code-viewer query snapshot delete --id snap-abc123`
26336
27620
  li.tabIndex = -1;
26337
27621
  li.dataset.index = String(i2);
26338
27622
  li.dataset.path = f2.path;
27623
+ li.dataset.type = f2.type || "blob";
27624
+ if (f2.type === "commit") {
27625
+ li.title = commitEntryBadge(f2.submodule).title;
27626
+ }
26339
27627
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
26340
27628
  if (f2.status) {
26341
27629
  li.appendChild(fileBadge(f2.status));
26342
27630
  } else {
26343
27631
  const icon = document.createElement("span");
26344
27632
  icon.className = "d2h-icon-wrapper";
26345
- icon.innerHTML = fileEntryIcon();
27633
+ icon.innerHTML = sidebarEntryIcon(f2);
26346
27634
  li.appendChild(icon);
26347
27635
  }
26348
27636
  const name = document.createElement("span");
@@ -26350,6 +27638,9 @@ code-viewer query snapshot delete --id snap-abc123`
26350
27638
  name.textContent = f2.path;
26351
27639
  name.title = f2.path;
26352
27640
  li.appendChild(name);
27641
+ const kindTag = fileKindTag(f2);
27642
+ if (kindTag)
27643
+ li.appendChild(kindTag);
26353
27644
  li.addEventListener("click", () => {
26354
27645
  if (onFileClick)
26355
27646
  onFileClick(f2);
@@ -26517,6 +27808,7 @@ code-viewer query snapshot delete --id snap-abc123`
26517
27808
  }
26518
27809
  function applyFilter() {
26519
27810
  const input = $("#sb-filter");
27811
+ syncSidebarFilterClearButton();
26520
27812
  if ($("#filelist").classList.contains("tree-virtual")) {
26521
27813
  rerenderVirtualSidebar();
26522
27814
  return;
@@ -26582,6 +27874,23 @@ code-viewer query snapshot delete --id snap-abc123`
26582
27874
  SIDEBAR_FILTER_RAF = 0;
26583
27875
  applyFilter();
26584
27876
  }
27877
+ function syncSidebarFilterClearButton() {
27878
+ const input = document.querySelector("#sb-filter");
27879
+ const button = document.querySelector("#sb-filter-clear");
27880
+ if (!input || !button)
27881
+ return;
27882
+ button.hidden = input.value.length === 0;
27883
+ }
27884
+ function clearSidebarFilter() {
27885
+ const input = document.querySelector("#sb-filter");
27886
+ if (!input?.value)
27887
+ return;
27888
+ input.value = "";
27889
+ syncSidebarFilterClearButton();
27890
+ flushSidebarFilter();
27891
+ applyFilter();
27892
+ input.focus();
27893
+ }
26585
27894
  function applySidebarWidth(w, options = {}) {
26586
27895
  const cw = Math.max(180, Math.min(900, w));
26587
27896
  document.documentElement.style.setProperty("--sidebar-w", `${cw}px`);
@@ -26917,6 +28226,8 @@ code-viewer query snapshot delete --id snap-abc123`
26917
28226
  applyFilter,
26918
28227
  scheduleApplyFilter,
26919
28228
  flushSidebarFilter,
28229
+ syncSidebarFilterClearButton,
28230
+ clearSidebarFilter,
26920
28231
  markActive,
26921
28232
  rerenderVirtualSidebar,
26922
28233
  ensureVirtualSidebarDirLoaded,
@@ -27002,7 +28313,19 @@ code-viewer query snapshot delete --id snap-abc123`
27002
28313
  syncHeaderMenu,
27003
28314
  getSidebarRowByPath,
27004
28315
  getSidebarVirtualActivePath,
27005
- pushUndo
28316
+ pushUndo,
28317
+ newFolderButtonTitle,
28318
+ openDirectoryInOsTitle,
28319
+ moveFolderToTrashTitle,
28320
+ uploadButtonLabel,
28321
+ dropFilesIntoCopy,
28322
+ uploadFailedMessage,
28323
+ emptyDirectoryLabel,
28324
+ uploadConfirmText,
28325
+ sortColumnLabels,
28326
+ repositoryFallback,
28327
+ repositoryRootFallback,
28328
+ commitEntryMeta
27006
28329
  } = deps;
27007
28330
  let REPO_SORT = {
27008
28331
  key: "name",
@@ -27045,6 +28368,15 @@ code-viewer query snapshot delete --id snap-abc123`
27045
28368
  function fileEntryIcon() {
27046
28369
  return iconSvg("octicon-file", FILE_16_PATH);
27047
28370
  }
28371
+ function commitEntryIcon() {
28372
+ return iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH);
28373
+ }
28374
+ function isWorktreeRef(ref) {
28375
+ return canTrashWorktreeRef(ref);
28376
+ }
28377
+ function canBrowseRepoEntry(entry, ref) {
28378
+ return entry.type === "tree" || entry.type === "commit" && isWorktreeRef(ref) && !entry.submodule;
28379
+ }
27048
28380
  function closeRepoContextMenu() {
27049
28381
  document.querySelector(".gdp-context-menu")?.remove();
27050
28382
  }
@@ -27223,8 +28555,9 @@ code-viewer query snapshot delete --id snap-abc123`
27223
28555
  const button = document.createElement("button");
27224
28556
  button.type = "button";
27225
28557
  button.className = "gdp-file-header-icon gdp-trash-path";
27226
- button.title = "move folder to Trash";
27227
- button.setAttribute("aria-label", "move folder to Trash");
28558
+ const trashTitle = moveFolderToTrashTitle();
28559
+ button.title = trashTitle;
28560
+ button.setAttribute("aria-label", trashTitle);
27228
28561
  button.innerHTML = iconSvg("octicon-trash", TRASH_16_PATH);
27229
28562
  button.addEventListener("click", async (event) => {
27230
28563
  event.stopPropagation();
@@ -27236,8 +28569,9 @@ code-viewer query snapshot delete --id snap-abc123`
27236
28569
  const button = document.createElement("button");
27237
28570
  button.type = "button";
27238
28571
  button.className = "gdp-file-header-icon gdp-create-dir";
27239
- button.title = "new folder";
27240
- button.setAttribute("aria-label", "new folder");
28572
+ const newFolderTitle = newFolderButtonTitle();
28573
+ button.title = newFolderTitle;
28574
+ button.setAttribute("aria-label", newFolderTitle);
27241
28575
  button.innerHTML = iconSvg("octicon-plus", PLUS_16_PATH);
27242
28576
  button.addEventListener("click", async (event) => {
27243
28577
  event.stopPropagation();
@@ -27252,7 +28586,7 @@ code-viewer query snapshot delete --id snap-abc123`
27252
28586
  dropPanel.className = "gdp-upload-panel";
27253
28587
  const copy = document.createElement("div");
27254
28588
  copy.className = "gdp-upload-copy";
27255
- copy.textContent = `Drop files into ${path || getProjectName() || "repository"}`;
28589
+ copy.textContent = dropFilesIntoCopy(path || getProjectName() || repositoryFallback());
27256
28590
  const input = document.createElement("input");
27257
28591
  input.type = "file";
27258
28592
  input.multiple = true;
@@ -27260,11 +28594,11 @@ code-viewer query snapshot delete --id snap-abc123`
27260
28594
  const button = document.createElement("button");
27261
28595
  button.type = "button";
27262
28596
  button.className = "gdp-btn gdp-btn-sm";
27263
- button.textContent = "Upload files";
28597
+ button.textContent = uploadButtonLabel();
27264
28598
  button.addEventListener("click", () => input.click());
27265
28599
  const error2 = document.createElement("div");
27266
28600
  error2.className = "gdp-upload-error";
27267
- const fail = (message = "Upload failed") => {
28601
+ const fail = (message = uploadFailedMessage()) => {
27268
28602
  error2.textContent = message;
27269
28603
  dropPanel.classList.add("failed");
27270
28604
  setTimeout(() => dropPanel.classList.remove("failed"), 1600);
@@ -27275,7 +28609,7 @@ code-viewer query snapshot delete --id snap-abc123`
27275
28609
  await uploadFiles(path, input.files);
27276
28610
  error2.textContent = "";
27277
28611
  } catch (uploadError) {
27278
- fail(uploadError instanceof Error ? uploadError.message : "Upload failed");
28612
+ fail(uploadError instanceof Error ? uploadError.message : uploadFailedMessage());
27279
28613
  } finally {
27280
28614
  input.value = "";
27281
28615
  }
@@ -27294,7 +28628,7 @@ code-viewer query snapshot delete --id snap-abc123`
27294
28628
  await uploadFiles(path, files);
27295
28629
  error2.textContent = "";
27296
28630
  } catch (uploadError) {
27297
- fail(uploadError instanceof Error ? uploadError.message : "Upload failed");
28631
+ fail(uploadError instanceof Error ? uploadError.message : uploadFailedMessage());
27298
28632
  }
27299
28633
  });
27300
28634
  dropPanel.append(copy, button, input, error2);
@@ -27314,7 +28648,7 @@ code-viewer query snapshot delete --id snap-abc123`
27314
28648
  const root = document.createElement("button");
27315
28649
  root.type = "button";
27316
28650
  root.className = path ? "gdp-file-breadcrumb-part" : "gdp-file-breadcrumb-current";
27317
- root.textContent = getProjectName() || "repository";
28651
+ root.textContent = getProjectName() || repositoryFallback();
27318
28652
  root.addEventListener("click", () => {
27319
28653
  setRoute(repoRoute(target, ""));
27320
28654
  loadRepo();
@@ -27361,7 +28695,7 @@ code-viewer query snapshot delete --id snap-abc123`
27361
28695
  pathHeader.appendChild(createRepoBreadcrumb(meta.ref, meta.path || ""));
27362
28696
  if (meta.path)
27363
28697
  pathHeader.appendChild(createCopyPathButton(meta.path));
27364
- pathHeader.appendChild(createOpenPathButton(meta.path || "", "directory", "open this folder in OS"));
28698
+ pathHeader.appendChild(createOpenPathButton(meta.path || "", "directory", openDirectoryInOsTitle()));
27365
28699
  toolbar.appendChild(pathHeader);
27366
28700
  if (canTrashWorktreeRef(meta.ref)) {
27367
28701
  toolbar.appendChild(createNewFolderButton(meta.path || "", () => loadRepo()));
@@ -27414,24 +28748,30 @@ code-viewer query snapshot delete --id snap-abc123`
27414
28748
  });
27415
28749
  list2.appendChild(row);
27416
28750
  }
27417
- sortedRepoEntries(meta.entries).forEach((entry) => {
28751
+ sortedRepoEntries(meta.entries, meta.ref).forEach((entry) => {
28752
+ const browsable = canBrowseRepoEntry(entry, meta.ref);
28753
+ const nonBrowsableCommit = entry.type === "commit" && !browsable;
27418
28754
  const row = document.createElement("button");
27419
28755
  row.type = "button";
27420
- row.className = `gdp-repo-row ${entry.type}`;
28756
+ row.className = nonBrowsableCommit ? `gdp-repo-row ${entry.type} gdp-repo-row-gitlink` : `gdp-repo-row ${entry.type}`;
27421
28757
  const icon = document.createElement("span");
27422
- icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
27423
- if (entry.type === "tree")
28758
+ icon.className = browsable ? "dir-icon" : nonBrowsableCommit ? "d2h-icon-wrapper gdp-repo-row-gitlink-icon" : "d2h-icon-wrapper";
28759
+ if (browsable)
27424
28760
  setFolderIcon(icon, true);
27425
28761
  else
27426
- icon.innerHTML = fileEntryIcon();
28762
+ icon.innerHTML = entry.type === "commit" ? commitEntryIcon() : fileEntryIcon();
27427
28763
  const name = document.createElement("span");
27428
28764
  name.className = "name";
27429
28765
  name.textContent = entry.name;
27430
- const metaBlock = createRepoEntryMeta(entry);
28766
+ if (nonBrowsableCommit) {
28767
+ row.title = commitEntryMeta(entry.submodule).title;
28768
+ row.setAttribute("aria-disabled", "true");
28769
+ }
28770
+ const metaBlock = createRepoEntryMeta(entry, browsable);
27431
28771
  const size = createRepoEntrySize(entry);
27432
28772
  row.append(icon, name, metaBlock, size);
27433
28773
  row.addEventListener("click", () => {
27434
- if (entry.type === "tree") {
28774
+ if (browsable) {
27435
28775
  setRoute(repoRoute(meta.ref, entry.path));
27436
28776
  loadRepo();
27437
28777
  } else if (entry.type === "blob") {
@@ -27451,7 +28791,7 @@ code-viewer query snapshot delete --id snap-abc123`
27451
28791
  if (!meta.entries.length) {
27452
28792
  const empty = document.createElement("div");
27453
28793
  empty.className = "gdp-repo-empty";
27454
- empty.textContent = "No files in this directory.";
28794
+ empty.textContent = emptyDirectoryLabel();
27455
28795
  list2.appendChild(empty);
27456
28796
  }
27457
28797
  };
@@ -27538,7 +28878,8 @@ code-viewer query snapshot delete --id snap-abc123`
27538
28878
  order: index + 1,
27539
28879
  path: entry.path,
27540
28880
  display_path: entry.path,
27541
- type: entry.type,
28881
+ type: canBrowseRepoEntry(entry, normalizedRef) ? "tree" : entry.type,
28882
+ submodule: entry.submodule,
27542
28883
  children_omitted: entry.children_omitted,
27543
28884
  children_omitted_reason: entry.children_omitted_reason
27544
28885
  }));
@@ -27601,12 +28942,19 @@ code-viewer query snapshot delete --id snap-abc123`
27601
28942
  }
27602
28943
  });
27603
28944
  }
27604
- function createRepoEntryMeta(entry) {
28945
+ function createRepoEntryMeta(entry, browsable) {
27605
28946
  const meta = document.createElement("span");
27606
28947
  meta.className = "meta";
28948
+ if (entry.type === "commit" && !browsable) {
28949
+ meta.classList.add("gdp-repo-row-gitlink-badge");
28950
+ const badge = commitEntryMeta(entry.submodule);
28951
+ meta.textContent = badge.label;
28952
+ meta.title = badge.title;
28953
+ return meta;
28954
+ }
27607
28955
  const updated = formatFileDate(entry.updated_at || entry.commit_updated_at);
27608
28956
  const created = formatFileDate(entry.created_at);
27609
- if (entry.type === "tree" && updated) {
28957
+ if (browsable && updated) {
27610
28958
  meta.textContent = updated;
27611
28959
  if (created)
27612
28960
  meta.title = `Created ${created}`;
@@ -27634,13 +28982,15 @@ code-viewer query snapshot delete --id snap-abc123`
27634
28982
  const time = new Date(raw).getTime();
27635
28983
  return Number.isNaN(time) ? -1 : time;
27636
28984
  }
27637
- function sortedRepoEntries(entries) {
28985
+ function sortedRepoEntries(entries, ref = "worktree") {
27638
28986
  const direction = REPO_SORT.direction === "asc" ? 1 : -1;
27639
28987
  return [...entries].sort((a2, b2) => {
27640
- if (REPO_SORT.key === "name" && a2.type !== b2.type) {
27641
- if (a2.type === "tree")
28988
+ const aBrowsable = canBrowseRepoEntry(a2, ref);
28989
+ const bBrowsable = canBrowseRepoEntry(b2, ref);
28990
+ if (REPO_SORT.key === "name" && aBrowsable !== bBrowsable) {
28991
+ if (aBrowsable)
27642
28992
  return -1;
27643
- if (b2.type === "tree")
28993
+ if (bBrowsable)
27644
28994
  return 1;
27645
28995
  }
27646
28996
  let result = 0;
@@ -27672,10 +29022,11 @@ code-viewer query snapshot delete --id snap-abc123`
27672
29022
  const spacer = document.createElement("span");
27673
29023
  spacer.className = "gdp-repo-sort-spacer";
27674
29024
  header.appendChild(spacer);
29025
+ const sortLabels = sortColumnLabels();
27675
29026
  const columns = [
27676
- { key: "name", label: "Name" },
27677
- { key: "updated", label: "Updated" },
27678
- { key: "size", label: "Size" }
29027
+ { key: "name", label: sortLabels.name },
29028
+ { key: "updated", label: sortLabels.updated },
29029
+ { key: "size", label: sortLabels.size }
27679
29030
  ];
27680
29031
  columns.forEach((column) => {
27681
29032
  const button = document.createElement("button");
@@ -27798,11 +29149,12 @@ code-viewer query snapshot delete --id snap-abc123`
27798
29149
  const list2 = Array.from(files);
27799
29150
  if (!list2.length)
27800
29151
  return;
27801
- const label = path || getProjectName() || "repository root";
29152
+ const label = path || getProjectName() || repositoryRootFallback();
29153
+ const confirmText = uploadConfirmText(list2.length, label);
27802
29154
  const ok = await showConfirmDialog({
27803
- title: "Upload files?",
27804
- body: `Upload ${list2.length} file${list2.length === 1 ? "" : "s"} into ${label}?`,
27805
- confirmLabel: "Upload"
29155
+ title: confirmText.title,
29156
+ body: confirmText.body,
29157
+ confirmLabel: confirmText.confirmLabel
27806
29158
  });
27807
29159
  if (!ok)
27808
29160
  return;
@@ -30176,13 +31528,20 @@ code-viewer query snapshot delete --id snap-abc123`
30176
31528
  const loadBar = document.querySelector("#load-bar");
30177
31529
  if (loadBar)
30178
31530
  loadBar.classList.toggle("active", state.inFlight > 0);
31531
+ const text2 = uiText().global;
31532
+ const statusEl = document.querySelector("#status");
31533
+ if (statusEl) {
31534
+ statusEl.title = state.inFlight > 0 ? text2.statusInFlightTitle(state.inFlight, state.cancellable) : statusEl.querySelector(".status-label")?.textContent ?? "";
31535
+ }
30179
31536
  const cancelButton = document.querySelector("#cancel-requests");
30180
31537
  if (!cancelButton)
30181
31538
  return;
30182
31539
  const cancellable = state.cancellable > 0;
30183
31540
  cancelButton.disabled = !cancellable;
30184
31541
  cancelButton.classList.toggle("active", cancellable);
30185
- cancelButton.title = cancellable ? `cancel ${state.cancellable} in-flight request${state.cancellable === 1 ? "" : "s"}` : "no in-flight requests";
31542
+ const cancelTitle = cancellable ? text2.cancelRequestsActiveTitle(state.cancellable) : text2.cancelRequestsInactiveTitle;
31543
+ cancelButton.title = cancelTitle;
31544
+ cancelButton.setAttribute("aria-label", cancelTitle);
30186
31545
  }
30187
31546
  function cancelInFlightRequests() {
30188
31547
  NETWORK_ACTIVITY.cancelAll();
@@ -30695,13 +32054,31 @@ code-viewer query snapshot delete --id snap-abc123`
30695
32054
  setRepoSidebarRef: (ref) => {
30696
32055
  REPO_SIDEBAR_REF = ref;
30697
32056
  },
30698
- isTestPath: (path) => TEST_RE.test(path)
32057
+ isTestPath: (path) => TEST_RE.test(path),
32058
+ sidebarToggleTitle: (hidden) => hidden ? uiText().sidebar.show : uiText().sidebar.hide,
32059
+ openDirectoryInOsTitle: () => uiText().sidebar.openDirectoryInOs,
32060
+ omittedDirectoryBadge: (reason) => {
32061
+ const text2 = uiText().sidebar;
32062
+ return reason === "heavy" ? { label: text2.omittedHeavyLabel, title: text2.omittedHeavyTitle } : { label: text2.omittedPrivateLabel, title: text2.omittedPrivateTitle };
32063
+ },
32064
+ commitEntryBadge: (submodule) => {
32065
+ const text2 = uiText().sidebar;
32066
+ return submodule ? {
32067
+ label: text2.commitEntrySubmoduleLabel,
32068
+ title: text2.commitEntrySubmoduleTitle
32069
+ } : {
32070
+ label: text2.commitEntryGitlinkLabel,
32071
+ title: text2.commitEntryGitlinkTitle
32072
+ };
32073
+ }
30699
32074
  });
30700
32075
  const {
30701
32076
  renderSidebar,
30702
32077
  applyFilter,
30703
32078
  scheduleApplyFilter,
30704
32079
  flushSidebarFilter,
32080
+ syncSidebarFilterClearButton,
32081
+ clearSidebarFilter,
30705
32082
  markActive,
30706
32083
  rerenderVirtualSidebar,
30707
32084
  ensureVirtualSidebarDirLoaded,
@@ -30838,6 +32215,35 @@ code-viewer query snapshot delete --id snap-abc123`
30838
32215
  getSidebarVirtualActivePath,
30839
32216
  pushUndo: (undo) => {
30840
32217
  UNDO_STACK.unshift(undo);
32218
+ },
32219
+ newFolderButtonTitle: () => uiText().repo.newFolder,
32220
+ openDirectoryInOsTitle: () => uiText().sidebar.openDirectoryInOs,
32221
+ moveFolderToTrashTitle: () => uiText().repo.moveFolderToTrash,
32222
+ uploadButtonLabel: () => uiText().repo.uploadButton,
32223
+ dropFilesIntoCopy: (target) => uiText().repo.dropFilesInto(target),
32224
+ uploadFailedMessage: () => uiText().repo.uploadFailed,
32225
+ emptyDirectoryLabel: () => uiText().repo.emptyDirectory,
32226
+ uploadConfirmText: (count, target) => {
32227
+ const text2 = uiText().repo;
32228
+ return {
32229
+ title: text2.uploadConfirmTitle,
32230
+ body: text2.uploadConfirmBody(count, target),
32231
+ confirmLabel: text2.uploadConfirmLabel
32232
+ };
32233
+ },
32234
+ sortColumnLabels: () => {
32235
+ const text2 = uiText().repo;
32236
+ return {
32237
+ name: text2.sortName,
32238
+ updated: text2.sortUpdated,
32239
+ size: text2.sortSize
32240
+ };
32241
+ },
32242
+ repositoryFallback: () => uiText().repo.repositoryFallback,
32243
+ repositoryRootFallback: () => uiText().repo.repositoryRootFallback,
32244
+ commitEntryMeta: (submodule) => {
32245
+ const text2 = uiText().repo;
32246
+ return submodule ? { label: text2.submoduleLabel, title: text2.submoduleTitle } : { label: text2.gitlinkLabel, title: text2.gitlinkTitle };
30841
32247
  }
30842
32248
  });
30843
32249
  const {
@@ -30880,10 +32286,20 @@ code-viewer query snapshot delete --id snap-abc123`
30880
32286
  theme: "toggle theme",
30881
32287
  product: "code viewer",
30882
32288
  copyAiContext: "Copy AI context (Shift+Click to include code)",
32289
+ copyAiContextLabel: "AI context",
30883
32290
  copyAiContextCopied: "Copied AI context",
30884
32291
  copyAiContextCopiedWithCode: (lines) => `Copied AI context + code (${lines} line${lines === 1 ? "" : "s"})`,
30885
32292
  copyAiContextFailed: "Copy failed",
30886
- copyAiContextEmpty: "Nothing to copy here"
32293
+ copyAiContextEmpty: "Nothing to copy here",
32294
+ statusLive: "Live",
32295
+ statusLoading: "Loading",
32296
+ statusError: "Error",
32297
+ statusIdle: "Idle",
32298
+ statusInFlightTitle: (count, cancellable) => `${count} request${count === 1 ? "" : "s"} in flight${cancellable > 0 ? " (cancellable)" : ""}`,
32299
+ cancelRequestsActiveTitle: (count) => `cancel ${count} in-flight request${count === 1 ? "" : "s"}`,
32300
+ cancelRequestsInactiveTitle: "no in-flight requests",
32301
+ brandHome: "Repository home",
32302
+ menuViews: "Views"
30887
32303
  },
30888
32304
  topbar: {
30889
32305
  resetRange: "reset to HEAD .. worktree",
@@ -30892,6 +32308,7 @@ code-viewer query snapshot delete --id snap-abc123`
30892
32308
  unified: "unified",
30893
32309
  split: "split",
30894
32310
  ignoreWs: "ignore whitespace changes (-w)",
32311
+ ignoreWsLabel: "ws",
30895
32312
  syntaxLoading: "loading...",
30896
32313
  syntaxOn: "syntax on",
30897
32314
  syntaxOff: "syntax off",
@@ -30900,10 +32317,38 @@ code-viewer query snapshot delete --id snap-abc123`
30900
32317
  syntaxErrorTitle: "failed to load syntax highlighter",
30901
32318
  syntaxOffTitle: "syntax highlighting off",
30902
32319
  hideTests: "hide test files (test|spec)",
32320
+ hideTestsLabel: "no test",
30903
32321
  autoUpdate: "auto",
30904
32322
  autoUpdateOnTitle: "auto update on file change",
30905
32323
  autoUpdateOffTitle: "auto update off — manual reload"
30906
32324
  },
32325
+ diff: {
32326
+ files: (count) => `${count} file${count === 1 ? "" : "s"}`,
32327
+ updated: (time) => `updated ${time}`,
32328
+ updatedTitle: "last updated",
32329
+ kindAdded: "added",
32330
+ kindDeleted: "deleted",
32331
+ kindRenamed: "renamed",
32332
+ kindHeavy: "heavy",
32333
+ kindBinary: "binary",
32334
+ kindMedia: "media",
32335
+ viewedProgress: (viewed, total) => `${viewed}/${total} viewed`,
32336
+ viewedProgressTitle: "review progress",
32337
+ nextUnviewed: "next unviewed",
32338
+ nextUnviewedTitle: "Jump to the next unviewed file (n)",
32339
+ allViewed: "all viewed",
32340
+ allViewedTitle: "All visible files are viewed",
32341
+ noChangesTitle: "No changes",
32342
+ noChangesBody: "The working tree is clean against this ref.",
32343
+ noChangesReload: "Reload diff",
32344
+ noChangesReloadTitle: "Reload this diff range",
32345
+ noChangesHistory: "Open history",
32346
+ noChangesHistoryTitle: "Open commit history for this range",
32347
+ emptyDiffTitle: "Empty diff",
32348
+ emptyDiffBody: "This commit has no changes against its first parent.",
32349
+ noCommitSelectedTitle: "No commit selected",
32350
+ noCommitSelectedBody: "Select a commit from the list to see its changes."
32351
+ },
30907
32352
  changeBanner: {
30908
32353
  text: "Files changed",
30909
32354
  reload: "Reload",
@@ -30923,14 +32368,56 @@ code-viewer query snapshot delete --id snap-abc123`
30923
32368
  view: "view",
30924
32369
  tree: "tree",
30925
32370
  flat: "flat",
32371
+ treeTitle: "tree view",
32372
+ flatTitle: "flat list",
30926
32373
  filter: "Filter files… / ⌘K",
30927
32374
  filterTitle: "Filter files. Use /pattern/ for regex. Press / to focus this field, Cmd/Ctrl+K for the full-file palette, Ctrl+G for grep, ? for help.",
30928
- hide: "hide sidebar"
32375
+ filterClear: "Clear",
32376
+ filterClearTitle: "Clear file filter",
32377
+ hide: "hide sidebar",
32378
+ show: "show sidebar",
32379
+ repoTarget: "repository target",
32380
+ openDirectoryInOs: "open this folder in OS",
32381
+ omittedHeavyLabel: "skipped",
32382
+ omittedHeavyTitle: "Tree expansion is skipped, but the directory detail can be opened",
32383
+ omittedPrivateLabel: "private",
32384
+ omittedPrivateTitle: "This directory cannot be opened from the browser",
32385
+ commitEntryGitlinkLabel: "GIT",
32386
+ commitEntryGitlinkTitle: "Git commit entry",
32387
+ commitEntrySubmoduleLabel: "SUB",
32388
+ commitEntrySubmoduleTitle: "Git submodule pinned to a commit"
32389
+ },
32390
+ repo: {
32391
+ newFolder: "new folder",
32392
+ moveFolderToTrash: "move folder to Trash",
32393
+ uploadButton: "Upload files",
32394
+ dropFilesInto: (target) => `Drop files into ${target}`,
32395
+ uploadFailed: "Upload failed",
32396
+ emptyDirectory: "No files in this directory.",
32397
+ uploadConfirmTitle: "Upload files?",
32398
+ uploadConfirmBody: (count, target) => `Upload ${count} file${count === 1 ? "" : "s"} into ${target}?`,
32399
+ uploadConfirmLabel: "Upload",
32400
+ sortName: "Name",
32401
+ sortUpdated: "Updated",
32402
+ sortSize: "Size",
32403
+ repositoryFallback: "repository",
32404
+ repositoryRootFallback: "repository root",
32405
+ gitlinkLabel: "gitlink",
32406
+ gitlinkTitle: "Git commit entry is not directly browsable at this ref",
32407
+ submoduleLabel: "submodule",
32408
+ submoduleTitle: "Git submodule pinned to a commit"
30929
32409
  },
30930
32410
  history: {
30931
32411
  title: "Commits",
30932
32412
  filter: "Filter commits...",
30933
- filterTitle: "Filter commits by message, SHA, author:name, or path:file."
32413
+ filterTitle: "Filter commits by message, SHA, author:name, or path:file.",
32414
+ refreshTitle: "Refresh commit history"
32415
+ },
32416
+ quickHelp: {
32417
+ buttonTitle: "quick help (shortcuts)",
32418
+ panelTitle: "Quick Help",
32419
+ close: "close quick help",
32420
+ viewAll: "View all keybindings →"
30934
32421
  },
30935
32422
  settings: {
30936
32423
  title: "Viewer Settings",
@@ -30991,30 +32478,69 @@ code-viewer query snapshot delete --id snap-abc123`
30991
32478
  theme: "テーマ切り替え",
30992
32479
  product: "code viewer",
30993
32480
  copyAiContext: "AI 用コンテキストをコピー(Shift+Click でコードも添付)",
32481
+ copyAiContextLabel: "AI文脈",
30994
32482
  copyAiContextCopied: "コピーしました",
30995
32483
  copyAiContextCopiedWithCode: (lines) => `コピーしました(コード付き・${lines}行)`,
30996
32484
  copyAiContextFailed: "コピーに失敗しました",
30997
- copyAiContextEmpty: "コピーする内容がありません"
32485
+ copyAiContextEmpty: "コピーする内容がありません",
32486
+ statusLive: "稼働中",
32487
+ statusLoading: "更新中",
32488
+ statusError: "エラー",
32489
+ statusIdle: "待機中",
32490
+ statusInFlightTitle: (count, cancellable) => `${count}件のリクエストを実行中${cancellable > 0 ? "(キャンセル可能)" : ""}`,
32491
+ cancelRequestsActiveTitle: (count) => `実行中のリクエストを${count}件キャンセル`,
32492
+ cancelRequestsInactiveTitle: "実行中のリクエストはありません",
32493
+ brandHome: "リポジトリホーム",
32494
+ menuViews: "ビュー切り替え"
30998
32495
  },
30999
32496
  topbar: {
31000
32497
  resetRange: "HEAD .. worktree に戻す",
31001
32498
  reload: "diff を再読み込み (R)",
31002
32499
  layout: "レイアウト",
31003
- unified: "unified",
31004
- split: "split",
32500
+ unified: "統合",
32501
+ split: "分割",
31005
32502
  ignoreWs: "空白差分を無視 (-w)",
32503
+ ignoreWsLabel: "空白",
31006
32504
  syntaxLoading: "読み込み中...",
31007
- syntaxOn: "syntax on",
31008
- syntaxOff: "syntax off",
32505
+ syntaxOn: "構文あり",
32506
+ syntaxOff: "構文なし",
31009
32507
  syntaxOnTitle: "シンタックスハイライト有効",
31010
32508
  syntaxLoadingTitle: "シンタックスハイライトを読み込み中",
31011
32509
  syntaxErrorTitle: "シンタックスハイライトの読み込みに失敗",
31012
32510
  syntaxOffTitle: "シンタックスハイライト無効",
31013
32511
  hideTests: "test/spec ファイルを隠す",
32512
+ hideTestsLabel: "テスト非表示",
31014
32513
  autoUpdate: "自動",
31015
32514
  autoUpdateOnTitle: "ファイル変更時に自動更新",
31016
32515
  autoUpdateOffTitle: "自動更新オフ — 手動で再読み込み"
31017
32516
  },
32517
+ diff: {
32518
+ files: (count) => `${count}ファイル`,
32519
+ updated: (time) => `更新 ${time}`,
32520
+ updatedTitle: "最終更新",
32521
+ kindAdded: "追加",
32522
+ kindDeleted: "削除",
32523
+ kindRenamed: "名前変更",
32524
+ kindHeavy: "大容量",
32525
+ kindBinary: "バイナリ",
32526
+ kindMedia: "メディア",
32527
+ viewedProgress: (viewed, total) => `${viewed}/${total} 確認済み`,
32528
+ viewedProgressTitle: "確認進捗",
32529
+ nextUnviewed: "次の未確認",
32530
+ nextUnviewedTitle: "次の未確認ファイルへ移動 (n)",
32531
+ allViewed: "すべて確認済み",
32532
+ allViewedTitle: "表示中のファイルはすべて確認済みです",
32533
+ noChangesTitle: "変更はありません",
32534
+ noChangesBody: "この参照との差分はありません。",
32535
+ noChangesReload: "diff を更新",
32536
+ noChangesReloadTitle: "この差分範囲を再読み込み",
32537
+ noChangesHistory: "履歴を開く",
32538
+ noChangesHistoryTitle: "この範囲のコミット履歴を開く",
32539
+ emptyDiffTitle: "空の差分",
32540
+ emptyDiffBody: "このコミットは最初の親との差分がありません。",
32541
+ noCommitSelectedTitle: "コミット未選択",
32542
+ noCommitSelectedBody: "一覧からコミットを選ぶと変更内容を表示します。"
32543
+ },
31018
32544
  changeBanner: {
31019
32545
  text: "ファイルに変更がありました",
31020
32546
  reload: "再読み込みする",
@@ -31034,14 +32560,56 @@ code-viewer query snapshot delete --id snap-abc123`
31034
32560
  view: "表示",
31035
32561
  tree: "ツリー",
31036
32562
  flat: "一覧",
32563
+ treeTitle: "ツリー表示",
32564
+ flatTitle: "一覧表示",
31037
32565
  filter: "ファイル絞り込み… / ⌘K",
31038
32566
  filterTitle: "ファイルを絞り込みます。/pattern/ は正規表現。/ でこの欄にフォーカス、Cmd/Ctrl+K で全ファイルパレット、Ctrl+G で grep、? でヘルプ。",
31039
- hide: "サイドバーを隠す"
32567
+ filterClear: "解除",
32568
+ filterClearTitle: "ファイル絞り込みを解除",
32569
+ hide: "サイドバーを隠す",
32570
+ show: "サイドバーを表示",
32571
+ repoTarget: "リポジトリの対象",
32572
+ openDirectoryInOs: "このフォルダをOSで開く",
32573
+ omittedHeavyLabel: "省略",
32574
+ omittedHeavyTitle: "ツリー展開は省略されていますが、詳細パネルでは開けます",
32575
+ omittedPrivateLabel: "非公開",
32576
+ omittedPrivateTitle: "このディレクトリはブラウザから開けません",
32577
+ commitEntryGitlinkLabel: "GIT",
32578
+ commitEntryGitlinkTitle: "Git のコミットに固定された参照です。フォルダではないため直接は開けません。",
32579
+ commitEntrySubmoduleLabel: "SUB",
32580
+ commitEntrySubmoduleTitle: "Git サブモジュール: 特定のコミットに固定されています。フォルダではないため直接は開けません。"
32581
+ },
32582
+ repo: {
32583
+ newFolder: "新規フォルダ",
32584
+ moveFolderToTrash: "フォルダをゴミ箱へ移動",
32585
+ uploadButton: "ファイルをアップロード",
32586
+ dropFilesInto: (target) => `${target} にファイルをドロップ`,
32587
+ uploadFailed: "アップロードに失敗しました",
32588
+ emptyDirectory: "このディレクトリにファイルはありません。",
32589
+ uploadConfirmTitle: "ファイルをアップロードしますか?",
32590
+ uploadConfirmBody: (count, target) => `${target} に ${count} 件のファイルをアップロードしますか?`,
32591
+ uploadConfirmLabel: "アップロード",
32592
+ sortName: "名前",
32593
+ sortUpdated: "更新日時",
32594
+ sortSize: "サイズ",
32595
+ repositoryFallback: "リポジトリ",
32596
+ repositoryRootFallback: "リポジトリのルート",
32597
+ gitlinkLabel: "固定コミット",
32598
+ gitlinkTitle: "特定のコミットに固定された参照です。この ref では直接開けません。",
32599
+ submoduleLabel: "サブモジュール",
32600
+ submoduleTitle: "Git サブモジュール: 特定のコミットに固定されています。直接は開けません。"
31040
32601
  },
31041
32602
  history: {
31042
32603
  title: "コミット",
31043
32604
  filter: "コミットを絞り込み...",
31044
- filterTitle: "メッセージ、SHA、author:name、path:file でコミットを絞り込みます。"
32605
+ filterTitle: "メッセージ、SHA、author:name、path:file でコミットを絞り込みます。",
32606
+ refreshTitle: "コミット履歴を更新"
32607
+ },
32608
+ quickHelp: {
32609
+ buttonTitle: "クイックヘルプ(ショートカット)",
32610
+ panelTitle: "クイックヘルプ",
32611
+ close: "クイックヘルプを閉じる",
32612
+ viewAll: "すべてのキーバインドを見る →"
31045
32613
  },
31046
32614
  settings: {
31047
32615
  title: "ビューア設定",
@@ -31117,6 +32685,8 @@ code-viewer query snapshot delete --id snap-abc123`
31117
32685
  });
31118
32686
  setElementText(".global-help-link[data-route='help']", text2.nav.help);
31119
32687
  setElementText(".product-label", text2.global.product);
32688
+ document.querySelector(".brand")?.setAttribute("aria-label", text2.global.brandHome);
32689
+ document.querySelector(".app-menu")?.setAttribute("aria-label", text2.global.menuViews);
31120
32690
  const annotationsToggle = document.querySelector("#annotations-toggle");
31121
32691
  if (annotationsToggle) {
31122
32692
  annotationsToggle.title = text2.global.annotations;
@@ -31132,27 +32702,51 @@ code-viewer query snapshot delete --id snap-abc123`
31132
32702
  theme.title = text2.global.theme;
31133
32703
  theme.setAttribute("aria-label", text2.global.theme);
31134
32704
  }
32705
+ const quickHelpBtn = document.querySelector("#quick-help-btn");
32706
+ if (quickHelpBtn) {
32707
+ quickHelpBtn.title = text2.quickHelp.buttonTitle;
32708
+ quickHelpBtn.setAttribute("aria-label", text2.quickHelp.buttonTitle);
32709
+ }
32710
+ QUICK_HELP?.localize();
32711
+ const doctorTitle = doctorText(STATE.language).title;
32712
+ const doctorBtn = document.querySelector("#doctor-btn");
32713
+ if (doctorBtn) {
32714
+ doctorBtn.title = doctorTitle;
32715
+ doctorBtn.setAttribute("aria-label", doctorTitle);
32716
+ }
32717
+ document.querySelector("#doctor-sheet")?.setAttribute("aria-label", doctorTitle);
31135
32718
  const copyAiContext = document.querySelector("#copy-ai-context");
31136
32719
  if (copyAiContext) {
31137
32720
  copyAiContext.title = text2.global.copyAiContext;
31138
32721
  copyAiContext.setAttribute("aria-label", text2.global.copyAiContext);
32722
+ const copyAiContextLabel = copyAiContext.querySelector(".ai-context-label");
32723
+ if (copyAiContextLabel)
32724
+ copyAiContextLabel.textContent = text2.global.copyAiContextLabel;
31139
32725
  }
31140
32726
  const refReset = document.querySelector("#ref-reset");
31141
- if (refReset)
32727
+ if (refReset) {
31142
32728
  refReset.title = text2.topbar.resetRange;
32729
+ refReset.setAttribute("aria-label", text2.topbar.resetRange);
32730
+ }
31143
32731
  const reload = document.querySelector("#reload-prom");
31144
- if (reload)
32732
+ if (reload) {
31145
32733
  reload.title = text2.topbar.reload;
32734
+ reload.setAttribute("aria-label", text2.topbar.reload);
32735
+ }
31146
32736
  const layoutGroup = document.querySelector("#topbar .seg");
31147
32737
  layoutGroup?.setAttribute("aria-label", text2.topbar.layout);
31148
32738
  setElementText('#topbar .seg button[data-layout="line-by-line"]', text2.topbar.unified);
31149
32739
  setElementText('#topbar .seg button[data-layout="side-by-side"]', text2.topbar.split);
31150
32740
  const ignoreWs = document.querySelector("#ignore-ws");
31151
- if (ignoreWs)
32741
+ if (ignoreWs) {
31152
32742
  ignoreWs.title = text2.topbar.ignoreWs;
32743
+ ignoreWs.textContent = text2.topbar.ignoreWsLabel;
32744
+ }
31153
32745
  const hideTests = document.querySelector("#hide-tests");
31154
- if (hideTests)
32746
+ if (hideTests) {
31155
32747
  hideTests.title = text2.topbar.hideTests;
32748
+ hideTests.textContent = text2.topbar.hideTestsLabel;
32749
+ }
31156
32750
  applyAutoUpdateButton();
31157
32751
  setHighlightButton(STATE.syntaxHighlight && getHljs() ? "loaded" : "idle");
31158
32752
  setElementText(".sb-title", text2.sidebar.files);
@@ -31172,15 +32766,33 @@ code-viewer query snapshot delete --id snap-abc123`
31172
32766
  sbView?.setAttribute("aria-label", text2.sidebar.view);
31173
32767
  setElementText('.sb-view-seg button[data-view="tree"]', text2.sidebar.tree);
31174
32768
  setElementText('.sb-view-seg button[data-view="flat"]', text2.sidebar.flat);
32769
+ const sbViewTree = document.querySelector('.sb-view-seg button[data-view="tree"]');
32770
+ if (sbViewTree)
32771
+ sbViewTree.title = text2.sidebar.treeTitle;
32772
+ const sbViewFlat = document.querySelector('.sb-view-seg button[data-view="flat"]');
32773
+ if (sbViewFlat)
32774
+ sbViewFlat.title = text2.sidebar.flatTitle;
31175
32775
  const filter = document.querySelector("#sb-filter");
31176
32776
  if (filter) {
31177
32777
  filter.placeholder = text2.sidebar.filter;
31178
32778
  filter.title = text2.sidebar.filterTitle;
31179
32779
  }
32780
+ const filterClear = document.querySelector("#sb-filter-clear");
32781
+ if (filterClear) {
32782
+ filterClear.textContent = text2.sidebar.filterClear;
32783
+ filterClear.title = text2.sidebar.filterClearTitle;
32784
+ filterClear.setAttribute("aria-label", text2.sidebar.filterClearTitle);
32785
+ }
32786
+ const repoTarget = document.querySelector("#repo-target");
32787
+ if (repoTarget) {
32788
+ repoTarget.title = text2.sidebar.repoTarget;
32789
+ repoTarget.setAttribute("aria-label", text2.sidebar.repoTarget);
32790
+ }
31180
32791
  const sidebarToggle = document.querySelector("#sidebar-toggle");
31181
32792
  if (sidebarToggle) {
31182
- sidebarToggle.title = text2.sidebar.hide;
31183
- sidebarToggle.setAttribute("aria-label", text2.sidebar.hide);
32793
+ const sidebarToggleTitle = STATE.sidebarHidden ? text2.sidebar.show : text2.sidebar.hide;
32794
+ sidebarToggle.title = sidebarToggleTitle;
32795
+ sidebarToggle.setAttribute("aria-label", sidebarToggleTitle);
31184
32796
  }
31185
32797
  setElementText(".sidebar-toggle-label", text2.sidebar.files);
31186
32798
  setElementText(".history-title", text2.history.title);
@@ -31191,6 +32803,11 @@ code-viewer query snapshot delete --id snap-abc123`
31191
32803
  historyFilter.placeholder = text2.history.filter;
31192
32804
  historyFilter.title = text2.history.filterTitle;
31193
32805
  }
32806
+ document.querySelectorAll(".history-refresh").forEach((button) => {
32807
+ button.title = text2.history.refreshTitle;
32808
+ button.setAttribute("aria-label", text2.history.refreshTitle);
32809
+ });
32810
+ relocalizeHistory?.();
31194
32811
  setElementText(".scope-settings-head strong", text2.settings.title);
31195
32812
  const settingsClose = document.querySelector("#scope-settings-close");
31196
32813
  settingsClose?.setAttribute("aria-label", text2.settings.close);
@@ -31259,7 +32876,9 @@ code-viewer query snapshot delete --id snap-abc123`
31259
32876
  setButtonLabel(document.querySelector("#query-history-panel-close"), text2.annotations.close);
31260
32877
  relocalizeDatabase?.();
31261
32878
  }
32879
+ let relocalizeHistory = null;
31262
32880
  let relocalizeDatabase = null;
32881
+ let QUICK_HELP = null;
31263
32882
  function setViewerLanguage(language, persist = true) {
31264
32883
  const next = normalizeViewerLanguage(language);
31265
32884
  STATE.language = next;
@@ -31286,6 +32905,13 @@ code-viewer query snapshot delete --id snap-abc123`
31286
32905
  el.classList.remove("live", "refreshing", "error");
31287
32906
  if (s2)
31288
32907
  el.classList.add(s2);
32908
+ const text2 = uiText();
32909
+ const label = s2 === "live" ? text2.global.statusLive : s2 === "refreshing" ? text2.global.statusLoading : s2 === "error" ? text2.global.statusError : text2.global.statusIdle;
32910
+ const labelEl = el.querySelector(".status-label");
32911
+ if (labelEl)
32912
+ labelEl.textContent = label;
32913
+ el.setAttribute("aria-label", label);
32914
+ updateNetworkActivity();
31289
32915
  }
31290
32916
  function applyTheme() {
31291
32917
  document.documentElement.dataset.theme = STATE.theme;
@@ -31587,8 +33213,10 @@ code-viewer query snapshot delete --id snap-abc123`
31587
33213
  input.readOnly = true;
31588
33214
  input.autocomplete = "off";
31589
33215
  input.placeholder = options.placeholder;
31590
- if (options.title)
33216
+ if (options.title) {
31591
33217
  input.title = options.title;
33218
+ input.setAttribute("aria-label", options.title);
33219
+ }
31592
33220
  if (options.value != null)
31593
33221
  input.value = options.value;
31594
33222
  const caret = document.createElement("span");
@@ -31692,7 +33320,8 @@ code-viewer query snapshot delete --id snap-abc123`
31692
33320
  currentRange,
31693
33321
  setRoute,
31694
33322
  setPreferredSourceTab: (tab) => SOURCE_VIEW.setPreferredSourceTab(tab),
31695
- createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref)
33323
+ createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref),
33324
+ emptyText: () => uiText().diff
31696
33325
  }, historyRoute);
31697
33326
  activeFileHistoryDiffHost = mount.diffHost;
31698
33327
  activeFileHistoryEmptyHost = mount.emptyHost;
@@ -31705,6 +33334,9 @@ code-viewer query snapshot delete --id snap-abc123`
31705
33334
  function withAnnotationSessionParam(rawUrl) {
31706
33335
  return ANNOTATIONS_UI ? ANNOTATIONS_UI.withSessionParam(rawUrl) : rawUrl;
31707
33336
  }
33337
+ function urlForRoute(route) {
33338
+ return withDoctorOverlay(withAnnotationSessionParam(buildRoute(route)), parseDoctorOverlay(window.location.pathname, window.location.search));
33339
+ }
31708
33340
  function historyStateForRoute(route) {
31709
33341
  return route.screen === "file" ? {
31710
33342
  screen: "file",
@@ -31779,7 +33411,7 @@ code-viewer query snapshot delete --id snap-abc123`
31779
33411
  if (nextRoute.screen === "repo" || nextRoute.screen === "file" && (nextRoute.view === "blob" || nextRoute.view === "blame" || nextRoute.view === "history")) {
31780
33412
  STATE.repoRef = nextRoute.ref || "worktree";
31781
33413
  }
31782
- const url = withDoctorOverlay(withAnnotationSessionParam(buildRoute(nextRoute)), parseDoctorOverlay(window.location.pathname, window.location.search));
33414
+ const url = urlForRoute(nextRoute);
31783
33415
  const state = historyStateForRoute(nextRoute);
31784
33416
  if (replace2)
31785
33417
  history.replaceState(state, "", url);
@@ -32024,6 +33656,7 @@ code-viewer query snapshot delete --id snap-abc123`
32024
33656
  SERVER_GENERATION = generation;
32025
33657
  },
32026
33658
  invalidateRepoSidebar,
33659
+ diffText: () => uiText().diff,
32027
33660
  getDiffRoot: () => activeFileHistoryDiffHost || $("#diff"),
32028
33661
  getEmptyPane: () => activeFileHistoryEmptyHost || $("#empty"),
32029
33662
  isEmbeddedDiffMode: () => !!activeFileHistoryDiffHost
@@ -32057,10 +33690,26 @@ code-viewer query snapshot delete --id snap-abc123`
32057
33690
  if (themeButton) {
32058
33691
  themeButton.innerHTML = iconSvg("octicon-moon", MOON_16_PATH);
32059
33692
  }
32060
- const copyAiContextButton = document.querySelector("#copy-ai-context");
32061
- if (copyAiContextButton) {
32062
- copyAiContextButton.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
33693
+ const copyAiContextIcon = document.querySelector("#copy-ai-context .goi-icon");
33694
+ if (copyAiContextIcon) {
33695
+ copyAiContextIcon.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
33696
+ }
33697
+ const autoUpdateIcon = document.querySelector("#auto-update .goi-icon");
33698
+ if (autoUpdateIcon) {
33699
+ autoUpdateIcon.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
32063
33700
  }
33701
+ const cancelRequestsIcon = document.querySelector("#cancel-requests .goi-icon");
33702
+ if (cancelRequestsIcon) {
33703
+ cancelRequestsIcon.innerHTML = iconSvg("octicon-x", X_16_PATH);
33704
+ }
33705
+ }
33706
+ function setRefActionIcons() {
33707
+ const refReset = document.querySelector("#ref-reset");
33708
+ if (refReset)
33709
+ refReset.innerHTML = iconSvg("octicon-x", X_16_PATH);
33710
+ const reload = document.querySelector("#reload-prom");
33711
+ if (reload)
33712
+ reload.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
32064
33713
  }
32065
33714
  applySidebarFontSize();
32066
33715
  applyCodeFontSize();
@@ -32070,6 +33719,7 @@ code-viewer query snapshot delete --id snap-abc123`
32070
33719
  hydrateRefSelectorMounts();
32071
33720
  setSidebarTreeActionIcons();
32072
33721
  setGlobalHeaderIcons();
33722
+ setRefActionIcons();
32073
33723
  $$(".sb-view-seg button").forEach((b2) => {
32074
33724
  b2.addEventListener("click", () => {
32075
33725
  STATE.sbView = b2.dataset.view || "tree";
@@ -32100,11 +33750,15 @@ code-viewer query snapshot delete --id snap-abc123`
32100
33750
  };
32101
33751
  }
32102
33752
  }
33753
+ const databaseQuerySql = STATE.route.screen === "database" && STATE.route.tab === "query" ? document.querySelector(".db-container:not([hidden]) .db-query-editor:not([hidden]) .db-query-textarea")?.value : undefined;
32103
33754
  const text2 = aiContextClipboardText({
32104
33755
  route: STATE.route,
32105
33756
  diffFrom: STATE.from,
32106
33757
  diffTo: STATE.to,
32107
- selectionCode
33758
+ selectionCode,
33759
+ diffMeta: window._lastMeta ? visibleDiffMetaForBrief(window._lastMeta) : null,
33760
+ viewedFiles: STATE.viewedFiles,
33761
+ databaseQuerySql
32108
33762
  });
32109
33763
  const finish = (ok, withCode, lineCount, reason) => {
32110
33764
  const label = reason === "empty" ? uiText().global.copyAiContextEmpty : ok ? withCode ? uiText().global.copyAiContextCopiedWithCode(lineCount) : uiText().global.copyAiContextCopied : uiText().global.copyAiContextFailed;
@@ -32336,7 +33990,10 @@ code-viewer query snapshot delete --id snap-abc123`
32336
33990
  }
32337
33991
  const sbFilter = $("#sb-filter");
32338
33992
  if (sbFilter) {
32339
- sbFilter.addEventListener("input", () => scheduleApplyFilter());
33993
+ sbFilter.addEventListener("input", () => {
33994
+ syncSidebarFilterClearButton();
33995
+ scheduleApplyFilter();
33996
+ });
32340
33997
  sbFilter.addEventListener("keydown", (e2) => {
32341
33998
  if (isImeComposing(e2))
32342
33999
  return;
@@ -32351,6 +34008,7 @@ code-viewer query snapshot delete --id snap-abc123`
32351
34008
  } else if (e2.key === "Escape") {
32352
34009
  if (sbFilter.value) {
32353
34010
  sbFilter.value = "";
34011
+ syncSidebarFilterClearButton();
32354
34012
  flushSidebarFilter();
32355
34013
  applyFilter();
32356
34014
  } else {
@@ -32359,6 +34017,11 @@ code-viewer query snapshot delete --id snap-abc123`
32359
34017
  }
32360
34018
  });
32361
34019
  }
34020
+ const sbFilterClear = document.querySelector("#sb-filter-clear");
34021
+ if (sbFilterClear) {
34022
+ syncSidebarFilterClearButton();
34023
+ sbFilterClear.addEventListener("click", clearSidebarFilter);
34024
+ }
32362
34025
  function focusFileFilter() {
32363
34026
  const input = $("#sb-filter");
32364
34027
  input.focus();
@@ -32510,17 +34173,13 @@ code-viewer query snapshot delete --id snap-abc123`
32510
34173
  }));
32511
34174
  return true;
32512
34175
  }
34176
+ if (action === "next-unviewed-file") {
34177
+ if (DIFF_VIEW.scrollToNextUnviewedFile())
34178
+ scheduleMainSurfaceFocus();
34179
+ return true;
34180
+ }
32513
34181
  if (action === "open-help") {
32514
- openHelpKeybindings({
32515
- getRoute: () => STATE.route,
32516
- getLanguage: () => STATE.language,
32517
- currentRange,
32518
- setRoute,
32519
- setPageMode,
32520
- renderHelpPage,
32521
- setStatus,
32522
- cancelActiveSourceLoad
32523
- });
34182
+ QUICK_HELP?.toggle();
32524
34183
  return true;
32525
34184
  }
32526
34185
  return false;
@@ -32573,6 +34232,103 @@ code-viewer query snapshot delete --id snap-abc123`
32573
34232
  if (window.location.pathname === "/") {
32574
34233
  setRoute(STATE.route, true);
32575
34234
  }
34235
+ function normalizedHistoryRefForEmptyDiff() {
34236
+ const candidate = STATE.to && STATE.to !== "worktree" ? STATE.to : STATE.from || "HEAD";
34237
+ return candidate && candidate !== "worktree" && !candidate.startsWith("--") ? candidate : "HEAD";
34238
+ }
34239
+ function emptyDiffHistoryRoute() {
34240
+ return {
34241
+ screen: "history",
34242
+ ref: normalizedHistoryRefForEmptyDiff(),
34243
+ range: currentRange()
34244
+ };
34245
+ }
34246
+ function setEmptyActionContent(action, iconName, iconPath, label, title) {
34247
+ action.title = title;
34248
+ action.setAttribute("aria-label", title);
34249
+ action.replaceChildren();
34250
+ const icon = document.createElement("span");
34251
+ icon.className = "empty-action-icon";
34252
+ icon.setAttribute("aria-hidden", "true");
34253
+ icon.innerHTML = iconSvg(iconName, iconPath);
34254
+ const text2 = document.createElement("span");
34255
+ text2.className = "empty-action-label";
34256
+ text2.textContent = label;
34257
+ action.append(icon, text2);
34258
+ }
34259
+ function navigateToEmptyDiffHistory(event) {
34260
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
34261
+ return;
34262
+ event.preventDefault();
34263
+ const route = emptyDiffHistoryRoute();
34264
+ history.pushState(historyStateForRoute(route), "", urlForRoute(route));
34265
+ window.scrollTo(0, 0);
34266
+ applyRouteFromLocation();
34267
+ }
34268
+ function ensureEmptyDiffActions(empty) {
34269
+ let actions = empty.querySelector(".empty-actions");
34270
+ if (actions)
34271
+ return actions;
34272
+ actions = document.createElement("div");
34273
+ actions.className = "empty-actions";
34274
+ actions.hidden = true;
34275
+ const reload = document.createElement("button");
34276
+ reload.type = "button";
34277
+ reload.className = "empty-action empty-action-primary";
34278
+ reload.dataset.emptyAction = "reload";
34279
+ reload.addEventListener("click", () => reloadDiffFromUi(reload));
34280
+ const historyLink = document.createElement("a");
34281
+ historyLink.className = "empty-action";
34282
+ historyLink.dataset.emptyAction = "history";
34283
+ historyLink.addEventListener("click", navigateToEmptyDiffHistory);
34284
+ actions.append(reload, historyLink);
34285
+ empty.appendChild(actions);
34286
+ return actions;
34287
+ }
34288
+ function syncEmptyDiffPane(empty, onHistory) {
34289
+ empty.classList.toggle("empty-with-actions", !onHistory);
34290
+ const text2 = uiText().diff;
34291
+ const h2 = empty.querySelector("h2");
34292
+ if (h2)
34293
+ h2.textContent = onHistory ? text2.emptyDiffTitle : text2.noChangesTitle;
34294
+ const p2 = empty.querySelector("p");
34295
+ if (p2)
34296
+ p2.textContent = onHistory ? text2.emptyDiffBody : text2.noChangesBody;
34297
+ const existingActions = empty.querySelector(".empty-actions");
34298
+ if (onHistory) {
34299
+ if (existingActions)
34300
+ existingActions.hidden = true;
34301
+ return;
34302
+ }
34303
+ const actions = ensureEmptyDiffActions(empty);
34304
+ const reload = actions.querySelector('[data-empty-action="reload"]');
34305
+ const historyLink = actions.querySelector('[data-empty-action="history"]');
34306
+ if (reload)
34307
+ setEmptyActionContent(reload, "octicon-sync", SYNC_16_PATH, text2.noChangesReload, text2.noChangesReloadTitle);
34308
+ if (historyLink) {
34309
+ const route = emptyDiffHistoryRoute();
34310
+ historyLink.href = urlForRoute(route);
34311
+ setEmptyActionContent(historyLink, "octicon-git-branch", GIT_BRANCH_16_PATH, text2.noChangesHistory, text2.noChangesHistoryTitle);
34312
+ }
34313
+ actions.hidden = false;
34314
+ }
34315
+ function reloadDiffFromUi(trigger) {
34316
+ const topbarButton = $("#reload-prom");
34317
+ topbarButton.classList.add("spinning");
34318
+ if (trigger && trigger !== topbarButton) {
34319
+ trigger.classList.add("spinning");
34320
+ trigger.setAttribute("aria-busy", "true");
34321
+ }
34322
+ load().finally(() => {
34323
+ setTimeout(() => {
34324
+ topbarButton.classList.remove("spinning");
34325
+ if (trigger && trigger !== topbarButton) {
34326
+ trigger.classList.remove("spinning");
34327
+ trigger.setAttribute("aria-busy", "false");
34328
+ }
34329
+ }, 200);
34330
+ });
34331
+ }
32576
34332
  function load(options = {}) {
32577
34333
  if (STATE.route.screen === "help") {
32578
34334
  setStatus("live");
@@ -32598,12 +34354,7 @@ code-viewer query snapshot delete --id snap-abc123`
32598
34354
  const empty = activeFileHistoryEmptyHost || $("#empty");
32599
34355
  if (empty) {
32600
34356
  const onHistory = STATE.route.screen === "history" || isFileHistoryRoute(STATE.route);
32601
- const h2 = empty.querySelector("h2");
32602
- if (h2)
32603
- h2.textContent = onHistory ? "Empty diff" : "No changes";
32604
- const p2 = empty.querySelector("p");
32605
- if (p2)
32606
- p2.textContent = onHistory ? "This commit has no changes against its first parent." : "The working tree is clean against this ref.";
34357
+ syncEmptyDiffPane(empty, onHistory);
32607
34358
  }
32608
34359
  }
32609
34360
  const routeAtRequest = STATE.route;
@@ -32716,12 +34467,13 @@ code-viewer query snapshot delete --id snap-abc123`
32716
34467
  DIFF_VIEW.clearLoadQueue();
32717
34468
  if (activeFileHistoryEmptyHost) {
32718
34469
  activeFileHistoryEmptyHost.classList.remove("hidden");
34470
+ const text2 = uiText().diff;
32719
34471
  const h2 = activeFileHistoryEmptyHost.querySelector("h2");
32720
34472
  if (h2)
32721
- h2.textContent = "No commit selected";
34473
+ h2.textContent = text2.noCommitSelectedTitle;
32722
34474
  const p2 = activeFileHistoryEmptyHost.querySelector("p");
32723
34475
  if (p2)
32724
- p2.textContent = "Select a commit from the list to see its changes.";
34476
+ p2.textContent = text2.noCommitSelectedBody;
32725
34477
  }
32726
34478
  setStatus("live");
32727
34479
  return;
@@ -32740,13 +34492,32 @@ code-viewer query snapshot delete --id snap-abc123`
32740
34492
  invalidateRepoSidebar,
32741
34493
  clearLoadQueue: () => DIFF_VIEW.clearLoadQueue(),
32742
34494
  placeSidebarToggle,
32743
- setStatus
34495
+ setStatus,
34496
+ emptyText: () => uiText().diff
32744
34497
  });
32745
34498
  },
32746
34499
  getSyntaxHighlight: () => STATE.syntaxHighlight,
32747
34500
  getLanguage: () => STATE.language,
32748
34501
  trackLoad
32749
34502
  });
34503
+ relocalizeHistory = () => HISTORY_VIEW.localize();
34504
+ QUICK_HELP = createQuickHelp({
34505
+ $,
34506
+ getLanguage: () => STATE.language,
34507
+ getText: () => uiText().quickHelp,
34508
+ openFullKeybindings: () => {
34509
+ openHelpKeybindings({
34510
+ getRoute: () => STATE.route,
34511
+ getLanguage: () => STATE.language,
34512
+ currentRange,
34513
+ setRoute,
34514
+ setPageMode,
34515
+ renderHelpPage,
34516
+ setStatus,
34517
+ cancelActiveSourceLoad
34518
+ });
34519
+ }
34520
+ });
32750
34521
  const DOCTOR_VIEW = createDoctorView({
32751
34522
  $: (sel) => document.querySelector(sel),
32752
34523
  escapeHtml: escapeHtml3,
@@ -32968,13 +34739,7 @@ code-viewer query snapshot delete --id snap-abc123`
32968
34739
  });
32969
34740
  if (STATE.syntaxHighlight)
32970
34741
  setSyntaxHighlight(true, false);
32971
- $("#reload-prom").addEventListener("click", () => {
32972
- const btn = $("#reload-prom");
32973
- btn.classList.add("spinning");
32974
- load().finally(() => {
32975
- setTimeout(() => btn.classList.remove("spinning"), 200);
32976
- });
32977
- });
34742
+ $("#reload-prom").addEventListener("click", () => reloadDiffFromUi());
32978
34743
  function applyHideTests() {
32979
34744
  const btn = $("#hide-tests");
32980
34745
  if (btn)
@@ -32996,26 +34761,34 @@ code-viewer query snapshot delete --id snap-abc123`
32996
34761
  applyViewedState();
32997
34762
  applyHideTestsToMeta();
32998
34763
  }
32999
- function applyHideTestsToMeta() {
33000
- const meta = window._lastMeta;
33001
- if (!meta || !meta.totals)
33002
- return;
34764
+ function visibleDiffMetaForBrief(meta) {
34765
+ if (!meta.totals)
34766
+ return meta;
33003
34767
  const effective = STATE.hideTests && !isRepositorySidebarMode();
33004
- if (!effective) {
33005
- renderMeta(meta);
33006
- return;
33007
- }
34768
+ if (!effective)
34769
+ return meta;
33008
34770
  let additions = 0;
33009
34771
  let deletions = 0;
33010
- let files = 0;
33011
- for (const f2 of STATE.files) {
34772
+ const visibleFiles = [];
34773
+ for (const f2 of meta.files) {
33012
34774
  if (TEST_RE.test(f2.path || ""))
33013
34775
  continue;
33014
34776
  additions += f2.additions || 0;
33015
34777
  deletions += f2.deletions || 0;
33016
- files += 1;
34778
+ visibleFiles.push(f2);
33017
34779
  }
33018
- renderMeta({ ...meta, totals: { files, additions, deletions } });
34780
+ return {
34781
+ ...meta,
34782
+ files: visibleFiles,
34783
+ totals: { files: visibleFiles.length, additions, deletions }
34784
+ };
34785
+ }
34786
+ function applyHideTestsToMeta() {
34787
+ const meta = window._lastMeta;
34788
+ if (!meta?.totals)
34789
+ return;
34790
+ renderMeta(visibleDiffMetaForBrief(meta));
34791
+ applyViewedState();
33019
34792
  }
33020
34793
  applyHideTests();
33021
34794
  $("#hide-tests").addEventListener("click", () => {
@@ -33154,9 +34927,13 @@ code-viewer query snapshot delete --id snap-abc123`
33154
34927
  return;
33155
34928
  const text2 = uiText();
33156
34929
  btn.classList.toggle("active", STATE.autoUpdate);
33157
- btn.textContent = text2.topbar.autoUpdate;
33158
- btn.title = STATE.autoUpdate ? text2.topbar.autoUpdateOnTitle : text2.topbar.autoUpdateOffTitle;
34930
+ const autoUpdateTitle = STATE.autoUpdate ? text2.topbar.autoUpdateOnTitle : text2.topbar.autoUpdateOffTitle;
34931
+ btn.title = autoUpdateTitle;
34932
+ btn.setAttribute("aria-label", autoUpdateTitle);
33159
34933
  btn.setAttribute("aria-pressed", STATE.autoUpdate ? "true" : "false");
34934
+ const label = btn.querySelector(".auto-update-label");
34935
+ if (label)
34936
+ label.textContent = text2.topbar.autoUpdate;
33160
34937
  }
33161
34938
  function setAutoUpdate(on) {
33162
34939
  STATE.autoUpdate = on;
@@ -33359,6 +35136,8 @@ code-viewer query snapshot delete --id snap-abc123`
33359
35136
  paths = parsed.paths;
33360
35137
  } catch {}
33361
35138
  }
35139
+ if (isHistoryPanelRoute(STATE.route))
35140
+ HISTORY_VIEW.notePossibleUpdate();
33362
35141
  scheduleSseLoad(paths);
33363
35142
  });
33364
35143
  es.addEventListener("watch-limit", (event) => {