@youtyan/code-viewer 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,137 @@ ${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();
21482
+ if (meta.error) {
21483
+ const error2 = document.createElement("span");
21484
+ error2.className = "chip chip-error";
21485
+ error2.textContent = meta.error;
21486
+ el.appendChild(error2);
21487
+ }
20566
21488
  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);
21489
+ const files = document.createElement("span");
21490
+ files.className = "chip chip-files";
21491
+ files.textContent = text2.files(meta.totals.files);
21492
+ el.appendChild(files);
21493
+ const add2 = document.createElement("span");
21494
+ add2.className = "chip chip-add";
21495
+ add2.textContent = `+${meta.totals.additions}`;
21496
+ el.appendChild(add2);
21497
+ const del = document.createElement("span");
21498
+ del.className = "chip chip-del";
21499
+ del.textContent = `−${meta.totals.deletions}`;
21500
+ el.appendChild(del);
21501
+ }
21502
+ const kinds = summarizeDiffFileKinds(meta.files);
21503
+ const kindChip = (className, label, count) => {
21504
+ if (count <= 0)
21505
+ return;
21506
+ const chip = document.createElement("span");
21507
+ chip.className = `chip ${className}`;
21508
+ chip.textContent = `${count} ${label}`;
21509
+ el.appendChild(chip);
21510
+ };
21511
+ kindChip("chip-added", text2.kindAdded, kinds.added);
21512
+ kindChip("chip-deleted", text2.kindDeleted, kinds.deleted);
21513
+ kindChip("chip-renamed", text2.kindRenamed, kinds.renamed);
21514
+ kindChip("chip-heavy", text2.kindHeavy, kinds.heavy);
21515
+ kindChip("chip-binary", text2.kindBinary, kinds.binary);
21516
+ kindChip("chip-media", text2.kindMedia, kinds.media);
21517
+ const viewedProgress = viewedProgressFor(metaFilesForViewedProgress);
21518
+ if (viewedProgress.total > 0) {
21519
+ const viewed = document.createElement("span");
21520
+ viewed.className = "chip chip-viewed";
21521
+ viewed.title = text2.viewedProgressTitle;
21522
+ applyViewedProgressChipState(viewed, viewedProgress.viewed, viewedProgress.total);
21523
+ el.appendChild(viewed);
21524
+ const nextUnviewed = document.createElement("button");
21525
+ nextUnviewed.type = "button";
21526
+ nextUnviewed.className = "chip chip-next-unviewed";
21527
+ applyNextUnviewedButtonState(nextUnviewed, viewedProgress.viewed < viewedProgress.total);
21528
+ nextUnviewed.addEventListener("click", () => {
21529
+ scrollToNextUnviewedFile();
21530
+ });
21531
+ el.appendChild(nextUnviewed);
20571
21532
  }
20572
21533
  const u2 = document.createElement("span");
20573
- u2.className = "updated-at";
20574
- u2.title = "last updated";
20575
- u2.textContent = `updated ${new Date().toLocaleTimeString([], { hour12: false })}`;
21534
+ u2.className = "chip chip-updated";
21535
+ u2.title = text2.updatedTitle;
21536
+ u2.textContent = text2.updated(new Date().toLocaleTimeString([], { hour12: false }));
20576
21537
  el.appendChild(u2);
20577
21538
  }
20578
21539
  let SUPPRESS_SPY_UNTIL = 0;
@@ -20629,13 +21590,13 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20629
21590
  window.addEventListener("touchmove", () => {
20630
21591
  REANCHOR_UNTIL = 0;
20631
21592
  }, { passive: true });
20632
- function scrollToFile(path, line) {
21593
+ function scrollToFile(path, line, options) {
20633
21594
  const card = document.querySelector(diffCardSelector(path));
20634
21595
  if (!card)
20635
21596
  return;
20636
21597
  if (line)
20637
21598
  REANCHOR_UNTIL = performance.now() + 4000;
20638
- markActive(path);
21599
+ markActive(path, { reveal: options?.reveal });
20639
21600
  SUPPRESS_SPY_UNTIL = performance.now() + 1500;
20640
21601
  const onEnd = () => {
20641
21602
  SUPPRESS_SPY_UNTIL = 0;
@@ -20663,6 +21624,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20663
21624
  const viewed = STATE.viewedFiles.has(path);
20664
21625
  syncViewedCardDisplay(card, viewed);
20665
21626
  });
21627
+ syncViewedProgressChip();
20666
21628
  }
20667
21629
  let CLIENT_REQ_SEQ = 0;
20668
21630
  const LOAD_QUEUE = [];
@@ -20876,6 +21838,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20876
21838
  setupScrollSpy();
20877
21839
  scrollSpyInstalled = true;
20878
21840
  }
21841
+ syncViewedProgressChip();
20879
21842
  return {
20880
21843
  structureChanged: false,
20881
21844
  invalidatedCards,
@@ -21213,7 +22176,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21213
22176
  const button = card.querySelector(".gdp-file-toggle");
21214
22177
  if (button) {
21215
22178
  button.setAttribute("aria-expanded", collapsed ? "false" : "true");
21216
- button.title = collapsed ? "Expand file" : "Collapse file";
22179
+ const toggleLabel = collapsed ? "Expand file" : "Collapse file";
22180
+ button.title = toggleLabel;
22181
+ button.setAttribute("aria-label", toggleLabel);
21217
22182
  }
21218
22183
  const unfold = card.querySelector(".gdp-file-unfold");
21219
22184
  if (unfold)
@@ -21310,6 +22275,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21310
22275
  toggle.type = "button";
21311
22276
  toggle.className = "gdp-file-header-icon gdp-file-toggle";
21312
22277
  toggle.title = "Collapse file";
22278
+ toggle.setAttribute("aria-label", "Collapse file");
21313
22279
  toggle.setAttribute("aria-expanded", "true");
21314
22280
  toggle.innerHTML = iconSvg("octicon-chevron-down", CHEVRON_DOWN_16_PATH);
21315
22281
  toggle.addEventListener("click", (e2) => {
@@ -21331,6 +22297,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21331
22297
  copy.type = "button";
21332
22298
  copy.className = "gdp-file-header-icon gdp-copy-path";
21333
22299
  copy.title = "copy file path";
22300
+ copy.setAttribute("aria-label", "copy file path");
21334
22301
  copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
21335
22302
  copy.addEventListener("click", async (e2) => {
21336
22303
  e2.stopPropagation();
@@ -21671,6 +22638,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21671
22638
  addExpandHunksUI,
21672
22639
  scheduleIdleHighlight,
21673
22640
  scrollToFile,
22641
+ scrollToNextUnviewedFile,
21674
22642
  prefetchByPath,
21675
22643
  applyDiffRouteFocus,
21676
22644
  clearDiffLineFocus,
@@ -21899,12 +22867,17 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21899
22867
  deps.clearLoadQueue();
21900
22868
  if (deps.empty) {
21901
22869
  deps.empty.classList.remove("hidden");
22870
+ deps.empty.classList.remove("empty-with-actions");
22871
+ const text2 = deps.emptyText();
21902
22872
  const h2 = deps.empty.querySelector("h2");
21903
22873
  if (h2)
21904
- h2.textContent = "No commit selected";
22874
+ h2.textContent = text2.noCommitSelectedTitle;
21905
22875
  const p2 = deps.empty.querySelector("p");
21906
22876
  if (p2)
21907
- p2.textContent = "Select a commit from the list to see its changes.";
22877
+ p2.textContent = text2.noCommitSelectedBody;
22878
+ const actions = deps.empty.querySelector(".empty-actions");
22879
+ if (actions)
22880
+ actions.hidden = true;
21908
22881
  }
21909
22882
  deps.setStatus("live");
21910
22883
  }
@@ -21962,14 +22935,37 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21962
22935
  en: {
21963
22936
  worktreeLabel: "Uncommitted changes (Working tree)",
21964
22937
  bodyExpandClose: "Collapse",
21965
- bodyExpandMore: (n2) => `Show more (${n2} lines)`
22938
+ bodyExpandMore: (n2) => `Show more (${n2} lines)`,
22939
+ refreshLabel: "Refresh",
22940
+ refreshLabelPending: "Update",
22941
+ refreshTitle: "Refresh commit history",
22942
+ refreshTitlePending: "History may have changed. Refresh",
22943
+ refreshPendingStatus: "History may have changed",
22944
+ refreshResultUpdated: (sha) => `Updated: ${sha} is now latest`,
22945
+ refreshResultUnchanged: "No new commits",
22946
+ refreshResultUnchangedInView: "No new commits in this view",
22947
+ filterClearLabel: "Clear",
22948
+ filterClearTitle: "Clear commit filter"
21966
22949
  },
21967
22950
  ja: {
21968
22951
  worktreeLabel: "未コミット変更 (Working tree)",
21969
22952
  bodyExpandClose: "閉じる",
21970
- bodyExpandMore: (n2) => `もっと見る (${n2} 行)`
22953
+ bodyExpandMore: (n2) => `もっと見る (${n2} 行)`,
22954
+ refreshLabel: "更新",
22955
+ refreshLabelPending: "更新あり",
22956
+ refreshTitle: "コミット履歴を更新",
22957
+ refreshTitlePending: "新しい履歴がある可能性があります。更新",
22958
+ refreshPendingStatus: "新しい履歴がある可能性があります",
22959
+ refreshResultUpdated: (sha) => `更新: ${sha} が最新です`,
22960
+ refreshResultUnchanged: "新しいコミットはありません",
22961
+ refreshResultUnchangedInView: "この表示では新しいコミットはありません",
22962
+ filterClearLabel: "解除",
22963
+ filterClearTitle: "コミットフィルタを解除"
21971
22964
  }
21972
22965
  };
22966
+ function historyText(lang) {
22967
+ return HISTORY_TEXT[lang];
22968
+ }
21973
22969
  function historyWorktreeLabel(lang) {
21974
22970
  return HISTORY_TEXT[lang].worktreeLabel;
21975
22971
  }
@@ -21990,6 +22986,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21990
22986
  title.className = "history-title";
21991
22987
  title.textContent = "Commits";
21992
22988
  panelHead.appendChild(title);
22989
+ const refreshButton = document.createElement("button");
22990
+ refreshButton.type = "button";
22991
+ refreshButton.className = "history-refresh";
22992
+ refreshButton.title = HISTORY_TEXT.en.refreshTitle;
22993
+ refreshButton.setAttribute("aria-label", HISTORY_TEXT.en.refreshTitle);
22994
+ refreshButton.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
22995
+ const refreshLabel = document.createElement("span");
22996
+ refreshLabel.className = "history-refresh-label";
22997
+ refreshLabel.textContent = HISTORY_TEXT.en.refreshLabel;
22998
+ refreshButton.appendChild(refreshLabel);
22999
+ const refreshResult = document.createElement("span");
23000
+ refreshResult.className = "db-refresh-result history-refresh-result";
23001
+ refreshResult.setAttribute("aria-live", "polite");
23002
+ refreshResult.hidden = true;
21993
23003
  if (page) {
21994
23004
  const refMount = document.createElement("span");
21995
23005
  refMount.dataset.refSelectorMount = "";
@@ -21998,6 +23008,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21998
23008
  refMount.dataset.title = "history ref";
21999
23009
  panelHead.appendChild(refMount);
22000
23010
  }
23011
+ panelHead.append(refreshButton, refreshResult);
22001
23012
  const filterWrap = document.createElement("div");
22002
23013
  filterWrap.className = "history-filter-wrap";
22003
23014
  const filterInput = document.createElement("input");
@@ -22008,7 +23019,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22008
23019
  filterInput.type = "search";
22009
23020
  filterInput.placeholder = page ? "filter commits… (message, sha, author:name, path:file)" : "filter commits… (message, sha, author:name)";
22010
23021
  filterInput.autocomplete = "off";
22011
- filterWrap.appendChild(filterInput);
23022
+ const filterClearButton = document.createElement("button");
23023
+ filterClearButton.type = "button";
23024
+ if (page)
23025
+ filterClearButton.id = "history-filter-clear";
23026
+ filterClearButton.className = "history-filter-clear";
23027
+ filterClearButton.hidden = true;
23028
+ filterClearButton.textContent = HISTORY_TEXT.en.filterClearLabel;
23029
+ filterClearButton.title = HISTORY_TEXT.en.filterClearTitle;
23030
+ filterClearButton.setAttribute("aria-label", HISTORY_TEXT.en.filterClearTitle);
23031
+ filterWrap.append(filterInput, filterClearButton);
22012
23032
  const banner = document.createElement("div");
22013
23033
  if (page)
22014
23034
  banner.id = "history-banner";
@@ -22031,7 +23051,17 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22031
23051
  status.hidden = true;
22032
23052
  status.setAttribute("role", "status");
22033
23053
  panel.append(panelHead, filterWrap, banner, list2, sentinel, status);
22034
- return { panel, list: list2, banner, status, sentinel, filterInput };
23054
+ return {
23055
+ panel,
23056
+ list: list2,
23057
+ banner,
23058
+ status,
23059
+ sentinel,
23060
+ filterInput,
23061
+ filterClearButton,
23062
+ refreshButton,
23063
+ refreshResult
23064
+ };
22035
23065
  }
22036
23066
  function buildHistoryCommitInfoDom(options = {}) {
22037
23067
  const variant = options.variant || "page";
@@ -22129,6 +23159,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22129
23159
  status: deps.$("#history-status"),
22130
23160
  sentinel: deps.$("#history-sentinel"),
22131
23161
  filterInput: document.querySelector("#history-filter"),
23162
+ filterClearButton: document.querySelector("#history-filter-clear"),
23163
+ refreshButton: document.querySelector(".history-refresh"),
23164
+ refreshResult: document.querySelector(".history-refresh-result"),
22132
23165
  commitInfo: document.querySelector("#history-commit-info")
22133
23166
  };
22134
23167
  let activeMount = defaultMount;
@@ -22139,6 +23172,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22139
23172
  let sentinel = defaultMount.sentinel;
22140
23173
  let attachedList = null;
22141
23174
  let attachedFilterInput = null;
23175
+ let attachedFilterClearButton = null;
23176
+ let attachedRefreshButton = null;
22142
23177
  let filterTimer = null;
22143
23178
  let observer = null;
22144
23179
  let ref = "HEAD";
@@ -22152,6 +23187,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22152
23187
  let mode = "history";
22153
23188
  let routeRef = "HEAD";
22154
23189
  let pathFilter = "";
23190
+ let refreshStatus = { type: "none" };
23191
+ let freshSha = "";
22155
23192
  function historyScopeFromRoute(route = deps.getRoute()) {
22156
23193
  if (route.screen === "history") {
22157
23194
  const nextRef = route.ref || "HEAD";
@@ -22175,6 +23212,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22175
23212
  }
22176
23213
  return null;
22177
23214
  }
23215
+ function currentRefreshScopeKey() {
23216
+ const scope = historyScopeFromRoute();
23217
+ if (!scope)
23218
+ return "";
23219
+ return [
23220
+ scope.mode,
23221
+ scope.logRef,
23222
+ scope.routeRef,
23223
+ scope.pathFilter,
23224
+ query
23225
+ ].join("\x00");
23226
+ }
22178
23227
  function worktreeDiffRange() {
22179
23228
  return { from: "HEAD", to: "worktree" };
22180
23229
  }
@@ -22186,6 +23235,39 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22186
23235
  statusEl.textContent = message;
22187
23236
  statusEl.hidden = !message;
22188
23237
  }
23238
+ function latestCommitSha() {
23239
+ return commits.find((commit) => commit.sha !== HISTORY_WORKTREE_COMMIT)?.sha || "";
23240
+ }
23241
+ function clearRefreshResult() {
23242
+ refreshStatus = { type: "none" };
23243
+ freshSha = "";
23244
+ syncRefreshResult();
23245
+ }
23246
+ function setRefreshResult(previousTopSha, nextTopSha) {
23247
+ freshSha = previousTopSha && nextTopSha && previousTopSha !== nextTopSha ? nextTopSha : "";
23248
+ refreshStatus = freshSha ? { type: "updated", sha: nextTopSha.slice(0, 7) } : { type: "unchanged", scoped: Boolean(query || pathFilter) };
23249
+ renderList();
23250
+ }
23251
+ function refreshStatusMessage() {
23252
+ const text2 = historyText(deps.getLanguage());
23253
+ switch (refreshStatus.type) {
23254
+ case "pending":
23255
+ return text2.refreshPendingStatus;
23256
+ case "updated":
23257
+ return text2.refreshResultUpdated(refreshStatus.sha);
23258
+ case "unchanged":
23259
+ return refreshStatus.scoped ? text2.refreshResultUnchangedInView : text2.refreshResultUnchanged;
23260
+ case "none":
23261
+ return "";
23262
+ default: {
23263
+ const exhaustive = refreshStatus;
23264
+ return exhaustive;
23265
+ }
23266
+ }
23267
+ }
23268
+ function syncRefreshStatusText() {
23269
+ setStatusText(loading ? "loading..." : refreshStatusMessage() || (commits.length ? "" : "no commits"));
23270
+ }
22189
23271
  function commitInfoElement() {
22190
23272
  return activeMount.commitInfo || document.querySelector("#history-commit-info");
22191
23273
  }
@@ -22257,13 +23339,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22257
23339
  }
