@youtyan/code-viewer 0.1.42 → 0.1.43
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/README.md +1 -0
- package/dist/code-viewer.js +124 -2
- package/package.json +1 -1
- package/web/app.js +445 -2
- package/web/index.html +23 -0
- package/web/style.css +171 -0
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Requires Node.js 20 or newer when installed from npm. Development uses
|
|
|
9
9
|
|
|
10
10
|
- Browse repository files and folders in a persistent sidebar.
|
|
11
11
|
- View git diffs with unified or split layout, lazy loading, and viewed-file state.
|
|
12
|
+
- Browse commit history per branch and open any commit's changed files and diff, with shareable `/history?commit=<sha>` links.
|
|
12
13
|
- Open files directly from the repository or diff view, including large generated files.
|
|
13
14
|
- Preview Markdown with a table of contents, task lists, Mermaid diagrams, and Shiki code highlighting.
|
|
14
15
|
- Preview browser-safe media and show metadata for binary files that cannot be rendered.
|
package/dist/code-viewer.js
CHANGED
|
@@ -633,6 +633,106 @@ function refCommits(cwd, query = "", max = DEFAULT_REF_COMMIT_LIMIT) {
|
|
|
633
633
|
]);
|
|
634
634
|
return mergeCommitResults(limit, hashMatches, subjectMatches, authorMatches);
|
|
635
635
|
}
|
|
636
|
+
function parseHistoryLog(stdout) {
|
|
637
|
+
const parts = stdout.split("\x00");
|
|
638
|
+
const commits = [];
|
|
639
|
+
for (let index = 0;index < parts.length; ) {
|
|
640
|
+
if (!parts[index]) {
|
|
641
|
+
index++;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const sha = parts[index++] || "";
|
|
645
|
+
const subject = parts[index++] || "";
|
|
646
|
+
const author = parts[index++] || "";
|
|
647
|
+
const when = parts[index++] || "";
|
|
648
|
+
const parentsRaw = (parts[index++] || "").trim();
|
|
649
|
+
const body = (parts[index++] || "").trim();
|
|
650
|
+
if (sha)
|
|
651
|
+
commits.push({
|
|
652
|
+
sha,
|
|
653
|
+
subject,
|
|
654
|
+
author,
|
|
655
|
+
when,
|
|
656
|
+
parents: parentsRaw ? parentsRaw.split(/\s+/) : [],
|
|
657
|
+
body
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return commits;
|
|
661
|
+
}
|
|
662
|
+
function historyQueryArgs(query) {
|
|
663
|
+
const trimmed = query.trim().slice(0, 200).replace(/\0/g, "");
|
|
664
|
+
if (!trimmed)
|
|
665
|
+
return { filterArgs: [], pathspec: [], shaTerm: "" };
|
|
666
|
+
const prefixed = /^(author|path):(.*)$/.exec(trimmed);
|
|
667
|
+
if (prefixed) {
|
|
668
|
+
const term = prefixed[2].trim();
|
|
669
|
+
if (!term)
|
|
670
|
+
return { filterArgs: [], pathspec: [], shaTerm: "" };
|
|
671
|
+
if (prefixed[1] === "author") {
|
|
672
|
+
return {
|
|
673
|
+
filterArgs: [
|
|
674
|
+
"--regexp-ignore-case",
|
|
675
|
+
"--fixed-strings",
|
|
676
|
+
`--author=${term}`
|
|
677
|
+
],
|
|
678
|
+
pathspec: [],
|
|
679
|
+
shaTerm: ""
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
filterArgs: [],
|
|
684
|
+
pathspec: ["--", `:(icase)*${term}*`],
|
|
685
|
+
shaTerm: ""
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
filterArgs: [
|
|
690
|
+
"--regexp-ignore-case",
|
|
691
|
+
"--fixed-strings",
|
|
692
|
+
`--grep=${trimmed}`
|
|
693
|
+
],
|
|
694
|
+
pathspec: [],
|
|
695
|
+
shaTerm: /^[0-9a-f]{4,40}$/i.test(trimmed) ? trimmed : ""
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function commitHistory(cwd, options) {
|
|
699
|
+
const ref = (options.ref || "HEAD").trim();
|
|
700
|
+
if (!ref || ref.startsWith("-") || ref.includes("\x00"))
|
|
701
|
+
return { commits: [], hasMore: false, error: "invalid ref" };
|
|
702
|
+
const verified = run(["git", "rev-parse", "--verify", `${ref}^{commit}`], cwd);
|
|
703
|
+
if (verified.code !== 0)
|
|
704
|
+
return { commits: [], hasMore: false, error: "unknown ref" };
|
|
705
|
+
const skip = Math.max(0, Math.floor(options.skip) || 0);
|
|
706
|
+
const limit = Math.max(1, Math.min(Math.floor(options.limit) || 1, MAX_HISTORY_LIMIT));
|
|
707
|
+
const { filterArgs, pathspec, shaTerm } = historyQueryArgs(options.query || "");
|
|
708
|
+
const res = run([
|
|
709
|
+
"git",
|
|
710
|
+
"log",
|
|
711
|
+
"-z",
|
|
712
|
+
`--skip=${skip}`,
|
|
713
|
+
`--max-count=${limit + 1}`,
|
|
714
|
+
`--format=${HISTORY_FORMAT}`,
|
|
715
|
+
...filterArgs,
|
|
716
|
+
verified.stdout.trim(),
|
|
717
|
+
...pathspec
|
|
718
|
+
], cwd);
|
|
719
|
+
if (res.code !== 0)
|
|
720
|
+
return { commits: [], hasMore: false, error: "git log failed" };
|
|
721
|
+
let parsed = parseHistoryLog(res.stdout);
|
|
722
|
+
if (shaTerm && skip === 0) {
|
|
723
|
+
const bySha = run(["git", "rev-parse", "--verify", `${shaTerm}^{commit}`], cwd);
|
|
724
|
+
const sha = bySha.code === 0 ? bySha.stdout.trim() : "";
|
|
725
|
+
if (sha) {
|
|
726
|
+
const single = run(["git", "log", "-z", "-1", `--format=${HISTORY_FORMAT}`, sha], cwd);
|
|
727
|
+
if (single.code === 0) {
|
|
728
|
+
const hit = parseHistoryLog(single.stdout);
|
|
729
|
+
parsed = [...hit, ...parsed.filter((c) => c.sha !== sha)];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const hasMore = parsed.length > limit;
|
|
734
|
+
return { commits: hasMore ? parsed.slice(0, limit) : parsed, hasMore };
|
|
735
|
+
}
|
|
636
736
|
function nameStatus(args, cwd) {
|
|
637
737
|
const res = run([
|
|
638
738
|
"git",
|
|
@@ -1051,7 +1151,7 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
|
|
|
1051
1151
|
lineTruncated
|
|
1052
1152
|
};
|
|
1053
1153
|
}
|
|
1054
|
-
var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
1154
|
+
var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
|
|
1055
1155
|
var init_git = __esm(() => {
|
|
1056
1156
|
init_runtime();
|
|
1057
1157
|
DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
|
|
@@ -1814,7 +1914,13 @@ function normalizeNewDirectoryName(name) {
|
|
|
1814
1914
|
// web-src/core/routes.ts
|
|
1815
1915
|
var SPA_PATHS, APP_ENTRY_PATHS;
|
|
1816
1916
|
var init_routes = __esm(() => {
|
|
1817
|
-
SPA_PATHS = [
|
|
1917
|
+
SPA_PATHS = [
|
|
1918
|
+
"/todif",
|
|
1919
|
+
"/todiff",
|
|
1920
|
+
"/file",
|
|
1921
|
+
"/help",
|
|
1922
|
+
"/history"
|
|
1923
|
+
];
|
|
1818
1924
|
APP_ENTRY_PATHS = ["/", "/index.html"];
|
|
1819
1925
|
});
|
|
1820
1926
|
|
|
@@ -3159,6 +3265,20 @@ function handleRefCommits(url) {
|
|
|
3159
3265
|
const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
|
|
3160
3266
|
return json({ commits: refCommits(cwd, query, max) });
|
|
3161
3267
|
}
|
|
3268
|
+
function handleLog(url) {
|
|
3269
|
+
const ref = url.searchParams.get("ref") || "HEAD";
|
|
3270
|
+
const skip = Number(url.searchParams.get("skip") || "0");
|
|
3271
|
+
const limit = Number(url.searchParams.get("limit") || "50");
|
|
3272
|
+
const result = commitHistory(cwd, {
|
|
3273
|
+
ref,
|
|
3274
|
+
skip: Number.isFinite(skip) ? skip : 0,
|
|
3275
|
+
limit: Number.isFinite(limit) ? limit : 50,
|
|
3276
|
+
query: url.searchParams.get("q") || ""
|
|
3277
|
+
});
|
|
3278
|
+
if (result.error)
|
|
3279
|
+
return text(result.error, 400);
|
|
3280
|
+
return json({ commits: result.commits, hasMore: result.hasMore });
|
|
3281
|
+
}
|
|
3162
3282
|
function handleFileDiff(url) {
|
|
3163
3283
|
const path = url.searchParams.get("path") || "";
|
|
3164
3284
|
if (!safePath(path))
|
|
@@ -4201,6 +4321,8 @@ var init_preview = __esm(async () => {
|
|
|
4201
4321
|
return handleGrep(url);
|
|
4202
4322
|
if (url.pathname === "/_commits")
|
|
4203
4323
|
return handleRefCommits(url);
|
|
4324
|
+
if (url.pathname === "/_log")
|
|
4325
|
+
return handleLog(url);
|
|
4204
4326
|
if (url.pathname === "/file_diff")
|
|
4205
4327
|
return handleFileDiff(url);
|
|
4206
4328
|
if (url.pathname === "/file_range")
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -438,6 +438,15 @@
|
|
|
438
438
|
lang: params.get("lang") || "en",
|
|
439
439
|
section: params.get("section") || "keybindings"
|
|
440
440
|
};
|
|
441
|
+
case "/history": {
|
|
442
|
+
const commit = params.get("commit") || "";
|
|
443
|
+
return {
|
|
444
|
+
screen: "history",
|
|
445
|
+
ref: params.get("ref") || "HEAD",
|
|
446
|
+
...commit ? { commit } : {},
|
|
447
|
+
range
|
|
448
|
+
};
|
|
449
|
+
}
|
|
441
450
|
default:
|
|
442
451
|
return {
|
|
443
452
|
screen: "unknown",
|
|
@@ -475,6 +484,15 @@
|
|
|
475
484
|
const qs = params.toString();
|
|
476
485
|
return `/help${qs ? `?${qs}` : ""}`;
|
|
477
486
|
}
|
|
487
|
+
case "history": {
|
|
488
|
+
const params = new URLSearchParams;
|
|
489
|
+
if (route.ref && route.ref !== "HEAD")
|
|
490
|
+
params.set("ref", route.ref);
|
|
491
|
+
if (route.commit)
|
|
492
|
+
params.set("commit", route.commit);
|
|
493
|
+
const qs = params.toString();
|
|
494
|
+
return `/history${qs ? `?${qs}` : ""}`;
|
|
495
|
+
}
|
|
478
496
|
case "unknown":
|
|
479
497
|
return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
|
|
480
498
|
default:
|
|
@@ -9013,6 +9031,356 @@ ${frontmatter.yaml}
|
|
|
9013
9031
|
return { renderHelpPage };
|
|
9014
9032
|
}
|
|
9015
9033
|
|
|
9034
|
+
// web-src/core/history.ts
|
|
9035
|
+
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
9036
|
+
var HISTORY_PAGE_SIZE = 50;
|
|
9037
|
+
var HISTORY_AUTO_LOAD_MAX_PAGES = 20;
|
|
9038
|
+
function commitDiffRange(commit) {
|
|
9039
|
+
return { from: commit.parents[0] || EMPTY_TREE_SHA, to: commit.sha };
|
|
9040
|
+
}
|
|
9041
|
+
function shouldContinueAutoLoad(state) {
|
|
9042
|
+
if (state.found)
|
|
9043
|
+
return false;
|
|
9044
|
+
if (!state.hasMore)
|
|
9045
|
+
return false;
|
|
9046
|
+
return state.pagesLoaded < HISTORY_AUTO_LOAD_MAX_PAGES;
|
|
9047
|
+
}
|
|
9048
|
+
var MONTH_NAMES = [
|
|
9049
|
+
"January",
|
|
9050
|
+
"February",
|
|
9051
|
+
"March",
|
|
9052
|
+
"April",
|
|
9053
|
+
"May",
|
|
9054
|
+
"June",
|
|
9055
|
+
"July",
|
|
9056
|
+
"August",
|
|
9057
|
+
"September",
|
|
9058
|
+
"October",
|
|
9059
|
+
"November",
|
|
9060
|
+
"December"
|
|
9061
|
+
];
|
|
9062
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
9063
|
+
function historyGroupLabel(whenIso, now) {
|
|
9064
|
+
const t2 = Date.parse(whenIso);
|
|
9065
|
+
if (!Number.isFinite(t2))
|
|
9066
|
+
return "Unknown date";
|
|
9067
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
9068
|
+
if (t2 >= dayStart)
|
|
9069
|
+
return "Today";
|
|
9070
|
+
if (t2 >= dayStart - DAY_MS)
|
|
9071
|
+
return "Yesterday";
|
|
9072
|
+
if (t2 >= dayStart - 6 * DAY_MS)
|
|
9073
|
+
return "This week";
|
|
9074
|
+
const d2 = new Date(t2);
|
|
9075
|
+
if (d2.getFullYear() === now.getFullYear() && d2.getMonth() === now.getMonth())
|
|
9076
|
+
return "This month";
|
|
9077
|
+
return `${MONTH_NAMES[d2.getMonth()]} ${d2.getFullYear()}`;
|
|
9078
|
+
}
|
|
9079
|
+
|
|
9080
|
+
// web-src/views/history-view.ts
|
|
9081
|
+
function createHistoryView(deps) {
|
|
9082
|
+
const panel = deps.$("#history-panel");
|
|
9083
|
+
const list2 = deps.$("#history-list");
|
|
9084
|
+
const banner = deps.$("#history-banner");
|
|
9085
|
+
const statusEl = deps.$("#history-status");
|
|
9086
|
+
const sentinel = deps.$("#history-sentinel");
|
|
9087
|
+
let ref = "HEAD";
|
|
9088
|
+
let commits = [];
|
|
9089
|
+
let hasMore = false;
|
|
9090
|
+
let loading = false;
|
|
9091
|
+
let generation = 0;
|
|
9092
|
+
let selectedSha = "";
|
|
9093
|
+
let query = "";
|
|
9094
|
+
function setBanner(message) {
|
|
9095
|
+
banner.textContent = message;
|
|
9096
|
+
banner.hidden = !message;
|
|
9097
|
+
}
|
|
9098
|
+
function setStatusText(message) {
|
|
9099
|
+
statusEl.textContent = message;
|
|
9100
|
+
statusEl.hidden = !message;
|
|
9101
|
+
}
|
|
9102
|
+
function relativeWhen(iso) {
|
|
9103
|
+
const t2 = Date.parse(iso);
|
|
9104
|
+
if (!Number.isFinite(t2))
|
|
9105
|
+
return iso;
|
|
9106
|
+
const sec = Math.round((Date.now() - t2) / 1000);
|
|
9107
|
+
if (sec < 60)
|
|
9108
|
+
return "just now";
|
|
9109
|
+
const min = Math.round(sec / 60);
|
|
9110
|
+
if (min < 60)
|
|
9111
|
+
return `${min}m ago`;
|
|
9112
|
+
const hour = Math.round(min / 60);
|
|
9113
|
+
if (hour < 24)
|
|
9114
|
+
return `${hour}h ago`;
|
|
9115
|
+
const day = Math.round(hour / 24);
|
|
9116
|
+
if (day < 30)
|
|
9117
|
+
return `${day}d ago`;
|
|
9118
|
+
return iso.slice(0, 10);
|
|
9119
|
+
}
|
|
9120
|
+
function fetchPage(skip) {
|
|
9121
|
+
const url = `/_log?ref=${encodeURIComponent(ref)}&skip=${skip}&limit=${HISTORY_PAGE_SIZE}` + (query ? `&q=${encodeURIComponent(query)}` : "");
|
|
9122
|
+
return deps.trackLoad(fetch(url).then(async (r2) => {
|
|
9123
|
+
if (!r2.ok)
|
|
9124
|
+
throw new Error(await r2.text());
|
|
9125
|
+
return await r2.json();
|
|
9126
|
+
})).catch((err) => {
|
|
9127
|
+
setBanner(err instanceof Error ? err.message : "failed to load log");
|
|
9128
|
+
return null;
|
|
9129
|
+
});
|
|
9130
|
+
}
|
|
9131
|
+
function commitRow(commit) {
|
|
9132
|
+
const active = commit.sha === selectedSha ? " active" : "";
|
|
9133
|
+
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(relativeWhen(commit.when))}</span>` + `</span>` + `</li>`;
|
|
9134
|
+
}
|
|
9135
|
+
function renderList() {
|
|
9136
|
+
const now = new Date;
|
|
9137
|
+
const html = [];
|
|
9138
|
+
let lastGroup = "";
|
|
9139
|
+
for (const commit of commits) {
|
|
9140
|
+
const group = historyGroupLabel(commit.when, now);
|
|
9141
|
+
if (group !== lastGroup) {
|
|
9142
|
+
html.push(`<li class="history-group" aria-hidden="true">${deps.escapeHtml(group)}</li>`);
|
|
9143
|
+
lastGroup = group;
|
|
9144
|
+
}
|
|
9145
|
+
html.push(commitRow(commit));
|
|
9146
|
+
}
|
|
9147
|
+
list2.innerHTML = html.join("");
|
|
9148
|
+
setStatusText(loading ? "loading..." : commits.length ? "" : "no commits");
|
|
9149
|
+
}
|
|
9150
|
+
function updateCommitInfo(commit) {
|
|
9151
|
+
const info = document.querySelector("#history-commit-info");
|
|
9152
|
+
if (!info)
|
|
9153
|
+
return;
|
|
9154
|
+
if (!commit) {
|
|
9155
|
+
info.hidden = true;
|
|
9156
|
+
return;
|
|
9157
|
+
}
|
|
9158
|
+
const set2 = (sel, text2) => {
|
|
9159
|
+
const el = info.querySelector(sel);
|
|
9160
|
+
if (el)
|
|
9161
|
+
el.textContent = text2;
|
|
9162
|
+
};
|
|
9163
|
+
set2(".hci-sha", commit.sha);
|
|
9164
|
+
set2(".hci-author", commit.author);
|
|
9165
|
+
const t2 = Date.parse(commit.when);
|
|
9166
|
+
set2(".hci-date", Number.isFinite(t2) ? new Date(t2).toLocaleString() : commit.when);
|
|
9167
|
+
set2(".hci-subject", commit.subject);
|
|
9168
|
+
const body = info.querySelector(".hci-body");
|
|
9169
|
+
if (body) {
|
|
9170
|
+
body.textContent = commit.body;
|
|
9171
|
+
body.hidden = !commit.body;
|
|
9172
|
+
}
|
|
9173
|
+
info.hidden = false;
|
|
9174
|
+
}
|
|
9175
|
+
function updateActiveRow() {
|
|
9176
|
+
list2.querySelectorAll(".history-item").forEach((row) => {
|
|
9177
|
+
row.classList.toggle("active", row.dataset.sha === selectedSha);
|
|
9178
|
+
});
|
|
9179
|
+
}
|
|
9180
|
+
let inFlight = null;
|
|
9181
|
+
function loadNextPage() {
|
|
9182
|
+
if (inFlight)
|
|
9183
|
+
return inFlight;
|
|
9184
|
+
const started = doLoadNextPage().finally(() => {
|
|
9185
|
+
if (inFlight === started)
|
|
9186
|
+
inFlight = null;
|
|
9187
|
+
});
|
|
9188
|
+
inFlight = started;
|
|
9189
|
+
return started;
|
|
9190
|
+
}
|
|
9191
|
+
async function doLoadNextPage() {
|
|
9192
|
+
loading = true;
|
|
9193
|
+
const gen = generation;
|
|
9194
|
+
setStatusText("loading...");
|
|
9195
|
+
const page = await fetchPage(commits.length);
|
|
9196
|
+
if (gen !== generation)
|
|
9197
|
+
return false;
|
|
9198
|
+
loading = false;
|
|
9199
|
+
if (!page) {
|
|
9200
|
+
setStatusText("");
|
|
9201
|
+
return false;
|
|
9202
|
+
}
|
|
9203
|
+
commits = commits.concat(page.commits);
|
|
9204
|
+
hasMore = page.hasMore;
|
|
9205
|
+
renderList();
|
|
9206
|
+
return page.commits.length > 0;
|
|
9207
|
+
}
|
|
9208
|
+
async function selectCommit(commit, options = {}) {
|
|
9209
|
+
const gen = generation;
|
|
9210
|
+
selectedSha = commit.sha;
|
|
9211
|
+
updateActiveRow();
|
|
9212
|
+
updateCommitInfo(commit);
|
|
9213
|
+
if (gen !== generation)
|
|
9214
|
+
return;
|
|
9215
|
+
if (options.updateUrl !== false) {
|
|
9216
|
+
const range = commitDiffRange(commit);
|
|
9217
|
+
deps.setRoute({ screen: "history", ref, commit: commit.sha, range }, true);
|
|
9218
|
+
}
|
|
9219
|
+
if (gen !== generation)
|
|
9220
|
+
return;
|
|
9221
|
+
await deps.applyCommitRange(commitDiffRange(commit));
|
|
9222
|
+
if (gen !== generation)
|
|
9223
|
+
return;
|
|
9224
|
+
}
|
|
9225
|
+
let lookupFailed = false;
|
|
9226
|
+
async function fetchSingleCommit(sha) {
|
|
9227
|
+
const url = `/_log?ref=${encodeURIComponent(sha)}&skip=0&limit=1`;
|
|
9228
|
+
lookupFailed = false;
|
|
9229
|
+
try {
|
|
9230
|
+
const res = await deps.trackLoad(fetch(url).then(async (r2) => {
|
|
9231
|
+
if (r2.status === 400)
|
|
9232
|
+
return null;
|
|
9233
|
+
if (!r2.ok)
|
|
9234
|
+
throw new Error(await r2.text());
|
|
9235
|
+
return await r2.json();
|
|
9236
|
+
}));
|
|
9237
|
+
return res?.commits[0] || null;
|
|
9238
|
+
} catch {
|
|
9239
|
+
lookupFailed = true;
|
|
9240
|
+
return null;
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
async function resolveDeepLink(sha) {
|
|
9244
|
+
const gen = generation;
|
|
9245
|
+
let pagesLoaded = 0;
|
|
9246
|
+
if (commits.length === 0) {
|
|
9247
|
+
await loadNextPage();
|
|
9248
|
+
if (gen !== generation)
|
|
9249
|
+
return;
|
|
9250
|
+
pagesLoaded = 1;
|
|
9251
|
+
} else {
|
|
9252
|
+
pagesLoaded = 1;
|
|
9253
|
+
}
|
|
9254
|
+
for (;; ) {
|
|
9255
|
+
const found = commits.find((c2) => c2.sha.startsWith(sha));
|
|
9256
|
+
if (found) {
|
|
9257
|
+
await selectCommit(found, { updateUrl: false });
|
|
9258
|
+
scrollToSelected();
|
|
9259
|
+
return;
|
|
9260
|
+
}
|
|
9261
|
+
if (!shouldContinueAutoLoad({ pagesLoaded, found: false, hasMore }))
|
|
9262
|
+
break;
|
|
9263
|
+
const got = await loadNextPage();
|
|
9264
|
+
if (gen !== generation)
|
|
9265
|
+
return;
|
|
9266
|
+
pagesLoaded++;
|
|
9267
|
+
if (!got && !hasMore)
|
|
9268
|
+
break;
|
|
9269
|
+
}
|
|
9270
|
+
const single = await fetchSingleCommit(sha);
|
|
9271
|
+
if (gen !== generation)
|
|
9272
|
+
return;
|
|
9273
|
+
if (!single) {
|
|
9274
|
+
setBanner(lookupFailed ? `failed to load commit: ${sha}` : `commit not found: ${sha}`);
|
|
9275
|
+
updateCommitInfo(null);
|
|
9276
|
+
deps.showEmptyDiffPane();
|
|
9277
|
+
return;
|
|
9278
|
+
}
|
|
9279
|
+
setBanner(`showing commit outside the loaded ${ref} log`);
|
|
9280
|
+
commits = [single, ...commits];
|
|
9281
|
+
renderList();
|
|
9282
|
+
await selectCommit(single, { updateUrl: false });
|
|
9283
|
+
scrollToSelected();
|
|
9284
|
+
}
|
|
9285
|
+
function scrollToSelected() {
|
|
9286
|
+
list2.querySelector(`.history-item[data-sha="${CSS.escape(selectedSha)}"]`)?.scrollIntoView({ block: "center" });
|
|
9287
|
+
}
|
|
9288
|
+
let entering = Promise.resolve();
|
|
9289
|
+
function enterHistory(force) {
|
|
9290
|
+
entering = entering.then(() => doEnterHistory(force === true)).catch(() => {});
|
|
9291
|
+
return entering;
|
|
9292
|
+
}
|
|
9293
|
+
async function doEnterHistory(force = false) {
|
|
9294
|
+
const route = deps.getRoute();
|
|
9295
|
+
if (route.screen !== "history")
|
|
9296
|
+
return;
|
|
9297
|
+
const nextRef = route.ref || "HEAD";
|
|
9298
|
+
const refChanged = nextRef !== ref;
|
|
9299
|
+
if (refChanged || force || commits.length === 0) {
|
|
9300
|
+
generation++;
|
|
9301
|
+
ref = nextRef;
|
|
9302
|
+
commits = [];
|
|
9303
|
+
hasMore = false;
|
|
9304
|
+
loading = false;
|
|
9305
|
+
inFlight = null;
|
|
9306
|
+
selectedSha = "";
|
|
9307
|
+
setBanner("");
|
|
9308
|
+
updateCommitInfo(null);
|
|
9309
|
+
renderList();
|
|
9310
|
+
await loadNextPage();
|
|
9311
|
+
}
|
|
9312
|
+
const route2 = deps.getRoute();
|
|
9313
|
+
if (route2.screen !== "history")
|
|
9314
|
+
return;
|
|
9315
|
+
if (route2.commit) {
|
|
9316
|
+
await resolveDeepLink(route2.commit);
|
|
9317
|
+
} else {
|
|
9318
|
+
selectedSha = "";
|
|
9319
|
+
updateActiveRow();
|
|
9320
|
+
updateCommitInfo(null);
|
|
9321
|
+
deps.showEmptyDiffPane();
|
|
9322
|
+
}
|
|
9323
|
+
}
|
|
9324
|
+
function onRefPicked(nextRef) {
|
|
9325
|
+
const value = nextRef && nextRef !== "worktree" ? nextRef : "HEAD";
|
|
9326
|
+
deps.setRoute({
|
|
9327
|
+
screen: "history",
|
|
9328
|
+
ref: value,
|
|
9329
|
+
range: { from: "HEAD", to: "worktree" }
|
|
9330
|
+
}, false);
|
|
9331
|
+
enterHistory(true);
|
|
9332
|
+
}
|
|
9333
|
+
list2.addEventListener("click", (e2) => {
|
|
9334
|
+
const row = e2.target.closest(".history-item");
|
|
9335
|
+
if (!row?.dataset.sha)
|
|
9336
|
+
return;
|
|
9337
|
+
const commit = commits.find((c2) => c2.sha === row.dataset.sha);
|
|
9338
|
+
if (commit)
|
|
9339
|
+
selectCommit(commit);
|
|
9340
|
+
});
|
|
9341
|
+
function applyFilter(next) {
|
|
9342
|
+
const value = next.trim();
|
|
9343
|
+
if (value === query)
|
|
9344
|
+
return;
|
|
9345
|
+
query = value;
|
|
9346
|
+
generation++;
|
|
9347
|
+
commits = [];
|
|
9348
|
+
hasMore = false;
|
|
9349
|
+
loading = false;
|
|
9350
|
+
inFlight = null;
|
|
9351
|
+
setBanner("");
|
|
9352
|
+
renderList();
|
|
9353
|
+
loadNextPage();
|
|
9354
|
+
}
|
|
9355
|
+
const filterInput = document.querySelector("#history-filter");
|
|
9356
|
+
let filterTimer = null;
|
|
9357
|
+
filterInput?.addEventListener("input", () => {
|
|
9358
|
+
if (filterTimer)
|
|
9359
|
+
clearTimeout(filterTimer);
|
|
9360
|
+
filterTimer = setTimeout(() => {
|
|
9361
|
+
filterTimer = null;
|
|
9362
|
+
applyFilter(filterInput.value);
|
|
9363
|
+
}, 250);
|
|
9364
|
+
});
|
|
9365
|
+
filterInput?.addEventListener("keydown", (e2) => {
|
|
9366
|
+
if (e2.key === "Escape" && filterInput.value) {
|
|
9367
|
+
filterInput.value = "";
|
|
9368
|
+
applyFilter("");
|
|
9369
|
+
e2.stopPropagation();
|
|
9370
|
+
}
|
|
9371
|
+
});
|
|
9372
|
+
const observer = new IntersectionObserver((entries) => {
|
|
9373
|
+
if (!entries.some((entry) => entry.isIntersecting))
|
|
9374
|
+
return;
|
|
9375
|
+
if (deps.getRoute().screen !== "history")
|
|
9376
|
+
return;
|
|
9377
|
+
if (hasMore && !loading)
|
|
9378
|
+
loadNextPage();
|
|
9379
|
+
}, { root: panel, rootMargin: "200px" });
|
|
9380
|
+
observer.observe(sentinel);
|
|
9381
|
+
return { enterHistory, onRefPicked };
|
|
9382
|
+
}
|
|
9383
|
+
|
|
9016
9384
|
// web-src/views/hunk-expand.ts
|
|
9017
9385
|
function createHunkExpand(deps) {
|
|
9018
9386
|
function parseHunkHeader(text2) {
|
|
@@ -9631,7 +9999,7 @@ ${frontmatter.yaml}
|
|
|
9631
9999
|
const target = e2.target;
|
|
9632
10000
|
if (popover.contains(target))
|
|
9633
10001
|
return;
|
|
9634
|
-
if (target.id === "ref-from" || target.id === "ref-to" || target.id === "repo-ref" || target.id === "repo-target")
|
|
10002
|
+
if (target.id === "ref-from" || target.id === "ref-to" || target.id === "repo-ref" || target.id === "repo-target" || target.id === "history-ref")
|
|
9635
10003
|
return;
|
|
9636
10004
|
closePopover();
|
|
9637
10005
|
});
|
|
@@ -15239,6 +15607,15 @@ ${frontmatter.yaml}
|
|
|
15239
15607
|
document.body.classList.toggle("gdp-repo-blob-page", STATE.route.screen === "file" && STATE.route.view === "blob");
|
|
15240
15608
|
document.body.classList.toggle("gdp-repo-page", STATE.route.screen === "repo");
|
|
15241
15609
|
document.body.classList.toggle("gdp-help-page", STATE.route.screen === "help");
|
|
15610
|
+
document.body.classList.toggle("gdp-history-page", STATE.route.screen === "history");
|
|
15611
|
+
const historyPanel = $("#history-panel");
|
|
15612
|
+
if (historyPanel)
|
|
15613
|
+
historyPanel.hidden = STATE.route.screen !== "history";
|
|
15614
|
+
if (STATE.route.screen === "history") {
|
|
15615
|
+
const historyRefInput = $("#history-ref");
|
|
15616
|
+
if (historyRefInput)
|
|
15617
|
+
historyRefInput.value = STATE.route.ref || "HEAD";
|
|
15618
|
+
}
|
|
15242
15619
|
syncRepoTargetInput(repoFileTargetFromRoute() || "worktree");
|
|
15243
15620
|
}
|
|
15244
15621
|
function syncHeaderMenu() {
|
|
@@ -15258,6 +15635,13 @@ ${frontmatter.yaml}
|
|
|
15258
15635
|
if (link2.dataset.route === "diff") {
|
|
15259
15636
|
link2.href = buildRoute({ screen: "diff", range: currentRange() });
|
|
15260
15637
|
}
|
|
15638
|
+
if (link2.dataset.route === "history") {
|
|
15639
|
+
link2.href = buildRoute({
|
|
15640
|
+
screen: "history",
|
|
15641
|
+
ref: "HEAD",
|
|
15642
|
+
range: currentRange()
|
|
15643
|
+
});
|
|
15644
|
+
}
|
|
15261
15645
|
if (link2.dataset.route === "help") {
|
|
15262
15646
|
link2.href = buildRoute({
|
|
15263
15647
|
screen: "help",
|
|
@@ -15747,6 +16131,18 @@ ${frontmatter.yaml}
|
|
|
15747
16131
|
}
|
|
15748
16132
|
if (STATE.route.screen === "repo")
|
|
15749
16133
|
return loadRepo();
|
|
16134
|
+
{
|
|
16135
|
+
const empty = $("#empty");
|
|
16136
|
+
if (empty) {
|
|
16137
|
+
const onHistory = STATE.route.screen === "history";
|
|
16138
|
+
const h2 = empty.querySelector("h2");
|
|
16139
|
+
if (h2)
|
|
16140
|
+
h2.textContent = onHistory ? "Empty diff" : "No changes";
|
|
16141
|
+
const p2 = empty.querySelector("p");
|
|
16142
|
+
if (p2)
|
|
16143
|
+
p2.textContent = onHistory ? "This commit has no changes against its first parent." : "The working tree is clean against this ref.";
|
|
16144
|
+
}
|
|
16145
|
+
}
|
|
15750
16146
|
setStatus("refreshing");
|
|
15751
16147
|
const params = new URLSearchParams;
|
|
15752
16148
|
if (STATE.ignoreWs)
|
|
@@ -15772,6 +16168,9 @@ ${frontmatter.yaml}
|
|
|
15772
16168
|
else if (STATE.route.screen === "file" && STATE.route.view === "blob") {
|
|
15773
16169
|
setStatus("live");
|
|
15774
16170
|
applySourceRouteToShell();
|
|
16171
|
+
} else if (STATE.route.screen === "history") {
|
|
16172
|
+
setStatus("live");
|
|
16173
|
+
HISTORY_VIEW.enterHistory();
|
|
15775
16174
|
} else
|
|
15776
16175
|
load();
|
|
15777
16176
|
});
|
|
@@ -15801,12 +16200,42 @@ ${frontmatter.yaml}
|
|
|
15801
16200
|
renderHelpPage();
|
|
15802
16201
|
} else {
|
|
15803
16202
|
setRoute({ screen: "diff", range }, true);
|
|
16203
|
+
setPageMode();
|
|
15804
16204
|
load();
|
|
15805
16205
|
}
|
|
15806
16206
|
}
|
|
15807
16207
|
syncRefInputs();
|
|
15808
16208
|
syncHeaderMenu();
|
|
15809
|
-
|
|
16209
|
+
const HISTORY_VIEW = createHistoryView({
|
|
16210
|
+
$,
|
|
16211
|
+
escapeHtml: escapeHtml2,
|
|
16212
|
+
getRoute: () => STATE.route,
|
|
16213
|
+
setRoute,
|
|
16214
|
+
applyCommitRange: (range) => {
|
|
16215
|
+
STATE.from = range.from;
|
|
16216
|
+
STATE.to = range.to;
|
|
16217
|
+
syncRefInputs();
|
|
16218
|
+
return load();
|
|
16219
|
+
},
|
|
16220
|
+
showEmptyDiffPane: () => {
|
|
16221
|
+
const diff = $("#diff");
|
|
16222
|
+
if (diff)
|
|
16223
|
+
diff.innerHTML = "";
|
|
16224
|
+
const empty = $("#empty");
|
|
16225
|
+
if (empty) {
|
|
16226
|
+
empty.classList.remove("hidden");
|
|
16227
|
+
const h2 = empty.querySelector("h2");
|
|
16228
|
+
if (h2)
|
|
16229
|
+
h2.textContent = "No commit selected";
|
|
16230
|
+
const p2 = empty.querySelector("p");
|
|
16231
|
+
if (p2)
|
|
16232
|
+
p2.textContent = "Select a commit from the list to see its changes.";
|
|
16233
|
+
}
|
|
16234
|
+
setStatus("live");
|
|
16235
|
+
},
|
|
16236
|
+
trackLoad
|
|
16237
|
+
});
|
|
16238
|
+
const REF_PICKER = createRefPicker({
|
|
15810
16239
|
$,
|
|
15811
16240
|
escapeHtml: escapeHtml2,
|
|
15812
16241
|
currentRange,
|
|
@@ -15818,6 +16247,13 @@ ${frontmatter.yaml}
|
|
|
15818
16247
|
getRepoRef: () => STATE.repoRef,
|
|
15819
16248
|
getRoute: () => STATE.route
|
|
15820
16249
|
});
|
|
16250
|
+
if (REF_PICKER) {
|
|
16251
|
+
const historyRefInput = document.querySelector("#history-ref");
|
|
16252
|
+
if (historyRefInput) {
|
|
16253
|
+
historyRefInput.value = "HEAD";
|
|
16254
|
+
REF_PICKER.wireRefSelectorInput(historyRefInput, (ref) => HISTORY_VIEW.onRefPicked(ref));
|
|
16255
|
+
}
|
|
16256
|
+
}
|
|
15821
16257
|
$("#ref-reset").addEventListener("click", () => setRange("HEAD", "worktree"));
|
|
15822
16258
|
function applyRouteFromLocation() {
|
|
15823
16259
|
const parsedRoute = parseRoute(window.location.pathname, window.location.search, currentRange());
|
|
@@ -15843,6 +16279,13 @@ ${frontmatter.yaml}
|
|
|
15843
16279
|
loadRepo();
|
|
15844
16280
|
return;
|
|
15845
16281
|
}
|
|
16282
|
+
if (STATE.route.screen === "history") {
|
|
16283
|
+
cancelActiveSourceLoad("navigation");
|
|
16284
|
+
setPageMode();
|
|
16285
|
+
removeStandaloneSource();
|
|
16286
|
+
HISTORY_VIEW.enterHistory();
|
|
16287
|
+
return;
|
|
16288
|
+
}
|
|
15846
16289
|
if (STATE.route.screen !== "file") {
|
|
15847
16290
|
cancelActiveSourceLoad("navigation");
|
|
15848
16291
|
setPageMode();
|
package/web/index.html
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
<nav class="app-menu" aria-label="Views">
|
|
27
27
|
<a class="app-menu-item active" data-route="repo" href="/">Repository</a>
|
|
28
28
|
<a class="app-menu-item" data-route="diff" href="/todif?from=HEAD&to=worktree">Diff Viewer</a>
|
|
29
|
+
<a class="app-menu-item" data-route="history" href="/history">History</a>
|
|
29
30
|
</nav>
|
|
30
31
|
<div class="global-actions">
|
|
31
32
|
<button id="annotations-toggle" class="global-icon-action" title="code annotations" aria-label="code annotations">💬<span id="annotations-count" hidden></span></button>
|
|
@@ -76,6 +77,19 @@
|
|
|
76
77
|
|
|
77
78
|
<div id="load-bar" aria-hidden="true"></div>
|
|
78
79
|
|
|
80
|
+
<aside id="history-panel" aria-label="Commit history" hidden>
|
|
81
|
+
<div class="history-head">
|
|
82
|
+
<span class="history-title">Commits</span>
|
|
83
|
+
<span data-ref-selector-mount data-ref-id="history-ref" data-placeholder="ref..." data-title="history ref"></span>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="history-filter-wrap">
|
|
86
|
+
<input id="history-filter" type="search" placeholder="filter commits… (message, sha, author:name, path:file)" autocomplete="off" />
|
|
87
|
+
</div>
|
|
88
|
+
<div id="history-banner" class="history-banner" role="status" hidden></div>
|
|
89
|
+
<ol id="history-list"></ol>
|
|
90
|
+
<div id="history-sentinel" aria-hidden="true"></div>
|
|
91
|
+
<div id="history-status" class="history-status" role="status" hidden></div>
|
|
92
|
+
</aside>
|
|
79
93
|
<aside id="sidebar">
|
|
80
94
|
<div class="sb-head">
|
|
81
95
|
<span class="sb-title">Files</span>
|
|
@@ -180,6 +194,15 @@
|
|
|
180
194
|
</aside>
|
|
181
195
|
|
|
182
196
|
<main id="content">
|
|
197
|
+
<section id="history-commit-info" hidden aria-label="Selected commit">
|
|
198
|
+
<div class="hci-head">
|
|
199
|
+
<span id="hci-sha" class="hci-sha"></span>
|
|
200
|
+
<span id="hci-author" class="hci-author"></span>
|
|
201
|
+
<span id="hci-date" class="hci-date"></span>
|
|
202
|
+
</div>
|
|
203
|
+
<h2 id="hci-subject" class="hci-subject"></h2>
|
|
204
|
+
<pre id="hci-body" class="hci-body" hidden></pre>
|
|
205
|
+
</section>
|
|
183
206
|
<div id="empty" class="empty hidden">
|
|
184
207
|
<div class="emoji">✨</div>
|
|
185
208
|
<h2>No changes</h2>
|
package/web/style.css
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
--topbar-h: 56px;
|
|
51
51
|
--chrome-h: calc(var(--global-header-h) + var(--topbar-h));
|
|
52
52
|
--sidebar-w: 308px;
|
|
53
|
+
--history-w: 320px;
|
|
53
54
|
--sidebar-restore-rail-w: 44px;
|
|
54
55
|
/* shadows */
|
|
55
56
|
--shadow-sm: 0 1px 0 rgba(31,35,40,0.04);
|
|
@@ -900,6 +901,165 @@ body[data-code-font-size="xlarge"] { --code-font-size: 15px; }
|
|
|
900
901
|
100% { transform: translateX(450%); }
|
|
901
902
|
}
|
|
902
903
|
|
|
904
|
+
/* ===== Commit history panel ===== */
|
|
905
|
+
#history-panel {
|
|
906
|
+
display: none;
|
|
907
|
+
position: fixed;
|
|
908
|
+
left: 0;
|
|
909
|
+
top: var(--chrome-h);
|
|
910
|
+
bottom: 0;
|
|
911
|
+
width: var(--history-w);
|
|
912
|
+
background: var(--bg-soft);
|
|
913
|
+
border-right: 1px solid var(--border);
|
|
914
|
+
overflow-y: auto;
|
|
915
|
+
z-index: 31;
|
|
916
|
+
flex-direction: column;
|
|
917
|
+
}
|
|
918
|
+
body.gdp-history-page #history-panel { display: flex; }
|
|
919
|
+
body.gdp-history-page #sidebar { left: var(--history-w); }
|
|
920
|
+
body.gdp-history-page main#content {
|
|
921
|
+
margin-left: calc(var(--history-w) + var(--sidebar-w));
|
|
922
|
+
}
|
|
923
|
+
body.gdp-history-page.gdp-sidebar-hidden main#content {
|
|
924
|
+
margin-left: var(--history-w);
|
|
925
|
+
}
|
|
926
|
+
body.gdp-history-page #sidebar-resizer {
|
|
927
|
+
left: calc(var(--history-w) + var(--sidebar-w) - 4px);
|
|
928
|
+
}
|
|
929
|
+
#history-panel[hidden] {
|
|
930
|
+
display: none !important;
|
|
931
|
+
}
|
|
932
|
+
.history-head {
|
|
933
|
+
display: flex;
|
|
934
|
+
align-items: center;
|
|
935
|
+
gap: 8px;
|
|
936
|
+
padding: 13px 14px 10px;
|
|
937
|
+
position: sticky;
|
|
938
|
+
top: 0;
|
|
939
|
+
background: var(--bg-soft);
|
|
940
|
+
border-bottom: 1px solid var(--border);
|
|
941
|
+
z-index: 1;
|
|
942
|
+
}
|
|
943
|
+
.history-title { font-weight: 600; font-size: 13px; }
|
|
944
|
+
.history-filter-wrap {
|
|
945
|
+
padding: 0 12px 8px;
|
|
946
|
+
background: var(--bg-soft);
|
|
947
|
+
border-bottom: 1px solid var(--border);
|
|
948
|
+
}
|
|
949
|
+
#history-filter {
|
|
950
|
+
width: 100%;
|
|
951
|
+
background: var(--bg);
|
|
952
|
+
border: 1px solid var(--border);
|
|
953
|
+
color: var(--fg);
|
|
954
|
+
border-radius: 6px;
|
|
955
|
+
font: inherit;
|
|
956
|
+
font-size: 12px;
|
|
957
|
+
padding: 5px 8px;
|
|
958
|
+
}
|
|
959
|
+
.history-banner {
|
|
960
|
+
margin: 8px 10px 0;
|
|
961
|
+
padding: 6px 10px;
|
|
962
|
+
border: 1px solid var(--border);
|
|
963
|
+
border-radius: 6px;
|
|
964
|
+
background: var(--bg-mute);
|
|
965
|
+
color: var(--fg-muted);
|
|
966
|
+
font-size: 12px;
|
|
967
|
+
}
|
|
968
|
+
#history-list { list-style: none; margin: 0; padding: 0 0 24px; }
|
|
969
|
+
.history-group {
|
|
970
|
+
display: flex;
|
|
971
|
+
align-items: center;
|
|
972
|
+
gap: 8px;
|
|
973
|
+
padding: 14px 14px 6px;
|
|
974
|
+
background: var(--bg-mute);
|
|
975
|
+
border-top: 1px solid var(--border);
|
|
976
|
+
border-bottom: 1px solid var(--border);
|
|
977
|
+
color: var(--accent);
|
|
978
|
+
font-size: 11.5px;
|
|
979
|
+
font-weight: 700;
|
|
980
|
+
text-transform: uppercase;
|
|
981
|
+
letter-spacing: 0.06em;
|
|
982
|
+
}
|
|
983
|
+
.history-group::before {
|
|
984
|
+
content: "";
|
|
985
|
+
width: 4px;
|
|
986
|
+
height: 12px;
|
|
987
|
+
border-radius: 2px;
|
|
988
|
+
background: var(--accent);
|
|
989
|
+
}
|
|
990
|
+
#history-list > .history-group:first-child { border-top: 0; }
|
|
991
|
+
.history-item {
|
|
992
|
+
padding: 8px 14px;
|
|
993
|
+
cursor: pointer;
|
|
994
|
+
border-left: 3px solid transparent;
|
|
995
|
+
border-bottom: 1px solid var(--border);
|
|
996
|
+
}
|
|
997
|
+
.history-item:hover { background: var(--bg-mute); }
|
|
998
|
+
.history-item.active {
|
|
999
|
+
background: var(--bg-mute);
|
|
1000
|
+
border-left-color: var(--accent);
|
|
1001
|
+
}
|
|
1002
|
+
.history-item .subject {
|
|
1003
|
+
display: block;
|
|
1004
|
+
font-size: 12.5px;
|
|
1005
|
+
overflow: hidden;
|
|
1006
|
+
text-overflow: ellipsis;
|
|
1007
|
+
white-space: nowrap;
|
|
1008
|
+
}
|
|
1009
|
+
.history-item .meta2 {
|
|
1010
|
+
display: flex;
|
|
1011
|
+
gap: 8px;
|
|
1012
|
+
color: var(--fg-muted);
|
|
1013
|
+
font-size: 11px;
|
|
1014
|
+
margin-top: 2px;
|
|
1015
|
+
}
|
|
1016
|
+
.history-item .sha { font-family: "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; }
|
|
1017
|
+
.history-status {
|
|
1018
|
+
padding: 10px 14px;
|
|
1019
|
+
color: var(--fg-muted);
|
|
1020
|
+
font-size: 12px;
|
|
1021
|
+
}
|
|
1022
|
+
#history-sentinel { height: 1px; }
|
|
1023
|
+
#history-commit-info {
|
|
1024
|
+
display: none;
|
|
1025
|
+
margin: 12px 16px 4px;
|
|
1026
|
+
padding: 12px 16px;
|
|
1027
|
+
border: 1px solid var(--border);
|
|
1028
|
+
border-radius: 8px;
|
|
1029
|
+
background: var(--bg-soft);
|
|
1030
|
+
}
|
|
1031
|
+
body.gdp-history-page #history-commit-info:not([hidden]) { display: block; }
|
|
1032
|
+
#history-commit-info .hci-head {
|
|
1033
|
+
display: flex;
|
|
1034
|
+
flex-wrap: wrap;
|
|
1035
|
+
align-items: baseline;
|
|
1036
|
+
gap: 10px;
|
|
1037
|
+
color: var(--fg-muted);
|
|
1038
|
+
font-size: 12px;
|
|
1039
|
+
}
|
|
1040
|
+
#history-commit-info .hci-sha {
|
|
1041
|
+
font-family: "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
1042
|
+
user-select: all;
|
|
1043
|
+
}
|
|
1044
|
+
#history-commit-info .hci-subject {
|
|
1045
|
+
margin: 6px 0 0;
|
|
1046
|
+
font-size: 15px;
|
|
1047
|
+
line-height: 1.4;
|
|
1048
|
+
overflow-wrap: anywhere;
|
|
1049
|
+
}
|
|
1050
|
+
#history-commit-info .hci-body {
|
|
1051
|
+
margin: 8px 0 0;
|
|
1052
|
+
padding: 0;
|
|
1053
|
+
background: transparent;
|
|
1054
|
+
border: 0;
|
|
1055
|
+
font-family: inherit;
|
|
1056
|
+
font-size: 12.5px;
|
|
1057
|
+
line-height: 1.6;
|
|
1058
|
+
color: var(--fg-muted);
|
|
1059
|
+
white-space: pre-wrap;
|
|
1060
|
+
overflow-wrap: anywhere;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
903
1063
|
/* ===== Sidebar ===== */
|
|
904
1064
|
#sidebar {
|
|
905
1065
|
position: fixed;
|
|
@@ -3713,6 +3873,17 @@ body.gdp-file-detail-page #empty {
|
|
|
3713
3873
|
}
|
|
3714
3874
|
#content { margin-left: 0; max-width: none; padding-top: calc(var(--chrome-h) + 240px); }
|
|
3715
3875
|
.controls input[type="search"] { width: 130px; }
|
|
3876
|
+
body.gdp-history-page #history-panel {
|
|
3877
|
+
position: static;
|
|
3878
|
+
width: auto;
|
|
3879
|
+
border-right: 0;
|
|
3880
|
+
border-bottom: 1px solid var(--border);
|
|
3881
|
+
}
|
|
3882
|
+
body.gdp-history-page #sidebar { left: 0; }
|
|
3883
|
+
body.gdp-history-page main#content,
|
|
3884
|
+
body.gdp-history-page.gdp-sidebar-hidden main#content {
|
|
3885
|
+
margin-left: 0;
|
|
3886
|
+
}
|
|
3716
3887
|
}
|
|
3717
3888
|
|
|
3718
3889
|
/* ===== File shell (placeholder + loaded card) ===== */
|