22258
23340
  function commitRow(commit) {
22259
23341
  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>`;
23342
+ const fresh = commit.sha === freshSha ? " history-item-fresh" : "";
23343
+ 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
23344
  }
22262
23345
  function worktreeRow() {
22263
23346
  const active = selectedSha === HISTORY_WORKTREE_COMMIT ? " active" : "";
22264
23347
  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
23348
  }
22266
23349
  function renderList() {
23350
+ syncRefreshButton(activeMount.refreshButton);
23351
+ syncRefreshResult(activeMount.refreshResult);
22267
23352
  const now = new Date;
22268
23353
  const html = mode === "history" ? [worktreeRow()] : [];
22269
23354
  let lastGroup = "";
@@ -22280,7 +23365,42 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22280
23365
  html.push(commitRow(commit));
22281
23366
  }
22282
23367
  list2.innerHTML = html.join("");
22283
- setStatusText(loading ? "loading..." : commits.length ? "" : "no commits");
23368
+ syncRefreshStatusText();
23369
+ }
23370
+ function syncRefreshButton(button) {
23371
+ if (!button)
23372
+ return;
23373
+ const text2 = historyText(deps.getLanguage());
23374
+ const hasPendingUpdate = refreshStatus.type === "pending";
23375
+ const title = hasPendingUpdate ? text2.refreshTitlePending : text2.refreshTitle;
23376
+ button.title = title;
23377
+ button.setAttribute("aria-label", title);
23378
+ button.classList.toggle("has-update", hasPendingUpdate);
23379
+ const label = button.querySelector(".history-refresh-label");
23380
+ if (label) {
23381
+ label.textContent = hasPendingUpdate ? text2.refreshLabelPending : text2.refreshLabel;
23382
+ }
23383
+ }
23384
+ function syncRefreshResult(result) {
23385
+ const el = result ?? activeMount.refreshResult;
23386
+ if (!el)
23387
+ return;
23388
+ const message = refreshStatus.type === "pending" || refreshStatus.type === "updated" || refreshStatus.type === "unchanged" ? refreshStatusMessage() : "";
23389
+ el.textContent = message;
23390
+ el.hidden = message.length === 0;
23391
+ el.classList.toggle("changed", refreshStatus.type === "updated");
23392
+ el.classList.toggle("pending", refreshStatus.type === "pending");
23393
+ }
23394
+ function syncFilterClearButton(button) {
23395
+ const clearButton = button ?? activeMount.filterClearButton;
23396
+ if (!clearButton)
23397
+ return;
23398
+ const input = activeMount.filterInput ?? null;
23399
+ const text2 = historyText(deps.getLanguage());
23400
+ clearButton.textContent = text2.filterClearLabel;
23401
+ clearButton.title = text2.filterClearTitle;
23402
+ clearButton.setAttribute("aria-label", text2.filterClearTitle);
23403
+ clearButton.hidden = !(input?.value || "");
22284
23404
  }
22285
23405
  async function updateCommitInfo(commit) {
22286
23406
  const info = commitInfoElement();
@@ -22559,6 +23679,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22559
23679
  selectionGeneration++;
22560
23680
  selectedSha = "";
22561
23681
  setBanner("");
23682
+ clearRefreshResult();
22562
23683
  await updateCommitInfo(null);
22563
23684
  renderList();
22564
23685
  await loadNextPage();
@@ -22629,6 +23750,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22629
23750
  query = value;
22630
23751
  generation++;
22631
23752
  selectionGeneration++;
23753
+ clearRefreshResult();
22632
23754
  commits = [];
22633
23755
  hasMore = false;
22634
23756
  loading = false;
@@ -22648,6 +23770,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22648
23770
  const input = activeFilterInputFromEvent(event);
22649
23771
  if (!input)
22650
23772
  return;
23773
+ syncFilterClearButton();
22651
23774
  if (filterTimer)
22652
23775
  clearTimeout(filterTimer);
22653
23776
  filterTimer = setTimeout(() => {
@@ -22663,13 +23786,54 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22663
23786
  return;
22664
23787
  if (e2.key === "Escape" && input.value) {
22665
23788
  input.value = "";
23789
+ syncFilterClearButton();
22666
23790
  applyFilter("");
22667
23791
  e2.stopPropagation();
22668
23792
  }
22669
23793
  }
23794
+ function handleFilterClearClick() {
23795
+ const input = attachedFilterInput;
23796
+ if (!input?.value)
23797
+ return;
23798
+ if (filterTimer) {
23799
+ clearTimeout(filterTimer);
23800
+ filterTimer = null;
23801
+ }
23802
+ input.value = "";
23803
+ syncFilterClearButton();
23804
+ applyFilter("");
23805
+ input.focus?.();
23806
+ }
23807
+ async function handleRefreshClick() {
23808
+ const button = attachedRefreshButton;
23809
+ if (button?.disabled)
23810
+ return;
23811
+ const refreshScopeKey = currentRefreshScopeKey();
23812
+ const previousTopSha = latestCommitSha();
23813
+ clearRefreshResult();
23814
+ if (button) {
23815
+ button.disabled = true;
23816
+ button.classList.add("spinning");
23817
+ }
23818
+ try {
23819
+ await enterHistory({ mount: activeMount, force: true });
23820
+ if (!refreshScopeKey || currentRefreshScopeKey() !== refreshScopeKey)
23821
+ return;
23822
+ setRefreshResult(previousTopSha, latestCommitSha());
23823
+ } finally {
23824
+ if (button) {
23825
+ button.classList.remove("spinning");
23826
+ button.disabled = false;
23827
+ }
23828
+ }
23829
+ }
22670
23830
  function activateMount(mount) {
22671
- if (activeMount === mount && attachedList === mount.list)
23831
+ if (activeMount === mount && attachedList === mount.list) {
23832
+ syncRefreshButton(mount.refreshButton);
23833
+ syncRefreshResult(mount.refreshResult);
23834
+ syncFilterClearButton(mount.filterClearButton);
22672
23835
  return;
23836
+ }
22673
23837
  if (attachedList) {
22674
23838
  attachedList.removeEventListener("click", handleListClick);
22675
23839
  attachedList = null;
@@ -22680,6 +23844,14 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22680
23844
  attachedFilterInput.removeEventListener("keydown", handleFilterKeydown);
22681
23845
  attachedFilterInput = null;
22682
23846
  }
23847
+ if (attachedFilterClearButton) {
23848
+ attachedFilterClearButton.removeEventListener("click", handleFilterClearClick);
23849
+ attachedFilterClearButton = null;
23850
+ }
23851
+ if (attachedRefreshButton) {
23852
+ attachedRefreshButton.removeEventListener("click", handleRefreshClick);
23853
+ attachedRefreshButton = null;
23854
+ }
22683
23855
  if (filterTimer) {
22684
23856
  clearTimeout(filterTimer);
22685
23857
  filterTimer = null;
@@ -22701,6 +23873,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22701
23873
  input.addEventListener("keydown", handleFilterKeydown);
22702
23874
  attachedFilterInput = input;
22703
23875
  }
23876
+ const filterClearButton = mount.filterClearButton ?? null;
23877
+ if (filterClearButton) {
23878
+ syncFilterClearButton(filterClearButton);
23879
+ filterClearButton.addEventListener("click", handleFilterClearClick);
23880
+ attachedFilterClearButton = filterClearButton;
23881
+ }
23882
+ const refreshButton = mount.refreshButton ?? null;
23883
+ if (refreshButton) {
23884
+ syncRefreshButton(refreshButton);
23885
+ syncRefreshResult(mount.refreshResult);
23886
+ refreshButton.addEventListener("click", handleRefreshClick);
23887
+ attachedRefreshButton = refreshButton;
23888
+ }
22704
23889
  observer = new IntersectionObserver((entries) => {
22705
23890
  if (!entries.some((entry) => entry.isIntersecting))
22706
23891
  return;
@@ -22737,6 +23922,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22737
23922
  enterHistory,
22738
23923
  leaveHistory,
22739
23924
  onRefPicked,
23925
+ localize: () => {
23926
+ syncRefreshButton(activeMount.refreshButton);
23927
+ syncRefreshResult(activeMount.refreshResult);
23928
+ syncFilterClearButton(activeMount.filterClearButton);
23929
+ renderList();
23930
+ },
23931
+ notePossibleUpdate: () => {
23932
+ refreshStatus = { type: "pending" };
23933
+ syncRefreshButton(activeMount.refreshButton);
23934
+ syncRefreshResult(activeMount.refreshResult);
23935
+ syncRefreshStatusText();
23936
+ },
22740
23937
  isWorktreeSelected: () => selectedSha === HISTORY_WORKTREE_COMMIT
22741
23938
  };
22742
23939
  }
@@ -22765,15 +23962,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22765
23962
  const diffPane = document.createElement("main");
22766
23963
  diffPane.className = "gdp-file-history-diff-pane";
22767
23964
  const commitInfo = buildHistoryCommitInfoDom({ variant: "file" });
23965
+ const emptyText = deps.emptyText();
22768
23966
  const empty = document.createElement("div");
22769
23967
  empty.className = "empty gdp-file-history-empty hidden";
22770
23968
  const emptyIcon = document.createElement("div");
22771
- emptyIcon.className = "emoji";
22772
- emptyIcon.textContent = "";
23969
+ emptyIcon.className = "empty-icon";
23970
+ emptyIcon.innerHTML = iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH);
22773
23971
  const emptyTitle = document.createElement("h2");
22774
- emptyTitle.textContent = "No commit selected";
23972
+ emptyTitle.textContent = emptyText.noCommitSelectedTitle;
22775
23973
  const emptyBody = document.createElement("p");
22776
- emptyBody.textContent = "Select a commit from the list to see its changes.";
23974
+ emptyBody.textContent = emptyText.noCommitSelectedBody;
22777
23975
  empty.append(emptyIcon, emptyTitle, emptyBody);
22778
23976
  const diffHost = document.createElement("div");
22779
23977
  diffHost.className = "gdp-file-history-diff";
@@ -22823,8 +24021,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22823
24021
  {
22824
24022
  selectors: [{ action: "open-help" }],
22825
24023
  description: {
22826
- en: "Open this help (keybindings)",
22827
- ja: "このヘルプ(キーバインド)を開く"
24024
+ en: "Open quick help",
24025
+ ja: "クイックヘルプを開く"
22828
24026
  }
22829
24027
  },
22830
24028
  {
@@ -22851,6 +24049,13 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22851
24049
  ja: "前 / 次の注釈へ移動"
22852
24050
  }
22853
24051
  },
24052
+ {
24053
+ selectors: [{ action: "next-unviewed-file" }],
24054
+ description: {
24055
+ en: "Jump to the next unviewed file",
24056
+ ja: "次の未確認ファイルへ移動"
24057
+ }
24058
+ },
22854
24059
  {
22855
24060
  selectors: [{ action: "layout-unified" }, { action: "layout-split" }],
22856
24061
  description: {
@@ -23096,8 +24301,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23096
24301
  addUnique(labels, item.label);
23097
24302
  return [labels.join(" / "), row.description[language]];
23098
24303
  }
23099
- function buildHelpKeybindingGroups(language, bindings = DEFAULT_KEY_BINDINGS) {
23100
- return HELP_KEYBINDING_GROUPS.map((group) => ({
24304
+ function buildHelpKeybindingGroups(language, bindings = DEFAULT_KEY_BINDINGS, onlyTitlesEn) {
24305
+ const groups = onlyTitlesEn ? HELP_KEYBINDING_GROUPS.filter((group) => onlyTitlesEn.includes(group.title.en)) : HELP_KEYBINDING_GROUPS;
24306
+ return groups.map((group) => ({
23101
24307
  title: group.title[language],
23102
24308
  rows: group.rows.map((row) => buildRow(row, language, bindings))
23103
24309
  }));
@@ -23202,7 +24408,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23202
24408
  },
23203
24409
  {
23204
24410
  kind: "paragraph",
23205
- text: 'From the terminal — and for AI agents or CI — the same report is available without a browser. `code-viewer doctor` prints a status summary; add `--json` for the full DoctorReport (matches the /_doctor endpoint). Exit code is 1 when worstStatus is "error", so it doubles as a CI gate.'
24411
+ text: 'From the terminal — and for AI agents or CI — the same report is available without a browser. `code-viewer doctor` prints a status summary; add `--json` for the full DoctorReport (matches the /_doctor endpoint). Use `--bin git=/absolute/path` or `--bin docker=/absolute/path` when PATH resolves the wrong tool. Exit code is 1 when worstStatus is "error", so it doubles as a CI gate.'
23206
24412
  },
23207
24413
  {
23208
24414
  kind: "command",
@@ -23213,7 +24419,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23213
24419
  kind: "command",
23214
24420
  title: "Doctor JSON for agents and CI",
23215
24421
  command: `code-viewer doctor --json
23216
- code-viewer doctor --cwd /path/to/repo --port 64160 --json`
24422
+ code-viewer doctor --cwd /path/to/repo --port 64160 --json
24423
+ code-viewer doctor --bin git=/opt/bin/git --bin docker=/opt/bin/docker --json`
23217
24424
  }
23218
24425
  ]
23219
24426
  }
@@ -23469,7 +24676,7 @@ code-viewer annotate add-db --db app.db --tab query \\
23469
24676
  ],
23470
24677
  [
23471
24678
  "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."
24679
+ "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
24680
  ],
23474
24681
  [
23475
24682
  "Detail footer & related panel",
@@ -23477,7 +24684,7 @@ code-viewer annotate add-db --db app.db --tab query \\
23477
24684
  ],
23478
24685
  [
23479
24686
  "Schema tab",
23480
- "Column definitions, indexes, foreign keys, triggers, and DDL."
24687
+ "Column definitions, indexes, foreign keys, triggers, and DDL, with an in-tab refresh action for reloading the current table structure."
23481
24688
  ],
23482
24689
  [
23483
24690
  "Query editor",
@@ -23854,7 +25061,7 @@ code-viewer query snapshot delete --id snap-abc123`
23854
25061
  },
23855
25062
  {
23856
25063
  kind: "paragraph",
23857
- text: 'ターミナルからは `code-viewer doctor` で同じレポートを取得できます。AI エージェントや CI からは `--json` で /_doctor と同じ DoctorReport を受け取れます。worstStatus が "error" なら exit code 1 を返すので CI ガードに直接使えます。'
25064
+ text: 'ターミナルからは `code-viewer doctor` で同じレポートを取得できます。AI エージェントや CI からは `--json` で /_doctor と同じ DoctorReport を受け取れます。PATH と別の実行ファイルを使いたい場合は `--bin git=/absolute/path` や `--bin docker=/absolute/path` を指定できます。worstStatus が "error" なら exit code 1 を返すので CI ガードに直接使えます。'
23858
25065
  },
23859
25066
  {
23860
25067
  kind: "command",
@@ -23865,7 +25072,8 @@ code-viewer query snapshot delete --id snap-abc123`
23865
25072
  kind: "command",
23866
25073
  title: "AI / CI 用 doctor JSON",
23867
25074
  command: `code-viewer doctor --json
23868
- code-viewer doctor --cwd /path/to/repo --port 64160 --json`
25075
+ code-viewer doctor --cwd /path/to/repo --port 64160 --json
25076
+ code-viewer doctor --bin git=/opt/bin/git --bin docker=/opt/bin/docker --json`
23869
25077
  }
23870
25078
  ]
23871
25079
  }
@@ -24121,7 +25329,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24121
25329
  ],
24122
25330
  [
24123
25331
  "Data タブ",
24124
- "ページネーション付きグリッド。ソート、フィルター、セルコピー、CSV/JSON エクスポート(最大10万行、現在のフィルター/ソートを反映)。設定バーで Edit モードを ON にするとインライン編集が可能 — セルをダブルクリックで編集、行操作で挿入/削除を予約、コミット単位で一括適用。未コミットの編集行/セルは黄色でハイライト。"
25332
+ "ページネーション付きグリッド。ソート、フィルター、セルコピー、CSV/JSON エクスポート(最大10万行、現在のフィルター/ソートを反映)、全体検索と列フィルターを保持した表だけの再読み込みに対応。再読み込み結果はボタン横に表示され、行数変化に気づけます。設定バーで Edit モードを ON にするとインライン編集が可能 — セルをダブルクリックで編集、行操作で挿入/削除を予約、コミット単位で一括適用。未コミットの編集行/セルは黄色でハイライト。"
24125
25333
  ],
24126
25334
  [
24127
25335
  "詳細フッタ・関連パネル",
@@ -24129,7 +25337,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24129
25337
  ],
24130
25338
  [
24131
25339
  "Schema タブ",
24132
- "カラム定義、インデックス、外部キー、トリガー、DDL"
25340
+ "カラム定義、インデックス、外部キー、トリガー、DDL。現在の表構造だけを再読み込みするタブ内更新にも対応。"
24133
25341
  ],
24134
25342
  [
24135
25343
  "クエリエディター",
@@ -24801,6 +26009,7 @@ code-viewer query snapshot delete --id snap-abc123`
24801
26009
  const button = document.createElement("button");
24802
26010
  button.className = "gdp-expand-btn";
24803
26011
  button.title = spec.title;
26012
+ button.setAttribute("aria-label", spec.title);
24804
26013
  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
26014
  button.addEventListener("click", (e2) => {
24806
26015
  e2.stopPropagation();
@@ -25139,6 +26348,86 @@ code-viewer query snapshot delete --id snap-abc123`
25139
26348
  };
25140
26349
  }
25141
26350
 
26351
+ // web-src/views/quick-help.ts
26352
+ var QUICK_HELP_GROUP_TITLES_EN = ["Global", "Main Panel"];
26353
+ function createQuickHelp(deps) {
26354
+ const popover = deps.$("#quick-help-popover");
26355
+ const trigger = deps.$("#quick-help-btn");
26356
+ const titleEl = deps.$("#quick-help-title");
26357
+ const closeBtn = deps.$("#quick-help-close");
26358
+ const groupsHost = deps.$("#quick-help-groups");
26359
+ const fullLink = deps.$("#quick-help-full-link");
26360
+ function isOpen() {
26361
+ return !popover.hidden;
26362
+ }
26363
+ function renderContent() {
26364
+ groupsHost.innerHTML = "";
26365
+ const groups = buildHelpKeybindingGroups(deps.getLanguage(), undefined, QUICK_HELP_GROUP_TITLES_EN);
26366
+ for (const group of groups) {
26367
+ const section = document.createElement("section");
26368
+ section.className = "gdp-help-group";
26369
+ const title = document.createElement("h3");
26370
+ title.textContent = group.title;
26371
+ section.append(title, renderHelpTable(group.rows));
26372
+ groupsHost.appendChild(section);
26373
+ }
26374
+ }
26375
+ function applyText() {
26376
+ const text2 = deps.getText();
26377
+ titleEl.textContent = text2.panelTitle;
26378
+ popover.setAttribute("aria-label", text2.panelTitle);
26379
+ closeBtn.setAttribute("aria-label", text2.close);
26380
+ fullLink.textContent = text2.viewAll;
26381
+ }
26382
+ function open() {
26383
+ applyText();
26384
+ renderContent();
26385
+ popover.hidden = false;
26386
+ closeBtn.focus();
26387
+ }
26388
+ function close() {
26389
+ if (!isOpen())
26390
+ return;
26391
+ popover.hidden = true;
26392
+ }
26393
+ function toggle() {
26394
+ if (isOpen())
26395
+ close();
26396
+ else
26397
+ open();
26398
+ }
26399
+ trigger.addEventListener("click", toggle);
26400
+ closeBtn.addEventListener("click", close);
26401
+ fullLink.addEventListener("click", (e2) => {
26402
+ e2.preventDefault();
26403
+ close();
26404
+ deps.openFullKeybindings();
26405
+ });
26406
+ document.addEventListener("keydown", (e2) => {
26407
+ if (isImeComposing(e2))
26408
+ return;
26409
+ if (e2.key !== "Escape")
26410
+ return;
26411
+ if (!isOpen())
26412
+ return;
26413
+ close();
26414
+ });
26415
+ document.addEventListener("mousedown", (e2) => {
26416
+ if (!isOpen())
26417
+ return;
26418
+ const target = e2.target;
26419
+ if (popover.contains(target) || trigger.contains(target))
26420
+ return;
26421
+ close();
26422
+ });
26423
+ function localize() {
26424
+ applyText();
26425
+ if (isOpen())
26426
+ renderContent();
26427
+ }
26428
+ return { open, close, toggle, isOpen, localize };
26429
+ }
26430
+
25142
26431
  // web-src/views/ref-picker.ts
25143
26432
  function createRefPicker(deps) {
25144
26433
  function wireRefSelectorInput(input, onPick) {
@@ -25570,7 +26859,11 @@ code-viewer query snapshot delete --id snap-abc123`
25570
26859
  trackLoad,
25571
26860
  getRepoSidebarRef,
25572
26861
  setRepoSidebarRef,
25573
- isTestPath
26862
+ isTestPath,
26863
+ sidebarToggleTitle,
26864
+ openDirectoryInOsTitle,
26865
+ omittedDirectoryBadge,
26866
+ commitEntryBadge
25574
26867
  } = deps;
25575
26868
  const VIRTUAL_SIDEBAR_THRESHOLD = 3000;
25576
26869
  const VIRTUAL_SIDEBAR_ROW_HEIGHT = 29;
@@ -25647,8 +26940,9 @@ code-viewer query snapshot delete --id snap-abc123`
25647
26940
  button = createSidebarToggleButton();
25648
26941
  bindSidebarToggleButton(button);
25649
26942
  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");
26943
+ const toggleTitle = sidebarToggleTitle(STATE.sidebarHidden);
26944
+ button.title = toggleTitle;
26945
+ button.setAttribute("aria-label", toggleTitle);
25652
26946
  syncSidebarToggleIcon(button);
25653
26947
  return button;
25654
26948
  }
@@ -25813,12 +27107,13 @@ code-viewer query snapshot delete --id snap-abc123`
25813
27107
  if (dir.children_omitted) {
25814
27108
  const omitted = document.createElement("span");
25815
27109
  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";
27110
+ const badge = omittedDirectoryBadge(dir.children_omitted_reason);
27111
+ omitted.textContent = badge.label;
27112
+ omitted.title = badge.title;
25818
27113
  label.appendChild(omitted);
25819
27114
  }
25820
27115
  li.appendChild(label);
25821
- li.appendChild(createOpenPathButton(dir.path, "directory", "open this folder in OS"));
27116
+ li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
25822
27117
  const collapsed = STATE.collapsedDirs.has(dir.path);
25823
27118
  if (collapsed)
25824
27119
  li.classList.add("collapsed");
@@ -25866,40 +27161,7 @@ code-viewer query snapshot delete --id snap-abc123`
25866
27161
  ul.appendChild(childUl);
25867
27162
  } else {
25868
27163
  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
- });
27164
+ const li = createTreeFileRow(f2, depth, onFileClick);
25903
27165
  ul.appendChild(li);
25904
27166
  }
25905
27167
  }
@@ -25983,7 +27245,8 @@ code-viewer query snapshot delete --id snap-abc123`
25983
27245
  order: dir.minOrder + (index + 1) / 1e5,
25984
27246
  path: entry.path,
25985
27247
  display_path: entry.path,
25986
- type: entry.type,
27248
+ type: meta.ref === "worktree" && entry.type === "commit" && !entry.submodule ? "tree" : entry.type,
27249
+ submodule: entry.submodule,
25987
27250
  children_omitted: entry.children_omitted,
25988
27251
  children_omitted_reason: entry.children_omitted_reason
25989
27252
  }));
@@ -26033,12 +27296,13 @@ code-viewer query snapshot delete --id snap-abc123`
26033
27296
  if (dir.children_omitted) {
26034
27297
  const omitted = document.createElement("span");
26035
27298
  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";
27299
+ const badge = omittedDirectoryBadge(dir.children_omitted_reason);
27300
+ omitted.textContent = badge.label;
27301
+ omitted.title = badge.title;
26038
27302
  label.appendChild(omitted);
26039
27303
  }
26040
27304
  li.appendChild(label);
26041
- li.appendChild(createOpenPathButton(dir.path, "directory", "open this folder in OS"));
27305
+ li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
26042
27306
  const updateIcon = () => {
26043
27307
  setFolderIcon(dirIcon, li.classList.contains("collapsed"));
26044
27308
  };
@@ -26104,12 +27368,37 @@ code-viewer query snapshot delete --id snap-abc123`
26104
27368
  }
26105
27369
  return li;
26106
27370
  }
27371
+ function fileKindTag(f2) {
27372
+ if (f2.type === "commit") {
27373
+ const badge = commitEntryBadge(f2.submodule);
27374
+ const tag2 = document.createElement("span");
27375
+ tag2.className = `kind-tag ${f2.submodule ? "submodule" : "gitlink"}`;
27376
+ tag2.textContent = badge.label;
27377
+ tag2.title = badge.title;
27378
+ return tag2;
27379
+ }
27380
+ const kind = classifyDiffFileKind(f2);
27381
+ if (!kind.heavy && !kind.binary && !kind.media)
27382
+ return null;
27383
+ const tag = document.createElement("span");
27384
+ const isBinaryLike = kind.binary || kind.media;
27385
+ tag.className = `kind-tag ${isBinaryLike ? "binary" : "heavy"}`;
27386
+ tag.textContent = isBinaryLike ? "B" : "!";
27387
+ tag.title = isBinaryLike ? "binary/media file" : "large diff";
27388
+ return tag;
27389
+ }
27390
+ function sidebarEntryIcon(f2) {
27391
+ return f2.type === "commit" ? iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH) : fileEntryIcon();
27392
+ }
26107
27393
  function createTreeFileRow(f2, depth, onFileClick) {
26108
27394
  const li = document.createElement("li");
26109
27395
  li.className = "tree-file";
26110
27396
  li.tabIndex = -1;
26111
27397
  li.dataset.path = f2.path;
26112
- li.dataset.type = "blob";
27398
+ li.dataset.type = f2.type || "blob";
27399
+ if (f2.type === "commit") {
27400
+ li.title = commitEntryBadge(f2.submodule).title;
27401
+ }
26113
27402
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
26114
27403
  li.classList.toggle("hidden-by-tests", STATE.hideTests && !isRepositorySidebarMode() && isTestPath(f2.path || ""));
26115
27404
  li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
@@ -26121,7 +27410,7 @@ code-viewer query snapshot delete --id snap-abc123`
26121
27410
  } else {
26122
27411
  const icon = document.createElement("span");
26123
27412
  icon.className = "d2h-icon-wrapper";
26124
- icon.innerHTML = fileEntryIcon();
27413
+ icon.innerHTML = sidebarEntryIcon(f2);
26125
27414
  li.appendChild(icon);
26126
27415
  }
26127
27416
  const name = document.createElement("span");
@@ -26129,6 +27418,9 @@ code-viewer query snapshot delete --id snap-abc123`
26129
27418
  name.textContent = f2.path.split("/").pop();
26130
27419
  name.title = f2.path;
26131
27420
  li.appendChild(name);
27421
+ const kindTag = fileKindTag(f2);
27422
+ if (kindTag)
27423
+ li.appendChild(kindTag);
26132
27424
  li.addEventListener("click", () => {
26133
27425
  if (onFileClick)
26134
27426
  onFileClick(f2);
@@ -26336,13 +27628,17 @@ code-viewer query snapshot delete --id snap-abc123`
26336
27628
  li.tabIndex = -1;
26337
27629
  li.dataset.index = String(i2);
26338
27630
  li.dataset.path = f2.path;
27631
+ li.dataset.type = f2.type || "blob";
27632
+ if (f2.type === "commit") {
27633
+ li.title = commitEntryBadge(f2.submodule).title;
27634
+ }
26339
27635
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
26340
27636
  if (f2.status) {
26341
27637
  li.appendChild(fileBadge(f2.status));
26342
27638
  } else {
26343
27639
  const icon = document.createElement("span");
26344
27640
  icon.className = "d2h-icon-wrapper";
26345
- icon.innerHTML = fileEntryIcon();
27641
+ icon.innerHTML = sidebarEntryIcon(f2);
26346
27642
  li.appendChild(icon);
26347
27643
  }
26348
27644
  const name = document.createElement("span");
@@ -26350,6 +27646,9 @@ code-viewer query snapshot delete --id snap-abc123`
26350
27646
  name.textContent = f2.path;
26351
27647
  name.title = f2.path;
26352
27648
  li.appendChild(name);
27649
+ const kindTag = fileKindTag(f2);
27650
+ if (kindTag)
27651
+ li.appendChild(kindTag);
26353
27652
  li.addEventListener("click", () => {
26354
27653
  if (onFileClick)
26355
27654
  onFileClick(f2);
@@ -26517,6 +27816,7 @@ code-viewer query snapshot delete --id snap-abc123`
26517
27816
  }
26518
27817
  function applyFilter() {
26519
27818
  const input = $("#sb-filter");
27819
+ syncSidebarFilterClearButton();
26520
27820
  if ($("#filelist").classList.contains("tree-virtual")) {
26521
27821
  rerenderVirtualSidebar();
26522
27822
  return;
@@ -26582,6 +27882,23 @@ code-viewer query snapshot delete --id snap-abc123`
26582
27882
  SIDEBAR_FILTER_RAF = 0;
26583
27883
  applyFilter();
26584
27884
  }
27885
+ function syncSidebarFilterClearButton() {
27886
+ const input = document.querySelector("#sb-filter");
27887
+ const button = document.querySelector("#sb-filter-clear");
27888
+ if (!input || !button)
27889
+ return;
27890
+ button.hidden = input.value.length === 0;
27891
+ }
27892
+ function clearSidebarFilter() {
27893
+ const input = document.querySelector("#sb-filter");
27894
+ if (!input?.value)
27895
+ return;
27896
+ input.value = "";
27897
+ syncSidebarFilterClearButton();
27898
+ flushSidebarFilter();
27899
+ applyFilter();
27900
+ input.focus();
27901
+ }
26585
27902
  function applySidebarWidth(w, options = {}) {
26586
27903
  const cw = Math.max(180, Math.min(900, w));
26587
27904
  document.documentElement.style.setProperty("--sidebar-w", `${cw}px`);
@@ -26917,6 +28234,8 @@ code-viewer query snapshot delete --id snap-abc123`
26917
28234
  applyFilter,
26918
28235
  scheduleApplyFilter,
26919
28236
  flushSidebarFilter,
28237
+ syncSidebarFilterClearButton,
28238
+ clearSidebarFilter,
26920
28239
  markActive,
26921
28240
  rerenderVirtualSidebar,
26922
28241
  ensureVirtualSidebarDirLoaded,
@@ -27002,7 +28321,19 @@ code-viewer query snapshot delete --id snap-abc123`
27002
28321
  syncHeaderMenu,
27003
28322
  getSidebarRowByPath,
27004
28323
  getSidebarVirtualActivePath,
27005
- pushUndo
28324
+ pushUndo,
28325
+ newFolderButtonTitle,
28326
+ openDirectoryInOsTitle,
28327
+ moveFolderToTrashTitle,
28328
+ uploadButtonLabel,
28329
+ dropFilesIntoCopy,
28330
+ uploadFailedMessage,
28331
+ emptyDirectoryLabel,
28332
+ uploadConfirmText,
28333
+ sortColumnLabels,
28334
+ repositoryFallback,
28335
+ repositoryRootFallback,
28336
+ commitEntryMeta
27006
28337
  } = deps;
27007
28338
  let REPO_SORT = {
27008
28339
  key: "name",
@@ -27045,6 +28376,15 @@ code-viewer query snapshot delete --id snap-abc123`
27045
28376
  function fileEntryIcon() {
27046
28377
  return iconSvg("octicon-file", FILE_16_PATH);
27047
28378
  }
28379
+ function commitEntryIcon() {
28380
+ return iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH);
28381
+ }
28382
+ function isWorktreeRef(ref) {
28383
+ return canTrashWorktreeRef(ref);
28384
+ }
28385
+ function canBrowseRepoEntry(entry, ref) {
28386
+ return entry.type === "tree" || entry.type === "commit" && isWorktreeRef(ref) && !entry.submodule;
28387
+ }
27048
28388
  function closeRepoContextMenu() {
27049
28389
  document.querySelector(".gdp-context-menu")?.remove();
27050
28390
  }
@@ -27223,8 +28563,9 @@ code-viewer query snapshot delete --id snap-abc123`
27223
28563
  const button = document.createElement("button");
27224
28564
  button.type = "button";
27225
28565
  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");
28566
+ const trashTitle = moveFolderToTrashTitle();
28567
+ button.title = trashTitle;
28568
+ button.setAttribute("aria-label", trashTitle);
27228
28569
  button.innerHTML = iconSvg("octicon-trash", TRASH_16_PATH);
27229
28570
  button.addEventListener("click", async (event) => {
27230
28571
  event.stopPropagation();
@@ -27236,8 +28577,9 @@ code-viewer query snapshot delete --id snap-abc123`
27236
28577
  const button = document.createElement("button");
27237
28578
  button.type = "button";
27238
28579
  button.className = "gdp-file-header-icon gdp-create-dir";
27239
- button.title = "new folder";
27240
- button.setAttribute("aria-label", "new folder");
28580
+ const newFolderTitle = newFolderButtonTitle();
28581
+ button.title = newFolderTitle;
28582
+ button.setAttribute("aria-label", newFolderTitle);
27241
28583
  button.innerHTML = iconSvg("octicon-plus", PLUS_16_PATH);
27242
28584
  button.addEventListener("click", async (event) => {
27243
28585
  event.stopPropagation();
@@ -27252,7 +28594,7 @@ code-viewer query snapshot delete --id snap-abc123`
27252
28594
  dropPanel.className = "gdp-upload-panel";
27253
28595
  const copy = document.createElement("div");
27254
28596
  copy.className = "gdp-upload-copy";
27255
- copy.textContent = `Drop files into ${path || getProjectName() || "repository"}`;
28597
+ copy.textContent = dropFilesIntoCopy(path || getProjectName() || repositoryFallback());
27256
28598
  const input = document.createElement("input");
27257
28599
  input.type = "file";
27258
28600
  input.multiple = true;
@@ -27260,11 +28602,11 @@ code-viewer query snapshot delete --id snap-abc123`
27260
28602
  const button = document.createElement("button");
27261
28603
  button.type = "button";
27262
28604
  button.className = "gdp-btn gdp-btn-sm";
27263
- button.textContent = "Upload files";
28605
+ button.textContent = uploadButtonLabel();
27264
28606
  button.addEventListener("click", () => input.click());
27265
28607
  const error2 = document.createElement("div");
27266
28608
  error2.className = "gdp-upload-error";
27267
- const fail = (message = "Upload failed") => {
28609
+ const fail = (message = uploadFailedMessage()) => {
27268
28610
  error2.textContent = message;
27269
28611
  dropPanel.classList.add("failed");
27270
28612
  setTimeout(() => dropPanel.classList.remove("failed"), 1600);
@@ -27275,7 +28617,7 @@ code-viewer query snapshot delete --id snap-abc123`
27275
28617
  await uploadFiles(path, input.files);
27276
28618
  error2.textContent = "";
27277
28619
  } catch (uploadError) {
27278
- fail(uploadError instanceof Error ? uploadError.message : "Upload failed");
28620
+ fail(uploadError instanceof Error ? uploadError.message : uploadFailedMessage());
27279
28621
  } finally {
27280
28622
  input.value = "";
27281
28623
  }
@@ -27294,7 +28636,7 @@ code-viewer query snapshot delete --id snap-abc123`
27294
28636
  await uploadFiles(path, files);
27295
28637
  error2.textContent = "";
27296
28638
  } catch (uploadError) {
27297
- fail(uploadError instanceof Error ? uploadError.message : "Upload failed");
28639
+ fail(uploadError instanceof Error ? uploadError.message : uploadFailedMessage());
27298
28640
  }
27299
28641
  });
27300
28642
  dropPanel.append(copy, button, input, error2);
@@ -27314,7 +28656,7 @@ code-viewer query snapshot delete --id snap-abc123`
27314
28656
  const root = document.createElement("button");
27315
28657
  root.type = "button";
27316
28658
  root.className = path ? "gdp-file-breadcrumb-part" : "gdp-file-breadcrumb-current";
27317
- root.textContent = getProjectName() || "repository";
28659
+ root.textContent = getProjectName() || repositoryFallback();
27318
28660
  root.addEventListener("click", () => {
27319
28661
  setRoute(repoRoute(target, ""));
27320
28662
  loadRepo();
@@ -27361,7 +28703,7 @@ code-viewer query snapshot delete --id snap-abc123`
27361
28703
  pathHeader.appendChild(createRepoBreadcrumb(meta.ref, meta.path || ""));
27362
28704
  if (meta.path)
27363
28705
  pathHeader.appendChild(createCopyPathButton(meta.path));
27364
- pathHeader.appendChild(createOpenPathButton(meta.path || "", "directory", "open this folder in OS"));
28706
+ pathHeader.appendChild(createOpenPathButton(meta.path || "", "directory", openDirectoryInOsTitle()));
27365
28707
  toolbar.appendChild(pathHeader);
27366
28708
  if (canTrashWorktreeRef(meta.ref)) {
27367
28709
  toolbar.appendChild(createNewFolderButton(meta.path || "", () => loadRepo()));
@@ -27414,24 +28756,30 @@ code-viewer query snapshot delete --id snap-abc123`
27414
28756
  });
27415
28757
  list2.appendChild(row);
27416
28758
  }
27417
- sortedRepoEntries(meta.entries).forEach((entry) => {
28759
+ sortedRepoEntries(meta.entries, meta.ref).forEach((entry) => {
28760
+ const browsable = canBrowseRepoEntry(entry, meta.ref);
28761
+ const nonBrowsableCommit = entry.type === "commit" && !browsable;
27418
28762
  const row = document.createElement("button");
27419
28763
  row.type = "button";
27420
- row.className = `gdp-repo-row ${entry.type}`;
28764
+ row.className = nonBrowsableCommit ? `gdp-repo-row ${entry.type} gdp-repo-row-gitlink` : `gdp-repo-row ${entry.type}`;
27421
28765
  const icon = document.createElement("span");
27422
- icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
27423
- if (entry.type === "tree")
28766
+ icon.className = browsable ? "dir-icon" : nonBrowsableCommit ? "d2h-icon-wrapper gdp-repo-row-gitlink-icon" : "d2h-icon-wrapper";
28767
+ if (browsable)
27424
28768
  setFolderIcon(icon, true);
27425
28769
  else
27426
- icon.innerHTML = fileEntryIcon();
28770
+ icon.innerHTML = entry.type === "commit" ? commitEntryIcon() : fileEntryIcon();
27427
28771
  const name = document.createElement("span");
27428
28772
  name.className = "name";
27429
28773
  name.textContent = entry.name;
27430
- const metaBlock = createRepoEntryMeta(entry);
28774
+ if (nonBrowsableCommit) {
28775
+ row.title = commitEntryMeta(entry.submodule).title;
28776
+ row.setAttribute("aria-disabled", "true");
28777
+ }
28778
+ const metaBlock = createRepoEntryMeta(entry, browsable);
27431
28779
  const size = createRepoEntrySize(entry);
27432
28780
  row.append(icon, name, metaBlock, size);
27433
28781
  row.addEventListener("click", () => {
27434
- if (entry.type === "tree") {
28782
+ if (browsable) {
27435
28783
  setRoute(repoRoute(meta.ref, entry.path));
27436
28784
  loadRepo();
27437
28785
  } else if (entry.type === "blob") {
@@ -27451,7 +28799,7 @@ code-viewer query snapshot delete --id snap-abc123`
27451
28799
  if (!meta.entries.length) {
27452
28800
  const empty = document.createElement("div");
27453
28801
  empty.className = "gdp-repo-empty";
27454
- empty.textContent = "No files in this directory.";
28802
+ empty.textContent = emptyDirectoryLabel();
27455
28803
  list2.appendChild(empty);
27456
28804
  }
27457
28805
  };
@@ -27538,7 +28886,8 @@ code-viewer query snapshot delete --id snap-abc123`
27538
28886
  order: index + 1,
27539
28887
  path: entry.path,
27540
28888
  display_path: entry.path,
27541
- type: entry.type,
28889
+ type: canBrowseRepoEntry(entry, normalizedRef) ? "tree" : entry.type,
28890
+ submodule: entry.submodule,
27542
28891
  children_omitted: entry.children_omitted,
27543
28892
  children_omitted_reason: entry.children_omitted_reason
27544
28893
  }));
@@ -27601,12 +28950,19 @@ code-viewer query snapshot delete --id snap-abc123`
27601
28950
  }
27602
28951
  });
27603
28952
  }
27604
- function createRepoEntryMeta(entry) {
28953
+ function createRepoEntryMeta(entry, browsable) {
27605
28954
  const meta = document.createElement("span");
27606
28955
  meta.className = "meta";
28956
+ if (entry.type === "commit" && !browsable) {
28957
+ meta.classList.add("gdp-repo-row-gitlink-badge");
28958
+ const badge = commitEntryMeta(entry.submodule);
28959
+ meta.textContent = badge.label;
28960
+ meta.title = badge.title;
28961
+ return meta;
28962
+ }
27607
28963
  const updated = formatFileDate(entry.updated_at || entry.commit_updated_at);
27608
28964
  const created = formatFileDate(entry.created_at);
27609
- if (entry.type === "tree" && updated) {
28965
+ if (browsable && updated) {
27610
28966
  meta.textContent = updated;
27611
28967
  if (created)
27612
28968
  meta.title = `Created ${created}`;
@@ -27634,13 +28990,15 @@ code-viewer query snapshot delete --id snap-abc123`
27634
28990
  const time = new Date(raw).getTime();
27635
28991
  return Number.isNaN(time) ? -1 : time;
27636
28992
  }
27637
- function sortedRepoEntries(entries) {
28993
+ function sortedRepoEntries(entries, ref = "worktree") {
27638
28994
  const direction = REPO_SORT.direction === "asc" ? 1 : -1;
27639
28995
  return [...entries].sort((a2, b2) => {
27640
- if (REPO_SORT.key === "name" && a2.type !== b2.type) {
27641
- if (a2.type === "tree")
28996
+ const aBrowsable = canBrowseRepoEntry(a2, ref);
28997
+ const bBrowsable = canBrowseRepoEntry(b2, ref);
28998
+ if (REPO_SORT.key === "name" && aBrowsable !== bBrowsable) {
28999
+ if (aBrowsable)
27642
29000
  return -1;
27643
- if (b2.type === "tree")
29001
+ if (bBrowsable)
27644
29002
  return 1;
27645
29003
  }
27646
29004
  let result = 0;
@@ -27672,10 +29030,11 @@ code-viewer query snapshot delete --id snap-abc123`
27672
29030
  const spacer = document.createElement("span");
27673
29031
  spacer.className = "gdp-repo-sort-spacer";
27674
29032
  header.appendChild(spacer);
29033
+ const sortLabels = sortColumnLabels();
27675
29034
  const columns = [
27676
- { key: "name", label: "Name" },
27677
- { key: "updated", label: "Updated" },
27678
- { key: "size", label: "Size" }
29035
+ { key: "name", label: sortLabels.name },
29036
+ { key: "updated", label: sortLabels.updated },
29037
+ { key: "size", label: sortLabels.size }
27679
29038
  ];
27680
29039
  columns.forEach((column) => {
27681
29040
  const button = document.createElement("button");
@@ -27798,11 +29157,12 @@ code-viewer query snapshot delete --id snap-abc123`
27798
29157
  const list2 = Array.from(files);
27799
29158
  if (!list2.length)
27800
29159
  return;
27801
- const label = path || getProjectName() || "repository root";
29160
+ const label = path || getProjectName() || repositoryRootFallback();
29161
+ const confirmText = uploadConfirmText(list2.length, label);
27802
29162
  const ok = await showConfirmDialog({
27803
- title: "Upload files?",
27804
- body: `Upload ${list2.length} file${list2.length === 1 ? "" : "s"} into ${label}?`,
27805
- confirmLabel: "Upload"
29163
+ title: confirmText.title,
29164
+ body: confirmText.body,
29165
+ confirmLabel: confirmText.confirmLabel
27806
29166
  });
27807
29167
  if (!ok)
27808
29168
  return;
@@ -30176,13 +31536,20 @@ code-viewer query snapshot delete --id snap-abc123`
30176
31536
  const loadBar = document.querySelector("#load-bar");
30177
31537
  if (loadBar)
30178
31538
  loadBar.classList.toggle("active", state.inFlight > 0);
31539
+ const text2 = uiText().global;
31540
+ const statusEl = document.querySelector("#status");
31541
+ if (statusEl) {
31542
+ statusEl.title = state.inFlight > 0 ? text2.statusInFlightTitle(state.inFlight, state.cancellable) : statusEl.querySelector(".status-label")?.textContent ?? "";
31543
+ }
30179
31544
  const cancelButton = document.querySelector("#cancel-requests");
30180
31545
  if (!cancelButton)
30181
31546
  return;
30182
31547
  const cancellable = state.cancellable > 0;
30183
31548
  cancelButton.disabled = !cancellable;
30184
31549
  cancelButton.classList.toggle("active", cancellable);
30185
- cancelButton.title = cancellable ? `cancel ${state.cancellable} in-flight request${state.cancellable === 1 ? "" : "s"}` : "no in-flight requests";
31550
+ const cancelTitle = cancellable ? text2.cancelRequestsActiveTitle(state.cancellable) : text2.cancelRequestsInactiveTitle;
31551
+ cancelButton.title = cancelTitle;
31552
+ cancelButton.setAttribute("aria-label", cancelTitle);
30186
31553
  }
30187
31554
  function cancelInFlightRequests() {
30188
31555
  NETWORK_ACTIVITY.cancelAll();
@@ -30695,13 +32062,31 @@ code-viewer query snapshot delete --id snap-abc123`
30695
32062
  setRepoSidebarRef: (ref) => {
30696
32063
  REPO_SIDEBAR_REF = ref;
30697
32064
  },
30698
- isTestPath: (path) => TEST_RE.test(path)
32065
+ isTestPath: (path) => TEST_RE.test(path),
32066
+ sidebarToggleTitle: (hidden) => hidden ? uiText().sidebar.show : uiText().sidebar.hide,
32067
+ openDirectoryInOsTitle: () => uiText().sidebar.openDirectoryInOs,
32068
+ omittedDirectoryBadge: (reason) => {
32069
+ const text2 = uiText().sidebar;
32070
+ return reason === "heavy" ? { label: text2.omittedHeavyLabel, title: text2.omittedHeavyTitle } : { label: text2.omittedPrivateLabel, title: text2.omittedPrivateTitle };
32071
+ },
32072
+ commitEntryBadge: (submodule) => {
32073
+ const text2 = uiText().sidebar;
32074
+ return submodule ? {
32075
+ label: text2.commitEntrySubmoduleLabel,
32076
+ title: text2.commitEntrySubmoduleTitle
32077
+ } : {
32078
+ label: text2.commitEntryGitlinkLabel,
32079
+ title: text2.commitEntryGitlinkTitle
32080
+ };
32081
+ }
30699
32082
  });
30700
32083
  const {
30701
32084
  renderSidebar,
30702
32085
  applyFilter,
30703
32086
  scheduleApplyFilter,
30704
32087
  flushSidebarFilter,
32088
+ syncSidebarFilterClearButton,
32089
+ clearSidebarFilter,
30705
32090
  markActive,
30706
32091
  rerenderVirtualSidebar,
30707
32092
  ensureVirtualSidebarDirLoaded,
@@ -30838,6 +32223,35 @@ code-viewer query snapshot delete --id snap-abc123`
30838
32223
  getSidebarVirtualActivePath,
30839
32224
  pushUndo: (undo) => {
30840
32225
  UNDO_STACK.unshift(undo);
32226
+ },
32227
+ newFolderButtonTitle: () => uiText().repo.newFolder,
32228
+ openDirectoryInOsTitle: () => uiText().sidebar.openDirectoryInOs,
32229
+ moveFolderToTrashTitle: () => uiText().repo.moveFolderToTrash,
32230
+ uploadButtonLabel: () => uiText().repo.uploadButton,
32231
+ dropFilesIntoCopy: (target) => uiText().repo.dropFilesInto(target),
32232
+ uploadFailedMessage: () => uiText().repo.uploadFailed,
32233
+ emptyDirectoryLabel: () => uiText().repo.emptyDirectory,
32234
+ uploadConfirmText: (count, target) => {
32235
+ const text2 = uiText().repo;
32236
+ return {
32237
+ title: text2.uploadConfirmTitle,
32238
+ body: text2.uploadConfirmBody(count, target),
32239
+ confirmLabel: text2.uploadConfirmLabel
32240
+ };
32241
+ },
32242
+ sortColumnLabels: () => {
32243
+ const text2 = uiText().repo;
32244
+ return {
32245
+ name: text2.sortName,
32246
+ updated: text2.sortUpdated,
32247
+ size: text2.sortSize
32248
+ };
32249
+ },
32250
+ repositoryFallback: () => uiText().repo.repositoryFallback,
32251
+ repositoryRootFallback: () => uiText().repo.repositoryRootFallback,
32252
+ commitEntryMeta: (submodule) => {
32253
+ const text2 = uiText().repo;
32254
+ return submodule ? { label: text2.submoduleLabel, title: text2.submoduleTitle } : { label: text2.gitlinkLabel, title: text2.gitlinkTitle };
30841
32255
  }
30842
32256
  });
30843
32257
  const {
@@ -30880,10 +32294,20 @@ code-viewer query snapshot delete --id snap-abc123`
30880
32294
  theme: "toggle theme",
30881
32295
  product: "code viewer",
30882
32296
  copyAiContext: "Copy AI context (Shift+Click to include code)",
32297
+ copyAiContextLabel: "AI context",
30883
32298
  copyAiContextCopied: "Copied AI context",
30884
32299
  copyAiContextCopiedWithCode: (lines) => `Copied AI context + code (${lines} line${lines === 1 ? "" : "s"})`,
30885
32300
  copyAiContextFailed: "Copy failed",
30886
- copyAiContextEmpty: "Nothing to copy here"
32301
+ copyAiContextEmpty: "Nothing to copy here",
32302
+ statusLive: "Live",
32303
+ statusLoading: "Loading",
32304
+ statusError: "Error",
32305
+ statusIdle: "Idle",
32306
+ statusInFlightTitle: (count, cancellable) => `${count} request${count === 1 ? "" : "s"} in flight${cancellable > 0 ? " (cancellable)" : ""}`,
32307
+ cancelRequestsActiveTitle: (count) => `cancel ${count} in-flight request${count === 1 ? "" : "s"}`,
32308
+ cancelRequestsInactiveTitle: "no in-flight requests",
32309
+ brandHome: "Repository home",
32310
+ menuViews: "Views"
30887
32311
  },
30888
32312
  topbar: {
30889
32313
  resetRange: "reset to HEAD .. worktree",
@@ -30892,6 +32316,7 @@ code-viewer query snapshot delete --id snap-abc123`
30892
32316
  unified: "unified",
30893
32317
  split: "split",
30894
32318
  ignoreWs: "ignore whitespace changes (-w)",
32319
+ ignoreWsLabel: "ws",
30895
32320
  syntaxLoading: "loading...",
30896
32321
  syntaxOn: "syntax on",
30897
32322
  syntaxOff: "syntax off",
@@ -30900,10 +32325,38 @@ code-viewer query snapshot delete --id snap-abc123`
30900
32325
  syntaxErrorTitle: "failed to load syntax highlighter",
30901
32326
  syntaxOffTitle: "syntax highlighting off",
30902
32327
  hideTests: "hide test files (test|spec)",
32328
+ hideTestsLabel: "no test",
30903
32329
  autoUpdate: "auto",
30904
32330
  autoUpdateOnTitle: "auto update on file change",
30905
32331
  autoUpdateOffTitle: "auto update off — manual reload"
30906
32332
  },
32333
+ diff: {
32334
+ files: (count) => `${count} file${count === 1 ? "" : "s"}`,
32335
+ updated: (time) => `updated ${time}`,
32336
+ updatedTitle: "last updated",
32337
+ kindAdded: "added",
32338
+ kindDeleted: "deleted",
32339
+ kindRenamed: "renamed",
32340
+ kindHeavy: "heavy",
32341
+ kindBinary: "binary",
32342
+ kindMedia: "media",
32343
+ viewedProgress: (viewed, total) => `${viewed}/${total} viewed`,
32344
+ viewedProgressTitle: "review progress",
32345
+ nextUnviewed: "next unviewed",
32346
+ nextUnviewedTitle: "Jump to the next unviewed file (n)",
32347
+ allViewed: "all viewed",
32348
+ allViewedTitle: "All visible files are viewed",
32349
+ noChangesTitle: "No changes",
32350
+ noChangesBody: "The working tree is clean against this ref.",
32351
+ noChangesReload: "Reload diff",
32352
+ noChangesReloadTitle: "Reload this diff range",
32353
+ noChangesHistory: "Open history",
32354
+ noChangesHistoryTitle: "Open commit history for this range",
32355
+ emptyDiffTitle: "Empty diff",
32356
+ emptyDiffBody: "This commit has no changes against its first parent.",
32357
+ noCommitSelectedTitle: "No commit selected",
32358
+ noCommitSelectedBody: "Select a commit from the list to see its changes."
32359
+ },
30907
32360
  changeBanner: {
30908
32361
  text: "Files changed",
30909
32362
  reload: "Reload",
@@ -30923,14 +32376,56 @@ code-viewer query snapshot delete --id snap-abc123`
30923
32376
  view: "view",
30924
32377
  tree: "tree",
30925
32378
  flat: "flat",
32379
+ treeTitle: "tree view",
32380
+ flatTitle: "flat list",
30926
32381
  filter: "Filter files… / ⌘K",
30927
32382
  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"
32383
+ filterClear: "Clear",
32384
+ filterClearTitle: "Clear file filter",
32385
+ hide: "hide sidebar",
32386
+ show: "show sidebar",
32387
+ repoTarget: "repository target",
32388
+ openDirectoryInOs: "open this folder in OS",
32389
+ omittedHeavyLabel: "skipped",
32390
+ omittedHeavyTitle: "Tree expansion is skipped, but the directory detail can be opened",
32391
+ omittedPrivateLabel: "private",
32392
+ omittedPrivateTitle: "This directory cannot be opened from the browser",
32393
+ commitEntryGitlinkLabel: "GIT",
32394
+ commitEntryGitlinkTitle: "Git commit entry",
32395
+ commitEntrySubmoduleLabel: "SUB",
32396
+ commitEntrySubmoduleTitle: "Git submodule pinned to a commit"
32397
+ },
32398
+ repo: {
32399
+ newFolder: "new folder",
32400
+ moveFolderToTrash: "move folder to Trash",
32401
+ uploadButton: "Upload files",
32402
+ dropFilesInto: (target) => `Drop files into ${target}`,
32403
+ uploadFailed: "Upload failed",
32404
+ emptyDirectory: "No files in this directory.",
32405
+ uploadConfirmTitle: "Upload files?",
32406
+ uploadConfirmBody: (count, target) => `Upload ${count} file${count === 1 ? "" : "s"} into ${target}?`,
32407
+ uploadConfirmLabel: "Upload",
32408
+ sortName: "Name",
32409
+ sortUpdated: "Updated",
32410
+ sortSize: "Size",
32411
+ repositoryFallback: "repository",
32412
+ repositoryRootFallback: "repository root",
32413
+ gitlinkLabel: "gitlink",
32414
+ gitlinkTitle: "Git commit entry is not directly browsable at this ref",
32415
+ submoduleLabel: "submodule",
32416
+ submoduleTitle: "Git submodule pinned to a commit"
30929
32417
  },
30930
32418
  history: {
30931
32419
  title: "Commits",
30932
32420
  filter: "Filter commits...",
30933
- filterTitle: "Filter commits by message, SHA, author:name, or path:file."
32421
+ filterTitle: "Filter commits by message, SHA, author:name, or path:file.",
32422
+ refreshTitle: "Refresh commit history"
32423
+ },
32424
+ quickHelp: {
32425
+ buttonTitle: "quick help (shortcuts)",
32426
+ panelTitle: "Quick Help",
32427
+ close: "close quick help",
32428
+ viewAll: "View all keybindings →"
30934
32429
  },
30935
32430
  settings: {
30936
32431
  title: "Viewer Settings",
@@ -30991,30 +32486,69 @@ code-viewer query snapshot delete --id snap-abc123`
30991
32486
  theme: "テーマ切り替え",
30992
32487
  product: "code viewer",
30993
32488
  copyAiContext: "AI 用コンテキストをコピー(Shift+Click でコードも添付)",
32489
+ copyAiContextLabel: "AI文脈",
30994
32490
  copyAiContextCopied: "コピーしました",
30995
32491
  copyAiContextCopiedWithCode: (lines) => `コピーしました(コード付き・${lines}行)`,
30996
32492
  copyAiContextFailed: "コピーに失敗しました",
30997
- copyAiContextEmpty: "コピーする内容がありません"
32493
+ copyAiContextEmpty: "コピーする内容がありません",
32494
+ statusLive: "稼働中",
32495
+ statusLoading: "更新中",
32496
+ statusError: "エラー",
32497
+ statusIdle: "待機中",
32498
+ statusInFlightTitle: (count, cancellable) => `${count}件のリクエストを実行中${cancellable > 0 ? "(キャンセル可能)" : ""}`,
32499
+ cancelRequestsActiveTitle: (count) => `実行中のリクエストを${count}件キャンセル`,
32500
+ cancelRequestsInactiveTitle: "実行中のリクエストはありません",
32501
+ brandHome: "リポジトリホーム",
32502
+ menuViews: "ビュー切り替え"
30998
32503
  },
30999
32504
  topbar: {
31000
32505
  resetRange: "HEAD .. worktree に戻す",
31001
32506
  reload: "diff を再読み込み (R)",
31002
32507
  layout: "レイアウト",
31003
- unified: "unified",
31004
- split: "split",
32508
+ unified: "統合",
32509
+ split: "分割",
31005
32510
  ignoreWs: "空白差分を無視 (-w)",
32511
+ ignoreWsLabel: "空白",
31006
32512
  syntaxLoading: "読み込み中...",
31007
- syntaxOn: "syntax on",
31008
- syntaxOff: "syntax off",
32513
+ syntaxOn: "構文あり",
32514
+ syntaxOff: "構文なし",
31009
32515
  syntaxOnTitle: "シンタックスハイライト有効",
31010
32516
  syntaxLoadingTitle: "シンタックスハイライトを読み込み中",
31011
32517
  syntaxErrorTitle: "シンタックスハイライトの読み込みに失敗",
31012
32518
  syntaxOffTitle: "シンタックスハイライト無効",
31013
32519
  hideTests: "test/spec ファイルを隠す",
32520
+ hideTestsLabel: "テスト非表示",
31014
32521
  autoUpdate: "自動",
31015
32522
  autoUpdateOnTitle: "ファイル変更時に自動更新",
31016
32523
  autoUpdateOffTitle: "自動更新オフ — 手動で再読み込み"
31017
32524
  },
32525
+ diff: {
32526
+ files: (count) => `${count}ファイル`,
32527
+ updated: (time) => `更新 ${time}`,
32528
+ updatedTitle: "最終更新",
32529
+ kindAdded: "追加",
32530
+ kindDeleted: "削除",
32531
+ kindRenamed: "名前変更",
32532
+ kindHeavy: "大容量",
32533
+ kindBinary: "バイナリ",
32534
+ kindMedia: "メディア",
32535
+ viewedProgress: (viewed, total) => `${viewed}/${total} 確認済み`,
32536
+ viewedProgressTitle: "確認進捗",
32537
+ nextUnviewed: "次の未確認",
32538
+ nextUnviewedTitle: "次の未確認ファイルへ移動 (n)",
32539
+ allViewed: "すべて確認済み",
32540
+ allViewedTitle: "表示中のファイルはすべて確認済みです",
32541
+ noChangesTitle: "変更はありません",
32542
+ noChangesBody: "この参照との差分はありません。",
32543
+ noChangesReload: "diff を更新",
32544
+ noChangesReloadTitle: "この差分範囲を再読み込み",
32545
+ noChangesHistory: "履歴を開く",
32546
+ noChangesHistoryTitle: "この範囲のコミット履歴を開く",
32547
+ emptyDiffTitle: "空の差分",
32548
+ emptyDiffBody: "このコミットは最初の親との差分がありません。",
32549
+ noCommitSelectedTitle: "コミット未選択",
32550
+ noCommitSelectedBody: "一覧からコミットを選ぶと変更内容を表示します。"
32551
+ },
31018
32552
  changeBanner: {
31019
32553
  text: "ファイルに変更がありました",
31020
32554
  reload: "再読み込みする",
@@ -31034,14 +32568,56 @@ code-viewer query snapshot delete --id snap-abc123`
31034
32568
  view: "表示",
31035
32569
  tree: "ツリー",
31036
32570
  flat: "一覧",
32571
+ treeTitle: "ツリー表示",
32572
+ flatTitle: "一覧表示",
31037
32573
  filter: "ファイル絞り込み… / ⌘K",
31038
32574
  filterTitle: "ファイルを絞り込みます。/pattern/ は正規表現。/ でこの欄にフォーカス、Cmd/Ctrl+K で全ファイルパレット、Ctrl+G で grep、? でヘルプ。",
31039
- hide: "サイドバーを隠す"
32575
+ filterClear: "解除",
32576
+ filterClearTitle: "ファイル絞り込みを解除",
32577
+ hide: "サイドバーを隠す",
32578
+ show: "サイドバーを表示",
32579
+ repoTarget: "リポジトリの対象",
32580
+ openDirectoryInOs: "このフォルダをOSで開く",
32581
+ omittedHeavyLabel: "省略",
32582
+ omittedHeavyTitle: "ツリー展開は省略されていますが、詳細パネルでは開けます",
32583
+ omittedPrivateLabel: "非公開",
32584
+ omittedPrivateTitle: "このディレクトリはブラウザから開けません",
32585
+ commitEntryGitlinkLabel: "GIT",
32586
+ commitEntryGitlinkTitle: "Git のコミットに固定された参照です。フォルダではないため直接は開けません。",
32587
+ commitEntrySubmoduleLabel: "SUB",
32588
+ commitEntrySubmoduleTitle: "Git サブモジュール: 特定のコミットに固定されています。フォルダではないため直接は開けません。"
32589
+ },
32590
+ repo: {
32591
+ newFolder: "新規フォルダ",
32592
+ moveFolderToTrash: "フォルダをゴミ箱へ移動",
32593
+ uploadButton: "ファイルをアップロード",
32594
+ dropFilesInto: (target) => `${target} にファイルをドロップ`,
32595
+ uploadFailed: "アップロードに失敗しました",
32596
+ emptyDirectory: "このディレクトリにファイルはありません。",
32597
+ uploadConfirmTitle: "ファイルをアップロードしますか?",
32598
+ uploadConfirmBody: (count, target) => `${target} に ${count} 件のファイルをアップロードしますか?`,
32599
+ uploadConfirmLabel: "アップロード",
32600
+ sortName: "名前",
32601
+ sortUpdated: "更新日時",
32602
+ sortSize: "サイズ",
32603
+ repositoryFallback: "リポジトリ",
32604
+ repositoryRootFallback: "リポジトリのルート",
32605
+ gitlinkLabel: "固定コミット",
32606
+ gitlinkTitle: "特定のコミットに固定された参照です。この ref では直接開けません。",
32607
+ submoduleLabel: "サブモジュール",
32608
+ submoduleTitle: "Git サブモジュール: 特定のコミットに固定されています。直接は開けません。"
31040
32609
  },
31041
32610
  history: {
31042
32611
  title: "コミット",
31043
32612
  filter: "コミットを絞り込み...",
31044
- filterTitle: "メッセージ、SHA、author:name、path:file でコミットを絞り込みます。"
32613
+ filterTitle: "メッセージ、SHA、author:name、path:file でコミットを絞り込みます。",
32614
+ refreshTitle: "コミット履歴を更新"
32615
+ },
32616
+ quickHelp: {
32617
+ buttonTitle: "クイックヘルプ(ショートカット)",
32618
+ panelTitle: "クイックヘルプ",
32619
+ close: "クイックヘルプを閉じる",
32620
+ viewAll: "すべてのキーバインドを見る →"
31045
32621
  },
31046
32622
  settings: {
31047
32623
  title: "ビューア設定",
@@ -31117,6 +32693,8 @@ code-viewer query snapshot delete --id snap-abc123`
31117
32693
  });
31118
32694
  setElementText(".global-help-link[data-route='help']", text2.nav.help);
31119
32695
  setElementText(".product-label", text2.global.product);
32696
+ document.querySelector(".brand")?.setAttribute("aria-label", text2.global.brandHome);
32697
+ document.querySelector(".app-menu")?.setAttribute("aria-label", text2.global.menuViews);
31120
32698
  const annotationsToggle = document.querySelector("#annotations-toggle");
31121
32699
  if (annotationsToggle) {
31122
32700
  annotationsToggle.title = text2.global.annotations;
@@ -31132,27 +32710,51 @@ code-viewer query snapshot delete --id snap-abc123`
31132
32710
  theme.title = text2.global.theme;
31133
32711
  theme.setAttribute("aria-label", text2.global.theme);
31134
32712
  }
32713
+ const quickHelpBtn = document.querySelector("#quick-help-btn");
32714
+ if (quickHelpBtn) {
32715
+ quickHelpBtn.title = text2.quickHelp.buttonTitle;
32716
+ quickHelpBtn.setAttribute("aria-label", text2.quickHelp.buttonTitle);
32717
+ }
32718
+ QUICK_HELP?.localize();
32719
+ const doctorTitle = doctorText(STATE.language).title;
32720
+ const doctorBtn = document.querySelector("#doctor-btn");
32721
+ if (doctorBtn) {
32722
+ doctorBtn.title = doctorTitle;
32723
+ doctorBtn.setAttribute("aria-label", doctorTitle);
32724
+ }
32725
+ document.querySelector("#doctor-sheet")?.setAttribute("aria-label", doctorTitle);
31135
32726
  const copyAiContext = document.querySelector("#copy-ai-context");
31136
32727
  if (copyAiContext) {
31137
32728
  copyAiContext.title = text2.global.copyAiContext;
31138
32729
  copyAiContext.setAttribute("aria-label", text2.global.copyAiContext);
32730
+ const copyAiContextLabel = copyAiContext.querySelector(".ai-context-label");
32731
+ if (copyAiContextLabel)
32732
+ copyAiContextLabel.textContent = text2.global.copyAiContextLabel;
31139
32733
  }
31140
32734
  const refReset = document.querySelector("#ref-reset");
31141
- if (refReset)
32735
+ if (refReset) {
31142
32736
  refReset.title = text2.topbar.resetRange;
32737
+ refReset.setAttribute("aria-label", text2.topbar.resetRange);
32738
+ }
31143
32739
  const reload = document.querySelector("#reload-prom");
31144
- if (reload)
32740
+ if (reload) {
31145
32741
  reload.title = text2.topbar.reload;
32742
+ reload.setAttribute("aria-label", text2.topbar.reload);
32743
+ }
31146
32744
  const layoutGroup = document.querySelector("#topbar .seg");
31147
32745
  layoutGroup?.setAttribute("aria-label", text2.topbar.layout);
31148
32746
  setElementText('#topbar .seg button[data-layout="line-by-line"]', text2.topbar.unified);
31149
32747
  setElementText('#topbar .seg button[data-layout="side-by-side"]', text2.topbar.split);
31150
32748
  const ignoreWs = document.querySelector("#ignore-ws");
31151
- if (ignoreWs)
32749
+ if (ignoreWs) {
31152
32750
  ignoreWs.title = text2.topbar.ignoreWs;
32751
+ ignoreWs.textContent = text2.topbar.ignoreWsLabel;
32752
+ }
31153
32753
  const hideTests = document.querySelector("#hide-tests");
31154
- if (hideTests)
32754
+ if (hideTests) {
31155
32755
  hideTests.title = text2.topbar.hideTests;
32756
+ hideTests.textContent = text2.topbar.hideTestsLabel;
32757
+ }
31156
32758
  applyAutoUpdateButton();
31157
32759
  setHighlightButton(STATE.syntaxHighlight && getHljs() ? "loaded" : "idle");
31158
32760
  setElementText(".sb-title", text2.sidebar.files);
@@ -31172,15 +32774,33 @@ code-viewer query snapshot delete --id snap-abc123`
31172
32774
  sbView?.setAttribute("aria-label", text2.sidebar.view);
31173
32775
  setElementText('.sb-view-seg button[data-view="tree"]', text2.sidebar.tree);
31174
32776
  setElementText('.sb-view-seg button[data-view="flat"]', text2.sidebar.flat);
32777
+ const sbViewTree = document.querySelector('.sb-view-seg button[data-view="tree"]');
32778
+ if (sbViewTree)
32779
+ sbViewTree.title = text2.sidebar.treeTitle;
32780
+ const sbViewFlat = document.querySelector('.sb-view-seg button[data-view="flat"]');
32781
+ if (sbViewFlat)
32782
+ sbViewFlat.title = text2.sidebar.flatTitle;
31175
32783
  const filter = document.querySelector("#sb-filter");
31176
32784
  if (filter) {
31177
32785
  filter.placeholder = text2.sidebar.filter;
31178
32786
  filter.title = text2.sidebar.filterTitle;
31179
32787
  }
32788
+ const filterClear = document.querySelector("#sb-filter-clear");
32789
+ if (filterClear) {
32790
+ filterClear.textContent = text2.sidebar.filterClear;
32791
+ filterClear.title = text2.sidebar.filterClearTitle;
32792
+ filterClear.setAttribute("aria-label", text2.sidebar.filterClearTitle);
32793
+ }
32794
+ const repoTarget = document.querySelector("#repo-target");
32795
+ if (repoTarget) {
32796
+ repoTarget.title = text2.sidebar.repoTarget;
32797
+ repoTarget.setAttribute("aria-label", text2.sidebar.repoTarget);
32798
+ }
31180
32799
  const sidebarToggle = document.querySelector("#sidebar-toggle");
31181
32800
  if (sidebarToggle) {
31182
- sidebarToggle.title = text2.sidebar.hide;
31183
- sidebarToggle.setAttribute("aria-label", text2.sidebar.hide);
32801
+ const sidebarToggleTitle = STATE.sidebarHidden ? text2.sidebar.show : text2.sidebar.hide;
32802
+ sidebarToggle.title = sidebarToggleTitle;
32803
+ sidebarToggle.setAttribute("aria-label", sidebarToggleTitle);
31184
32804
  }
31185
32805
  setElementText(".sidebar-toggle-label", text2.sidebar.files);
31186
32806
  setElementText(".history-title", text2.history.title);
@@ -31191,6 +32811,11 @@ code-viewer query snapshot delete --id snap-abc123`
31191
32811
  historyFilter.placeholder = text2.history.filter;
31192
32812
  historyFilter.title = text2.history.filterTitle;
31193
32813
  }
32814
+ document.querySelectorAll(".history-refresh").forEach((button) => {
32815
+ button.title = text2.history.refreshTitle;
32816
+ button.setAttribute("aria-label", text2.history.refreshTitle);
32817
+ });
32818
+ relocalizeHistory?.();
31194
32819
  setElementText(".scope-settings-head strong", text2.settings.title);
31195
32820
  const settingsClose = document.querySelector("#scope-settings-close");
31196
32821
  settingsClose?.setAttribute("aria-label", text2.settings.close);
@@ -31259,7 +32884,9 @@ code-viewer query snapshot delete --id snap-abc123`
31259
32884
  setButtonLabel(document.querySelector("#query-history-panel-close"), text2.annotations.close);
31260
32885
  relocalizeDatabase?.();
31261
32886
  }
32887
+ let relocalizeHistory = null;
31262
32888
  let relocalizeDatabase = null;
32889
+ let QUICK_HELP = null;
31263
32890
  function setViewerLanguage(language, persist = true) {
31264
32891
  const next = normalizeViewerLanguage(language);
31265
32892
  STATE.language = next;
@@ -31286,6 +32913,13 @@ code-viewer query snapshot delete --id snap-abc123`
31286
32913
  el.classList.remove("live", "refreshing", "error");
31287
32914
  if (s2)
31288
32915
  el.classList.add(s2);
32916
+ const text2 = uiText();
32917
+ const label = s2 === "live" ? text2.global.statusLive : s2 === "refreshing" ? text2.global.statusLoading : s2 === "error" ? text2.global.statusError : text2.global.statusIdle;
32918
+ const labelEl = el.querySelector(".status-label");
32919
+ if (labelEl)
32920
+ labelEl.textContent = label;
32921
+ el.setAttribute("aria-label", label);
32922
+ updateNetworkActivity();
31289
32923
  }
31290
32924
  function applyTheme() {
31291
32925
  document.documentElement.dataset.theme = STATE.theme;
@@ -31587,8 +33221,10 @@ code-viewer query snapshot delete --id snap-abc123`
31587
33221
  input.readOnly = true;
31588
33222
  input.autocomplete = "off";
31589
33223
  input.placeholder = options.placeholder;
31590
- if (options.title)
33224
+ if (options.title) {
31591
33225
  input.title = options.title;
33226
+ input.setAttribute("aria-label", options.title);
33227
+ }
31592
33228
  if (options.value != null)
31593
33229
  input.value = options.value;
31594
33230
  const caret = document.createElement("span");
@@ -31692,7 +33328,8 @@ code-viewer query snapshot delete --id snap-abc123`
31692
33328
  currentRange,
31693
33329
  setRoute,
31694
33330
  setPreferredSourceTab: (tab) => SOURCE_VIEW.setPreferredSourceTab(tab),
31695
- createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref)
33331
+ createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref),
33332
+ emptyText: () => uiText().diff
31696
33333
  }, historyRoute);
31697
33334
  activeFileHistoryDiffHost = mount.diffHost;
31698
33335
  activeFileHistoryEmptyHost = mount.emptyHost;
@@ -31705,6 +33342,9 @@ code-viewer query snapshot delete --id snap-abc123`
31705
33342
  function withAnnotationSessionParam(rawUrl) {
31706
33343
  return ANNOTATIONS_UI ? ANNOTATIONS_UI.withSessionParam(rawUrl) : rawUrl;
31707
33344
  }
33345
+ function urlForRoute(route) {
33346
+ return withDoctorOverlay(withAnnotationSessionParam(buildRoute(route)), parseDoctorOverlay(window.location.pathname, window.location.search));
33347
+ }
31708
33348
  function historyStateForRoute(route) {
31709
33349
  return route.screen === "file" ? {
31710
33350
  screen: "file",
@@ -31779,7 +33419,7 @@ code-viewer query snapshot delete --id snap-abc123`
31779
33419
  if (nextRoute.screen === "repo" || nextRoute.screen === "file" && (nextRoute.view === "blob" || nextRoute.view === "blame" || nextRoute.view === "history")) {
31780
33420
  STATE.repoRef = nextRoute.ref || "worktree";
31781
33421
  }
31782
- const url = withDoctorOverlay(withAnnotationSessionParam(buildRoute(nextRoute)), parseDoctorOverlay(window.location.pathname, window.location.search));
33422
+ const url = urlForRoute(nextRoute);
31783
33423
  const state = historyStateForRoute(nextRoute);
31784
33424
  if (replace2)
31785
33425
  history.replaceState(state, "", url);
@@ -32024,6 +33664,7 @@ code-viewer query snapshot delete --id snap-abc123`
32024
33664
  SERVER_GENERATION = generation;
32025
33665
  },
32026
33666
  invalidateRepoSidebar,
33667
+ diffText: () => uiText().diff,
32027
33668
  getDiffRoot: () => activeFileHistoryDiffHost || $("#diff"),
32028
33669
  getEmptyPane: () => activeFileHistoryEmptyHost || $("#empty"),
32029
33670
  isEmbeddedDiffMode: () => !!activeFileHistoryDiffHost
@@ -32057,10 +33698,26 @@ code-viewer query snapshot delete --id snap-abc123`
32057
33698
  if (themeButton) {
32058
33699
  themeButton.innerHTML = iconSvg("octicon-moon", MOON_16_PATH);
32059
33700
  }
32060
- const copyAiContextButton = document.querySelector("#copy-ai-context");
32061
- if (copyAiContextButton) {
32062
- copyAiContextButton.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
33701
+ const copyAiContextIcon = document.querySelector("#copy-ai-context .goi-icon");
33702
+ if (copyAiContextIcon) {
33703
+ copyAiContextIcon.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
32063
33704
  }
33705
+ const autoUpdateIcon = document.querySelector("#auto-update .goi-icon");
33706
+ if (autoUpdateIcon) {
33707
+ autoUpdateIcon.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
33708
+ }
33709
+ const cancelRequestsIcon = document.querySelector("#cancel-requests .goi-icon");
33710
+ if (cancelRequestsIcon) {
33711
+ cancelRequestsIcon.innerHTML = iconSvg("octicon-x", X_16_PATH);
33712
+ }
33713
+ }
33714
+ function setRefActionIcons() {
33715
+ const refReset = document.querySelector("#ref-reset");
33716
+ if (refReset)
33717
+ refReset.innerHTML = iconSvg("octicon-x", X_16_PATH);
33718
+ const reload = document.querySelector("#reload-prom");
33719
+ if (reload)
33720
+ reload.innerHTML = iconSvg("octicon-sync", SYNC_16_PATH);
32064
33721
  }
32065
33722
  applySidebarFontSize();
32066
33723
  applyCodeFontSize();
@@ -32070,6 +33727,7 @@ code-viewer query snapshot delete --id snap-abc123`
32070
33727
  hydrateRefSelectorMounts();
32071
33728
  setSidebarTreeActionIcons();
32072
33729
  setGlobalHeaderIcons();
33730
+ setRefActionIcons();
32073
33731
  $$(".sb-view-seg button").forEach((b2) => {
32074
33732
  b2.addEventListener("click", () => {
32075
33733
  STATE.sbView = b2.dataset.view || "tree";
@@ -32100,11 +33758,15 @@ code-viewer query snapshot delete --id snap-abc123`
32100
33758
  };
32101
33759
  }
32102
33760
  }
33761
+ 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
33762
  const text2 = aiContextClipboardText({
32104
33763
  route: STATE.route,
32105
33764
  diffFrom: STATE.from,
32106
33765
  diffTo: STATE.to,
32107
- selectionCode
33766
+ selectionCode,
33767
+ diffMeta: window._lastMeta ? visibleDiffMetaForBrief(window._lastMeta) : null,
33768
+ viewedFiles: STATE.viewedFiles,
33769
+ databaseQuerySql
32108
33770
  });
32109
33771
  const finish = (ok, withCode, lineCount, reason) => {
32110
33772
  const label = reason === "empty" ? uiText().global.copyAiContextEmpty : ok ? withCode ? uiText().global.copyAiContextCopiedWithCode(lineCount) : uiText().global.copyAiContextCopied : uiText().global.copyAiContextFailed;
@@ -32336,7 +33998,10 @@ code-viewer query snapshot delete --id snap-abc123`
32336
33998
  }
32337
33999
  const sbFilter = $("#sb-filter");
32338
34000
  if (sbFilter) {
32339
- sbFilter.addEventListener("input", () => scheduleApplyFilter());
34001
+ sbFilter.addEventListener("input", () => {
34002
+ syncSidebarFilterClearButton();
34003
+ scheduleApplyFilter();
34004
+ });
32340
34005
  sbFilter.addEventListener("keydown", (e2) => {
32341
34006
  if (isImeComposing(e2))
32342
34007
  return;
@@ -32351,6 +34016,7 @@ code-viewer query snapshot delete --id snap-abc123`
32351
34016
  } else if (e2.key === "Escape") {
32352
34017
  if (sbFilter.value) {
32353
34018
  sbFilter.value = "";
34019
+ syncSidebarFilterClearButton();
32354
34020
  flushSidebarFilter();
32355
34021
  applyFilter();
32356
34022
  } else {
@@ -32359,6 +34025,11 @@ code-viewer query snapshot delete --id snap-abc123`
32359
34025
  }
32360
34026
  });
32361
34027
  }
34028
+ const sbFilterClear = document.querySelector("#sb-filter-clear");
34029
+ if (sbFilterClear) {
34030
+ syncSidebarFilterClearButton();
34031
+ sbFilterClear.addEventListener("click", clearSidebarFilter);
34032
+ }
32362
34033
  function focusFileFilter() {
32363
34034
  const input = $("#sb-filter");
32364
34035
  input.focus();
@@ -32510,17 +34181,13 @@ code-viewer query snapshot delete --id snap-abc123`
32510
34181
  }));
32511
34182
  return true;
32512
34183
  }
34184
+ if (action === "next-unviewed-file") {
34185
+ if (DIFF_VIEW.scrollToNextUnviewedFile())
34186
+ scheduleMainSurfaceFocus();
34187
+ return true;
34188
+ }
32513
34189
  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
- });
34190
+ QUICK_HELP?.toggle();
32524
34191
  return true;
32525
34192
  }
32526
34193
  return false;
@@ -32573,6 +34240,103 @@ code-viewer query snapshot delete --id snap-abc123`
32573
34240
  if (window.location.pathname === "/") {
32574
34241
  setRoute(STATE.route, true);
32575
34242
  }
34243
+ function normalizedHistoryRefForEmptyDiff() {
34244
+ const candidate = STATE.to && STATE.to !== "worktree" ? STATE.to : STATE.from || "HEAD";
34245
+ return candidate && candidate !== "worktree" && !candidate.startsWith("--") ? candidate : "HEAD";
34246
+ }
34247
+ function emptyDiffHistoryRoute() {
34248
+ return {
34249
+ screen: "history",
34250
+ ref: normalizedHistoryRefForEmptyDiff(),
34251
+ range: currentRange()
34252
+ };
34253
+ }
34254
+ function setEmptyActionContent(action, iconName, iconPath, label, title) {
34255
+ action.title = title;
34256
+ action.setAttribute("aria-label", title);
34257
+ action.replaceChildren();
34258
+ const icon = document.createElement("span");
34259
+ icon.className = "empty-action-icon";
34260
+ icon.setAttribute("aria-hidden", "true");
34261
+ icon.innerHTML = iconSvg(iconName, iconPath);
34262
+ const text2 = document.createElement("span");
34263
+ text2.className = "empty-action-label";
34264
+ text2.textContent = label;
34265
+ action.append(icon, text2);
34266
+ }
34267
+ function navigateToEmptyDiffHistory(event) {
34268
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
34269
+ return;
34270
+ event.preventDefault();
34271
+ const route = emptyDiffHistoryRoute();
34272
+ history.pushState(historyStateForRoute(route), "", urlForRoute(route));
34273
+ window.scrollTo(0, 0);
34274
+ applyRouteFromLocation();
34275
+ }
34276
+ function ensureEmptyDiffActions(empty) {
34277
+ let actions = empty.querySelector(".empty-actions");
34278
+ if (actions)
34279
+ return actions;
34280
+ actions = document.createElement("div");
34281
+ actions.className = "empty-actions";
34282
+ actions.hidden = true;
34283
+ const reload = document.createElement("button");
34284
+ reload.type = "button";
34285
+ reload.className = "empty-action empty-action-primary";
34286
+ reload.dataset.emptyAction = "reload";
34287
+ reload.addEventListener("click", () => reloadDiffFromUi(reload));
34288
+ const historyLink = document.createElement("a");
34289
+ historyLink.className = "empty-action";
34290
+ historyLink.dataset.emptyAction = "history";
34291
+ historyLink.addEventListener("click", navigateToEmptyDiffHistory);
34292
+ actions.append(reload, historyLink);
34293
+ empty.appendChild(actions);
34294
+ return actions;
34295
+ }
34296
+ function syncEmptyDiffPane(empty, onHistory) {
34297
+ empty.classList.toggle("empty-with-actions", !onHistory);
34298
+ const text2 = uiText().diff;
34299
+ const h2 = empty.querySelector("h2");
34300
+ if (h2)
34301
+ h2.textContent = onHistory ? text2.emptyDiffTitle : text2.noChangesTitle;
34302
+ const p2 = empty.querySelector("p");
34303
+ if (p2)
34304
+ p2.textContent = onHistory ? text2.emptyDiffBody : text2.noChangesBody;
34305
+ const existingActions = empty.querySelector(".empty-actions");
34306
+ if (onHistory) {
34307
+ if (existingActions)
34308
+ existingActions.hidden = true;
34309
+ return;
34310
+ }
34311
+ const actions = ensureEmptyDiffActions(empty);
34312
+ const reload = actions.querySelector('[data-empty-action="reload"]');
34313
+ const historyLink = actions.querySelector('[data-empty-action="history"]');
34314
+ if (reload)
34315
+ setEmptyActionContent(reload, "octicon-sync", SYNC_16_PATH, text2.noChangesReload, text2.noChangesReloadTitle);
34316
+ if (historyLink) {
34317
+ const route = emptyDiffHistoryRoute();
34318
+ historyLink.href = urlForRoute(route);
34319
+ setEmptyActionContent(historyLink, "octicon-git-branch", GIT_BRANCH_16_PATH, text2.noChangesHistory, text2.noChangesHistoryTitle);
34320
+ }
34321
+ actions.hidden = false;
34322
+ }
34323
+ function reloadDiffFromUi(trigger) {
34324
+ const topbarButton = $("#reload-prom");
34325
+ topbarButton.classList.add("spinning");
34326
+ if (trigger && trigger !== topbarButton) {
34327
+ trigger.classList.add("spinning");
34328
+ trigger.setAttribute("aria-busy", "true");
34329
+ }
34330
+ load().finally(() => {
34331
+ setTimeout(() => {
34332
+ topbarButton.classList.remove("spinning");
34333
+ if (trigger && trigger !== topbarButton) {
34334
+ trigger.classList.remove("spinning");
34335
+ trigger.setAttribute("aria-busy", "false");
34336
+ }
34337
+ }, 200);
34338
+ });
34339
+ }
32576
34340
  function load(options = {}) {
32577
34341
  if (STATE.route.screen === "help") {
32578
34342
  setStatus("live");
@@ -32598,12 +34362,7 @@ code-viewer query snapshot delete --id snap-abc123`
32598
34362
  const empty = activeFileHistoryEmptyHost || $("#empty");
32599
34363
  if (empty) {
32600
34364
  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.";
34365
+ syncEmptyDiffPane(empty, onHistory);
32607
34366
  }
32608
34367
  }
32609
34368
  const routeAtRequest = STATE.route;
@@ -32629,7 +34388,7 @@ code-viewer query snapshot delete --id snap-abc123`
32629
34388
  return null;
32630
34389
  const result = renderShell(data, options.changedPaths);
32631
34390
  applyHideTestsToMeta();
32632
- setStatus("live");
34391
+ setStatus(data.error ? "error" : "live");
32633
34392
  return result;
32634
34393
  }).catch(() => {
32635
34394
  if (!isCurrentDiffRequest())
@@ -32716,12 +34475,13 @@ code-viewer query snapshot delete --id snap-abc123`
32716
34475
  DIFF_VIEW.clearLoadQueue();
32717
34476
  if (activeFileHistoryEmptyHost) {
32718
34477
  activeFileHistoryEmptyHost.classList.remove("hidden");
34478
+ const text2 = uiText().diff;
32719
34479
  const h2 = activeFileHistoryEmptyHost.querySelector("h2");
32720
34480
  if (h2)
32721
- h2.textContent = "No commit selected";
34481
+ h2.textContent = text2.noCommitSelectedTitle;
32722
34482
  const p2 = activeFileHistoryEmptyHost.querySelector("p");
32723
34483
  if (p2)
32724
- p2.textContent = "Select a commit from the list to see its changes.";
34484
+ p2.textContent = text2.noCommitSelectedBody;
32725
34485
  }
32726
34486
  setStatus("live");
32727
34487
  return;
@@ -32740,13 +34500,32 @@ code-viewer query snapshot delete --id snap-abc123`
32740
34500
  invalidateRepoSidebar,
32741
34501
  clearLoadQueue: () => DIFF_VIEW.clearLoadQueue(),
32742
34502
  placeSidebarToggle,
32743
- setStatus
34503
+ setStatus,
34504
+ emptyText: () => uiText().diff
32744
34505
  });
32745
34506
  },
32746
34507
  getSyntaxHighlight: () => STATE.syntaxHighlight,
32747
34508
  getLanguage: () => STATE.language,
32748
34509
  trackLoad
32749
34510
  });
34511
+ relocalizeHistory = () => HISTORY_VIEW.localize();
34512
+ QUICK_HELP = createQuickHelp({
34513
+ $,
34514
+ getLanguage: () => STATE.language,
34515
+ getText: () => uiText().quickHelp,
34516
+ openFullKeybindings: () => {
34517
+ openHelpKeybindings({
34518
+ getRoute: () => STATE.route,
34519
+ getLanguage: () => STATE.language,
34520
+ currentRange,
34521
+ setRoute,
34522
+ setPageMode,
34523
+ renderHelpPage,
34524
+ setStatus,
34525
+ cancelActiveSourceLoad
34526
+ });
34527
+ }
34528
+ });
32750
34529
  const DOCTOR_VIEW = createDoctorView({
32751
34530
  $: (sel) => document.querySelector(sel),
32752
34531
  escapeHtml: escapeHtml3,
@@ -32968,13 +34747,7 @@ code-viewer query snapshot delete --id snap-abc123`
32968
34747
  });
32969
34748
  if (STATE.syntaxHighlight)
32970
34749
  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
- });
34750
+ $("#reload-prom").addEventListener("click", () => reloadDiffFromUi());
32978
34751
  function applyHideTests() {
32979
34752
  const btn = $("#hide-tests");
32980
34753
  if (btn)
@@ -32996,26 +34769,34 @@ code-viewer query snapshot delete --id snap-abc123`
32996
34769
  applyViewedState();
32997
34770
  applyHideTestsToMeta();
32998
34771
  }
32999
- function applyHideTestsToMeta() {
33000
- const meta = window._lastMeta;
33001
- if (!meta || !meta.totals)
33002
- return;
34772
+ function visibleDiffMetaForBrief(meta) {
34773
+ if (!meta.totals)
34774
+ return meta;
33003
34775
  const effective = STATE.hideTests && !isRepositorySidebarMode();
33004
- if (!effective) {
33005
- renderMeta(meta);
33006
- return;
33007
- }
34776
+ if (!effective)
34777
+ return meta;
33008
34778
  let additions = 0;
33009
34779
  let deletions = 0;
33010
- let files = 0;
33011
- for (const f2 of STATE.files) {
34780
+ const visibleFiles = [];
34781
+ for (const f2 of meta.files) {
33012
34782
  if (TEST_RE.test(f2.path || ""))
33013
34783
  continue;
33014
34784
  additions += f2.additions || 0;
33015
34785
  deletions += f2.deletions || 0;
33016
- files += 1;
34786
+ visibleFiles.push(f2);
33017
34787
  }
33018
- renderMeta({ ...meta, totals: { files, additions, deletions } });
34788
+ return {
34789
+ ...meta,
34790
+ files: visibleFiles,
34791
+ totals: { files: visibleFiles.length, additions, deletions }
34792
+ };
34793
+ }
34794
+ function applyHideTestsToMeta() {
34795
+ const meta = window._lastMeta;
34796
+ if (!meta?.totals)
34797
+ return;
34798
+ renderMeta(visibleDiffMetaForBrief(meta));
34799
+ applyViewedState();
33019
34800
  }
33020
34801
  applyHideTests();
33021
34802
  $("#hide-tests").addEventListener("click", () => {
@@ -33154,9 +34935,13 @@ code-viewer query snapshot delete --id snap-abc123`
33154
34935
  return;
33155
34936
  const text2 = uiText();
33156
34937
  btn.classList.toggle("active", STATE.autoUpdate);
33157
- btn.textContent = text2.topbar.autoUpdate;
33158
- btn.title = STATE.autoUpdate ? text2.topbar.autoUpdateOnTitle : text2.topbar.autoUpdateOffTitle;
34938
+ const autoUpdateTitle = STATE.autoUpdate ? text2.topbar.autoUpdateOnTitle : text2.topbar.autoUpdateOffTitle;
34939
+ btn.title = autoUpdateTitle;
34940
+ btn.setAttribute("aria-label", autoUpdateTitle);
33159
34941
  btn.setAttribute("aria-pressed", STATE.autoUpdate ? "true" : "false");
34942
+ const label = btn.querySelector(".auto-update-label");
34943
+ if (label)
34944
+ label.textContent = text2.topbar.autoUpdate;
33160
34945
  }
33161
34946
  function setAutoUpdate(on) {
33162
34947
  STATE.autoUpdate = on;
@@ -33359,6 +35144,8 @@ code-viewer query snapshot delete --id snap-abc123`
33359
35144
  paths = parsed.paths;
33360
35145
  } catch {}
33361
35146
  }
35147
+ if (isHistoryPanelRoute(STATE.route))
35148
+ HISTORY_VIEW.notePossibleUpdate();
33362
35149
  scheduleSseLoad(paths);
33363
35150
  });
33364
35151
  es.addEventListener("watch-limit", (event) => {